Jump to content

Karl's Blog

  • entries
    16
  • comments
    36
  • views
    3,992

2) Common Commands (Minikernel Developer's Guide)

Karl G

601 views

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).




6502 Registers

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

Some examples:

   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.





Arithmetic

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.


Addition examples:

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


Subtraction examples:


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


Multiplication examples:


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




Division Examples:


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:

   const noscore=1


Now, when we compile, we will see this:

blogentry-48311-0-07052200-1536276196_thumb.png

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:
blogentry-48311-0-58754400-1536277417_thumb.png

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.



0 Comments


Recommended Comments

There are no comments to display.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...