ASM day 2: Getting up to speed by learning as much as I can
You're introducing me to Envee B'dizuk already..?
Welcome to day 2 of my personal path to learning assembly. As before I will be using the Easy6502 simulator found here, and this is also where you can follow along with the tutorial I’m studying.
Through my own curiosity and getting new things kindly taught to me, I’ve learned just a bit more about the basics.
How the program is read
I now know that the “PC” value I noticed in the debugger is the Program Counter register, counting up in 16 bits. This is what keeps track of what position the program is at in the machine code.
Going through my program with the Step button, I quickly noticed the counter does not increment one by one, or even consistently. The key that helped me understand this behavior is the Disassemble button. For the first example program it showed me the following:
Address Hexdump Disassembly
-------------------------------
$0600 a9 01 LDA #$01
$0602 8d 00 02 STA $0200
$0605 a9 05 LDA #$05
$0607 8d 01 02 STA $0201
I was taught that the first a9
in the “Hexdump” column is there to signal the LDA
instruction to the processor. The Program Counter increments, and then reads the value of 01 (the #$01
that the LDA was provided as an ‘argument’). That’s it for that line of assembly code, the PC
increments and then there’s the 8d
for the next STA
instruction, which continues to span over the next two bytes. Now it makes sense that the PC
doesn’t increase consistently: in the Debugger we see it stop after every line of assembly code, and that can be one or more bytes in machine language!
I deduced that the processor must know how many bytes should follow after that first number, the opcode. It’s also interesting to note that the opcode will change depending on what the argument tells it to do.
Address Hexdump Disassembly
-------------------------------
$0600 a9 10 LDA #$10
$0602 a5 10 LDA $10
Opcode a9
specifies that a literal value $10
should be loaded into A
, while opcode a5
tells the processor that register $10
’s content is to be loaded into A
. It might be the same LDA
instruction in assembly language, but it’s not the same opcode in machine language. When you consider that you’re literally telling it to do two different things, it does make sense.
All of these opcodes can be found in a supposedly logical order in a big table. It goes over my head right now, but it’s interesting! Masswerk’s page I found here seemed so useful that I quickly took a few screenshots and constructed a cheat sheet from it:
Brain Training
So I will not deny that all of this is a lot information for me to take in. However, I really want to have as much of this knowledge memorized as possible. Even if I don’t understand what it all means yet, I’d rather know it by heart than having to keep looking it up, especially when trying to fluently read and write code.
While I have struggled with memorization, what has worked best for me is something called a spaced repetition system, a clever way to use flash cards for any data you want to remember. Anki being the most used software for this.
I got to work and created a deck of flash cards with the instruction abbreviation as the question and the full name as the answer.
I’ve made this “6502 Instruction names” deck available here to import into Anki, if you are interested.
I tested myself on the entire deck of 68 cards (it also quizzes you on some registers and flag abbreviations), and just as I had hoped I already feel more confident. I got a slight feel for how things are called and what things might be possible.
Because we’re dealing with hexadecimal numbers a lot, I decided to memorize the 256 8-bit values in the same way, from hexadecimal to decimal and vice versa. This was a bit more taxing than the instruction names to be quizzed about, and all this calculating gave me flashbacks to DS Brain Training… But similarly here, I’m already more comfortable converting between “hex” and “dec” now.
Applying some new knowledge
I decided to spice up my previous little program a bit. Starting with adding comments, because I know that last day’s long list of instructions drawing the laser guns was not very kind to read. I happened to find out that the semicolon can be used to comment out any text, so let’s do something about this! I used BRK
(BReaK) statements for the first time to stop the program from running at certain points of interest. That way I could easily figure out what was happening there and add in a few comments.
Furthermore, thanks to the Disassembly option I now know which program addresses correspond with which lines in assembly code. And I really wanted to see if I could make the JMP
(JuMP) instruction work. Loops are important in pretty much any program! And without much trouble I added a JMP $09c0
at the end and it worked exactly as I thought it would. Why’s that you ask? It’s because rabbits know how to JMP
. ^-^
In Easy6502, the $fe
register returns a random value for each instruction. A random value equals a random color! So I figured out the position of the laser gun LEDs, and decided to make them throw an RGB rave party—
I won’t deny that I’m quite happy with this result! When there’s so little I can do as of now, small achievements count a lot for me.
The tutorial - Registers, flags, and weird names
It turns out the SR or Status Register is something the processor provides for you to read from, so that you can find out what certain effects an instructions may have had. The register uses 7 flags, each with its own bit. It’s also refered to as NV-BDIZC
. This is a jumble of letters representing each of the flag’s meanings.
Negative - oVerflow, Break - Decimal - Interrupt - Zero - Carry.
I tried to make this easier to remember. Hmm, NV-BDIZC
? Let’s pronounce it Envee B’dizuk, an odd name, but it actually made me able to recall the Negative oVerflow etc etc. line of bits in order! Mnemonics are powerful enough to be worth the embarrassing nonsense it makes you come up with.
The tutorial shows how the Carry flag is triggered once the Accumulator overflows past $ff
after an ADC (AdD with Carry) instruction. For people who like old games as I do, that overflow is pretty famous for causing all kinds of unintended results. I wouldn’t have guessed there is a flag to catch this type of error.
I did memory monitoring for the first time. I could see a value being put into a memory address amidst a sea of 00
s. It’s cool to me.
I got to play around with TAX (Transport A to X), TAY (Transport A to Y), TXA (Transport X to A) and TYA (Transport Y to A) instructions (I’ve already got these memorized c: ). I wrote this for the exercise:
LDA #$fa ; Load the hex value $c0 into the A register
TAX ; Transfer the value in the A register to X
INX ; Increment X register
TXA ; Transfer X register to Accumulator
ADC #$01 ; Add with Carry the Accumulator
TAY ; Transfer Accumulator to Y register
INY ; Increment Y register
TYA ; Transfer Y register to Accumulator
JMP $0602 ; Loop
It was fun to watch the register values and flags jump around in the Debugger, as pointless as the program itself is. It shows you how the Carry flag is only triggered when overflow happens with the ADC
instruction, not the increment instructions.
For some reason, doing any SBC (SuBtract with Carry) instruction triggers the Carry flag, and I do not understand why. I made a timer counting a register down to 0, where it then triggers the Zero flag. Next up is learning how to do something with that!
And that concludes this day. Thank you for being along for this journey with me. ^-^
There's another important detail about "Add with Carry" that I think the tutorial doesn't exactly mention: the carry flag is not just an *output*, set if overflow occurs, but it's also an *input*.
Imagine doing a sum like "183 + 17" on paper, rather than in your head.
- 3 + 7 is 10, so we write down 0 and carry the 1
- 8 + 1 is 9, but we also have to add the 1 we carried before, which is another 10, so we write 0 and carry another 1
- 1 + 0 is 1, but we also have to add the 1 we carried the second time, which is 2
- and so we get the correct answer, 200
"Add with Carry" adds the accumulator, the new values *and any previously carried 1*, clears the carry flag, then sets it again if the addition caused a new carry. That means you can use it to add 16, 24, 32 bit or even larger numbers as in the example above, treating each byte as if it were a single digit.
When you do long-hand subtraction, sometimes you have the opposite problem: you can't (for example) subtract 6 from 4, so you have to borrow a 1 from the next higher decimal place. That's why "Subtract with Carry" uses the carry flag, although I'm not exactly sure *how* it uses it - left as an exercise for the reader. :)