Available languages:

Programming Like It’s 1982. Part V – Flow Control

In the previous post we saw how the processor moves data and does arithmetic. But a program that only executes instructions in a straight line does not do much of anything useful. At some point it needs to make a decision. Go one way or another, depending on a result. It needs loops, conditionals, the ability to behave differently depending on the state of things.

In 6510 assembly, all of that is built on a single mechanism. Flags.

The Status Register and its flags

Every operation the processor executes leaves traces. When you add two numbers and the result is zero, the processor notes it. When a subtraction produces a negative number, the processor notes it. When a sum overflows the limit of a byte, the processor notes it. These traces live in a special register called the Status Register, or SR.

The SR does not store a number. It stores eight independent bits, each with its own meaning. These bits are called flags, little markers that the processor raises or lowers automatically as operations happen.

6510 Status Register

Of all the flags, three show up in practically every program:

The Z flag (Zero) is set when the result of an operation is exactly zero. A subtraction that gives zero, a sum that gives zero, a register that was zeroed out. All of that raises Z.

The N flag (Negative) is set when the highest bit of the result is 1, which in signed arithmetic indicates a negative number. If you subtract a larger number from a smaller one, N goes to 1.

The C flag (Carry) is set when a sum exceeds 255, the maximum that fits in one byte. It is the “carry the one” of binary arithmetic. If you add $FF + $01, the result would be 256, which does not fit in 8 bits. A becomes $00 and the bit that did not fit goes into the Carry.

These three flags are the foundation of every decision an assembly program makes.

Comparing values

Before branching, you need to compare. For that there are CMP (compare with A) and CPX (compare with X).

CPX #$05    ; compare X with the value 5

What this instruction does internally is a subtraction. It calculates X - 5 and discards the result, keeping only the effect on the flags. If X equals 5, the result is zero and Z is set. If X is less than 5, the result is negative and N is set. If X is greater than 5, neither of them is set.

The result itself goes in the trash. What matters are the flags that remained.

Conditional branches, the if without if

With the flags in place, the branch instructions come in. They read a specific flag and decide whether to keep going or jump to another address.

InstructionCondition to branch
BEQZ = 1 (result was zero, values are equal)
BNEZ = 0 (result was not zero, values are different)
BMIN = 1 (result was negative)
BPLN = 0 (result was positive or zero)
BCSC = 1 (carry set, unsigned result exceeded 255)
BCCC = 0 (carry clear)

There is a detail that trips people up at first. The branch logic is inverted compared to what you would write in Python or Go. In modern languages you think “if the condition is true, execute this block”. In assembly you think “if the condition is false, skip this block”.

Before looking at the code, one piece of syntax appears here for the first time. Labels. A label is a name you give to a specific location in your program. When you write done: in your code, the assembler replaces every reference to done with the actual memory address of that instruction. The processor never sees the name. By the time the code runs, done has already become a number. Labels exist so you do not have to recalculate and hardcode memory addresses every time you add or remove a line.

To implement if x == 5, you would write:

        CPX #$05         ; compare X with 5
        BNE done         ; if X != 5, skip
        JSR do_something ; X is 5, call the function
done:
        RTS

BNE turns away whoever does not have a ticket, not whoever does. The end result is the same, but the reasoning inverts. Getting used to this takes a bit of time, but after a few loops it becomes instinct.

Building a loop

A while in Python:

x = 0
while x != 10:
    do_something()
    x += 1

The equivalent in assembly:

        LDX #$00         ; x = 0

loop:
        JSR do_something ; do_something()
        INX              ; x++
        CPX #$0A         ; compare X with 10
        BNE loop         ; if X != 10, go back to loop

        RTS

The BNE at the end is the gatekeeper. As long as the comparison does not result in zero, as long as X has not reached 10, the branch happens and the loop restarts. When X finally equals 10, Z is set, BNE does not branch, and execution moves forward.

Pointers and indirect addressing

Every example so far accessed fixed addresses. LDA $0200, STA $D418. But sometimes you do not know at write time which address you want to access. You know at run time, because the address depends on some calculation. That is what pointers solve.

On the 6510, you can store a 16-bit address in two consecutive Zero Page bytes and then tell the processor to go to the address stored there, add Y to it, and read whatever is at that position.

; Store the address $0400 in Zero Page at $42 and $43
LDA #$00
STA $42     ; low byte: $00
LDA #$04
STA $43     ; high byte: $04

; Now read RAM[$0400 + Y] using the pointer
LDY #$03
LDA ($42),Y ; A = RAM[$0400 + 3] = RAM[$0403]

The notation ($42),Y means to read the 16-bit address stored in Zero Page at $42 and $43, add Y to it, and use the result as the final address. In C this would be exactly ptr[3] where ptr points to $0400.

A pointer is not an abstract concept. It is an address stored in memory that the processor knows how to follow. Once you see it that way, everything from dynamic arrays to function tables starts making sense at the hardware level.


In the next post we reach the most surprising chapter of the series. How writing a byte to a memory address could play a musical note, move a sprite on screen, or read the state of a joystick button. No driver, no syscall, no abstraction at all.