ASM day 4: Grappling with addressing modes
And rewriting my very first program to be much smarter
Welcome back to day 4 of my classic development journey, where for the moment I’m following the Easy6502 tutorial that you may find here.
I’ve been memorizing the 6502 processor’s table of opcodes before I even knew what any of it meant, but straight away I noticed the instructions are duplicated many times over. By now, I know that these are the different addressing mode flavors that each of the instructions come in.
An addressing mode is one of the various ways in which you can use an instruction, either by giving it a byte to handle directly, or a memory address, or perhaps a memory address with an offset. It kind of reminds me of how in normal programming a function can have multiple arguments, but here it is within a much more limited structure.
We have many of these modes to learn: implied, absolute (X/Y-indexed), zeropage (X/Y-indexed), immediate, relative, X-indexed indirect and Indirect, Y-indexed. This was a lot to take in for me, and just trying to understand it and taking my notes took up most of the time I had planned for the day. Hence, I’m not writing this on the same day I did these exercises.
Here are the notes I took:
Figuring out what modes do in practice
So because this amount of information is hard to summarize, I will focus on how these various addressing modes may be used in practice.
Implied mode
The most simple mode: we do not need to specify anything for this instruction, as it knows what to do by just by calling its name. Say:INX
, or “Increment X”, and nothing more is needed.Immediate mode
For when we want to pass a single byte (an 8-bit, 0-255 value) to the instruction. Say:LDA #$01
. We’ve loaded the number 1 into the Accumulator. As we’ve learned before, the#
indicates the01
value is a number, not an address.Zeropage mode
We can indicate an address to the instruction using a single byte, as long as it’s on the zeroth page of memory:$0000
to$00ff
. Say:LDA $01
, shorthand for ‘load the value at address$0001
into the Accumulator’.This makes the first 256 positions in memory kind of special. I’ve heard the zeropaged addresses may be treated as second rank registers in addition to X and Y.
Zeropage X/Y-indexed modes
This is how we can finally state: ‘read or write from “this + X” or “that + Y” address’. Following with:Absolute, and Absolute X/Y-indexed modes
We can pass the instruction a full address (say:JMP $1234
), or offset it by X or Y. Say:STA $1000,X
. If X is 1, this means ‘store the value at$1001
into the Accumulator’.
We now know enough to be able to fill the screen with random colors, which is something we previously couldn’t do!
LDA #$00 ; Immediate mode: Initialize A
LDX #$00 ; Immediate mode: Initialize X, the screen position
loop:
LDA $fe ; Zeropage mode: Load a value from the randomizer register
STA $0200,X ; Zeropage X-indexed mode: Write to an offset screen position
INX ; Implied mode: Increment the screen position
JMP loop ; Absolute mode: the 'loop' label is actually a full address.
Relative mode
This is what Branching instructions use to jump a relative amount of bytes through the program. Once again it’s only an 8-bit value, so that’s why it can’t jump too far back or ahead.
And finally, that leaves us with the indirect modes:
Indirect mode (vanilla)
We point the instruction to a full address, which at that position should contain the actual address of interest for the instruction to act on.
Because of how the 6502 reads values of two bytes in reverse (a fact we just have to accept), it reads the second byte as the first part and the first byte as the second part.
Let’s visualize what “JMP (40f0)”
does. The parentheses indicate that it’s indirectly addressed. As you can see below, it starts to read the value starting at$40f0
in memory in reverse, and that’s the address you get to jump to:$cc01
.
MEMORY 40f0 40f1 40f2 40f3
VALUE 01 cc 00 00
\ /
/ \
JMP (40f0) -> $cc 01
X-indexed indirect mode
This is the same as before, but it starts reading from the specified address offset by X. Also, we can now only read from zeropaged addresses.
Indirect mode, Y-indexed
It starts reading from the zeropaged address we hand the instruction, then finds the register of interest and adds Y to it.
As this is going on for long enough, I will leave writing a program with Indirect mode for a later day.
Rewriting my first program to be smarter!
Not long ago I wrote this simple program only by loading values and storing them back into screen memory by hand. It’s simple, and actually very efficient. But now instead of simply listing out the pixels, our mission is to create these laser gun images again but through logic.
Let’s start by storing the laser gun graphics into memory so we’re able to reuse it, kind of like a sprite. The guns aren’t only drawn four times over, but many parts of it are repeated. Therefore we can use the familiar LDA and STA commands to store the image’s 4 unique rows neatly in memory:
0000: 00 06 0e 03 01 03 0e 06 00 00 06 0e 01 0e 06 00
0010: 00 00 0b 0b 0c 0c 0f 0f 00 0b 0a 0c 0c 0f 0f 0c
We then jot down some logic to make these lines appear back on the screen in the most simple fashion. We use X to keep track of the memory position we read the color at, and Y to keep track of the screen position we write our color to. We then repeat through this code using a loop, and read and write at incrementing positions through zeropage memory offsets.
Now I tried to write each 8 pixels of the image on its own line, by adding the screen width of #$20
(32 pixels) to Y after 8 pixels are drawn.
Oops, I didn’t think the addition through… We were already 8 pixels in and trigger the Branch at 9, so we have to add the whole screen’s width minus 9 to Y instead.
There, we successfully wrote the graphics data stored in memory to the screen in its compressed state. We now have to write logic to repeat lines, and elongate the image into its original shape.
Since the X and Y registers are already taken now, we’re going to have to use page zero to store more variables than just those two. When we do some operations, we’ll have to back up our Y register temporarily. I really wanted to prevent this, but I could not think of a way to use less variables than this.
We will have an area in memory starting at $20
that indicates where every line needs to start reading in the sprite data area. By pointing to a position in the sprite data, each line is simple to repeat and define. If we say for line 1 to 16 you start reading at 0, where the blue laser beam is, then we now drawn 4 long laser beam images. In effect, this is kind of like a tile map. This data looks as follows:
0020: 00 60 68 00 00 60 68 68 00 60 68 00 00 60 68 60
0030: 08 18 70 18 10 10 10 10 10 10 10 10 10 10 10 10
0040: ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Then, for our new zeropage area variables:
$50
will count at which line we are at.$52
will count at which line we are at.$61
will be where we backup Y.
In a loop, we keep reading colors from a zeropaged offset address through LDA $00,X
, and keep storing them into screen memory with an absolute offset address through STA $0200,Y
, because $0200
is where the screen memory starts.
We branch to the AddLine
label when we’ve completed 8 pixels (LDA $52
, CMP #$09
), and increment the line order and line progress counters. We look back into line order memory ($20
+) to see at what position in the graphics memory we should start reading the colors for the next line, which we set X to. If our line order starting position returns $ff
, then the program knows to branch to the exit
label and BRK
.
We have to juggle some variables around, and I had to do a lot of reasoning in my head when the program just didn’t draw anything coherent. Luckily, I found out that the Debugger can jump to any label, so I could step through specific parts without having to step through the whole program.
And things started to take shape:
And there we are back to the original result from Day 1, but this time drawn with more complicated logic!
Actually, it’s missing the differently colored LEDs, but instead of storing unique graphics lines for that, let’s one-up our old result and make the laser beams different colors instead.
We don’t have enough space to fit all the new colored graphics into memory, so we cheat and continue our sprite area from address $60
onwards. I pick some nice colors so we now have red and green laser beams too!
The program can draw each beam differently by simply changing where each line should start in graphics memory.
LDA #$00 ; Gun 1 Blue Laser Upper Row
STA $20
LDA #$60 ; Gun 2 Red Laser Upper Row
STA $21
LDA #$68 ; Gun 3 Green Laser Upper Row
STA $22
LDA #$00 ; Gun 4 Blue Laser Upper Row
STA $23
It’s quite easy to do this now: we just change each of the lines we want, and leave the “body” of the guns alone to draw the same lines from graphics memory for all 4 guns.
So finally, for the grand result:
And that brings us to the end! There are still some loose ends regarding addressing modes.1 I need to consider how to deal with these topics that want to take up more than a single day. There’s much to learn for me, which is most definitely the point of everything I’m doing here.
Thanks a lot for reading this day’s post, and see you in the next one. ^-^
The final program contains a bug in its logic, and it only works when I initialize the line order counter ($50
) as #$01
. I cannot figure out why. If you do know the reason for my bug, then I’d love if you told me.
I spent some time looking into your logic bug, but I couldn't figure out exactly how it worked, since I got confused by the way you were shuffling values between registers and memory. I wondered how the code would look if it worked in the other direction - keeping values in memory and only loading them into registers when necessary. This is what I came up with (excluding the tile map and tile data, which is the same as your code):
; Let $50+51 be our destination in VRAM
; since VRAM is outside the zero-page,
; we need two bytes
LDA #$00
STA $50
LDA #$02
STA $51
; Let $52 be our source in the tile-map
LDA #$20
STA $52
; The tile-map is always in the zero-page,
; but let's use a 16-bit address
; so we can use indirect addressing
LDA #$00
STA $53
draw_tile_loop:
; Each tile starts at the first pixel
LDY #$00
; Read the value pointed to by $0052
; into the accumulator.
; Without a "vanilla indirect" variant of LDA,
; we'll use "indirect, Y-indexed"
; since we just set Y to 0.
LDA ($52),Y
; If the tile-start is $FF...
CMP #$FF
; ...then we're at the end of the tile map
BEQ all_done
; Otherwise, let $54 be our source in the tile
STA $54
; Keep $55 as 00, so we can use indirect mode
LDA #$00
STA $55
draw_tile_data_loop:
; Copy the pixel from the tile data to VRAM
LDA ($54),Y
STA ($50),Y
; Prepare to copy the next pixel
INY
; Have we drawn all 8 pixels of this tile?
CPY #$08
; If not, go back and do the next one.
BNE draw_tile_data_loop
; Otherwise, we've done this tile,
; let's move to the next tile in the tile map
; We know the tile map is all in zero-page,
; so we can get away with INC.
INC $52
; Let's move to the next tile in VRAM too
; Since this is outside zero-page,
; we need to do a full 16-bit addition with carry
CLC
LDA $50
ADC #$08 ; 8 pixels in a tile!
STA $50
LDA $51
ADC #$00 ; propagate the carry
STA $51
JMP draw_tile_loop
all_done:
BRK
As a bonus, it demonstrates the "indirect Y-indexed" mode! This version might be considered a little more wasteful, since it spends bytes of the zero-page to store values that will always be 0, and because it uses "LDA ($52),Y" to emulate the missing "indirect zero-page" mode. It works, though, and the inner pixel-copying loop ("draw_tile_data_loop") is quite tidy, I think!