Most 6502 instructions either update or depend on the status register in some way. However, its effects can seem a bit arcane and unintuitive to the novice. In fact, there are definite patterns to this behaviour, which I hope to illustrate below. As with most aspects of the 6502's design, there are good reasons for most of it.
For the purposes of this discussion, I'll focus on the 65C02. The NMOS 6502 is the same, with a few instructions missing and a couple of minor quirks. The 65C816 is noticeably more complicated.
The first aspect to realise is that the status register does not hold a value - it holds six independent bits, laid out as NVxxDIZC, with the x's always reading as 1 if you push the status register on the stack (using PHP) and never being changed by any means. All the other bits can be restored from the stack by PLP or RTI. Aside from that, each bit is updated by its own set of circumstances, and has a different effect on the CPU's behaviour.
There is one other circumstance where the status register is pushed onto the stack - when entering an interrupt handler. In this case there is an additional "virtual" flag known as B, which indicates whether the interrupt was a hardware interrupt (when B is cleared) or a software BRK (when B is set). This "virtual flag" does not exist in the status register; it is simply manipulated while shoving it on the stack by the microcode that handles interrupts in the 6502. The only way to test it is to inspect the saved status byte on the stack. The proper technique is detailed
elsewhere.
The N, V, Z, and C bits are arithmetic status flags, which are set by the ALU and can all be branched on. I'll save them for later, and get the others out of the way first.
The D bit controls the Decimal mode of the ADC and SBC instructions, and is changed using SED and CLD. On the 65C02, it is automatically cleared at Reset and on entry to any interrupt handler; on the NMOS 6502, you have to do that yourself. Most programs won't need or want to use Decimal mode, so just leave the D flag cleared. If you
do want to enter the wonderful world of Binary Coded Decimal,
there's a tutorial here.
The I bit is the Interrupt mask, which can be changed by SEI and CLI. When set, it prevents an /IRQ (interrupt request, active low) signal from entering the interrupt handler; this can be useful if you're doing something ticklish that should not be interrupted. However, the BRK instruction and /NMI (non maskable interrupt, active low) are not affected by this. The I bit is automatically set on entering an interrupt handler, and is automatically restored to clear (if appropriate) by a subsequent RTI - so don't use CLI just before RTI, it's not needed and might land you with recursive interrupt handling! There is
much more about interrupts here.
Which brings us back to NVZC, the flags that you'll be working with most often. These are abbreviations for
Negative, oVerflow, Zero, and Carry.
V is probably the least used of these four. You can test it using BVC (branch on V clear) or BVS (…set). It is altered only by ADC, SBC, BIT, and CLV (besides those restore-from-stack operations which can change all of them) - and notably
not by CMP, CPX or CPY, and there is no SEV instruction. For ADC and SBC, it indicates whether a
signed overflow occurred, so that the two's complement sign bit of the result byte differs from the correct result of adding (or subtracting) two signed bytes. For BIT, it simply reflects bit 6 of the value tested in memory - except the immediate form, which updates only Z.
C is the other flag that is altered only by a minority of instructions. It can be tested using BCC or BCS, changed directly by CLC and SEC, is both an input and output of ADC, SBC, ROL, and ROR, and is an output
only of CMP, CPX, CPY, ASL, and LSR. Because it's affected by relatively few instructions and is easy to set to either state, it is often used as an error flag from subroutines. Important to note: C is
not affected by INX, INY, INC, DEX, DEY, DEC.
N and Z are both updated by
any instruction that either performs an ALU operation, or loads data into A, X, or Y, including transfer instructions between registers (except TXS, whose destination is the stack pointer). They are left alone by store instructions. The associated branch instructions are not so clearly named as for V and C: BEQ (branch if equal) tests for Z set, BNE (branch if not equal) for Z clear, BMI (branch if minus) for N set, BPL (branch if plus) for N clear.
The general rule is that N receives bit 7 (the two's complement sign bit) of the result, and Z is set if the full 8-bit result is zero, and cleared otherwise. The only exceptions are TRB, TSB, and the immediate form of BIT, which update only Z and leave N (and V) alone. For other addressing modes of BIT, N reflects bit 7 of the value tested in memory, regardless of whether it was masked; Z reflects whether the masked value was zero.
You will often want to use the status flags to compare values.
There's a tutorial about that.
So let's have some example code:
Code:
; Add two 16-bit unsigned numbers
CLC ; must initialise Carry before ADC; there is no ADD instruction without a Carry input.
LDA i+0 ; low byte of first operand
ADC j+0 ; low byte of second operand; C now reflects Carry out of this partial sum
STA k+0 ; low byte of result
LDA i+1 ; high byte…
ADC j+1 ; Carry used here, new Carry generated
STA k+1
BCS overflow ; if this branch is taken, the sum didn't fit in 16 bits.
; Add two 16-bit signed numbers
CLC ; must initialise Carry before ADC; there is no ADD instruction without a Carry input.
LDA i+0 ; low byte of first operand
ADC j+0 ; low byte of second operand; C now reflects Carry out of this partial sum
STA k+0 ; low byte of result
LDA i+1 ; high byte…
ADC j+1 ; Carry used here, new Carry generated, V flag now reflects overflow into sign bit
STA k+1
BVS overflow ; if this branch is taken, the sum didn't fit in 15 bits plus sign bit.
; Subtract two 16-bit unsigned numbers
SEC ; must initialise Carry before SBC; there is no SUB instruction without a Carry input.
LDA i+0
SBC j+0
STA k+0
LDA i+1
ABC j+1
STA k+1
BCC underflow ; if this branch is taken, there was a borrow from beyond the highest bit - so the result is negative
; Compare two 16-bit unsigned numbers
LDA i+1 ; the high bytes are most significant
CMP j+1 ; unlike SBC, CMP does not use the Carry flag as an input
BNE :+ ; only if the high bytes are equal should we test the low bytes
LDA i+0
CMP j+0
: BEQ equal ; the real tests begin here
BCS i_greater ; this test requires the BEQ preceding it to be accurate, otherwise it reflects (i >= j) instead of (i > j).
BCC j_greater ; this test can be used alone for (i < j)
; execute a task ten times
LDX #10
: NOP ; replace this with the task
DEX ; this automatically sets N and Z with the result of the decrement
BNE :-
; alternative, with increasing index
LDY #0
: NOP ; replace this with the task
INY
CPY #10 ; unlike the previous example, this clobbers the Carry
BNE :-
; check if a 16-bit value is zero
LDA i+0
ORA i+1
BEQ zero
BNE nonzero
; test a status flag in the most-significant bit of a hardware register
BIT register
BMI flag_set
BPL flag_clear
; test a status flag in the least-significant bit of a hardware register
LDA #1
BIT register
BNE flag_set
BEQ flag_clear
; multiply a 16-bit value by 2
ASL i+0 ; inserts a 0 bit at the least-significant end, and pushes the most-significant bit into the Carry
ROL i+1 ; inserts the Carry into the least-significant end, and pushes the most-significant bit into the Carry
BCS overflow ; the result doesn't fit in 16 bits
Hopefully this clears up at least some confusion…