TMS9918A and BIOS 1.6 — for the COB, DEV, KIM, VCS, PicoCalc and ACEs still on the original video card editionCurrent docs →
Skip to content

Registers, flags, and addressing ​

The whole processor fits on a postcard. Three registers you compute with, a stack pointer, a program counter, and eight flags.

The three you use ​

WidthWhat it's for
A — the accumulator8 bitsArithmetic and logic. Everything that adds, subtracts, shifts or compares goes through here.
X8 bitsAn index, a counter, and the only register that can reach the stack pointer.
Y8 bitsAn index, and the second half of (zp),Y — the pointer-plus-offset mode.

X and Y are nearly interchangeable, with two differences worth remembering: (zp,X) and (zp),Y each work with only one of them, and TXS/TSX — which move the stack pointer about — are X only.

The other two ​

SP, the stack pointer, is 8 bits and always points somewhere in $0100–$01FF. It starts at $FF and counts down as things are pushed. You almost never touch it; the exception is a cartridge, which sets it up itself before anything else (Writing a cartridge).

PC, the program counter, is 16 bits and holds the address of the next instruction. Every jump and branch is a way of writing to it.

The flags ​

One byte, called P. PHP and PLP push and pull it.

FlagSet when
N — negativeThe result's top bit is 1
V — overflowA signed addition or subtraction went out of range
B — breakA BRK caused this interrupt, not the hardware
D — decimalSet by SED: arithmetic works in packed decimal until CLD
I — interrupt disableSet by SEI: hardware interrupts are ignored until CLI
Z — zeroThe result was zero
C — carryAn addition carried out, a subtraction did not borrow, or a shift pushed a bit out
The eight flags of the status register P, one bit at a time bit 7 N negative bit 6 V overflow bit 5 – unused bit 4 B break bit 3 D decimal bit 2 I no IRQs bit 1 Z zero bit 0 C carry The Monitor prints them in this order, a letter where a flag is set: ---B-IZC break, interrupts off, zero, carry.
Bit 5 is not a flag and always reads 1. The Monitor prints the other seven in this order, left to right.

Four of them do the work. Z and N are set by almost every instruction that produces a value, so lda Count followed by beq Empty needs no test in between. C carries between bytes when you do 16-bit arithmetic, and it is also how a lot of the ROM reports success: carry clear means it worked, carry set means it didn't.

The two rules people get wrong

CLC before ADC, every time — the carry is added in. And SEC before SBC, because on a 6502 the carry is a borrow, inverted: set means "no borrow yet".

Addressing modes ​

An instruction's addressing mode is how it finds the byte it works on. There are eleven, and they are the actual grammar of the language.

ModeLooks likeWhere the byte comes from
Immediatelda #42The number is in the instruction
Zero pagelda $40Address $0040 — one byte of address, one cycle saved
Zero page,Xlda $40,x$0040 plus X, wrapping inside page zero
Absolutelda $1234Any address
Absolute,X / ,Ylda $1234,xThat address plus the index
Indirectjmp ($0300)The address stored at $0300 — JMP only
Zero page indirectlda ($40)The address stored at $0040 — a 65C02 addition
Indexed indirectlda ($40,x)The address stored at $0040 + X
Indirect indexedlda ($40),yThe address stored at $0040, plus Y
Relativebne LoopA branch, ±127 bytes from here
Impliedinx, rtsNo operand at all

The two that matter most ​

Zero page is the first 256 bytes, and instructions that use it are a byte shorter and a cycle faster than the same instruction reaching anywhere else. The Kernal uses $00–$39. The 198 bytes from $3A to $FF are yours while your program is in charge, and putting your hot variables there is the cheapest speed-up available.

Indirect indexed — (zp),Y — is how you walk through memory:

asm
Source := $40                   ; two zero-page bytes hold the address

  lda #<Message                 ; low byte of where the text starts
  sta Source
  lda #>Message                 ; high byte
  sta Source + 1

  ldy #0
Copy:
  lda (Source),y                ; the Y'th byte of the message
  beq Done                      ; strings end with a zero
  jsr Chrout
  iny
  bra Copy
Done:
  rts

#< and #> are the assembler's way of saying "low half of that address" and "high half". You will type them constantly: every ROM routine that takes a pointer takes it as a low byte and a high byte.

Doing 16-bit arithmetic ​

The processor is 8 bits wide, so anything bigger is done a byte at a time with the carry passing between them:

asm
  clc
  lda ScoreLow
  adc #10
  sta ScoreLow
  lda ScoreHigh
  adc #0                        ; adds only the carry
  sta ScoreHigh

Subtraction is the same shape with sec and sbc. Comparing a 16-bit value is cmp on the high byte, then on the low byte if the high bytes matched.

Jumping and branching ​

JMP goes anywhere. JSR goes anywhere and remembers where it came from; RTS comes back. Branches — BEQ, BNE, BCC, BCS, BMI, BPL, BVC, BVS, and the 65C02's BRA — are relative and reach 127 bytes each way.

When a branch cannot reach, the fix is to invert it and jump:

asm
  beq Near                      ; too far? then:
  bne Skip
  jmp Near
Skip:

cc65 will do that rewriting for you if the source says .macpack longbranch and you write jeq instead of beq.

Next: the instruction set.

Written for BIOS v1.6. Released under the MIT License.