Retrochallenge 2017/04:
Personal Computer Space Transactor 2001

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 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 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:

Background graphics for Personal Computer Space Transactor 2001

The stars as displayed by our first program.

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 inexed 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):

Examples of basic instructions are:

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:

And the really fancy ones:

These two latter ones come especially handy for tables look-ups, but, Alas, there are only so many zero-page addresses, and most of them are already used by the operating system.

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:

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 Y-indexed scheme to store our value in the proper column (as in "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.

— This series is part of Retrochallenge 2017/04. —