6502 assembly code may look a bit intimidating for those who are unfamiliar, with all of the cryptic-seeming three-letter commands (opcodes). In reality, the language is pretty simple, if a bit tedious in places. There is no need to memorize the whole list of commands - you will use a subset of the available commands most frequently, and you can look up the others as you need them.
Many people recommend the Easy 6502 tutorial to learn the language. It's a well-done tutorial with a built-in assembler and debugger, but I personally preferred learning by doing actual Atari 2600 programming. That is the approach I am taking here. Feel free to check out that tutorial and learn from that instead if you wish, but come back here and go to "Putting it All Together" later in this lesson.
Note - Throughout this lesson, I show the same operations being performed with batari Basic vs assembly language. Another way to see examples of this would be to compile a bB program with the code in question, and then look at the assembly language file that bB generates (e.g. game.bas.asm).
There are 3 registers that you need to know about at the moment. They hold one byte of data each, but they are part of the CPU itself, and not part of RAM. They are used to fetch and store data to and from RAM, as counters for loops, and to store offsets to memory when accessing data, among other functions. These registers are:
A - The A register is also often called the accumulator, and is where arithmetic and logic operations are performed.
X and Y - These are known as the index registers, and are used for counters for loops and to keep track of memory offsets when accessing data.
Loading and Storing Values
Each of the 3 registers, A, X, and Y, may be used to load values and to store them in memory. Which register you choose will depend on what you have free at the time. For our purposes at the moment, these 3 commands are equivalent:
LDA - Load a value into the A register
LDX - Load a value into the X register
LDY - Load a value into the Y register
You can load a number with these commands, in which case you will use the # symbol before the number to tell the assembler that you mean a literal number and not a memory address. You can also load a number via a defined constant name, either defined in your bB program, or defined in your assembly source. Finally, you can also load a value from a variable using the variable name. In this case you do not use the # symbol before the variable name. Some examples:
lda #20 ; Load the number 20 into register A ldx #$20 ; Load the number 32 ($20 in hex notation) into register X ldy #myconst ; Load the value of the defined constant "myconst" into register Y lda temp1 ; Load the value stored in the variable temp1 into register A
Each of these 3 registers has an associated store command as well that can be used to store the contents of that register into memory. Note that all variables that are available in your bB program are accessible via assembly, too:
STA - Store the value in the A register into the specified memory location
STX - Store the value in the X register into the specified memory location
STY - Store the value in the Y register into the specified memory location
sta temp1 ; Store the contents of A in the temp1 variable stx temp2 ; Store the contents of X in the temp2 variable sty myvar ; Store the contents of Y in the myvar variable
Comparison to batari Basic
Here are some simple operations in bB, followed by how the same would be done in assembler using what we have learned so far.
batari Basic code:
temp1 = 5 temp2 = myconst myvar = temp3
Equivalent assembly code:
lda #5 sta temp1 lda #myconst sta temp2 lda temp3 sta myvar
Note that I used the A register for my assembly example here, but I could have just as easily used the X or Y registers by replacing LDA and STA with LDX and STX, or LDY and STY.
Warning - It is a common newbie mistake to leave off the "#" before loading actual numbers. Without the "#" the assembler interprets the number as a memory address. For example, lda #$91 would load the value of missile0y into the A register instead of the number $91.
I will just cover the simplest cases for now: adding and subtracting single-byte unsigned values, and multiplying and dividing by powers of two only. All of these are performed on the contents of the accumulator (A register). Here are the relevant commands:
ADC (ADd with Carry): Perform addition
SBC (SuBtract with Carry): Perform subtraction
ASL (Arithmetic Shift Left): Shift bits to the left, effectively multiplying by 2
LSR (Logical Shift Right): Shift bits to the right, effectively dividing by 2
SEC (SEt Carry): Sets the carry flag
CLC (CLear Carry): Clears the carry flag
The carry flag is useful for 16-bit arithmetic operations, which I am not covering here, but it also needs to be cleared before an ADC (addition) operation, and set before a SBC (subtraction) operation, or the result could be incorrect.
Tip - If you need a way to remember clearing carry before addition and setting carry before subtraction, then think of it this way: you clear the carry before adding to have a place to put any carry from the addition operation, and you fill it before subtraction to give a place to borrow from for the subtraction operation.
Add 14 and 27:
lda #14 clc adc #27
Add the value of the "length" constant to $17 (hex):
lda #$17 clc adc #length
Add the value of temp2 to the value of temp1:
lda temp1 clc adc temp2 sta temp1
Subtract 9 from 125:
lda #125 sec sbc #9
Subtract $10 (hex) from the value of the "ticks" constant:
lda #ticks sec sbc #$10
Subtract the value of temp3 from temp4:
lda temp4 sec sbc temp3 sta temp4
Multiply 33 by 2:
lda #33 asl
Multiply the value of the "starting_lives" constant by 4:
lda #starting_lives asl asl
Multiply the value of temp1 by 16:
lda temp1 asl asl asl asl sta temp1
Divide 132 by 2:
lda #132 lsr
Divide the value of the "cities" constant by 8:
lda #cities lsr lsr lsr
Divide the value of temp6 by 32:
lda temp6 lsr lsr lsr lsr lsr sta temp6
More Comparison to batari Basic
Here's an example of arithmetic operations in bB compared to how we would perform the same in assembly language:
batari Basic code:
temp1 = 8 + 14 temp2 = temp3 - 8 temp4 = temp4 * 2 temp5 = temp6 / 4
Equivalent assembly code:
lda #8 clc adc #14 sta temp1 lda temp3 sec sbc #8 sta temp2 lda temp4 asl sta temp4 lda temp6 lsr lsr sta temp5
Incrementing, Decrementing, and Looping
The X and Y registers can be easily incremented or decremented by 1 with the following commands:
INX - Increment X by 1
INY - Increment Y by 1
DEX - Decrement X by 1
DEY - Decrement Y by 1
Additionally, a variable may be incremented or decremented by 1 directly with the following commands:
INC - Increment variable by 1
DEC - Decrement variable by 1
As previously mentioned, since the X and Y registers can be easily incrememented and decremented, they are often used as counters for loops. Loops in 6502 assembly usually count down instead of up, since flags get set if the value reaches 0, or if it drops below 0, allowing us to loop on those conditions. Common commands for this include:
BNE - (Branch if Not Equal): branch to the specified address if the zero flag is not set
BPL - (Branch on PLus): branch to the specified address if the negative flag is not set
Our simple minikernel uses a loop in this way:
ldx #14 ; Load the number 14 into Xminikernelloop sta WSYNC stx COLUBK dex ; Decrement X by 1 bne minikernelloop ; Branch back to minikernelloop of X is not 0 rts
Even More Comparison to batari Basic
As before, I'll show a comparison to how the same operations would be performed in bB vs assembly language:
batari Basic code:
temp1 = temp1 + 1 temp2 = 10myloop temp2 = temp2 - 1 if temp2 <> 0 then goto myloop
Since registers aren't used directly in bB code, the following isn't exactly equivalent, since we would probably use registers for looping instead of a temp variable:
inc temp1 ldx #10myloop dex bne myloop
However, if we did not have a free register to use, we could use temp variables instead. This example is fully equivalent to the batari Basic code example:
inc temp1 lda #10 sta temp2myloop dec temp2 bne myloop
Putting it All Together
Now that we have gone over some common commands, it should be easier to understand our minikernel code as it exists now, and modify it a bit.
Right now, we have:
minikernel ldx #14minikernelloop sta WSYNC stx COLUBK dex bne minikernelloop rts
We load 14 into the X register, wait for a new scanline, store the current value of X into COLUBK, decrement X, then loop until X is 0. Based on NTSC color values, this gives a gray gradient bar. How about we make a bar that shows all 16 hues at their brightest instead?
First, just so we don't make our screen too big with the extra lines, let's get rid of the score portion of our game. This will get rid of the pips for lives, but we will replace these with graphics in our minikernel soon, anyway. At the top of cannons.bas, add the following line:
Now, when we compile, we will see this:
Now, how to modify our code? Let's change ldx #14 to ldx #16 since we will be displaying 16 different color lines.
Also, we won't be storing X in COLUBK anymore, so we will need to use another register. Looking at the color chart, value 15 is bright white. Bright yellow is 31, bright orange is 47, etc., with each being 16 apart. We can therefore add 16 on each line, and store the new value in COLUBK.
Since we are talking about doing addition, we will need to use the accumulator (register A).
We first load 15 into the accumulator before the loop, and we add 16 to on each loop iteration:
minikernel ldx #16 ; 16 loop iterations lda #15 ; Start with bright white color valueminikernelloop sta WSYNC sta COLUBK clc ; Clear the carry flag before addition adc #16 ; Add 16 to jump to next color dex bne minikernelloop rts
Now let's see how that looks:
Very pretty! It kind of makes me want to replace the cannons with unicorns. :-)
Summary and Next Lesson
We have covered some of the most common assembler commands that you will use when making minikernels. A 6502 assembly language tutorial exists for those who wish to learn that way. The language itself is not difficult, and there is no need to memorize the whole list. Here are two assembly language command reference pages that I use:
In the next lesson, we will cover pointers to data, and drawing player graphics in our minikernel.