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
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:
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
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.
Thanks for reading Cyanne’s Debug Room! Subscribe for free to receive new posts and support my work.
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
00s. 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. :)