Overview
4 Maths, loops, conditions, and bits
This chapter builds the core toolset you need to make NES games in 6502 assembly: doing math on 8‑bit values, making decisions with CPU flags, moving execution around your program with jumps, branches, and subroutines, and manipulating data at the bit level. It explains why these fundamentals matter in games—updating positions and scores, reacting to input, coordinating routines—and introduces the stack as a fast, dedicated memory area for temporary storage and subroutine returns.
You start with number work: addition via ADC (clearing the carry first with CLC) and subtraction via SBC (setting the carry first with SEC), followed by single‑step changes using increment and decrement on memory and the X/Y registers. The chapter walks through the main addressing modes you’ll use—immediate, zero‑page, absolute, indexed (X/Y), and the two zero‑page indirect forms (indexed by X or by Y)—showing how effective addresses are formed and why zero‑page is fast. Each operation updates status flags (Carry, Zero, Negative, Overflow), which become the basis for control flow and comparisons later.
Conditions and flow control hinge on CMP (non‑destructive compare) and the status flags, enabling short‑range branches (BPL/BMI, BVC/BVS, BCC/BCS, BNE/BEQ), absolute jumps, and structured subroutines with JSR and RTS/RTI. To keep code readable and reusable, you use labels—global, local/proc‑scoped, “cheap” local (@), and unnamed (:+/:-)—while minding branch distance limits. The data section introduces bytes, bits, and nibbles; tests bits with BIT; modifies patterns and masks using AND, EOR (XOR), and ORA; and shifts/rotates (ASL/LSR, ROL/ROR) for fast multiply/divide and sprite/tile bit‑twiddling. Finally, it covers the CPU stack (a fixed 256‑byte area) and its push/pop helpers for A and the status register (PHA/PLA, PHP/PLP), along with the crucial rule to balance pushes and pops inside subroutines.
Summary
- Both the addition and subtraction commands also use the Carry register, so additional commands are provided to Set or Clear the Carry flag.
- There are commands available to quickly increment or decrement the X and Y registers.
- Branches can only jump 127 bytes either forward or backward.
- To return from a normal subroutine you use the RTS statement, but to return from a subroutine called by an interrupt use an RTI statement.
- There are eight different branch instructions, each one branch when one of the flags is either On (1) or Off (0).
- Bytes can be shifted to the left or right.
- Bytes can also be rotated to the left or right.
Hopefully, these last two chapters have given you a start on understanding programming in assembly language. Next, we start making a program to run on an NES console.
FAQ
How does ADC work, and why should I clear the Carry flag before adding?
ADC (Add with Carry) computes A = A + value + Carry. Because the Carry flag participates in the sum, you typically start a fresh addition with CLC to ensure the previous carry doesn’t affect the result. After ADC, Carry indicates an unsigned overflow (sum exceeded 255), and Zero/Negative are set from the result.
How does SBC work, and why should I set the Carry flag before subtracting?
SBC (SuBtract with Carry) computes A = A − value − (1 − Carry). To perform a “pure” A − value, set Carry first with SEC. After SBC, Carry means “no borrow” (i.e., A ≥ value in unsigned terms), while Zero/Negative reflect the result; Overflow indicates signed overflow.
What’s the difference between zero-page and absolute addressing on the 6502?
Zero-page addresses ($0000–$00FF) are one-byte addresses that assemble to shorter, faster instructions. Absolute addresses use two bytes and can access anywhere in the 64 KB space. Use zero-page for frequently accessed variables to save ROM space and cycles; use absolute for general memory and hardware registers outside zero-page.
How do indexed addressing modes with X or Y work for reads/writes and math?
Indexed modes add X or Y to a base address, then operate on the computed address. Examples include zero-page indexed ($00,x) and absolute indexed ($HHLL,x) for reads/writes or math (e.g., ADC $0300,x). They’re ideal for arrays, tables, or structures where X/Y selects an element.
What’s the difference between (zp),Y and (zp,X) indirect addressing?
(zp),Y: Read a 16-bit address from a zero-page pointer, then add Y to that address; access memory at [ptr] + Y. Good for iterating over data referenced by a pointer with Y as an index. (zp,X): Add X to the zero-page pointer location first, then read the 16-bit address from that adjusted zero-page pair; access memory at [ptr+X]. Useful for selecting one of several pointers with X.
What are INC/DEC, INX/DEX, and INY/DEY used for, and which flags do they affect?
They change values by ±1 efficiently. INC/DEC modify a memory byte; INX/DEX change X; INY/DEY change Y. These instructions update Zero (set if result is 0) and Negative (reflects bit 7) but do not affect Carry. They’re perfect for counters, positions, and loop indices.
How do CMP and the status flags drive conditional branches?
CMP subtracts a value from A without changing A, but sets flags as if A − value occurred: Z set if equal, C set if A ≥ value (unsigned), N reflects the sign of the difference, and V unaffected. Branches test flags to decide control flow, e.g. BEQ (Z=1), BNE (Z=0), BCS (C=1), BCC (C=0), BMI (N=1), BPL (N=0), BVS (V=1), BVC (V=0). Branches use a 1-byte relative offset, allowing a short jump (about 129 bytes forward or 126 bytes backward as described in the chapter).
When should I use JMP versus JSR/RTS (or RTI)?
JMP unconditionally transfers execution to a new address and never returns. JSR calls a subroutine: it pushes the return address on the stack, runs the subroutine, and RTS returns to the caller. RTI is used to return from an interrupt. Prefer labels over hard-coded addresses so the assembler calculates targets, and ensure any pushes inside subroutines are balanced with pops before RTS.
What do BIT, AND, EOR, and ORA do, and when are they useful in games?
- BIT tests bits in a memory value against A without changing either; it sets Z (A AND M == 0), and copies input bits 6/7 to V/N. Great for checking flags. - AND keeps only bits set in both A and the input; useful for masking. - EOR toggles bits where inputs differ; handy for flipping flags or simple effects. - ORA sets bits that are set in either input; good for forcing flags on.
How do ASL/LSR differ from ROL/ROR, and why are they handy?
- ASL shifts left, brings in 0 at bit 0, and moves old bit 7 to Carry (quick ×2). - LSR shifts right, brings in 0 at bit 7, and moves old bit 0 to Carry (quick ÷2, truncating). - ROL rotates left through Carry: old Carry enters bit 0; old bit 7 goes to Carry. - ROR rotates right through Carry: old Carry enters bit 7; old bit 0 goes to Carry. These are useful for fast math and manipulating sprite/tile bit patterns.
How does the 6502 stack work on the NES, and what are PHA/PLA and PHP/PLP?
The stack lives at $0100–$01FF and is last-in, first-out. The CPU uses it automatically for JSR/RTS and interrupts. You can also use: PHA to push A, PLA to pull A, PHP to push the status register P, and PLP to pull P. These single-byte instructions are fast and help save/restore registers or temporary values, but always balance pushes/pops to avoid corrupting return addresses.