C64 assembly programming in 2020

Mostly for myself, since this machine is super well documented. I had some false starts, wanting to do everything the hard way, first wrote a text-editor in basic, on a C64, and then started writing an assembler, but realized that my basic skills are too weak and I gave up.. My plan had been to bootstrap everything, from editor, to assembler, to sprite editor, and THEN use those to program some simple game "because".. but, yeah, I don't have that kind of time anymore.

So instead, I'm going to learn how to program in a bit more modern environment, I'll try not to take TOO much advantage of the emulator capabilities, but, to be honest, having an Internet connection and targeting what is probably the most well-documented machine ever, EZ mode is perma-enabled.. I'd have liked to have the experience of figuring out this stuff "the authentic way" but how do you deprive yourself of just enough information while still making progress?

So for my learning experience, I'm choosing to make use of the documentation available, and to use a modern editor and graphics tools, and then cross-assemble and run in the emulator (EZ mode indeed)

Index

  1. Memory Map
  2. Instruction Set
  3. Development Environment
  4. Hello World 1.0
  5. Hello World 1.5
  6. Hello Numbers
  7. List of tools and references

Memory map

If you're used to modern development, think of the memory map as a kind of API overview, it shows a lot about what functionality is available in the machine, and gives a nice entrypoint for investigating further.

There are plenty of them, and they all seem to have their strength and weaknesses, I've not yet found "the perfect one" if it exists.. I'll maybe try to make my own. What I would really like, is a memory map where I can visually see which block a given address is inside, and how relatively big or small that block is, and what it is often used for by people who know what they're doing. If there is IO or roms, I'd like to see what they are. Ideally, if something is a pointer, for example, the "sprite pointers", I'd like to be able to expand and get some explaination of what the default values (if any) are, and how the pointers are used. If it's a register, I'd like to know what the bits do and how they're initially set.

By far the best memory map I've found so far is https://sta.c64.org/cbm64mem.html there is a nice graphic on C64-Wiki.com but it lacks some of the details.

Instruction set

So, the memory map is kind of this nice overview of what stuff is in the machine, but the instruction set is a listing of all the cool stuff you can actually write.

Development environment

I'm using Kick Assembler for cross-assembly. It's a java program and can be easily run from the command line. I chose to make a small script called "kickass" and place in my $PATH so that calling the assembler is as easy as possible.

kickass:
#!/bin/bash
java -jar /home/dusted/code/c64/kickass/KickAss.jar $@

And that's that, to assemble a file, I just do:

~$ kickass awesome.asm

For editor, I use vim with this kickassembler syntax file, there's also a kickassembler extension for Visual Studio Code.

I could assemble the code, copy the PRG file to my Ultimate64 or to a floppy or tape, but that takes a long time, and instead, I'll just use x64 from VICE, as it can even take the prg file as an argument:

~$ kickass awesome.asm && x64 awesome.prg

This tries to assemble the program, and if the assembler didn't return error, runs the program in VICE.

Hello World 1.0

Before writing hello world, you need to know a few things about the C64 specifically. First, the machine is not as primitive as the Atari2600, you don't need to control the raster beam directly to get something on the screen. The C64 has multiple screen modes, the default one is text mode, which is perfect for hello world. In text-mode, the VIC (Video Interface Chip) looks somewhere ($0400 to $07ff)) in memory for what characters to put on screen, somewhere else (the character ROM at $D000 to $DFFF) for the pixel pattern defining those characters and a third place ($D800 to $DBFF)for the color of those pixels for that character (probably not in that order, but that doesn't matter).

So, in order to put hello world on the screen, even before figuring out which instructions to use, you must consult the memory map, figure out the (default) memory location of the screen and of the colors. Note that at boot, the character colors for the blank part of the screen is actually set to the background color, so you can't even see the letters you're putting there without changing their color too (unless you change the background color). Below is a simple hello world program I wrote:

//world.asm
BasicUpstart2(start) // Macro that outputs a bit of BASIC program to SYS into our assembly code

// Define some constants
.const bgcolor = $D021
.const screen = $0400
.const color = $D800

