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!