(Updated January 4, 2025)
Table of Contents
Moving Data
While this is a 16-bit addressable space, we can only perform operations 8 bits at a time because our registers are also 8-bit wide. As such, working with 16-bit or 32-bit quantities means some code repetition.
Let’s begin with some of the basics we’ve seen in other chapters. We’ll start by making sure we understand the differences in values.
LDA #$20
STA $21
The code above loads the accumulator with the value $20 (32 decimal) and stores it at a memory location of $21, which is also a zero-page location. The hash symbol (#) indicates that we mean the literal value expressed.
LDA $20
STA $21
The code above will move data between two memory locations. We load the accumulator with the value in memory location $20 and store it in $21. Again, these are both zero-page locations.
We can accomplish similar moves with the X
and Y
registers.
LDX #$20
STX $A000
;...
LDY $4000
STY $5000
The above uses the X
register to store the value $20 in memory location $A000, followed by using the Y
register to get a value from address $4000 and store it in $5000.
We can also move data between registers. The following will set all three registers to zero.
LDA #0
TAX
TAY
The TAX
and TAY
instructions transfer data from the accumulator to the specified index register. There are also TXA
and TYA
that move from the index registers into the accumulator.
There is no direct transfer from X
to Y
or Y
to X
. Instead, we would do the following.
TXA
TAY
Simple Math
One thing to keep in mind when using the 6502 is that it only provides instructions for addition and subtraction. There are no instructions for multiplication, division, or modulo. These operations are accomplished through repeated addition, subtraction, or bit shifting as appropriate.
Oh, one other thing: these operations only happen within the accumulator.
Mathematics on CPUs requires us to note the current state of the CPU just as much as we would the results of operations. Mathematical operations affect several of the CPU’s flags. But so does a seemingly innocuous action like loading the accumulator. Remember that these are just single bits, either 1 (set) or 0 (clear).
N - Negative - Set if the operation resulted in bit 7 being 1. Z - Zero - Set if the operation result was 0. C - Carry - Set if the operation needed a ninth bit. V - oVerflow - Set if the operation on two positive numbers was negative or the operation on two negative numbers was positive.
The zero flag is pretty easy to understand. This is true for the negative flag, but let’s take a quick peek at both.
LDA #0 ; Z is set
LDA #45 ; Z is clear
;...
LDA #45 ; N is clear
LDA #$80 ; N is set (10000000 is -128)
When adding and subtracting, we must consider the carry and borrow, respectively. All math on the 6502 involves the carry. This is due to the fact there there is only the ADC
instruction (and SBC
, for subtraction).
When we add, we start by clearing the carry. If the addition needs a ninth bit, the carry will be set. When subtracting, we set the carry. If the subtraction needs to borrow, the carry will be clear. This may not seem important now since we only deal with 8-bit math. But these details are essential when we expand to 16-bit and 32-bit values since the carry will help ensure our larger numbers perform carry and borrow across byte boundaries.
So, let’s look at the general setup for adding and subtracting.
LDA #$a0 ; 160 (-96)
CLC
ADC #$70 ; 112
; ...
LDA #$70 ; 112
SEC
SBC #$a0 ; 160 (-96)
When we do the add, the bits are set up like so:
C Accumulator 11 <---- Carry from addition 0 10100000 160 (-96) +01110000 112 -------- 1 00010000 272 (16)
One way to look at this is that the unsigned addition is 160 plus 112. The sum is 272, but since this exceeds an 8-bit result, we are left with 16 in the accumulator. The carry is set to represent the overflow into a ninth bit that would represent 256.
Another way to look at it is a signed addition. So, -96 plus 112 is 16. The carry is still set because the result needs 9 bits. But since this is signed math anyway, we don't much care about the carry as the result is correct.
Let's move on to subtraction. This one may take a moment to understand. Remember how borrowing works? You take one from the column to the left and place it alongside the digit you're working on within the current column. If we try to subtract 8 from 3, we must borrow from the next column to subtract 8 from 13. Like this:
3 13435 - 8 1 ------- 3 5 4
We use carry to do the same thing - that's our ability to borrow. Now, let's look at the subtraction. This example will reveal two specific details of the carry and the overflow flags.
C Accumulator 1 01110000 112 -10100000 160 (-96) -------- 0 11010000 208 (-48)
The most significant bit in the result is 1. So we know it's negative, and N
is set. The carry is also clear, which means we borrowed. One thing that may be difficult to see is that we subtracted 1 from 0 and got 1. But remember that we borrowed from the carry. So, we subtracted 1 from 10 (which is 2 in binary) and got 1. And that's why the carry is clear.
Another thing that may be difficult to see at first is how the unsigned result is 208. How do you subtract 160 from 122 to get 208? Well, we didn't, actually. We subtracted 160 from 368 (256 + 112). The carry acts as an extra bit for the top number, so it's bigger by 256.
If you want to see it another way, it's 112 plus 96 in signed arithmetic because it's 112 minus -96, which is still 208 but too large to represent in 8 bits.
Either way, we've overflowed.
Overflow
SEC
, CLC
, ADC
, SBC
, and EOR
are involved in math operations.
If the bit discussion on the overflow flag is too much to grasp initially, note the instructions and move on to the next section. These details will be made clearer as you proceed.
Much has been written about how overflow works, some of which is incorrect. Let's start by repeating that this chapter will cover the basics. As such, we will try to explain the basics of overflow. Everything said here is for 8-bit operations.
Simply put, overflow indicates that the result did not fit into the signed byte range -128 to 127. In other words, if the result is less than -128 or greater than 127, we've overflowed.
Another way to say it is an overflow happens when the carry from bit 6 differs from the carry from bit 7.
Addition
Our first case concerns addition. If two large positive numbers or two large negative numbers are added together, we will overflow. What constitutes large? If the sum of two positive numbers is greater than 127 or the sum of two negative numbers is less than -128, it is large.
C Accumulator C Accumulator C Accumulator C Accumulator 76543210 76543210 76543210 76543210 1 1 0 0 01010000 (80) 11000100 (-60) 01010000 (80) 11000000 (-64) -01010000 (80) +11011000 (-40) +10010000 (-112) +10110000 (-80) -------- -------- -------- -------- 0 10100000 (-96) 1 10011100 (-100) 0 11100000 (-32) 1 01110000 (112) OVERFLOW! OVERFLOW!
Our original claim is that two large positive numbers or two large negative numbers will overflow. This seems fairly straightforward. Ultimately, our example's first and last sums clearly show that we've overflowed. 80 + 80 is 160 but exceeds 127. Then there's -64 + -80, which is -144 but exceeds -128.
We've added one specific carry to the picture. The additional bit shows the carry from adding bit 6 from each number. These are used as indicators of overflow. If the overall carry (C) differs from the carry of bit 6, an overflow occurs. Using the first and last again, we see that those carries differ.
The starting carries for sums 2 and 3 are the same. The second sets the carry flag, while the third does not. We don't care since this is a throwaway in an 8-bit operation. The result for the signed addition is still correct.
Subtraction
Subtraction is a little more complicated, but we'll keep it simple. The same basic rule applies. An overflow happens when the carry from bit 6 differs from the carry from bit 7. But we're not going to subtract. We will still be adding, but cleverly implemented in the hardware.
Let's start by saying that subtracting positive from positive or negative from negative is guaranteed not to overflow. So we will set those aside.
Subtraction is also the adding of the negation of the subtrahend. 45 45 minuend -63 +(-63) -subtrahend -- ----- ---------- -18 -18 difference
Remember that we clear the carry for addition. Any resulting carry can be used in subsequent byte adds. But we clear it first because we are always adding with carry (ADC
), and missing that step could lead to addition errors.
Now, recall that the two's complement of any number inverts the sign. This is done by inverting all the bits (one's complement) and adding one. Our SBC
will take the value and perform a one's complement, add it to the accumulator, including the current carry, and the accumulator will have the result with a new carry. The setting of the carry is our plus one to the one's complement.
In the next example, we start with our standard subtraction setup, then convert it to addition.
C Accumulator 00101101 (45) -00111111 (63) -------- Instead of subtracting, we'll do addition. C Accumulator 1 the current carry 00101101 (45) +11000000 (-64) one's complement of subtrahend -------- 0 11101110 (-18)
Remember that we are not performing a full two's complement of the subtrahend because we are setting the carry. The above would be written as follows:
LDA #45
SEC
SBC #63
This behaves like the following:
LDA #63
EOR #$ff ; flip all the bits!
SEC ; "add" one
ADC #45
Yes, this example reverses the order, but the addition is commutative. The intent is to show the operation with minimal steps.
The EOR
performs an exclusive or. The exclusive or with 255 (all bits on) effectively flips all the bits. Exclusive or is only true when one bit or the other is true. The result is false (0) if both are true or false.
00111111 (63) EOR 11111111 (255) -------- 11000000
Using the same rules as addition, we present 4 cases where 2 overflow and two do not. Remember that we're rewriting with the one's complement of the subtrahend to perform addition with the carry set. So, look for the bit 7 and bit 6 carries to differ when overflowing.
C Accumulator C Accumulator C Accumulator C Accumulator 76543210 76543210 76543210 76543210 1 01010000 (80) 1 11000100 (-60) 1 10110000 (-80) 1 11000000 (-64) -10110000 (-80) -11011000 (-40) -01110000 (112) -10110000 (-80) -------- -------- -------- -------- 1 1 0 1 0 1 1 1 01010000 (80) 11000100 (-60) 10110000 (-80) 11000000 (-64) +01001111 (79) +00100111 (39) +10001111 (-111) +01001111 (79) -------- -------- -------- -------- 0 10100000 (-96) 0 11101100 (-20) 1 01000000 (64) 1 00010000 (16) OVERFLOW! OVERFLOW!
Compare and Branch
Now that we've discussed the basics of mathematics on the 6502, we can talk about branching. Branching is choosing a path based on the outcome of a question. This is the next logical topic since most of the questions involve inspecting values.
Nearly all programming involves asking questions. Is this greater than that? Are these two strings the same? Is this value negative? Based on the answer, we will do something. It's all about comparing and branching.
There are three comparison instructions:
CMP - compare operand with accumulator CPX - compare operand with X CPY - compare operand with Y
We first discussed the math section because comparisons are rooted in basic math. A comparison subtracts the operand from the value in the register and sets the flags accordingly. The overflow flag is not affected by a comparison operation. Only ADC
, SBC
, BIT
, and CLV
can affect this flag.
So, again, a comparison subtracts the operand from the register. Since there are only three registers, there are only three ways to compare.
Let's take this trivial example involving the accumulator:
LDA #32
CMP #33
The long way of saying it is we load the accumulator with 32 and then compare it with 33. We're asking the question
32 > 33? or REG > MEM?
What happens is the CPU does the following:
Set C = 1 (the carry!) 32 (accumulator) - 33 (immediate value) ----- -1 <-- result C is now 0 (we borrowed) N is now 1 (the result is negative) Z is now 0 (the result is NOT zero) V is not affected
The state of the flags affects how we branch. Eight branch instructions can be used to test the result of a comparison (or other math operation).
BCS - branch on carry set - REG >= M BCC - branch on carry clear - REG < M BEQ - branch on equal (zero set) - REG == M BNE - branch on not equal (zero clear) - REG != M BMI - branch on minus (negative set) - REG < M BPL - branch on plus (negative clear) - REG > M BVS - branch on overflow set - Only affected by SBC, ADC, and BIT BVC - branch on overflow clear - Only affected by SBC, ADC, and BIT
Notice the notations at the end of each line. These indicate how a branch will be taken. Overflow (V
) is a bit more complicated and will be discussed when we talk about arithmetic operations.
Branches can be hard to understand at first, but make complete sense. They react only to the flags. If the carry flag is set, the BCS instruction will be taken. In other words, the branch makes an implicit check of the flag is reacts to and then decides if it will do the work.
BRA
was introduced in later versions of the CPU). Instead we would use JMP
if we needed to make an unconditional branch. The downside to this is the JMP instruction is 3 bytes while the branch instruction are two. But as we will soon see there are limits in branching.Branch instructions use an offset to determine where they will go. Once it is determined if the branch will be taken, the movement can only be 127 bytes forward (toward higher memory) or 128 bytes back (toward lower memory). That seems like a small amount, but the CPUs of this era were trying to conserve how much memory programs would occupy, so this was one way to save a byte here and there. Remember that much of programming is asking questions and reacting to the answers. So, compare and branch takes up much of any program's life.
You may have realized at this point the branch range of movement is -128 through 127. This is also the range of a signed two's-complement byte value.
Now let's try to put some of what we've learned to good use.