// Store our program in "upper memory" above BASIC (there is a 4k unused block)
*= $C000
start:  // So far, no actual instructions/output has been produced (except for the BASIC program)
        // but this label gets the value of the next outputted byte, and that byte is going to
        // be located at $C000, so, this label gets $C000.

// Set background color black
lda #0          // Load 0 into register A (0 = black)
sta bgcolor     // Store value of register a (the 0) into the memory location 
                // that holds the background color ($D021)

// clear screen (set every character to a space)
ldx #0
    lda #' '
loop:       // There are 1000 bytes of characters
    sta screen,x
    sta screen+256,x
    sta screen+512,x
    sta screen+768-24,x// start this a bit earlier so we don't overwrite those 24 bytes not on screen
    inx
bne loop

// Print the string to the screen
ldx #0              // Let's use the X register as the index into our text string.

stringLoop:
    lda txt,x       // Load the character at txt+x bytes
    beq exit        // If lda set zero bit we loaded the "null" byte after the string and we exit.
    sta screen,x    // Else put it on the screen.
    lda #2          // Load 2 (the color red) into the A register
    sta color,x     // And put it at the corrosponding color-memory.
    inx             // Increase the index
    jmp stringLoop  // and repet

//return to basic
exit:
lda #1     // But first, move cursor to line 1 (the line after the one we wrote)
sta $00d6  // We just do this so the READY prompt appears after our own message..
rts

// Store a zero terminated string
txt:
    .text "hello world"
    .byte 0

    

And here's a hexdump of the resulting world.prg file:

