Episode 3: A Starry Afternoon
Where we finally start programming and put some stars in the sky, regardless of the hour or daytime.
Where to start? It is not only for sync with the order of the Computer Space PCBs, as we found them in the last episode, when we procrastinated extensively over a writeup on the original game, it's also rather logical to start with the generation of the background starfield, because we'll have to refer to this, whenever we were to clear a screen position later on in the development of our game. It's true, we could start by putting something more fancy on the screen, alternatively, but then, we would have to go back and revisit our code each time, we added another fundamental, like the background stars.
Stars — Stars? — Stars!
It may be tempting to simply fill the background by a random function returning a flag value for the presence or absence of a star, but then, we would run into some issues, whenever we had to repaint any of our 1000 screen positions. Either, we where to dedicate another precious K of RAM to storing the background data (we're decided already to use 1K for the title screen), or we would have to handle and manage various buffers, storing characters overwritten temporarily by any of our on-screen objects. But managing a handful of partial background buffers, involving possible race conditions, may turn out rather tedious. So it would be nice, if we had a routine somehow magically returning the background character for any given pair of screen coordinates.
(On some of the later PETs, we wouldn't have this issue at all, as these had 4 separate pages of screen memory, just perfect for storing off-screen data in a 1:1 mapping. Alas, the PET 2001 had just a single page of screen memory, thus we'll have to be inventive.)
As we've seen, the original game does it just alike, by feeding the output lines of the horizontal and vertical sync-chains into a logic network conisting of 2 ICs. So our approach will not only be the smart thing to do, but also the appropriate solution faithful to the original. So, how could we implement this "magical" background oracle?
Whenever we'll want our starfield orcale to return a character, we'll probably do it based on columns (0..39
) and rows (0..24
). Therefor, it'll be only appropriate to base our method on x and y coordinates as the input. We'll have a hard-coded string of bytes, one for each character position in a row, and another one, consisting of just a single byte per row. By masking one by the other (by a simple AND
), we may decide on the presence of a star based on the result being non-zero.
And this is what we come up with, using some random values:
starMaskX !byte $20, $00, $40, $0A, $08, $01, $82, $00 !byte $00, $00, $00, $00, $40, $00, $00, $02 !byte $00, $04, $20, $10, $88, $44, $00, $40 !byte $00, $01, $20, $00, $00, $42, $14, $00 !byte $48, $20, $00, $10, $18, $00, $00, $40 starMaskY !byte $40, $00, $01, $00, $00, $08, $00, $04 !byte $00, $40, $00, $02, $00, $00, $01, $00 !byte $04, $00, $10, $00, $20, $00, $01, $00 !byte $80
As may be observed, the values for the y-coordinates are simple powers of 2, selecting any of the bits in the code bytes for the columns. (Again, this is analogous to the multiplexer/decoder logic found in the real game.)
And this is, what it looks like on the screen:
And this is our oracle of celestial bodies, to be called as a subroutine:
;get a background screen code for row Y, col X getStar lda starMaskY, y beq blank ;is it zero? branch to 'blank' and starMaskX, x beq blank ;is result zero? continue at 'blank' lda #$2E ;return a dot in AC rts blank lda #$20 ;return a blank in AC rts
Wel'll put the coordinates in the index registers (X
and Y
) and – BAM! — thanks to indexed load instructions we'll return with just the right character in the accumulator (AC
).
The Program
Tools
Before we begin to make this a working program, there's still a decision to be made, namely on the tools to use, as in the assembler. I'll go with ACME (© 1998-2016 Marco Baye), a) because it's popular with Commodore folks, and b) it's an old-style assembler, using a basic syntax very much like the assemblers of the 1970s and 1980s did. (So this is just the right tool for a person like me, who hasn't touched the 6502 seriously for about 30 years. — No, you don't "org $401
", the PC is an asterisk, and a comment starts with a semicolon, not with C-style double-slashes!)
*****
Intermission: BASIC 6502 Basics
The MOS 6502 is one of the most straight-forward 8-bit CPUs you can think of. It's probably the very blueprint of a RISC processor. There are no block instructions, no 16-bit or double-sized registers (while the internal address space is 16-bits), and there's just a dozen-or-so basic instructions and a few branching instructions, a few transfer instructions to copy registers, and a few to set or clear some flags.
There are just a few registers (basically 3 working registers and a status register):
AC ...
accumulatorX ....
index registerY ....
secondary index registerPC ...
program counterSP ...
stack pointerP ....
processor flags (status register) comprising the flags (from MSB to LSB):N
— negativeV
— overflow (for BCD arithmetics)×
— (unused)B
— breakD
— decimal (BCD mode)I
— interrupt (IRQ disable)Z
— zeroC
— carry
Examples of basic instructions are:
LDA / STA ...
load/store ACLDX / STX ...
load/store XLDY / STY ...
load/store YAND .........
logical and with AC (result in AC)ORA .........
logical or with AC (result in AC)EOR .........
exlusive or (xor) with AC (result in AC)ADC .........
add with carry (to AC, result in AC)SBC .........
subtract with carry (from AC, result in AC)JMP .........
jumpJSR .........
jump to subroutine (return addr on stack)RTS .........
return from subroutineNOP .........
no op (2 cycles!)
For the complete set see: www.masswerk.at/6502/6502_instruction_set.html.
Addresses are given in bytes, littel-Endian, if 16-bit (meaning, first the lo-byte, than the hi-byte). The most fancy thing about the 6502 are its addressing modes, of which there's a couple of:
LDA #nn ........
immediate: value of literal constantLDA $HHLL ......
absolute: operand is $HHLLLDA $BB ........
absolute, zero page: operand is $BB in memory page #0 ($0000 .. $00FF)LDA $HHLL, X ...
absolute, X-indexed: operand is $HHLL + contents(X)LDA $HHLL, Y ...
absolute, Y-indexed: operand is $HHLL + contents(Y)LDA $BB, X .....
absolute, zero page, X-indexed: operand is $BB + contents(X)LDA $BB, Y .....
absolute, zero page, Y-indexed: operand is $BB + contents(Y)LDA ($HHLL) ....
indirect: operand is contents($HHLL), if address: contents( $HHLL $HHLL+1 )
And the really fancy ones:
LDA ($BB), Y ...
indirect, Y-indexed (zero page only): operand is contents($BB) + contents(Y)LDA ($BB, X) ...
X-indexed, indirect (zero page only): operand is contents( $BB + contents(X) )
These two latter ones come especially handy for tables look-ups, but,
You may ask, what the zero page is all about: It's just the very first 255 bytes of memory ($0000 .. $00FF), and using these ones (they have an opcode of their own) is generally faster (by a cycle) than normal 16-bit (2 bytes) address look-up. While, with older assemblers, you might have to specify this address mode by a prefix, like "*
", usually the assembler takes care of this automatically, like it does for the little-Endian byte order. (Otherwise, modern assemblers usually use a suffix to the instruction symbol to force zero page address modes.)
Finally, we may want to have a look at the conditional branching instructions, which are what is making the 6502 Turing-complete. These are based on the processor flags and branch to a relative offset provided in a single-byte operand. The operand is interpreted as a signed value, ranging from -126 to +129. Thanks to this, 6502 code is widely portable, with regard to the memory address it resides at.
The conditional branch instructions are:
BEQ ....
branch on equal: zero flag (Z) is setBNE ....
branch on not equal: zero flag (Z) is clearBMI ....
branch on minus: negative flag (N) setBPL ....
branch on plus: negative flag (N) clearBCS ....
branch on carry (C) setBCC ....
branch on carry (C) clearBVS ....
branch on overflow (V) setBVC ....
branch on overflow (V) clear
As we stated above, the 6502 is really a simple processor and this should be enough to follow the code.
*****
Getting Started
First, we must give our program a start. That is, as we load our code, we're by all means still in BASIC. So we'll have to use a SYS
command, remembering the specific start address, in order to call our machine code. Wouldn't it be nice, if we could run the program just the same by typing RUN
and hitting ENTER? For this, we'll have to include a tiny BASIC program, and we'll put it at the very start. (Speaking of start addresses, on the PET, a BASIC program usually resides at $0401.)
This is the anatomy of a simple (MS) BASIC program in memory:
"10 SYS 2048" => 0401: 0C 04 0A 00 9E 20 32 30 34 38 00 00 00 ^ ^ ^ | | ^ ^ | | | |--" 2048"---| | | | | | | | line number (2 bytes): 10 | end of line | | | | | 9E: BASIC token for "SYS" | | | $040C: addr of next line | \ | next line: 00 => end of program
For a program a bit more complex, we could figure it out in the assembler, but we also could just type it in normally and look the code up in the monitor that comes in the PET's ROM (as of ROM version 2 and higher):
LIST 10 REM PERSONAL COMPUTER SPACE 20 REM TRANSACTOR 2001 (NL,2017) 30 SYS 1105 READY. SYS 1024 B* PC IRQ SR AC XR YR SP .; 0401 E62E 32 04 5E 00 F8 .M 0401 0460 .: 0401 1F 04 0A 00 8F 20 50 45 .: 0409 52 53 4F 4E 41 4C 20 43 .: 0411 4F 4D 50 55 54 45 52 20 .: 0419 53 50 41 43 45 00 3F 04 .: 0421 14 00 8F 20 54 52 41 4E .: 0429 53 41 43 54 4F 52 20 32 .: 0431 30 30 31 20 28 4E 4C 2C .: 0439 32 30 31 37 29 00 4A 04 .: 0441 1E 00 9E 20 31 31 30 35 .: 0449 00 00 00 00 00 00 00 00 .: 0451 AA AA AA AA AA AA AA AA .: 0459 AA AA AA AA AA AA AA AA .
As may be observed, the empty space (AA AA ...
) starts at $0451 (decimal 1105), which will be our start address. (Actually, the last 5 zero-bytes were a residue in memory, but we may use them as a buffer, just the same.) Now we simply copy this sequence of bytes (from $0401 to $0450) into our assembler program:
!to "stars.prg", cbm ;set output file and format ; symbols ; zero-page ; BASIC input buffer at $23 .. $5A may be reused safely (cf, PET 2001 manual) PT1 = $24 ;used as a temporary address pointer ($24: LO, $25: HI) ; intro ; insert a tiny BASIC program, calling our code at $0451 (10105) ; ; 10 REM PERSONAL COMPUTER SPACE ; 20 REM TRANSACTOR 2001 (NL,2017) ; 30 SYS 1105 * = $0401 !byte $1F, $04, $0A, $00, $8F, $20, $50, $45 ; $0401 !byte $52, $53, $4F, $4E, $41, $4C, $20, $43 ; $0409 !byte $4F, $4D, $50, $55, $54, $45, $52, $20 ; $0411 !byte $53, $50, $41, $43, $45, $00, $3F, $04 ; $0419 !byte $14, $00, $8F, $20, $54, $52, $41, $4E ; $0421 !byte $53, $41, $43, $54, $4F, $52, $20, $32 ; $0429 !byte $30, $30, $31, $20, $28, $4E, $4C, $2C ; $0431 !byte $32, $30, $31, $37, $29, $00, $4A, $04 ; $0439 !byte $1E, $00, $9E, $20, $31, $31, $30, $35 ; $0441 !byte $00, $00, $00, $00, $00, $00, $00, $00 ; $0449 .. $0450 ; main * = $0451
Here, we also include a pseudo instruction for the output file and format (a CBM .prg
-file) and define a symbol for a pointer in the zero page, which is in a space safe to be used by any machine language program. After the insertion of our little BASIC-prespan, we're ready to go at $0451. (We really haven't to set the PC for this, as we've already reached this very address. However, it may serve to make things clearer.)
A Heavenly Canvas
Now it's time to loop over the 1000 (40 × 25) screen positions and store the respective background screen codes in them. Again, we're facing a more general problem, as we have to convert coordinates to addresses in screen RAM (starting at $8000).
We really do not want to do multiplications by 40 each time we want to convert a y-coordinate to a memory address, which may look like this:
;multiply YCOORD by 40 and add screen base address asl YCOORD ;arithmetic shift left (1 bit) asl YCOORD asl YCOORD lda YCOORD ;save in AC rol YCOORD+1 ;roll left 1 bit asl YCOORD rol YCOORD+1 clc ;clear carry flag for addition adc YCOORD sta YCOORD lda YCOORD+1 adc $C0 ;start of screen sta YCOORD+1 ...
(From: Hampshire, Nick, The PET Revealed; Computabits Ltd, Yeovil, UK, 1980; p. 164.)
Instead, we want to use one of the 6502's fancy indirect indexed lookups and get the address from a table. Like with the stars routine, we may have our coordinates in the index registers. Unfortunately, we'll have to use an indirect STA (pointer), Y
"). Thus, we have to exchange the X and Y registers in respect to our screen coordinates. The same applies for the stars routine, we have seen above.
; main * = $0451 ; display background jsr background ; enter an infinite loop loop nop jmp loop ; subroutines background ;fills the screen with stars ldx #24 ;max row offset (bottom) .row lda screenLinesLo, x ;set up addr in PT1 (lo) and PT1+1 (hi) sta PT1 lda screenLinesHi, x sta PT1+1 ldy #39 ;max col offset (right) .col jsr getStar sta (PT1), y ;display it dey ;decrement y (col) bpl .col ;still >= zero? dex ;decrement x (row) bpl .row ;not finished yet? rts getStar ;returns a background screen code (in AC) for row X, col Y lda starMaskY, x beq .blank and starMaskX, y beq .blank lda #$2E ; return a dot rts .blank lda #$20 ; return a blank rts ; data starMaskX !byte $20, $00, $40, $0A, $08, $01, $82, $00 !byte $00, $00, $00, $00, $40, $00, $00, $02 !byte $00, $04, $20, $10, $88, $44, $00, $40 !byte $00, $01, $20, $00, $00, $42, $14, $00 !byte $48, $20, $00, $10, $18, $00, $00, $40 starMaskY !byte $40, $00, $01, $00, $00, $08, $00, $04 !byte $00, $40, $00, $02, $00, $00, $01, $00 !byte $04, $00, $10, $00, $20, $00, $01, $00 !byte $80 screenLinesHi !byte $80 !byte $80 !byte $80 !byte $80 !byte $80 !byte $80 !byte $80 !byte $81 !byte $81 !byte $81 !byte $81 !byte $81 !byte $81 !byte $82 !byte $82 !byte $82 !byte $82 !byte $82 !byte $82 !byte $82 !byte $83 !byte $83 !byte $83 !byte $83 !byte $83 screenLinesLo !byte $00 !byte $28 !byte $50 !byte $78 !byte $A0 !byte $C8 !byte $F0 !byte $18 !byte $40 !byte $68 !byte $90 !byte $B8 !byte $E0 !byte $08 !byte $30 !byte $58 !byte $80 !byte $A8 !byte $D0 !byte $F8 !byte $20 !byte $48 !byte $70 !byte $98 !byte $C0
For the time being, after we've successfully painted the stars of the heavens (© 1962 Spacewar! source code comment) onto the screen, we'll spend the rest of the time in a simple NOP-loop, thus blocking the machine. The resulting program can be seen here in action (in-browser emulation): www.masswerk.at/pet/?prg=rc2017-04-e03-stars.
CBM Intrinsics
Speaking of program files, what is it that makes a CBM "prg" file a program file? It's just a simple 2-bytes prefix specifying the start address in little-Endian ("01 04
" for $0401).
Here's a hex-dump of the prg-file resulting from the assembly:
0000: 01 04 1F 04 0A 00 8F 20 50 45 52 53 4F 4E 41 4C ....... PERSONAL
0010: 20 43 4F 4D 50 55 54 45 52 20 53 50 41 43 45 00 COMPUTER SPACE.
0020: 3F 04 14 00 8F 20 54 52 41 4E 53 41 43 54 4F 52 ?.... TRANSACTOR
0030: 20 32 30 30 31 20 28 4E 4C 2C 32 30 31 37 29 00 2001 (NL,2017).
0040: 4A 04 1E 00 9E 20 31 31 30 35 00 00 00 00 00 00 J.... 1105......
0050: 00 00 20 58 04 EA 4C 54 04 A2 18 BD DC 04 85 24 .. X..LT.......$
0060: BD C3 04 85 25 A0 27 20 72 04 91 24 88 10 F8 CA ....%.' r..$....
0070: 10 E9 60 BD AA 04 F0 08 39 82 04 F0 03 A9 2E 60 ..`.....9......`
0080: A9 20 60 20 00 40 0A 08 01 82 00 00 00 00 00 40 . ` .@.........@
0090: 00 00 02 00 04 20 10 88 44 00 40 00 01 20 00 00 ..... ..D.@.. ..
00A0: 42 14 00 48 20 00 10 18 00 00 40 40 00 01 00 00 B..H .....@@....
00B0: 08 00 04 00 40 00 02 00 00 01 00 04 00 10 00 20 ....@..........
00C0: 00 01 00 80 80 80 80 80 80 80 80 81 81 81 81 81 ................
00D0: 81 82 82 82 82 82 82 82 83 83 83 83 83 00 28 50 ..............(P
00E0: 78 A0 C8 F0 18 40 68 90 B8 E0 08 30 58 80 A8 D0 x....@h....0X...
00F0: F8 20 48 70 98 C0 . Hp..
That's all for this episode, see you next time, stay tuned …
▶ Next: Episode 4: Alien Distractions
◀ Previous:  Episode 2: (Re)cherchez la Machine
▲ Back to the index.
April 2017, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2017/04. —