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.
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.
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
01value is a number, not an address.
We can indicate an address to the instruction using a single byte, as long as it’s on the zeroth page of memory:
LDA $01, shorthand for ‘load the value at address
$0001into 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
$1001into 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.
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
$40f0in memory in reverse, and that’s the address you get to jump to:
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 (
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
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 (
#$01. I cannot figure out why. If you do know the reason for my bug, then I’d love if you told me.