00000000  01 08 0c 08 0a 00 9e 34  39 31 35 32 00 00 00 00  |.......49152....|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
0000b800  00 a9 00 8d 21 d0 a2 00  a9 20 9d 00 04 9d 00 05  |....!.... ......|
0000b810  9d 00 06 9d e8 06 e8 d0  f1 a2 00 bd 30 c0 f0 0c  |............0...|
0000b820  9d 00 04 a9 02 9d 00 d8  e8 4c 1a c0 a9 01 85 d6  |.........L......|
0000b830  60 08 05 0c 0c 0f 20 17  0f 12 0c 04 00           |`..... ......|

A closer examination of the bytes in the PRG file. Note that C64 uses little-endian byte-ordering.

 01 08 = $0801
When a prg file is loaded with ",1" at the end of the LOAD command ( load "world",8,1 )
the the first two bytes are used by the KERNAL as the first address of memory at which to place
the rest of the file. That means that the first two bytes are not descibing anything to be but
in memory, but rather, where to put the rest of the file. So, when loading this program, the
KERNAL will place the first byte ( 0c ) in memory location $0801, and the next byte ( 08 )
in location $0802, and so on. $0801 is the first usable byte of BASIC memory, so, the bytes
will be loaded into basic memory. That is because the BasicUpstart2 macro at the beginning of
our assembler listing, generated this load address and a little BASIC program for us, the content
of the BASIC program is: "10 SYS 49152"

Let's take a look at the next two bytes of our prg file,
these are the bytes loaded into $0801 and $0802.
0c 08 = $080c 
BASIC programs are stored in memory in the following format:
PP PP LL LL TT  NN     PP PP ...
  |     |    |  |        |
  |     |    |  |        +--------------------------------------------+
  |     |    |  |                                                     | 
  |     |    |  + Null byte, indicating end of basic line             |
  |     |    + Token/ASCII/Data, multiple tokens/data bytes allowed.  |
  |     + Line number (16 bit)                                        |
  + Memory address of the NEXT line (so, the byte right after NN) ----+

0c 08 is thus the pointer to the NEXT BASIC line after the one that comes now.
 
0a 00 is the BASIC line number for the lines that comes now ( $000a = 10 ).

9e is the BASIC token for the SYS command, so, in BASIC, the letters for each
command is not saved, but rather converted to and from single-byte tokens,
much faster and takes up a lot less memory and storage.

34  39 31 35 32 = The PETSCII string "49152", which is the argument to the SYS command.
It is the decimal notation for $c000, the address of our first
assembler instruction. Note that this is treated as TEXT and not an integer,
which is why it takes up so many bytes, and why it is not in little-endian order.

00 This is the terminator for the BASIC line.

00 00 This is the pointer to the next basic line, it having a value of zero means
end of BASIC program. These two zeroes, are what the pointer 0c 08 points to.
Try, from $0801, add the number of bytes of our basic program, you end up at $080c.

The following zeroes are simply padding until address $C000.
Note that the hexdump shows the first byte as being at b801, but recall that
the prg file was loaded into memory beginnning at $0801.
Well, the first two bytes of the file is not loaded into memory, so
if we do 0x0801 + 0x0b801 - 2 we get the value: 0xc000

Time has come for our assembler program, the first byte located at $0c000
thanks to being offset by all that zero padding.

a9 =  OPcode for immediate-addressing-mode LDA, produced by LDA #0
00 = The single-byte operand for that instruction.
8d = OPcode for absolute-addressing-mode STA
21 d0 = $d021 = The two-byte operand for that instruction.
You can do the rest if you want to.

Hello World 1.5

The next iteration of the hello-world program makes a routine out of the code that printed the characters. That was done by adding some code to calculate the offset into screen memory and a bit of code to handle taking "arguments" in different ways.

Useful subroutines often take one or more arguments, and there are different ways of provding those arguments, depending on whether the routine needs to be re-entrant. The print routine does not. So I use in-place modification of some of the parameters, this can be done by placing a label at the instruction, and jumping the over the instrunction (thus reaching the operand address) by adding 1 to the label address.

Find the relevant parts below: (or download full hello1_5.asm program here)

// Put our texts a few places on screen
lda #txt
sta strPtr+2

lda #1
sta col+1
lda #0
ldy #0
jsr puts

lda #2
sta col+1
lda #10
ldy #8
jsr puts

lda #3
sta col+1
lda #17
ldy #15
jsr puts


lda #txt2
sta strPtr+2
lda #4
sta col+1
lda #20
ldy #24
jsr puts

// exit to basic, for example
rts

// puts: Print a string to the screen
// strPtr+1 = 16 bit pointer to the string to show
// col +1 = the color
// a = column, y = row

puts:                     // First, calculate offsets into screen and color memory
            sta scrOffs+1 // screen lowbyte is 00, so we just put the column directly.
            sta colOffs+1 // same goes for color lowbyte
            lda #>screen
            sta scrOffs+2
            lda #>color
            sta colOffs+2

            iny // add one to y so we can have y = 0 on first iteration.
  rloop:    dey
            beq putnext // skip when y reaches zero

            lda #40 // there are 40 columns to a row
            adc scrOffs+1
            clc // Clear the carry flag for the next add instruction.
            sta scrOffs+1
            lda #40
            adc colOffs+1
            sta colOffs+1
            bcc rloop     // if the carry bit was not set, just loop more
            clc           // if it was set, clear it and handle high-bytes
            inc scrOffs+2
            inc colOffs+2
            jmp rloop

  putnext:  ldx #0        // Let's use the X register as the index into our text string.

  strPtr:   lda $ffff,x   // Load the character at txt+x bytes
            beq exit      // If lda set zero bit we loaded the "null" byte after the string and we exit.
  scrOffs:  sta $ffff,x   // Else put it on the screen.
  col:      lda #00       // Load 2 (the color red) into the A register
  colOffs:  sta $ffff,x   // And put it at the corrosponding color-memory.
            inx           // Increase the index
            jmp strPtr    // and repeat
  exit:     rts



    

Hello Numbers!

I spent a fair deal of time wrapping my head around how to implement a simple "itoa" (integer to string) routine, and finally decided that for my uses, dealing with binary coded decimal is waaaahy easier.. I'm pretty satisfied that I was able to come up with these small routines myself, after failing to get the math behind the ones I've read online. Sure, they have disadvantages, and they probably run slow, but they're mine and I understand how they work. Integrating them into the hello-world program, is left as an exercise for the reader. I've not implemented a nice way of passing parameters to them yet, I will have to read up on zero-page addressing modes for that I'm afraid.


// Convert the number in the 10 bcd bytes to a string

// What's neat about decimal mode, is that instead of a byte holding from 0 to 255
// It holds from 0-99, and the way the bits are packed, is that the first
// nibble is the "tens" and the last nibble is the "ones"
// So the byte 0101 0011 in hex is $53 right, the decimal value when
// interpreting the number as binary, is 83, but look at those nibbles!
// 0101 = 5, and 0011 = 3.. so if we interpret it as BCD, the decimal value
// is actually 53, just as the hex representation.

// So now, to convert to strings, we simply go through the bytes in our decimal
// number one nibble at a time, and emit a character code for each. And find the
// character codes is easy as well:
// The reason we can OR with '$30' is because the last 4 bytes
// of the character code is zero, and the BCD number is 4 bytes long, so it
// "just so happens" to fit there. Otherwise, we could have added instead.
// The logic is: $30 = 0011 0000 = '0' so if we had.. A nibble of decimal value 5
// then we'd have for example a byte '0000 0101' in binary, if we OR those values:
// 0011 0000 O
// 0000 0101 R
// ---------
// 0011 0101 = 35 = '5'

// But wait! There are two decimal numbers in each byte?
// Easy, we just take them one at a time. First we mask out the high nibble,
// and emit our character code for that number, then we mask out the low nibble
// and shift the remainding bits down into the low nibble and use the same method
// to emit the next byte.
// Just note that the bytes come out "in reverse", so we store them from
// the end of the string towards the start, so it comes out in the right order.

bcdtos:
    ldx #$00        // Counter into the bcd number
    ldy #19         // counter into the string, starting from rightmost byte
bcdtosLoop:
    lda score,x     // Load a byte from the number
    and #$0f        // Keep only last 4 bits
    ora #$30        // OR them with the character code for '0'
    sta scoreStr,y  // Store the character in the string
    dey             // Move one left in the string
    lda score,x     // Load the same byte again
    and #$f0        // Keep only the first 4 bits
    ror             // Rotate those bits so they are the last 4 bits.
    ror
    ror
    ror
    ora #$30        // OR with the '0' character again.
    sta scoreStr,y  // and store it in the next location
    dey             // and move one more to the left in the string
    inx             // Next byte in the BCD number
    cpx #10         // Check if that was the last (our number is 10 bytes)
    bne bcdtosLoop  // if now, repeat.
    rts


// Add number (from 0 to 99) to the 10 byte bcd number.
// Note, to add the decimal number 53, do lda #$53 and then jsr bcdAdd
// Parameter: A - Number to add
bcdAdd:
    sei               // Disable interrupts, it may be bad when in BCD mode
    ldx #0            // Start from first byte in number (little endian
    sed               // Set Decimal mode
bcdCarryLoop:
    clc               // Clear the carry
    adc score,x       // Add whatever was in A with the byte
    sta score,x       // Store the result
    bcc bcdAddEnd     // If no carry, we're done.
    lda #1            // Otherwise, we'll add 1 to the NEXT byte
    inx               // So we target the next byte
    jmp bcdCarryLoop  // to add it (it's in A, remember!)
bcdAddEnd:            // When we're done
    cld               // Clear Decimal mode
    cli               // Enable interrupts
    rts


// Strips leading zeroes from the string
stripZero:
    ldx #0          // Start from first byte (leftmost) in string
stripZLoop:
    lda scoreStr,x  // Load that byte
    cmp #'0'        // See if it's the 0 character
    bne stripZEnd   // If it is NOT a zero, we're done
    lda #' '        // Else, a space is
    sta scoreStr,x  // saved instead.
    inx             // Target next byte
    jmp stripZLoop
stripZEnd:
    rts


// In the "constants" part of the program, which I chose to be after all the
// instructions, I added two labels, one for the destination string, and one
// for storing a giant bcd number.
scoreStr:
    .text "                                        "
    .byte 0
score:
    .byte $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 // 10 byte little endian bcd number,
                                                           // Max = 99999999999999999999.



    

Stay tuned for Hello Sprite...

Tools and references