Assembly 101: Branch out of range

For this post, I want to zoom in a little on the “Branch out of range” error that pops up every now and then. I’ll try and explain what it means, what’s the most likely cause, how to fix it, and why that fixes it.

The program counter

Before I can explain what a Branch out of range error actually means, I probably need to tell you about the program counter first. The 6502 processor on the NES uses a program counter to keep track of where we are in the game’s code. After executing an instruction, the counter gets updated so the CPU can execute the next instruction. Usually, the counter simply moves to the next operation in line:

P> 0CE4C A5 00      LDA temp
   0CE4E 85 01      STA temp1

The program counter points at address 0CE4C now. After loading the value of temp in the accumulator, the program counter updates to 0CE4E, so the program can execute the next instruction and store the value of the accumulator into temp1.

Manipulating the program counter

There are a few ways to manipulate the program counter. The two most common ways that you probably already are familiar with, are jumping and branching. Here is an example of the jump instruction, and how it manipulates the program counter:

    0C075 18         CLC
    0C076 69 10      ADC #$10
P>  0C078 4C E3 C0   JMP DoScreenUpdate
    ...
    0C0E3            DoScreenUpdate:

In this example, the JMP instruction tells the program counter to point to address C0E3. I’ve colored the operation’s bytecode, so you can see where it gets this information from.

A branch operation more or less does the same, but does it in a different way:

    0C063 A5 21      LDA screenUpdateByte
P>  0C065 D0 14      BNE notHandlingBottomBounds
    0C067            ...
    0C07B            notHandlingBottomBounds:

In this example, is the accumulator is not zero, the code branches to the notHandlingBottomBounds label at address C07B. So, how does this relate to the green byte that says 14? Well… after executing the BNE operation, the pointer ends up at address C067; if it does not need to branch, this is the correct address, but if it does need to branch (i.e. the accumulator is not zero), it has to add $14 to its value to end up at address C07B (CO67+14). So where a jump modifies the counter with an absolute address value, a branch uses a value relative to the counter’s current value.

Out of range?

As you can see in the above example, the branch value ($14) is stored in a single byte. This is a signed byte. A single signed byte can hold a value between -128 ($80) and 127 ($7F). This means that, when branching, the program counter can only “move” down 127 bytes, or up 128 bytes. So when we add a couple more lines of code between the branch instruction and the address it should branch out to, this might happen:

    0C063 A5 21      LDA screenUpdateByte
P>  0C065 D0 ??      BNE notHandlingBottomBounds
    0C067            ...
    0C0E7            notHandlingBottomBounds:

Here, when the accumulator is not zero, the program counter should branch out to address C0E7, which is 128 (or $80 in hex) bytes down from its current position. Bus since the relative branch value is a signed integer, it cannot be higher than 127 bytes. This is when the assembler throws a “Branch out of range” error; the program counter can’t branch out that far ahead into the code.

A solution

So far we’ve learned that jumps are absolute, and branches are relative. So, how can we fix a branch out of range error? The answer: by reversing the branch logic (branch out if the accumulator IS zero) and using a jump instruction to jump to the desired address:

    0C063 A5 21      LDA screenUpdateByte
P>  0C065 F0 03      BEQ +handlingBottomBounds
    0C067 4C EA C0       JMP notHandlingBottomBounds
    0C06A            +handlingBottomBounds:     ;; address C067 + 03 = C06A
    0C06A            ....
    0C0EA            notHandlingBottomBounds:

In this example, if the accumulator IS zero, it updates the program counter relatively (by adding 3 to the address) and thereby branches over the jump instruction; if it is not zero, it does not branch out and executes the next instruction, which is a JMP instruction that updates the program counter to C0EA absolutely. Problem solved!

Bonus: JEQ and JNE

If you’re concerned about these BNE here / JMP there constructions obfuscating your code too much, here’s two macros that you can add to your code:

MACRO JEQ address
    BNE +mbne
        JMP address
    +mbne
ENDM

MACRO JNE address
    BEQ +mbeq
        JMP address
    +mbeq
ENDM

If you add this snippet to a file in your System/Macros folder, you can (ab)use two new instructions: JEQ (Jump if EQual) and JNE (Jump if Not Equal). Now, next time your assembler throws you a branch out of range error, simply replace the delinquent BNE/BEQ with a JNE/JEQ and you’re good to go!