This is the executable guide to Dromaios's instruction-level Z80. It owns the
complete model: stored state, both register banks, all 698 documented instruction
forms, prefix decoding, reset, execution, IRQ/NMI entry, and the public interface.
The build generates its TypeScript implementation from the cpu fences below.
CPU tests · Public type checks · Coverage
Historical background
The Z80 grew out of Zilog, founded by Federico Faggin and Ralph Ungermann after leaving Intel in 1974. Masatoshi Shima joined the development in 1975. Their oral history at the Computer History Museum describes a design that retained 8080 machine-code compatibility while adding index registers, a second register set, more word operations, and a richer interrupt system. The processor reached the market in 1976.
Compatibility made existing software useful, but the Z80 was a distinct processor. Even familiar byte instructions need their own flag rules. The formal definitions below expose those differences rather than treating the Z80 as an 8080 with a few extra opcode handlers.
The programmer's machine
The model has an eight-bit data path and a sixteen-bit memory address space.
Bytes range from $00 to $FF; addresses range from $0000 to $FFFF.
As in the other specifications, $ introduces hexadecimal and an opcode pattern
lists bits from most to least significant. Instruction operands and memory
words put the low byte first.
Diagram source
<diagram padding="24" theme="light" background="#fffefa">
<graph direction="lr" gap="28" row-gap="130">
<rect id="main" width="160" height="66">A B C D E H L and flags</rect>
<rect id="alu" width="160" height="66">Byte ALU</rect>
<rect id="alternate" width="160" height="66">Alternate register bank</rect>
<rect id="pairs" width="160" height="66">BC DE HL</rect>
<rect id="memory" width="160" height="66">64 KiB memory</rect>
<rect id="index" width="160" height="66">IX IY plus signed displacement</rect>
<rect id="control" width="160" height="66">PC SP I R IFF1 IFF2 IM</rect>
<rect id="execution" width="160" height="66">Instruction and interrupt control</rect>
</graph>
<connect from="main" to="alu" arrow="both"/>
<connect from="main" to="alternate" arrow="both" label="EX AF,AF′ / EXX"/>
<connect from="main" to="pairs"/>
<connect from="pairs" to="memory"/>
<connect from="index" to="memory"/>
<connect from="control" to="execution"/>
</diagram>
B:C, D:E, and H:L are word views of stored bytes. H=$12, L=$34 means
HL=$1234; there is no second value to synchronize. An operand written
(HL) means the memory byte at that address. It is not another register.
IX and IY are separately stored words. Their indexed byte operations add a
signed displacement, wrapping the resulting address to sixteen bits.
The alternate bank contains A′, B′, C′, D′, E′, H′, L′, and flags F′.
EX AF,AF′ exchanges A and its flags; EXX exchanges BC, DE, and HL.
Ordinary instructions still name the main bank. These exchanges do not select
a persistent bank number: they swap the stored values. A program must manage
which values belong to which context.
| Register | Role |
|---|---|
| A | Accumulator and ordinary byte ALU destination |
| B, C, D, E, H, L | Byte operands or halves of BC, DE, HL |
| IX, IY | Index pointers |
| PC, SP | Next instruction address and RAM stack pointer |
| I | High byte used in interrupt mode 2 vector addressing |
| R | Refresh register; the decoder advances its low seven bits on M1 fetches |
| IFF1, IFF2 | Interrupt enable state |
| IM | Interrupt mode 0, 1, or 2 |
Flags distinguish parity from overflow
F holds six modeled flags: S (sign), Z (zero), H (half carry), P/V (parity or overflow), N (subtraction), and C (carry or borrow). P/V describes signed overflow for arithmetic and even bit parity for logical operations. Unlike the 8080, subtraction reports half borrow, and AND always sets H. These are instruction effects, not properties continuously recomputed from A: loading A with zero leaves the old Z unchanged.
| Operation | Result | P/V | C | Why |
|---|---|---|---|---|
$7F + $01 |
$80 |
1 | 0 | Signed overflow without unsigned carry |
$FF + $01 |
$00 |
0 | 1 | Unsigned carry without signed overflow |
$80 - $01 |
$7F |
1 | 0 | Signed overflow without unsigned borrow |
$03 XOR $00 |
$03 |
1 | 0 | Two set bits give even parity |
The model omits undocumented F bits 5 and 3. Packing emits zero in those positions and unpacking ignores them; that is a modeling limit, not a claim that the silicon holds them at zero.
Physical operation and the model boundary
Zilog's CPU manual describes M1 opcode fetches, refresh, memory and I/O transfers, and interrupt acknowledgement as distinct bus activities. The original design uses an eight-bit data bus, sixteen-bit address bus, a single clock input and a +5 V supply. WAIT and bus-request signals let the surrounding system delay transfers or take control of the buses.
Dromaios models instruction effects, not clock phases or electrical pins. R advances on modeled opcode fetches; operand and displacement reads do not all advance it. Refresh itself performs no recorded RAM access. Neither access counts nor R alone measure elapsed cycles. The execution contract below defines prefix fetching, reset, retirement, and explicit external interrupt entry.
Stored state
Each bank owns its own flags object. Architectural names are uppercase; stored
JavaScript fields are lowercase unless explicitly mapped. ALTERNATE.B means
B in the named bank, while plain B refers to the main bank. PC and SP are
independent words. The two deferral latches and STOPPED describe instruction
boundary behavior; they are not additional programmer-visible registers.
cpu "z80"
state {
register A: 8
register B: 8
register C: 8
register D: 8
register E: 8
register H: 8
register L: 8
flag S
flag Z
flag H
flag PV
flag N
flag C
bank ALTERNATE = alternate {
register A: 8
register B: 8
register C: 8
register D: 8
register E: 8
register H: 8
register L: 8
flag S
flag Z
flag H
flag PV
flag N
flag C
}
register IX: 16
register IY: 16
register PC: 16
register SP: 16
register I: 8
register R: 8
latch IFF1
latch IFF2
choice IM: 0, 1, 2
latch IRQDEFERRED = interruptDeferred
latch NMIDEFERRED = nmiDeferred
latch STOPPED = halted
}
CpuZ80State requires every stored field. The machine language uses uppercase
register and flag names, with PV for the manual's P/V flag. Public TypeScript
uses the stored names declared above. The main H register and H flag are
separate fields, as are their alternate counterparts. BC/DE/HL are derived
readonly views in each snapshot, never separately initialized state. There is
no public raw F or AF view; the internal packed view serves stack operations.
Construction and inspection
Memory uses the byte-connection contract, with RAM as the usual connection.
new CpuZ80(ram, initialState, ports?, onReti?) requires a 64 KiB byte-memory address space and explicit state.
It copies declared fields from both banks and both flag objects, then validates
byte and word ranges, Boolean flags and latches, and integer IM in 0–2. Invalid
numeric fields throw RangeError; invalid Boolean fields throw TypeError.
Construction performs no reset, execution, or RAM/device accesses.
The optional BytePorts connection
supplies readPort(port) and writePort(port, value) callbacks. Port addresses
are full 16-bit numbers and transferred values are bytes. The device owns its
state; restoring a CPU snapshot does not restore or reconnect the device.
onReti, when supplied, is called after a documented RETI retires. It is an
architectural notification, independent of port transfers, and adds no entry
to the memory/port log. RETN does not call it.
Each declared input property is read once; copying does not depend on property enumerability. Extra metadata and derived views are ignored, so a snapshot can also be used as initialization data. Caller objects, including initially shared bank or flag objects, do not remain connected to internal state.
snapshot() returns detached main and alternate banks, flags, register views,
and control state without accessing RAM. Public types are recursively readonly.
Objects are ordinary JavaScript values, not frozen objects: even if a caller
bypasses readonly typing and edits one, it cannot alter CPU state or another
snapshot. The CPU keeps mutable private state and retains no record history.
Instruction steps
step() attempts one instruction, or one block iteration, and returns CpuZ80StepRecord:
beforeandafter: independent complete snapshots.instruction: its starting address and the bytes actually fetched, in order;nullonly when already halted on entry.accesses: orderedCpuZ80Accessentries. Memoryread/writeentries carryaddressandvalue; portinput/outputentries carryportandvalue. Both kinds share one log in their actual transfer order.CpuZ80MemoryAccessremains the memory-only type alias.outcome:executed,halted, orunsupported. Onlyunsupportedcarriesreason: "opcode".
Supported instructions advance PC while fetching bytes, wrapping at 16 bits. Word operands, including immediate pair loads and absolute addresses, are fetched low byte first. NOP changes PC/R and consumes existing inhibition under the retirement rules. Stores record the write even when the value is unchanged, and never read the destination to reconstruct an old value. Captured instruction bytes survive stores that overwrite code. Subsequent steps fetch current RAM.
Each supported instruction increments the low seven bits of R for each opcode
fetch, preserving bit 7: once for an unprefixed instruction and twice for every
supported prefixed form. In DD/FD CB d op, only the first two bytes are M1
opcode fetches; neither the displacement nor the final opcode increments R.
Operand reads and data accesses do not increment R. For example, unprefixed
7F becomes 00, while prefixed FF becomes 81. PC and R are advanced only
after the complete supported encoding has been identified. LD A,R observes
this increment; LD R,A then replaces all eight bits with A.
An unsupported opcode is read and recorded, but PC, R, all other CPU state, and RAM remain unchanged. This atomic rejection is a model policy, including the choice to leave R unchanged despite the recorded reads. CB, ED, DD, and FD fetch a second byte, wrapping at FFFF. An unsupported entry retains both reads without fetching operands. DD/FD CB additionally fetch displacement and final opcode before rejecting an unsupported entry, retaining all four reads. SLL, index half-register operations, indexed-CB register destinations, repeated prefixes, ignored-prefix encodings, and undocumented ED aliases are unsupported. Repeating an unsupported attempt reads current RAM again and preserves all state.
HALT advances PC past its opcode, increments R once, sets halted, and reports
outcome: "halted" with the HALT instruction. Subsequent halted steps report
instruction: null, no accesses, and unchanged state, including R.
Physical HALT continues bus and refresh activity. Those cycles, clock timing,
and dummy accesses remain outside this instruction-level model. Interrupts
are explicitly offered between steps; step() does not poll devices.
There is no synthetic lesson-completion instruction or state; caller completion
belongs to the runner.
Reset and execution
Reset sets PC, I, R, and IM to zero, disables both interrupt flip-flops,
clears both deferral latches, and releases HALT. It preserves both register
banks, IX, IY, SP, and RAM. Reset records detached before/after snapshots and
an empty access list: it neither reads a vector nor fabricates a power-on state.
CpuZ80ResetRecord has no instruction or step outcome. Repeated reset has the
same defined effects and produces fresh records. Preserving registers whose
reset values the manual leaves unspecified is a deterministic model policy,
not a claim about physical power-on values. An example's fresh factory instead
restores its initial CPU, program, and RAM.
view NEXT "next instruction address": 16 {
current = register PC
return current
}
action setPC "write the next instruction address" (word: 16) {
PC <- word
}
action reset "apply the reset boundary" () {
PC <- u16(0)
I <- u8(0)
R <- u8(0)
IFF1 <- 0
IFF2 <- 0
IM <- 0
NMIDEFERRED <- 0
IRQDEFERRED <- 0
STOPPED <- 0
}
The complete opcode encoding is read and validated before PC or R changes. An unsupported prefix combination records the bytes read but preserves stored state. A halted step performs no access or retirement. For a supported opcode, PC first advances past the encoding, then R advances once for each opcode fetch. Only R's low seven bits count; bit 7 retains its stored value. Ordinary opcodes count once, CB/ED/DD/FD opcodes twice, and DD CB d op / FD CB d op also count twice: their displacement and final opcode are ordinary reads.
Operands are then read through the live PC, advancing it after each successful read. All addresses wrap to sixteen bits; words read low byte first. An access failure retains earlier effects, including committed PC/R, but does not retire the instruction. Each step executes one instruction or one repeating block iteration and records ordered memory and port accesses.
action refresh "advance the low seven refresh bits" (count: 8) {
original = register R
R <- or(and(original, u8($80)), and(add(original, count), u8($7F)))
}
action afterRetirement "consume NMI inhibition" () {
NMIDEFERRED <- 0
}
A successful instruction consumes previous IRQ inhibition. A defer irq
request from EI or an IFF-changing return replaces it with a new one-instruction
inhibition; every successful instruction also clears NMI inhibition. RETI
requests device notification, delivered only after these retirement effects.
A failing notification callback leaves the completed return intact.
Step, reset, and interrupt entry share a reentrancy guard, including during memory, port, acknowledgement, and RETI callbacks. Snapshot inspection remains available to callbacks. The entry contract selects NMI and IM 0/1/2 delivery explicitly, using the same decoder and retirement boundary for mode 0.
Pair and status views
Reading a pair captures its high byte and then its low byte. Snapshots derive both banks' pair values from their detached stored-state copies. The views do not fetch memory or change registers.
view BC "B:C register pair": 16 {
high = register B
low = register C
return concat(high, low)
}
view DE "D:E register pair": 16 {
high = register D
low = register E
return concat(high, low)
}
view HL "H:L register pair": 16 {
high = register H
low = register L
return concat(high, low)
}
view BC_ALT "alternate B:C register pair": 16 {
high = register ALTERNATE.B
low = register ALTERNATE.C
return concat(high, low)
}
view DE_ALT "alternate D:E register pair": 16 {
high = register ALTERNATE.D
low = register ALTERNATE.E
return concat(high, low)
}
view HL_ALT "alternate H:L register pair": 16 {
high = register ALTERNATE.H
low = register ALTERNATE.L
return concat(high, low)
}
F packs S Z 0 H 0 PV N C, reading the six flags in that order. PUSH AF
captures A before this view; POP AF writes A before replacing the flags object.
The status action below supplies that replacement to the existing stack body.
All six new flag values are computed before the old object is replaced.
view F "packed documented flags": 8 {
sign = flag S
zero = flag Z
half = flag H
parity = flag PV
subtract = flag N
carry = flag C
return pack<8>(sign, zero, 0, half, 0, parity, subtract, carry)
}
policy FFLAGS "restore all documented flags" (status: 8) {
S = bit(status, 7)
Z = bit(status, 6)
H = bit(status, 4)
PV = bit(status, 2)
N = bit(status, 1)
C = bit(status, 0)
}
action writeF "replace packed F" (status: 8) {
replace FFLAGS(status)
}
Writing register pairs
A pair write replaces its high byte before its low byte. These actions also serve the prefixed word definitions. Reading a pair always captures both bytes before any split write, even when source and destination alias.
action writeBC "write B:C" (word: 16) {
B <- highByte(word)
C <- lowByte(word)
}
action writeDE "write D:E" (word: 16) {
D <- highByte(word)
E <- lowByte(word)
}
action writeHL "write H:L" (word: 16) {
H <- highByte(word)
L <- lowByte(word)
}
Byte operands and transfers
In 01 ddd sss, ddd selects the destination and sss the source. Both fields
use the same catalogue: B/C/D/E/H/L/(HL)/A. The (HL),(HL) slot is HALT,
not a memory-to-memory load. 00 ddd 110 loads a following immediate byte.
Reading (HL) resolves H then L before the memory read. Writing it resolves
H then L after capturing the source, without reading destination memory.
source immediateByte "immediate byte": 8 {
byte = fetch
return byte
}
operands bytes {
000 "B" = register B
001 "C" = register C
010 "D" = register D
011 "E" = register E
100 "H" = register H
101 "L" = register L
110 "(HL)" = memory HL
111 "A" = register A
}
Capture the source completely before writing the destination. Register self-copies still read and write; immediate loads fetch before resolving a memory destination. Preserve all flags, the alternate bank, and control state. A failed source read prevents writeback.
family LD "01 ddd sss" for d in bytes, s in bytes named "LD {d},{s}" except "01 110 110" {
result = operand s
operand d <- result
}
family LDImmediate "00 ddd 110" for d in bytes named "LD {d},n" {
result = fetch
operand d <- result
}
Accumulator arithmetic and logic
The shared encodings are 10 ooo sss for a register or (HL) and
11 ooo 110 for an immediate byte. ooo selects the operation below.
Each body captures the operand, then incoming C if needed, then A. Selecting
A as the source deliberately reads it twice. Results wrap to eight bits.
All six flags update before A; CP performs the subtraction without writing A.
Indexed memory bodies reuse these actions after their specification-defined address decoding.
| ooo | Operation | P/V | H | N | C |
|---|---|---|---|---|---|
| 000 | ADD | Signed overflow | Half carry | 0 | Carry |
| 001 | ADC | Signed overflow | Half carry including C | 0 | Carry including C |
| 010 | SUB | Signed overflow | Half borrow | 1 | Borrow |
| 011 | SBC | Signed overflow | Half borrow including C | 1 | Borrow including C |
| 100 | AND | Even parity | 1 | 0 | 0 |
| 101 | XOR | Even parity | 0 | 0 | 0 |
| 110 | OR | Even parity | 0 | 0 | 0 |
| 111 | CP | Signed overflow | Half borrow | 1 | Borrow |
S and Z describe the result. The remaining flag values are explicit policy arguments so the arithmetic and logical distinctions stay visible at each use. Policy expressions read only captured values; incoming flags are never reread after updates begin. Flags update in S/Z/H/PV/N/C order without replacing the flag object, preserving access and failure behavior of the existing model.
policy ALU "Z80 byte result flags" (result: 8, half: flag, parity: flag, subtract: flag, carry: flag) {
S = negative(result)
Z = zero(result)
H = half
PV = parity
N = subtract
C = carry
}
ADD
Capture A without reading incoming flags. Apply the table's flags before writing the byte result to A. A failed operand read prevents every action effect.
action addAccumulator "ADD with a captured byte" (right: 8) {
left = register A
result = add(left, right)
apply ALU(result, halfCarry(left, right), addOverflow(left, right), 0, carry(left, right))
A <- result
}
family ADD {
encoding "10 000 sss" for s in bytes.read named "ADD A,{s}"
encoding "11 000 110" with s = immediateByte named "ADD A,n"
right = source s
perform addAccumulator(right)
}
ADC
Capture incoming C, then A. Apply the table's flags before writing the byte result to A. A failed operand read prevents every action effect.
action adcAccumulator "ADC with a captured byte" (right: 8) {
carry = flag C
left = register A
result = add(left, right, carry)
apply ALU(result, halfCarry(left, right, carry), addOverflow(left, right, carry), 0, carry(left, right, carry))
A <- result
}
family ADC {
encoding "10 001 sss" for s in bytes.read named "ADC A,{s}"
encoding "11 001 110" with s = immediateByte named "ADC A,n"
right = source s
perform adcAccumulator(right)
}
SUB
Capture A without reading incoming flags. Apply the table's flags before writing the byte result to A. A failed operand read prevents every action effect.
action subAccumulator "SUB with a captured byte" (right: 8) {
left = register A
result = subtract(left, right)
apply ALU(result, halfBorrow(left, right), overflow(left, right), 1, borrow(left, right))
A <- result
}
family SUB {
encoding "10 010 sss" for s in bytes.read named "SUB {s}"
encoding "11 010 110" with s = immediateByte named "SUB n"
right = source s
perform subAccumulator(right)
}
SBC
Capture incoming C, then A. Apply the table's flags before writing the byte result to A. A failed operand read prevents every action effect.
action sbcAccumulator "SBC with a captured byte" (right: 8) {
carry = flag C
left = register A
result = subtract(left, right, carry)
apply ALU(result, halfBorrow(left, right, carry), overflow(left, right, carry), 1, borrow(left, right, carry))
A <- result
}
family SBC {
encoding "10 011 sss" for s in bytes.read named "SBC A,{s}"
encoding "11 011 110" with s = immediateByte named "SBC A,n"
right = source s
perform sbcAccumulator(right)
}
AND
Capture A without reading incoming flags. Apply the table's flags before writing the byte result to A. A failed operand read prevents every action effect.
action andAccumulator "AND with a captured byte" (right: 8) {
left = register A
result = and(left, right)
apply ALU(result, 1, evenParity(result), 0, 0)
A <- result
}
family AND {
encoding "10 100 sss" for s in bytes.read named "AND {s}"
encoding "11 100 110" with s = immediateByte named "AND n"
right = source s
perform andAccumulator(right)
}
XOR
Capture A without reading incoming flags. Apply the table's flags before writing the byte result to A. A failed operand read prevents every action effect.
action xorAccumulator "XOR with a captured byte" (right: 8) {
left = register A
result = xor(left, right)
apply ALU(result, 0, evenParity(result), 0, 0)
A <- result
}
family XOR {
encoding "10 101 sss" for s in bytes.read named "XOR {s}"
encoding "11 101 110" with s = immediateByte named "XOR n"
right = source s
perform xorAccumulator(right)
}
OR
Capture A without reading incoming flags. Apply the table's flags before writing the byte result to A. A failed operand read prevents every action effect.
action orAccumulator "OR with a captured byte" (right: 8) {
left = register A
result = or(left, right)
apply ALU(result, 0, evenParity(result), 0, 0)
A <- result
}
family OR {
encoding "10 110 sss" for s in bytes.read named "OR {s}"
encoding "11 110 110" with s = immediateByte named "OR n"
right = source s
perform orAccumulator(right)
}
CP
Capture A without reading incoming flags. Apply the table's flags before retaining A without a write. A failed operand read prevents every action effect.
action cpAccumulator "CP with a captured byte" (right: 8) {
left = register A
result = subtract(left, right)
apply ALU(result, halfBorrow(left, right), overflow(left, right), 1, borrow(left, right))
}
family CP {
encoding "10 111 sss" for s in bytes.read named "CP {s}"
encoding "11 111 110" with s = immediateByte named "CP n"
right = source s
perform cpAccumulator(right)
}
The manual, printed pages 66–69 and 145–164, defines the byte
arithmetic rules. Its flag overview specifies overflow for arithmetic and
parity for logic; the P/V descriptions on the SBC and AND pages (156 and 158)
contain contradictory typographical errors. Independent checks below follow
the overview. CP preserves A even for CP A; SBC A,A yields $00 or $FF
depending on incoming borrow. The checksum example
passes ADD's carry through loads into ADC and branches on CP.
Increment and decrement
The encoding is 00 rrr 10d: rrr selects the same byte catalogue and d selects
INC (0) or DEC (1). Both preserve C without reading it. That lets a program
adjust a loop counter without destroying the carry in a multi-byte calculation.
INC sets P/V only when $7F becomes $80; DEC sets it only when $80 becomes
$7F. The memory case resolves its address once and retains it through
read/modify/write, even if a host callback changes H or L.
INC reads the original byte, computes the wrapped result, and updates S/Z/H/PV/N before writeback. A failed read leaves flags unchanged; a failed write retains the calculated flags. Preserve the alternate bank and controls. The resolved-memory action also serves the indexed instruction bodies.
policy INCFLAGS "Z80 INC, preserving C" (original: 8, result: 8) {
S = negative(result)
Z = zero(result)
H = halfCarry(original, u8(1))
PV = addOverflow(original, u8(1))
N = 0
}
action incMemory "INC at a resolved address" (address: 16) using memory {
original = memory(address)
result = add(original, u8(1))
apply INCFLAGS(original, result)
memory(address) <- result
}
family INC "00 rrr 100" for r in bytes named "INC {r}" except "00 110 100" {
original = operand r
result = add(original, u8(1))
apply INCFLAGS(original, result)
operand r <- result
}
family INCMemory "00 110 100" named "INC (HL)" {
address = source HL
perform incMemory(address)
}
DEC reads the original byte, computes the wrapped result, and updates S/Z/H/PV/N before writeback. A failed read leaves flags unchanged; a failed write retains the calculated flags. Preserve the alternate bank and controls. The resolved-memory action also serves the indexed instruction bodies.
policy DECFLAGS "Z80 DEC, preserving C" (original: 8, result: 8) {
S = negative(result)
Z = zero(result)
H = halfBorrow(original, u8(1))
PV = overflow(original, u8(1))
N = 1
}
action decMemory "DEC at a resolved address" (address: 16) using memory {
original = memory(address)
result = subtract(original, u8(1))
apply DECFLAGS(original, result)
memory(address) <- result
}
family DEC "00 rrr 101" for r in bytes named "DEC {r}" except "00 110 101" {
original = operand r
result = subtract(original, u8(1))
apply DECFLAGS(original, result)
operand r <- result
}
family DECMemory "00 110 101" named "DEC (HL)" {
address = source HL
perform decMemory(address)
}
Byte INC/DEC record half carry/borrow across the low nibble, unlike word INC/DEC, which preserve every flag. The transfer example checks the load matrix and both operand forms; see the manual, printed pages 71–74, 79–80, 85, and 99.
Word operands and transfers
In 00 pp 0001, pp selects BC, DE, HL, or SP. Fetch the low byte and then
the high byte before writing the selected word. Pair writes use the actions
above; SP is stored directly. No word transfer reads or updates flags.
source immediateWord "immediate word, low byte first": 16 {
low = fetch
high = fetch
return concat(high, low)
}
operands words {
00 "BC" = view BC write writeBC
01 "DE" = view DE write writeDE
10 "HL" = view HL write writeHL
11 "SP" = register SP
}
family LDWord "00 pp 0001" for p in words named "LD {p},nn" {
result = source immediateWord
operand p <- result
}
family LDSP "1111 1001" named "LD SP,HL" {
result = source HL
SP <- result
}
Absolute word transfers first fetch the complete address. Store then captures
HL and writes low followed by high; load reads both memory bytes before changing
H or L. The second address wraps after $FFFF. A failed second write retains
the first; a failed load leaves the destination untouched.
family storeHL "0010 0010" named "LD (nn),HL" {
address = source immediateWord
result = source HL
memory(address) <- lowByte(result)
memory(add(address, u16(1))) <- highByte(result)
}
family loadHL "0010 1010" named "LD HL,(nn)" {
address = source immediateWord
low = memory(address)
high = memory(add(address, u16(1)))
perform writeHL(concat(high, low))
}
A also transfers through BC or DE (00 0p d 010), or through a following
absolute address (00 11 d 010). d is zero for a store and one for a load.
Resolve the complete address before reading A for a store or memory for a
load. A failed load does not write A. Stores never read destination memory;
all six forms preserve both banks' flags and the other registers.
operands indirectPairs {
0 "BC" = value BC
1 "DE" = value DE
}
family storeA {
encoding "00 0p 0 010" for p in indirectPairs.read named "LD ({p}),A"
encoding "00 11 0 010" with p = immediateWord named "LD (nn),A"
address = source p
result = register A
memory(address) <- result
}
family loadA {
encoding "00 0p 1 010" for p in indirectPairs.read named "LD A,({p})"
encoding "00 11 1 010" with p = immediateWord named "LD A,(nn)"
address = source p
result = memory(address)
A <- result
}
Word arithmetic
00 pp q 011 increments (q=0) or decrements (q=1) BC/DE/HL/SP. Both capture
the complete word before replacing it, wrap at sixteen bits, and preserve every
flag without reading it. Word INC/DEC differ from their byte counterparts.
family INCWord "00 pp 0 011" for p in words named "INC {p}" {
original = operand p
operand p <- add(original, u16(1))
}
family DECWord "00 pp 1 011" for p in words named "DEC {p}" {
original = operand p
operand p <- subtract(original, u16(1))
}
ADD HL,ss uses 00 pp 1001, with the same pair selector. Read ss before HL,
including two complete reads for ADD HL,HL. Write H then L before updating
H/C/N in that order. S/Z/PV remain unchanged. The half-carry boundary is bit 11:
bit 12 of left XOR right XOR result identifies the carry into bit 12.
This policy also supplies the IX/IY word additions.
policy WORDADD "Z80 ADD word flags (H at bit 11)" (left: 16, right: 16, result: 16) {
H = bit(xor(xor(left, right), result), 12)
C = carry(left, right)
N = 0
}
family ADDWord "00 pp 1001" for p in words named "ADD HL,{p}" {
right = operand p
left = source HL
result = add(left, right)
perform writeHL(result)
apply WORDADD(left, right, result)
}
The descending RAM stack
SP names the top occupied byte. Pushing decrements SP before each write; popping increments it after each successful read. Every adjustment reads the live pointer independently. Thus a callback can affect a later adjustment, while a failed write retains its preceding decrement. A word push writes its high byte first, then its low byte. At the final SP, memory is little-endian.
The push action captures its argument before touching SP. Each pop source captures a complete byte or word before yielding it to its caller. None of these operations reads or writes flags or erases popped memory.
action pushByte "push a captured byte" (byte: 8) using memory {
pointer = register SP
SP <- subtract(pointer, u16(1))
address = register SP
memory(address) <- byte
}
action pushWord "push a captured word" (word: 16) using memory {
perform pushByte(highByte(word))
perform pushByte(lowByte(word))
}
source popByte "pop byte through SP": 8 {
address = register SP
byte = memory(address)
pointer = register SP
SP <- add(pointer, u16(1))
return byte
}
source popWord "pop little-endian word": 16 {
low = source popByte
high = source popByte
return concat(high, low)
}
view AF "A:F": 16 {
accumulator = register A
status = source F
return concat(accumulator, status)
}
action writeAF "write A and replace F" (word: 16) {
A <- highByte(word)
perform writeF(lowByte(word))
}
operands stackWords {
00 "BC" = view BC write writeBC
01 "DE" = view DE write writeDE
10 "HL" = view HL write writeHL
11 "AF" = view AF write writeAF
}
PUSH/POP encode 11 pp q 101/001, with q=0 and pp selecting BC/DE/HL/AF.
PUSH captures the entire register before the first stack access. POP commits
its destination only after both reads succeed. AF reads A before packing F;
a pop writes A before replacing the whole flag object, ignoring bits 5 and 3.
family PUSH "11 pp 0 101" for p in stackWords named "PUSH {p}" {
original = operand p
perform pushWord(original)
}
family POP "11 pp 0 001" for p in stackWords named "POP {p}" {
result = source popWord
operand p <- result
}
Exchanges and alternate registers
EX DE,HL exchanges H with D before L with E. Each byte exchange captures both old values, reads H/L before D/E, then writes D/E before H/L. It preserves flags, SP, and the alternate bank without inspecting them.
family EXDEHL "1110 1011" named "EX DE,HL" {
highH = register H
highD = register D
D <- highH
H <- highD
lowL = register L
lowE = register E
E <- lowL
L <- lowE
}
EX (SP),HL captures HL and SP before any memory access. Read stack low then high, write the original H then L back to those captured addresses, and replace HL only after both writes succeed. Preserve SP and flags. A failure retains completed writes but prevents register replacement; the second address wraps.
family EXStack "1110 0011" named "EX (SP),HL" {
original = source HL
address = register SP
low = memory(address)
high = memory(add(address, u16(1)))
memory(add(address, u16(1))) <- highByte(original)
memory(address) <- lowByte(original)
perform writeHL(concat(high, low))
}
EX AF,AF′ (00 001 000) reads alternate A before main A, then writes main
before alternate. It next exchanges the entire flag objects in the same order,
without reading or reconstructing individual bits. The explicit flag-group
exchange preserves that identity and partial-failure contract.
family EXAF "00 001 000" named "EX AF,AF′" {
other = register ALTERNATE.A
main = register A
A <- other
ALTERNATE.A <- main
exchange FLAGS, ALTERNATE.FLAGS
}
EXX (11 011 001) exchanges B/C/D/E/H/L individually in that order, reading
alternate then main and writing main then alternate for each byte. It preserves
both A registers and both flag objects, with no memory or control-state access.
family EXX "11 011 001" {
otherB = register ALTERNATE.B
mainB = register B
B <- otherB
ALTERNATE.B <- mainB
otherC = register ALTERNATE.C
mainC = register C
C <- otherC
ALTERNATE.C <- mainC
otherD = register ALTERNATE.D
mainD = register D
D <- otherD
ALTERNATE.D <- mainD
otherE = register ALTERNATE.E
mainE = register E
E <- otherE
ALTERNATE.E <- mainE
otherH = register ALTERNATE.H
mainH = register H
H <- otherH
ALTERNATE.H <- mainH
otherL = register ALTERNATE.L
mainL = register L
L <- otherL
ALTERNATE.L <- mainL
}
Jumps and relative branches
For absolute jumps, calls, and returns, ccc means Z/C/PV/S selected by its
high two bits, with its low bit selecting clear or set. PO/PE test P/V, which
may represent parity or signed overflow depending on the preceding instruction.
JP cc,nn fetches both target bytes before reading its condition. An untaken
path never reads or writes PC; a taken path does not read target memory.
conditions tests {
000 "NZ" = flag Z = 0
001 "Z" = flag Z = 1
010 "NC" = flag C = 0
011 "C" = flag C = 1
100 "PO" = flag PV = 0
101 "PE" = flag PV = 1
110 "P" = flag S = 0
111 "M" = flag S = 1
}
family conditionalJump "11 ccc 010" for c in tests named "JP {c},nn" {
target = source immediateWord
when test c {
PC <- target
}
}
family jump {
encoding "1100 0011" with t = immediateWord named "JP nn"
encoding "1110 1001" with t = HL named "JP (HL)"
target = source t
PC <- target
}
JR has only the four NZ/Z/NC/C conditions, encoded 00 1cc 000. Its signed
byte displacement is fetched before the condition. On a taken path, read the
post-fetch PC, add the sign-extended displacement, wrap to a word, and write
PC. JR without a condition uses 00 011 000. Preserve all flags.
conditions relativeTests {
00 "NZ" = flag Z = 0
01 "Z" = flag Z = 1
10 "NC" = flag C = 0
11 "C" = flag C = 1
}
action branch "add a signed displacement to live PC" (offset: 8) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
family JR "00 011 000" named "JR e" {
offset = fetch
perform branch(offset)
}
family conditionalJR "00 1cc 000" for c in relativeTests named "JR {c},e" {
offset = fetch
when test c {
perform branch(offset)
}
}
DJNZ (00 010 000) fetches its displacement, decrements B, then reads B
again to test it. A nonzero result takes the relative branch. It preserves
every flag; failed fetching prevents the decrement. The reread retains live
stored-state behavior at each declared effect.
family DJNZ "00 010 000" named "DJNZ e" {
offset = fetch
counter = register B
B <- subtract(counter, u8(1))
remaining = register B
when not(zero(remaining)) {
perform branch(offset)
}
}
JP (HL) takes its target from the pair itself, without a pointer read. DJNZ
with B=$00 wraps to $FF and branches; B=$01 falls through. The
counted-loop example
checks DJNZ, pair views, R wrapping, and HALT. See the manual,
printed pages 72, 165–171, and 265–279.
Calls, returns, and restarts
CALL captures its complete target before the condition. On a taken path it
captures the return PC, pushes both bytes, then changes PC. RST uses
11 ttt 111 to call one of eight fixed addresses in the first 64 bytes.
The shared action ensures both retain completed stack effects on failure,
with PC replaced only after both writes succeed.
action call "push return PC then jump" (target: 16) using memory {
returnPC = register PC
perform pushWord(returnPC)
PC <- target
}
codes vectors: 16 {
000 "00H" = $00
001 "08H" = $08
010 "10H" = $10
011 "18H" = $18
100 "20H" = $20
101 "28H" = $28
110 "30H" = $30
111 "38H" = $38
}
family callOrRestart {
encoding "1100 1101" with t = immediateWord named "CALL nn"
encoding "11 ttt 111" for t in vectors.read named "RST {t}"
target = source t
perform call(target)
}
family conditionalCall "11 ccc 100" for c in tests named "CALL {c},nn" {
target = source immediateWord
when test c {
perform call(target)
}
}
RET (11 001 001) pops the complete return address before writing PC.
Conditional returns use 11 ccc 000 and test the flag before any stack read.
Untaken returns never read SP or touch RAM. All forms preserve flags.
family RET "11 001 001" {
returnPC = source popWord
PC <- returnPC
}
family conditionalReturn "11 ccc 000" for c in tests named "RET {c}" {
when test c {
returnPC = source popWord
PC <- returnPC
}
}
RST preserves the interrupt-enable latches and mode: it is an ordinary call, independent of interrupt delivery. Stack/code overlap is allowed; calls capture targets before writes and returns read current RAM. PUSH AF writes zero in unmodeled F bits 5/3, so a POP/PUSH round trip can change those memory bits while preserving the six modeled flags. See the manual, printed pages 115–120 and 281–287, and the bit-count example, which combines three call levels with saved registers and CB shifts.
Accumulator rotates and carry controls
The pattern 00 ooo 111 selects RLCA/RRCA/RLA/RRA/DAA/CPL/SCF/CCF.
Accumulator rotates preserve S/Z/PV, unlike their CB-page counterparts.
Capture A before incoming C for through-carry forms; circular forms insert
the outgoing bit. Write A, update C, then clear N/H in that order. No memory
access occurs and no unlisted flags are read.
policy CARRY "replace C only" (carry: flag) {
C = carry
}
policy ROTATE "clear N then H" () {
N = 0
H = 0
}
family RLCA "00 000 111" {
original = register A
A <- shiftLeft(original, negative(original))
apply CARRY(negative(original))
apply ROTATE()
}
family RRCA "00 001 111" {
original = register A
A <- shiftRight(original, lowBit(original))
apply CARRY(lowBit(original))
apply ROTATE()
}
family RLA "00 010 111" {
original = register A
carry = flag C
A <- shiftLeft(original, carry)
apply CARRY(negative(original))
apply ROTATE()
}
family RRA "00 011 111" {
original = register A
carry = flag C
A <- shiftRight(original, carry)
apply CARRY(lowBit(original))
apply ROTATE()
}
CPL complements A before setting N then H. SCF sets C, then clears N/H. CCF captures C, copies it to H, complements C, and clears N, in that order. All three preserve S/Z/PV and the alternate bank without inspecting them.
policy COMPLEMENT "CPL flags" () {
N = 1
H = 1
}
policy SETCARRY "SCF flags" () {
C = 1
N = 0
H = 0
}
policy COMPLEMENTCARRY "CCF flags" (carry: flag) {
H = carry
C = not(carry)
N = 0
}
family CPL "00 101 111" {
original = register A
A <- xor(original, u8($FF))
apply COMPLEMENT()
}
family SCF "00 110 111" {
apply SETCARRY()
}
family CCF "00 111 111" {
carry = flag C
apply COMPLEMENTCARRY(carry)
}
Decimal adjustment
DAA (00 100 111) corrects a packed-BCD result. Read original A, H, C, then N.
The low correction is $06 when the low digit exceeds nine or incoming H is
set; the high correction is $60 when A exceeds $99 or incoming C is set.
Incoming N selects subtraction or addition of that correction. These rules
also define the model's result for inputs that are not valid packed BCD.
Replace the complete flags object with S/Z/H/PV/N/C, temporarily clearing N; write A, then restore the captured N. H is bit 4 of original A XOR result. P/V reports even parity. This explicit ordering preserves the established model contract, including observers that fail during state writes.
policy DECIMAL "replace DAA result flags" (original: 8, result: 8, decimalCarry: flag) {
S = negative(result)
Z = zero(result)
H = bit(xor(original, result), 4)
PV = evenParity(result)
N = 0
C = decimalCarry
}
policy SUBTRACTION "restore N" (subtract: flag) {
N = subtract
}
family DAA "00 100 111" {
original = register A
half = flag H
carry = flag C
subtract = flag N
adjustLow: flag = or(not(lessThan(and(original, u8($0F)), u8($0A), unsigned)), half)
adjustHigh: flag = or(not(lessThan(original, u8($9A), unsigned)), carry)
correction = or(select(adjustLow, u8($06), u8(0)), select(adjustHigh, u8($60), u8(0)))
result = select(subtract, subtract(original, correction), add(original, correction))
replace DECIMAL(original, result, adjustHigh)
A <- result
apply SUBTRACTION(subtract)
}
The manual, printed pages 173–174, describes valid packed-decimal adjustment. Explicit snapshots also allow arbitrary A/H/N/C; the model uses the same thresholds in addition and subtraction mode for these states, checked against independent references. The decimal-total example combines DAA, bank exchanges, word addition, conditional jumps, and RST.
External interrupt delivery
The caller offers one selected request at a completed boundary:
cpu.interrupt("irq", acknowledge); // acknowledge(): number supplies device bytes
cpu.interrupt("nmi");
The caller owns pending IRQ levels, NMI edges, and priority when both are pending. Ignored offers are not queued. IRQ requires a callback even in mode 1; it is never called for ignored requests. Invalid sources or a non-function IRQ callback throw before changing state.
| Request | Acceptance | Entry |
|---|---|---|
| IRQ | IFF1 set and interruptDeferred clear |
Release HALT, clear both IFFs, increment R once, and acknowledge a byte |
| NMI | nmiDeferred clear, independent of IFF1 and EI inhibition |
Release HALT, clear IFF1, preserve IFF2, increment R once, push PC and jump to 0066 |
NMI sets nmiDeferred, requiring one retired instruction before another NMI
can be accepted. This survives reconstruction, allowing nested NMI after an
instruction without losing the saved IFF2. IRQ inhibition is consumed by the
next instruction, not by NMI entry itself. The two latches express recognition
at this API's boundaries; they are not additional physical CPU registers.
| IRQ mode | Meaning of supplied byte | Remaining entry accesses |
|---|---|---|
| 0 | First opcode of an externally supplied instruction | Decode and execute through the ordinary handlers; request further instruction bytes as needed |
| 1 | Acknowledged and ignored | Push PC high then low and jump to 0038 |
| 2 | Low byte of a vector address whose high byte is I | Push PC high then low, then read the vector's low/high target bytes and jump |
Mode 2 uses all eight supplied bits, including odd vectors and FF. Both the
stack and vector's second-byte address wrap at 16 bits. Stack writes precede
vector reads, so overlapping locations supply the newly written values. The
reference implementation's interrupt paths
and hardware-oriented interrupt notes
confirm that ordering and the use of odd vectors despite the manual's even-address requirement.
Mode 0 instruction stream
All instruction bytes come from acknowledge(), including prefixes,
displacements, and immediate operands. Fetching them does not advance PC.
Ordinary data memory and ports still use the connected RAM and byte ports.
Each actual opcode fetch advances R: once unprefixed, twice for the supported
prefix pages; the displacement/final opcode of indexed CB do not advance R.
This applies to consumed prefix bytes even when the supplied encoding is rejected.
CALL/RST therefore save the interrupted PC, and relative branches use it as their base. Repeating block instructions still subtract two from PC; a later ordinary step fetches RAM at that address. No instruction stream is retained between calls. All 698 documented forms use the shared decoder and handlers; unsupported aliases and ignored prefixes have the same restrictions as normal execution. Acceptance effects remain if an encoding is unsupported.
The external stream and stationary PC follow Zilog's 1978 interrupt-structure application note as discussed and independently checked by the full mode-0 reference implementation. The callback abstracts the device's complete supplied stream. Operand reads are not claimed to be electrical interrupt-acknowledge cycles.
Interrupt records and failures
CpuZ80InterruptRecord contains detached before/after snapshots, the selected
source, and ordered CpuZ80InterruptAccess entries. External instruction/vector
bytes appear as { kind: "acknowledge", value }, alongside memory and port
accesses. The record has one of these outcomes:
ignored:instruction: null, empty accesses, and reasondisabledordeferred. Disabled IFF1 takes precedence over IRQ deferral.accepted: NMI or mode-1/2 entry, withinstruction: null.executedorhalted: mode-0 execution, with an instruction containingsource: "interrupt"and its actualbytes, without a fabricated RAM address.unsupported: rejected mode-0 encoding, the supplied instruction bytes, andreason: "opcode".
Acknowledged values must be integers in 0–255, including the ignored mode-1 byte. Callback errors and invalid values propagate without a returned record. Acceptance already cleared the appropriate IFFs, released HALT, and advanced R; those effects remain. A failing subsequent opcode fetch also retains its R increment. Successful bytes and device effects are never rolled back.
A failing push retains SP's predecrement for the attempted write and any earlier write. Mode-2 vector-read failure leaves the saved frame intact and PC unchanged. A return-read failure retains only successful pop increments, without restoring IFF1 or notifying the device. No API resumes a partially failed entry or return.
All mutating operations share the same execution guard. RAM, port,
acknowledgement, and RETI callbacks may inspect snapshots but cannot call
step(), reset(), or interrupt() recursively on the same CPU. The guard
clears after success or failure; external device state is restored separately.
Recognition limits
This is instruction-level delivery, without electrical sampling, wait states, bus arbitration, or refresh cycles. NMI's ignored opcode-fetch cycle is not recorded as a data read. The previously excluded NMOS LD A,I/R interrupt-time P/V quirk remains unmodeled, as do undocumented instructions, F bits 3/5, and hidden internal latches. Complete opcode coverage does not imply cycle accuracy or a complete peripheral interrupt controller.
The entry actions below make acceptance and stack effects explicit.
action acceptNmi "accept NMI and preserve IFF2" () {
STOPPED <- 0
IFF1 <- 0
perform refresh(u8(1))
NMIDEFERRED <- 1
}
action acceptIrq "accept IRQ before acknowledgement" () {
STOPPED <- 0
IFF1 <- 0
perform refresh(u8(1))
IFF2 <- 0
IRQDEFERRED <- 0
}
action enterNmi "push PC and enter NMI" () using memory {
perform call(u16($0066))
}
After acknowledgement, IM selects a supplied instruction or a vector entry.
Mode 1 ignores the byte and calls $0038. Mode 2 pushes PC before forming the
vector address from current I and the supplied byte; then it reads the target
low byte first. This preserves stack/vector overlap and each partial failure.
The low vector byte may be odd, and its following address wraps at sixteen bits.
action enterVector "push PC before selecting and reading the IRQ vector" (byte: 8) using memory {
returnPC = register PC
perform pushWord(returnPC)
modeOne = choice IM = 1
when modeOne {
PC <- u16($0038)
}
when not(modeOne) {
high = register I
address = concat(high, byte)
low = memory(address)
highTarget = memory(add(address, u16(1)))
PC <- concat(highTarget, low)
}
}
Execution and public interface
The entry table makes recognition order, acceptance actions, and exhaustive
mode selection reviewable alongside the hardware explanation. supplied
selects the stationary-PC stream described above; action entries receive the
acknowledged byte. The public interface declares both banks' detached pair
views and the names of the public bank types.
execution {
memory 16
counter NEXT write setPC
stopped STOPPED
word little
opcode advance on decode with action refresh
operand advance after read
failure retain
reset action reset
retire irq into IRQDEFERRED then action afterRetirement
notify reti after retire
interrupt entries {
source irq acknowledge {
when latch IFF1 otherwise "disabled"
unless latch IRQDEFERRED otherwise "deferred"
accept action acceptIrq
select IM {
case 0 supplied
case 1 action enterVector
case 2 action enterVector
}
}
source nmi {
unless latch NMIDEFERRED otherwise "deferred"
accept action acceptNmi
enter action enterNmi
}
}
}
interface CpuZ80 "Instruction-level Zilog Z80 with documented opcodes and boundary IRQ/NMI delivery." {
bank RegisterBank = alternate snapshot BankSnapshot
snapshot bc = BC
snapshot de = DE
snapshot hl = HL
snapshot alternate.bc = BC_ALT
snapshot alternate.de = DE_ALT
snapshot alternate.hl = HL_ALT
}
Port input and output
The Z80 places a full 16-bit address on the bus during I/O. Devices may decode only the low byte, but that choice belongs to the connection. The CPU records and passes the entire address, including the high byte's instruction-specific source. IN/OUT perform one transfer after fetching both instruction bytes:
| Forms | Port address | Transfer and flags |
|---|---|---|
| IN A,(n) / OUT (n),A | Old A as the high byte, immediate n as the low byte | Load/output A; preserve all six flags |
| IN r,(C) | Old BC, even when B or C is the destination | Load B/C/D/E/H/L/A; S/Z and even-parity P/V describe the input, H/N clear, C is preserved |
| OUT (C),r | BC | Output B/C/D/E/H/L/A; preserve all flags |
The undocumented register selector 110 (IN (C) / OUT (C),0) remains unsupported.
See the Zilog manual, printed pages 295–300 and 306–309.
An absent connection throws only when an instruction attempts port I/O. Ordinary instructions, unsupported attempts, halted steps, and reset do not call devices. Inputs must return integers in 0–255; invalid values throw before changing their destination or recording a completed input. Device and RAM exceptions propagate without returning a step record or rolling back earlier effects. Device-side effects, including consuming input before returning an invalid byte or throwing, remain the device's responsibility.
PC/R commit after a complete supported opcode is decoded. An immediate operand fetch advances PC only on success. Block I/O decrements B after its first transfer succeeds and before its second transfer: a failed output or memory write therefore leaves the decremented B visible. HL, flags, and repeat-PC updates occur only after both transfers succeed. This defines host-failure behavior at instruction level, without claiming hardware bus-fault handling.
RAM/device callbacks may inspect detached snapshots. Nested step(),
reset(), or interrupt() calls on the same CPU throw before changing state.
The execution guard clears even when a callback throws. Access records retain
captured values; subsequent execution, device changes, reset, or caller edits to other records
do not alter them.
Immediate ports and interrupt controls
OUT (n),A and IN A,(n) use 11 01 d 011: d selects output (0) or input (1).
Both capture old A for address bits 15..8 before fetching the low address byte.
OUT then rereads live A for the transferred value; IN writes A only after a
successful device read. Neither instruction accesses flags. Addressing devices
and retaining device state remain the machine's responsibility.
source immediatePort "old A and immediate port byte": 16 {
high = register A
low = fetch
return concat(high, low)
}
family OUT "11 01 0 011" named "OUT (n),A" {
port = source immediatePort
byte = register A
port(port) <- byte
}
family IN "11 01 1 011" named "IN A,(n)" {
port = source immediatePort
byte = port(port)
A <- byte
}
NOP changes nothing after opcode fetching. HALT sets STOPPED and retires normally; further steps stop until reset or an accepted interrupt releases it. DI and EI write IFF2 before IFF1. EI requests IRQ inhibition through the following instruction. The specification-bound execution runtime commits that request at successful retirement.
family NOP "00 000 000" {
}
family HALT "01 110 110" {
STOPPED <- 1
}
family DI "11 11 0 011" {
IFF2 <- 0
IFF1 <- 0
}
family EI "11 11 1 011" {
IFF2 <- 1
IFF1 <- 1
defer irq
}
CB rotations, shifts, and individual bits
A CB prefix gives the following byte the layout xx yyy rrr. The byte
catalogue still selects B/C/D/E/H/L/(HL)/A with rrr. For xx=00, yyy selects a
rotation or shift; for xx=01/10/11 it selects the bit tested, cleared, or set.
The undocumented SLL selector is deliberately absent.
| xx | yyy | Operation | Bit inserted into result | New C |
|---|---|---|---|---|
| 00 | 000 | RLC | old bit 7 | old bit 7 |
| 00 | 001 | RRC | old bit 0 | old bit 0 |
| 00 | 010 | RL | C | old bit 7 |
| 00 | 011 | RR | C | old bit 0 |
| 00 | 100 | SLA | zero | old bit 7 |
| 00 | 101 | SRA | old bit 7 | old bit 0 |
| 00 | 111 | SRL | zero | old bit 0 |
| 01 | bbb | BIT | no writeback; test bit bbb | unchanged |
| 10 | bbb | RES | bit bbb becomes zero | unchanged |
| 11 | bbb | SET | bit bbb becomes one | unchanged |
The page declaration owns ordinary CB dispatch. The shared execution runtime fetches and validates both opcode bytes before committing PC and refresh changes. An unsupported SLL form records both bytes but leaves PC and R unchanged during an ordinary step. Interrupt-supplied instructions retain their separate acknowledgement and refresh rules described above.
page CB = $CB
policy SHIFT "Z80 CB rotation and shift flags" (result: 8, carry: flag) {
S = negative(result)
Z = zero(result)
H = 0
PV = evenParity(result)
N = 0
C = carry
}
Each rotation or shift captures the operand before reading C, when needed. It updates S/Z/H/PV/N/C before writeback, unlike the accumulator-only rotates. For memory, capture HL once and reuse that address for both accesses. A failed read prevents flag updates and writeback; a failed write retains the new flags. The resolved-memory actions also serve indexed CB instructions, whose prefix and displacement layout is declared below. Alternate registers and control state are preserved.
RLC
Rotate left by one bit, returning old bit 7 to bit 0 and to C.
action rlcMemory "RLC memory" (address: 16) using memory {
original = memory(address)
result = shiftLeft(original, negative(original))
apply SHIFT(result, negative(original))
memory(address) <- result
}
family RLC "00 000 rrr" on CB for r in bytes named "RLC {r}" except "00 000 110" {
original = operand r
result = shiftLeft(original, negative(original))
apply SHIFT(result, negative(original))
operand r <- result
}
family RLCMemory "00 000 110" on CB named "RLC (HL)" {
address = source HL
perform rlcMemory(address)
}
RRC
Rotate right by one bit, returning old bit 0 to bit 7 and to C.
action rrcMemory "RRC memory" (address: 16) using memory {
original = memory(address)
result = shiftRight(original, lowBit(original))
apply SHIFT(result, lowBit(original))
memory(address) <- result
}
family RRC "00 001 rrr" on CB for r in bytes named "RRC {r}" except "00 001 110" {
original = operand r
result = shiftRight(original, lowBit(original))
apply SHIFT(result, lowBit(original))
operand r <- result
}
family RRCMemory "00 001 110" on CB named "RRC (HL)" {
address = source HL
perform rrcMemory(address)
}
RL
Rotate left through C. Capture the byte before reading C; old bit 7 becomes the new carry and the captured carry enters bit 0.
action rlMemory "RL memory" (address: 16) using memory {
original = memory(address)
carry = flag C
result = shiftLeft(original, carry)
apply SHIFT(result, negative(original))
memory(address) <- result
}
family RL "00 010 rrr" on CB for r in bytes named "RL {r}" except "00 010 110" {
original = operand r
carry = flag C
result = shiftLeft(original, carry)
apply SHIFT(result, negative(original))
operand r <- result
}
family RLMemory "00 010 110" on CB named "RL (HL)" {
address = source HL
perform rlMemory(address)
}
RR
Rotate right through C. Capture the byte before reading C; old bit 0 becomes the new carry and the captured carry enters bit 7.
action rrMemory "RR memory" (address: 16) using memory {
original = memory(address)
carry = flag C
result = shiftRight(original, carry)
apply SHIFT(result, lowBit(original))
memory(address) <- result
}
family RR "00 011 rrr" on CB for r in bytes named "RR {r}" except "00 011 110" {
original = operand r
carry = flag C
result = shiftRight(original, carry)
apply SHIFT(result, lowBit(original))
operand r <- result
}
family RRMemory "00 011 110" on CB named "RR (HL)" {
address = source HL
perform rrMemory(address)
}
SLA
Shift left by one bit, inserting zero at bit 0. Old bit 7 becomes C.
action slaMemory "SLA memory" (address: 16) using memory {
original = memory(address)
result = shiftLeft(original, 0)
apply SHIFT(result, negative(original))
memory(address) <- result
}
family SLA "00 100 rrr" on CB for r in bytes named "SLA {r}" except "00 100 110" {
original = operand r
result = shiftLeft(original, 0)
apply SHIFT(result, negative(original))
operand r <- result
}
family SLAMemory "00 100 110" on CB named "SLA (HL)" {
address = source HL
perform slaMemory(address)
}
SRA
Shift right while retaining the original sign bit. Old bit 0 becomes C.
action sraMemory "SRA memory" (address: 16) using memory {
original = memory(address)
result = shiftRight(original, negative(original))
apply SHIFT(result, lowBit(original))
memory(address) <- result
}
family SRA "00 101 rrr" on CB for r in bytes named "SRA {r}" except "00 101 110" {
original = operand r
result = shiftRight(original, negative(original))
apply SHIFT(result, lowBit(original))
operand r <- result
}
family SRAMemory "00 101 110" on CB named "SRA (HL)" {
address = source HL
perform sraMemory(address)
}
SRL
Shift right by one bit, inserting zero at bit 7. Old bit 0 becomes C.
action srlMemory "SRL memory" (address: 16) using memory {
original = memory(address)
result = shiftRight(original, 0)
apply SHIFT(result, lowBit(original))
memory(address) <- result
}
family SRL "00 111 rrr" on CB for r in bytes named "SRL {r}" except "00 111 110" {
original = operand r
result = shiftRight(original, 0)
apply SHIFT(result, lowBit(original))
operand r <- result
}
family SRLMemory "00 111 110" on CB named "SRL (HL)" {
address = source HL
perform srlMemory(address)
}
See the manual, printed pages 213–237 and 243–264. It leaves S/PV unspecified for BIT; the following policy uses independently checked behavior: PV equals Z, and S is set only for a set bit 7.
BIT, RES, and SET
The three bit families share the same bit-to-mask catalogue. BIT reads without writing, sets Z and P/V when the selected bit is clear, sets S only when testing a set bit 7, sets H, and clears N. It preserves C without reading it. RES and SET do not access flags; they write the result even if the selected bit already has the requested value. Every memory form retains its captured address, and a failed read prevents subsequent effects.
codes bitMasks: 8 {
000 "0" = $01
001 "1" = $02
010 "2" = $04
011 "3" = $08
100 "4" = $10
101 "5" = $20
110 "6" = $40
111 "7" = $80
}
policy BITFLAGS "Z80 BIT, preserving C" (result: 8) {
S = negative(result)
Z = zero(result)
H = 1
PV = zero(result)
N = 0
}
BIT isolates the selected bit and applies the test flags without writeback.
action bitMemory "BIT at a resolved address" (address: 16, mask: 8) using memory {
original = memory(address)
result = and(original, mask)
apply BITFLAGS(result)
}
family BIT "01 bbb rrr" on CB for b in bitMasks.read, r in bytes named "BIT {b},{r}" except "01 xxx 110" {
mask = source b
original = operand r
result = and(original, mask)
apply BITFLAGS(result)
}
family BITMemory "01 bbb 110" on CB for b in bitMasks.read named "BIT {b},(HL)" {
address = source HL
mask = source b
perform bitMemory(address, mask)
}
RES clears the selected bit with the complement of its mask. Preserve every flag, and write the result even if unchanged.
action resMemory "RES at a resolved address" (address: 16, mask: 8) using memory {
original = memory(address)
result = and(original, xor(mask, u8($FF)))
memory(address) <- result
}
family RES "10 bbb rrr" on CB for b in bitMasks.read, r in bytes named "RES {b},{r}" except "10 xxx 110" {
mask = source b
original = operand r
result = and(original, xor(mask, u8($FF)))
operand r <- result
}
family RESMemory "10 bbb 110" on CB for b in bitMasks.read named "RES {b},(HL)" {
address = source HL
mask = source b
perform resMemory(address, mask)
}
SET combines the byte with its mask. Preserve every flag, and write the result even if unchanged.
action setMemory "SET at a resolved address" (address: 16, mask: 8) using memory {
original = memory(address)
result = or(original, mask)
memory(address) <- result
}
family SET "11 bbb rrr" on CB for b in bitMasks.read, r in bytes named "SET {b},{r}" except "11 xxx 110" {
mask = source b
original = operand r
result = or(original, mask)
operand r <- result
}
family SETMemory "11 bbb 110" on CB for b in bitMasks.read named "SET {b},(HL)" {
address = source HL
mask = source b
perform setMemory(address, mask)
}
ED word, block, and control instructions
The ED prefix selects a second byte with two main layouts. 01 yyy zzz
contains register ports, word arithmetic/transfers, special registers, and
controls. 101 r d 0oo selects a block operation: r requests repetition,
d selects increment/decrement, and oo selects copy/search/input/output.
Only the 58 documented forms are included. Other ED bytes remain unsupported;
they are not treated as NOPs or aliases.
page ED = $ED
ADC and SBC on words
01 pp q 010 selects BC/DE/HL/SP with pp and SBC/ADC with q. Read the
source before HL, then capture C. Write H and L before updating flags in
S/Z/H/PV/N/C order. Unlike ADD HL,ss, these operations update S/Z/PV. H uses
the carry or borrow across bit 11, including incoming C; P/V reports signed
overflow. The alternate bank remains untouched.
policy WORDADC "Z80 ADC word flags (H at bit 11)" (left: 16, right: 16, result: 16, carry: flag) {
S = negative(result)
Z = zero(result)
H = bit(xor(xor(left, right), result), 12)
PV = addOverflow(left, right, carry)
N = 0
C = carry(left, right, carry)
}
policy WORDSBC "Z80 SBC word flags (H at bit 11)" (left: 16, right: 16, result: 16, carry: flag) {
S = negative(result)
Z = zero(result)
H = bit(xor(xor(left, right), result), 12)
PV = overflow(left, right, carry)
N = 1
C = borrow(left, right, carry)
}
family ADCWord "01 pp 1 010" on ED for p in words named "ADC HL,{p}" {
right = operand p
left = source HL
carry = flag C
result = add(left, right, carry)
perform writeHL(result)
apply WORDADC(left, right, result, carry)
}
family SBCWord "01 pp 0 010" on ED for p in words named "SBC HL,{p}" {
right = operand p
left = source HL
carry = flag C
result = subtract(left, right, carry)
perform writeHL(result)
apply WORDSBC(left, right, result, carry)
}
Absolute word transfers
01 pp d 011 stores (d=0) or loads (d=1) BC/DE/HL/SP. Fetch the complete
address before reading a register or data memory. Memory accesses are low byte
then high byte, wrapping after $FFFF; pair reads/writes are high byte first.
A failed load preserves its destination, while a failed second store retains
the first write. All flags are preserved. The ED HL forms have the same
behavior as the unprefixed HL transfers.
family EDStoreWord "01 pp 0 011" on ED for p in words named "LD (nn),{p}" {
address = source immediateWord
result = operand p
memory(address) <- lowByte(result)
memory(add(address, u16(1))) <- highByte(result)
}
family EDLoadWord "01 pp 1 011" on ED for p in words named "LD {p},(nn)" {
address = source immediateWord
low = memory(address)
high = memory(add(address, u16(1)))
operand p <- concat(high, low)
}
NEG and special registers
NEG subtracts captured A from zero. Replace the complete flags object before writing A, with nibble/byte borrow, signed overflow, and N set. It reads no incoming flags. Only $44 is included; undocumented aliases remain unsupported.
policy NEGFLAGS "Z80 NEG" (original: 8, result: 8) {
S = negative(result)
Z = zero(result)
H = halfBorrow(u8(0), original)
PV = overflow(u8(0), original)
N = 1
C = borrow(u8(0), original)
}
family NEG "01 000 100" on ED {
original = register A
result = subtract(u8(0), original)
replace NEGFLAGS(original, result)
A <- result
}
The special-register pattern is 01 0 d s 111: d=0 writes I/R from A and
d=1 reads I/R into A. All opcode refresh increments have already occurred.
Writing R replaces all eight bits, including bit 7. Reading into A captures
the special register, then IFF2, then C; it replaces flags before A. P/V
comes from IFF2, not parity. Writing I/R does not access flags or latches.
operands specialBytes {
0 "I" = register I
1 "R" = register R
}
policy SPECIALFLAGS "Z80 LD A,I/R" (result: 8, enabled: flag, carry: flag) {
S = negative(result)
Z = zero(result)
H = 0
PV = enabled
N = 0
C = carry
}
family writeSpecial "01 0 0 s 111" on ED for s in specialBytes named "LD {s},A" {
byte = register A
operand s <- byte
}
family readSpecial "01 0 1 s 111" on ED for s in specialBytes named "LD A,{s}" {
result = operand s
enabled = latch IFF2
carry = flag C
replace SPECIALFLAGS(result, enabled, carry)
A <- result
}
NEG overflows only for A=$80 and leaves C clear only for A=$00.
The NMOS interrupt-time P/V quirk of LD A,I/R remains unmodeled. See the
manual, printed pages 94–97, 106–109, 182, and 190–193.
Nibble rotations
RRD and RLD rotate three four-bit digits: A's low nibble and both nibbles at (HL). A's high nibble is retained. With A=$AB and (HL)=$CD, RRD produces A=$AD, (HL)=$BC; RLD produces A=$AC, (HL)=$DB.
Capture HL, read memory, then capture A. Write memory before reading C, replacing S/Z/parity with H/N clear, and writing A. A failed write therefore preserves A and flags. Later callbacks cannot change the captured address or result. The following policy also serves register port input.
policy INPUTFLAGS "Z80 byte input/result flags, preserving captured C" (result: 8, carry: flag) {
S = negative(result)
Z = zero(result)
H = 0
PV = evenParity(result)
N = 0
C = carry
}
family RRD "01 10 0 111" on ED {
address = source HL
memoryByte = memory(address)
accumulator = register A
result = or(and(accumulator, u8($F0)), and(memoryByte, u8($0F)))
memory(address) <- or(shiftBits(and(accumulator, u8($0F)), left, 4), shiftBits(memoryByte, right, 4))
carry = flag C
replace INPUTFLAGS(result, carry)
A <- result
}
family RLD "01 10 1 111" on ED {
address = source HL
memoryByte = memory(address)
accumulator = register A
result = or(and(accumulator, u8($F0)), shiftBits(memoryByte, right, 4))
memory(address) <- or(shiftBits(memoryByte, left, 4), and(accumulator, u8($0F)))
carry = flag C
replace INPUTFLAGS(result, carry)
A <- result
}
RLD/RRD are binary nibble operations for every input, with no decimal correction. See the manual, printed pages 238–242.
Block copies and searches
Each step performs one iteration. Repeating forms rewind current PC by two while another iteration is required, so the next step refetches both opcode bytes and exposes an interrupt boundary. BC starts as a sixteen-bit count; zero wraps to $FFFF on the first iteration. Direction is a wrapped word delta. The single/repeat sources bind encoding choices without accessing state.
codes directions: 16 {
0 "I" = $0001
1 "D" = $FFFF
}
source once "single iteration": 8 {
return u8(0)
}
source repeating "repeat while count remains": 8 {
return u8(1)
}
action rewindBlock "rewind current PC for another block iteration" () {
pc = register PC
PC <- subtract(pc, u16(2))
}
policy COPYFLAGS "Z80 block copy" (count: 16) {
N = 0
H = 0
PV = not(zero(count))
}
policy SEARCHFLAGS "Z80 block search" (accumulator: 8, byte: 8, result: 8, count: 16, carry: flag) {
S = negative(result)
Z = zero(result)
H = halfBorrow(accumulator, byte)
PV = not(zero(count))
N = 1
C = carry
}
LDI/LDD/LDIR/LDDR read (HL), then capture BC minus one. Capture DE and write the byte before rereading and adjusting DE. Clear N/H and set P/V from the captured remaining count, preserving S/Z/C. Reread and adjust HL, then write captured BC. Failed accesses retain completed effects and prevent later updates. Repeating copies rewind PC only when the captured count is nonzero.
family blockCopy {
encoding "101 0 d 00 0" on ED for d in directions.read with r = once named "LD{d}"
encoding "101 1 d 00 0" on ED for d in directions.read with r = repeating named "LD{d}R"
delta = source d
repeat = source r
address = source HL
byte = memory(address)
counter = source BC
count = subtract(counter, u16(1))
destination = source DE
memory(destination) <- byte
destinationAfterWrite = source DE
perform writeDE(add(destinationAfterWrite, delta))
apply COPYFLAGS(count)
addressAfterTransfer = source HL
perform writeHL(add(addressAfterTransfer, delta))
perform writeBC(count)
when not(zero(repeat)) {
when not(zero(count)) {
perform rewindBlock()
}
}
}
CPI/CPD/CPIR/CPDR read (HL), capture BC minus one, then read A and C. Replace comparison flags while preserving A and captured C; P/V indicates remaining count. Reread and adjust HL before writing captured BC. Repeating searches inspect the updated Z only when the count is nonzero; an unmatched byte then rewinds PC. A failed read prevents all later effects.
family blockSearch {
encoding "101 0 d 00 1" on ED for d in directions.read with r = once named "CP{d}"
encoding "101 1 d 00 1" on ED for d in directions.read with r = repeating named "CP{d}R"
delta = source d
repeat = source r
address = source HL
byte = memory(address)
counter = source BC
count = subtract(counter, u16(1))
accumulator = register A
result = subtract(accumulator, byte)
carry = flag C
replace SEARCHFLAGS(accumulator, byte, result, count, carry)
addressAfterTransfer = source HL
perform writeHL(add(addressAfterTransfer, delta))
perform writeBC(count)
when not(zero(repeat)) {
when not(zero(count)) {
matched = flag Z
when not(matched) {
perform rewindBlock()
}
}
}
}
An initial BC of zero allows 65,536 repeating copy iterations; searches can stop earlier on a match. A last-byte match sets Z with P/V clear, while an earlier match sets both. Overlapping copies propagate bytes in their selected direction. Caller budgets count iterations and can pause even a zero-count block; changed code/data affects the next refetch. The indexed-buffer example checks copy/search traces and resumption at every boundary. See the manual, printed pages 130–144.
Register port transfers
01 rrr 00 d selects input (d=0) or output (d=1). The rrr=110 slot is
undocumented and excluded. Capture BC before any transfer. Input reads the
port before sampling C, replacing flags, and writing the selected register;
P/V is parity. A failed input leaves flags and registers unchanged. Output
captures the selected byte after BC and does not access flags.
family inputRegister "01 rrr 00 0" on ED for r in bytes named "IN {r},(C)" except "01 110 00 0" {
port = source BC
result = port(port)
carry = flag C
replace INPUTFLAGS(result, carry)
operand r <- result
}
family outputRegister "01 rrr 00 1" on ED for r in bytes named "OUT (C),{r}" except "01 110 00 1" {
port = source BC
result = operand r
port(port) <- result
}
Block port transfers
Block I/O transfers one byte per step. Its eight-bit B counter permits 256 iterations when initially zero. Both directions capture HL before the first read, decrement live B after that read, then perform the write. A failed write retains the decrement but prevents HL and flag updates. Input uses BC before decrementing B; output uses BC afterward. Successful transfers advance the captured HL, with word wraparound.
The flag addend is adjusted C for input and updated L for output. H/C report carry from the transferred byte plus that addend. S/Z use live B, N uses the transferred sign, and P/V uses parity of the sum's low three bits XOR a second capture of live B. Replace flags in S/Z/H/C/N/PV order.
policy BLOCKIOFLAGS "Z80 block I/O" (result: 8, byte: 8, right: 8, parityCounter: 8) {
S = negative(result)
Z = zero(result)
H = carry(byte, right)
C = carry(byte, right)
N = negative(byte)
PV = evenParity(xor(and(add(byte, right), u8(7)), parityCounter))
}
policy REPEATHALF "repeat half-carry correction" (repeatCounter: 8, subtract: flag) {
H = equal(and(repeatCounter, u8(15)), select(subtract, u8(0), u8(15)))
}
policy REPEATPARITY "repeat parity correction" (parity: flag, operand: 8) {
PV = not(xor(parity, evenParity(and(operand, u8(7)))))
}
action correctRepeatParity "correct parity between block I/O iterations" (operand: 8) {
parity = flag PV
apply REPEATPARITY(parity, operand)
}
action repeatIo "repeat block I/O when live B is nonzero" () {
remaining = register B
when not(zero(remaining)) {
perform rewindBlock()
repeatCounter = register B
carry = flag C
when carry {
subtract = flag N
adjusted = select(subtract, subtract(repeatCounter, u8(1)), add(repeatCounter, u8(1)))
halfSubtract = flag N
apply REPEATHALF(repeatCounter, halfSubtract)
perform correctRepeatParity(adjusted)
}
when not(carry) {
perform correctRepeatParity(repeatCounter)
}
}
}
Repeating forms reread B after flag replacement. If it is nonzero they rewind live PC, then correct H/PV for the repeat phase. These intermediate flags are visible in snapshots. The correction reads B, C, and, when carry is set, N twice; it finally reads the current P/V. The ordering is explicit rather than folded into the transfer policy.
Input reads the port at old BC, decrements B, and writes the captured byte to the captured HL. Only after success does it advance HL and read C for the flag addend. INIR/INDR then apply the repeat phase.
family blockInput {
encoding "101 0 d 01 0" on ED for d in directions.read with r = once named "IN{d}"
encoding "101 1 d 01 0" on ED for d in directions.read with r = repeating named "IN{d}R"
delta = source d
repeat = source r
address = source HL
inputPort = source BC
byte = port(inputPort)
counter = register B
B <- subtract(counter, u8(1))
memory(address) <- byte
perform writeHL(add(address, delta))
addend = register C
right = add(addend, lowByte(delta))
result = register B
parityCounter = register B
replace BLOCKIOFLAGS(result, byte, right, parityCounter)
when not(zero(repeat)) {
perform repeatIo()
}
}
Output reads the byte at captured HL, decrements B, then reads the new BC for the port write. After success it advances HL and reads updated L for the flag addend. OTIR/OTDR then apply the same repeat phase.
family blockOutput {
encoding "101 0 d 01 1" on ED for d in directions.read with r = once named "OUT{d}"
encoding "101 1 d 01 1" on ED for d in directions.read with r = repeating named "OT{d}R"
delta = source d
repeat = source r
address = source HL
byte = memory(address)
counter = register B
B <- subtract(counter, u8(1))
outputPort = source BC
port(outputPort) <- byte
perform writeHL(add(address, delta))
right = register L
result = register B
parityCounter = register B
replace BLOCKIOFLAGS(result, byte, right, parityCounter)
when not(zero(repeat)) {
perform repeatIo()
}
}
When a repeating form continues, its extra repeat phase further changes H/PV:
| C | N | Parity adjustment | H after the repeat phase |
|---|---|---|---|
| 0 | Either | B | 0 |
| 1 | 0 | B + 1 | 1 exactly when B's low nibble is F |
| 1 | 1 | B − 1 | 1 exactly when B's low nibble is 0 |
Even parity of the adjustment's low three bits preserves the preceding P/V; odd parity inverts it. S/Z/N/C do not change during this repeat phase. The final iteration skips these extra H/PV changes. These intermediate flags are visible in snapshots and can be preserved by an interrupt handler between iterations.
The manual's block-I/O flag summaries are incomplete. This model follows the observed NMOS flag behavior described by David Banks's hardware investigation, including the repeat phase, and checked against an independent emulator below. It continues to omit F bits 3/5, hidden internal latches, and cycle timing. The Zilog manual, printed pages 301–305 and 310–315, provides the documented transfers and repetition rules.
Interrupt controls and retirement
DI (F3) clears IFF1/IFF2. EI (FB) sets both and sets interruptDeferred.
IM 0/1/2 (ED 46/56/5E) select the interrupt mode without changing IFFs or flags.
Only these documented mode encodings are supported. See the
Zilog manual, printed pages 17–20 and 182–186.
A successful instruction consumes old interruptDeferred and nmiDeferred;
EI renews IRQ inhibition instead. HALT and each repeating-block iteration
count as one retirement. Repeated EI therefore continues to defer IRQ.
Already halted steps, unsupported attempts, ignored interrupt offers, and
instructions that throw before retirement consume neither latch. Reset clears
both. These required Boolean fields also belong in explicit .machine state;
the existing examples initialize them to false.
RETN (ED 45) and RETI (ED 4D) pop PC low byte first, advance SP twice with
wrapping, and copy IFF2 to IFF1 without changing flags or IFF2. If the two IFFs
differed before the return, interruptDeferred is set through the following
instruction. Thus EI followed by RETI permits IRQ on return, while a return
that restores IFF1 after NMI first allows one instruction in the resumed code.
This distinction follows the interrupt-acceptance research.
RETI additionally calls onReti after PC/SP, IFF1, and inhibition latches have
committed. The callback may inspect that completed state. A callback failure
propagates without undoing the retired return; stack-read failure prevents both
IFF restoration and notification. No callback is required when no device needs
the notification. The same notice is emitted for RETI supplied in mode 0;
this is an architectural policy, not emulation of a peripheral's bus decoder.
IM selects a stored mode without changing flags, IFF1, or IFF2. In
01 0 mm 110, only mm=00/10/11 are documented, selecting modes 0/1/2.
The entry table uses that choice after IRQ acknowledgement.
family IM0 "01 0 00 110" on ED named "IM 0" {
IM <- 0
}
family IM1 "01 0 10 110" on ED named "IM 1" {
IM <- 1
}
family IM2 "01 0 11 110" on ED named "IM 2" {
IM <- 2
}
RETN and RETI pop and commit the complete PC before reading IFF1 and IFF2. If the latches differ, request IRQ deferral at retirement; then reread IFF2 into IFF1. Both preserve flags and completed stack effects on failure. RETI also requests device notification after architectural retirement. The shared runtime performs that callback: its failure cannot undo the completed return.
family RETN "01 00 0 101" on ED {
target = source popWord
PC <- target
enabled = latch IFF1
saved = latch IFF2
when xor(enabled, saved) {
defer irq
}
restored = latch IFF2
IFF1 <- restored
}
family RETI "01 00 1 101" on ED {
target = source popWord
PC <- target
enabled = latch IFF1
saved = latch IFF2
when xor(enabled, saved) {
defer irq
}
restored = latch IFF2
IFF1 <- restored
notify reti
}
Indexed instructions: DD and FD
DD and FD select IX and IY respectively. Only the documented forms below belong to these pages: unrelated opcodes, repeated prefixes, and IXH/IXL/IYH/IYL forms remain unsupported. A prefix does not generally turn every HL reference into an index reference. In byte transfers, H and L remain the real H and L registers.
page DD = $DD
page FD = $FD
The word forms use the index register directly, without a displacement. For
ADD index,rr (00 pp 1 001), pp=10 selects the same index as the destination;
the other choices remain BC, DE, and SP. Read the source before the destination,
write the result, then update H/N/C while preserving S/Z/PV.
operands wordsIX {
00 "BC" = view BC write writeBC
01 "DE" = view DE write writeDE
10 "IX" = register IX
11 "SP" = register SP
}
operands wordsIY {
00 "BC" = view BC write writeBC
01 "DE" = view DE write writeDE
10 "IY" = register IY
11 "SP" = register SP
}
family ADDIndex {
encoding "00 pp 1 001" on DD for p in wordsIX with i = register IX named "ADD {i},{p}"
encoding "00 pp 1 001" on FD for p in wordsIY with i = register IY named "ADD {i},{p}"
right = operand p
left = operand i
result = add(left, right)
operand i <- result
apply WORDADD(left, right, result)
}
INC/DEC index (00 10 q 011) wrap at 16 bits without accessing flags or memory.
family INCIndex {
encoding "00 10 0 011" on DD with i = register IX named "INC {i}"
encoding "00 10 0 011" on FD with i = register IY named "INC {i}"
original = operand i
operand i <- add(original, u16(1))
}
family DECIndex {
encoding "00 10 1 011" on DD with i = register IX named "DEC {i}"
encoding "00 10 1 011" on FD with i = register IY named "DEC {i}"
original = operand i
operand i <- subtract(original, u16(1))
}
LD index,nn and LD index,(nn) replace the index only after both bytes have been fetched or read. LD (nn),index fetches the full address before capturing the index and writing low then high. Adjacent memory addresses wrap at FFFF; a failed second access retains the first. LD SP,index only copies the word.
family LDIndexImmediate {
encoding "0010 0001" on DD with i = register IX named "LD {i},nn"
encoding "0010 0001" on FD with i = register IY named "LD {i},nn"
word = source immediateWord
operand i <- word
}
family LDIndexMemory {
encoding "0010 1010" on DD with i = register IX named "LD {i},(nn)"
encoding "0010 1010" on FD with i = register IY named "LD {i},(nn)"
address = source immediateWord
low = memory(address)
high = memory(add(address, u16(1)))
operand i <- concat(high, low)
}
family STIndexMemory {
encoding "0010 0010" on DD with i = register IX named "LD (nn),{i}"
encoding "0010 0010" on FD with i = register IY named "LD (nn),{i}"
address = source immediateWord
word = operand i
memory(address) <- lowByte(word)
memory(add(address, u16(1))) <- highByte(word)
}
family LDStackIndex {
encoding "1111 1001" on DD with i = register IX named "LD SP,{i}"
encoding "1111 1001" on FD with i = register IY named "LD SP,{i}"
word = operand i
SP <- word
}
PUSH captures the index before either stack access. POP replaces it only after both reads and SP updates. JP (index) copies the index to PC without reading memory. EX (SP),index captures the index, then SP; it reads low/high, writes high/low, and finally replaces the index. Flags and SP are unchanged by EX, and failed accesses retain completed effects without replacing the index.
family PUSHIndex {
encoding "1110 0101" on DD with i = register IX named "PUSH {i}"
encoding "1110 0101" on FD with i = register IY named "PUSH {i}"
word = operand i
perform pushWord(word)
}
family POPIndex {
encoding "1110 0001" on DD with i = register IX named "POP {i}"
encoding "1110 0001" on FD with i = register IY named "POP {i}"
word = source popWord
operand i <- word
}
family JPIndex {
encoding "1110 1001" on DD with i = register IX named "JP ({i})"
encoding "1110 1001" on FD with i = register IY named "JP ({i})"
target = operand i
PC <- target
}
family EXStackIndex {
encoding "1110 0011" on DD with i = register IX named "EX (SP),{i}"
encoding "1110 0011" on FD with i = register IY named "EX (SP),{i}"
original = operand i
address = register SP
low = memory(address)
high = memory(add(address, u16(1)))
memory(add(address, u16(1))) <- highByte(original)
memory(address) <- lowByte(original)
operand i <- concat(high, low)
}
Displaced byte operands
For ordinary DD/FD memory forms, fetch the signed displacement after decoding has committed PC and the two opcode fetches to R. Only then read the live index, add the sign-extended displacement, and wrap to a 16-bit address. Capture that address once, including for read/modify/write operations and LD (index+d),n, whose immediate byte follows the displacement.
source indexedIXAddress "IX plus fetched signed displacement": 16 {
displacement = fetch
base = register IX
return add(base, signExtend(displacement, 16))
}
source indexedIYAddress "IY plus fetched signed displacement": 16 {
displacement = fetch
base = register IY
return add(base, signExtend(displacement, 16))
}
LD r,(index+d) and LD (index+d),r encode the real byte register in rrr. Exclude rrr=110: these pages have no memory-to-memory transfer or prefixed HALT. Stores do not read the destination; transfers preserve every flag.
family LDIndexed {
encoding "01 rrr 110" on DD for r in bytes with a = indexedIXAddress named "LD {r},(IX+d)" except "01 110 110"
encoding "01 rrr 110" on FD for r in bytes with a = indexedIYAddress named "LD {r},(IY+d)" except "01 110 110"
address = source a
byte = memory(address)
operand r <- byte
}
family STIndexed {
encoding "01 110 rrr" on DD for r in bytes with a = indexedIXAddress named "LD (IX+d),{r}" except "01 110 110"
encoding "01 110 rrr" on FD for r in bytes with a = indexedIYAddress named "LD (IY+d),{r}" except "01 110 110"
address = source a
byte = operand r
memory(address) <- byte
}
family STIndexedImmediate {
encoding "00 110 110" on DD with a = indexedIXAddress named "LD (IX+d),n"
encoding "00 110 110" on FD with a = indexedIYAddress named "LD (IY+d),n"
address = source a
byte = fetch
memory(address) <- byte
}
INC/DEC use the same flag and write ordering as their (HL) forms. The eight
accumulator operations (10 ooo 110) read the indexed byte before applying
their existing arithmetic or logical action; CP retains A. A failed memory
read prevents all subsequent register and flag effects.
family INCIndexed {
encoding "00 110 100" on DD with a = indexedIXAddress named "INC (IX+d)"
encoding "00 110 100" on FD with a = indexedIYAddress named "INC (IY+d)"
address = source a
perform incMemory(address)
}
family DECIndexed {
encoding "00 110 101" on DD with a = indexedIXAddress named "DEC (IX+d)"
encoding "00 110 101" on FD with a = indexedIYAddress named "DEC (IY+d)"
address = source a
perform decMemory(address)
}
family ADDIndexed {
encoding "10 000 110" on DD with a = indexedIXAddress named "ADD A,(IX+d)"
encoding "10 000 110" on FD with a = indexedIYAddress named "ADD A,(IY+d)"
address = source a
right = memory(address)
perform addAccumulator(right)
}
family ADCIndexed {
encoding "10 001 110" on DD with a = indexedIXAddress named "ADC A,(IX+d)"
encoding "10 001 110" on FD with a = indexedIYAddress named "ADC A,(IY+d)"
address = source a
right = memory(address)
perform adcAccumulator(right)
}
family SUBIndexed {
encoding "10 010 110" on DD with a = indexedIXAddress named "SUB (IX+d)"
encoding "10 010 110" on FD with a = indexedIYAddress named "SUB (IY+d)"
address = source a
right = memory(address)
perform subAccumulator(right)
}
family SBCIndexed {
encoding "10 011 110" on DD with a = indexedIXAddress named "SBC A,(IX+d)"
encoding "10 011 110" on FD with a = indexedIYAddress named "SBC A,(IY+d)"
address = source a
right = memory(address)
perform sbcAccumulator(right)
}
family ANDIndexed {
encoding "10 100 110" on DD with a = indexedIXAddress named "AND (IX+d)"
encoding "10 100 110" on FD with a = indexedIYAddress named "AND (IY+d)"
address = source a
right = memory(address)
perform andAccumulator(right)
}
family XORIndexed {
encoding "10 101 110" on DD with a = indexedIXAddress named "XOR (IX+d)"
encoding "10 101 110" on FD with a = indexedIYAddress named "XOR (IY+d)"
address = source a
right = memory(address)
perform xorAccumulator(right)
}
family ORIndexed {
encoding "10 110 110" on DD with a = indexedIXAddress named "OR (IX+d)"
encoding "10 110 110" on FD with a = indexedIYAddress named "OR (IY+d)"
address = source a
right = memory(address)
perform orAccumulator(right)
}
family CPIndexed {
encoding "10 111 110" on DD with a = indexedIXAddress named "CP (IX+d)"
encoding "10 111 110" on FD with a = indexedIYAddress named "CP (IY+d)"
address = source a
right = memory(address)
perform cpAccumulator(right)
}
Indexed bit operations: DD/FD CB d opcode
These four-byte encodings put the displacement before the final opcode. The prefix bytes are opcode fetches; displacement and final opcode are ordinary reads, so only the two prefixes increment R. The page declarations describe that order explicitly. Decoding captures the displacement without reading the index or changing CPU state. A supported final opcode then commits the ordinary instruction boundary, reads the live index, and calculates the effective address. An unsupported form retains the fetched bytes but leaves PC and R unchanged. In interrupt mode 0, supplied opcode acknowledgements increment R as they occur, while the displacement and final opcode acknowledgements do not.
page DDCB = $CB on DD {
displacement:8 = read
opcode = read
}
page FDCB = $CB on FD {
displacement:8 = read
opcode = read
}
Only rrr=110 is documented: indexed bit operations act on memory without an
extra register copy. 00 ooo 110 selects the seven documented shifts/rotates;
ooo=110 (SLL) is excluded. 01/10/11 bbb 110 selects BIT/RES/SET. The same
memory actions and masks used on the CB page preserve flag order, unchanged
writes, and partial failures here.
family RLCIndexedBits {
encoding "00 000 110" on DDCB with i = register IX named "RLC ({i}+d)"
encoding "00 000 110" on FDCB with i = register IY named "RLC ({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
perform rlcMemory(address)
}
family RRCIndexedBits {
encoding "00 001 110" on DDCB with i = register IX named "RRC ({i}+d)"
encoding "00 001 110" on FDCB with i = register IY named "RRC ({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
perform rrcMemory(address)
}
family RLIndexedBits {
encoding "00 010 110" on DDCB with i = register IX named "RL ({i}+d)"
encoding "00 010 110" on FDCB with i = register IY named "RL ({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
perform rlMemory(address)
}
family RRIndexedBits {
encoding "00 011 110" on DDCB with i = register IX named "RR ({i}+d)"
encoding "00 011 110" on FDCB with i = register IY named "RR ({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
perform rrMemory(address)
}
family SLAIndexedBits {
encoding "00 100 110" on DDCB with i = register IX named "SLA ({i}+d)"
encoding "00 100 110" on FDCB with i = register IY named "SLA ({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
perform slaMemory(address)
}
family SRAIndexedBits {
encoding "00 101 110" on DDCB with i = register IX named "SRA ({i}+d)"
encoding "00 101 110" on FDCB with i = register IY named "SRA ({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
perform sraMemory(address)
}
family SRLIndexedBits {
encoding "00 111 110" on DDCB with i = register IX named "SRL ({i}+d)"
encoding "00 111 110" on FDCB with i = register IY named "SRL ({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
perform srlMemory(address)
}
family BITIndexedBits {
encoding "01 bbb 110" on DDCB for b in bitMasks.read with i = register IX named "BIT {b},({i}+d)"
encoding "01 bbb 110" on FDCB for b in bitMasks.read with i = register IY named "BIT {b},({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
mask = source b
perform bitMemory(address, mask)
}
family RESIndexedBits {
encoding "10 bbb 110" on DDCB for b in bitMasks.read with i = register IX named "RES {b},({i}+d)"
encoding "10 bbb 110" on FDCB for b in bitMasks.read with i = register IY named "RES {b},({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
mask = source b
perform resMemory(address, mask)
}
family SETIndexedBits {
encoding "11 bbb 110" on DDCB for b in bitMasks.read with i = register IX named "SET {b},({i}+d)"
encoding "11 bbb 110" on FDCB for b in bitMasks.read with i = register IY named "SET {b},({i}+d)"
base = operand i
address = add(base, signExtend(displacement, 16))
mask = source b
perform setMemory(address, mask)
}
Checks and limits
The specification is the only authored Z80 implementation. Integration tests edit its state, banks, instruction definitions, prefix layout, execution policies, interrupt gates and vectors, and public views, then check the generated CPU. Shared language tests also generate a differently named model with different storage, bus width, mode values, sources, and word order.
Tests exhaust all byte pairs and both carry inputs for all eight ALU operations against independent signed/unsigned arithmetic, low-digit carries/borrows, and binary-string parity. Every operand form also checks all incoming flag patterns, including A as its own source, unchanged CP results, and H/L operands. Other checks cover all immediate-load bytes and flag patterns, exact memory accesses, PC and R wrapping, overlapping stores, current RAM, and retained records. All unsupported first bytes and every prefix-page entry are checked; CB rejects its eight SLL encodings after the second byte. Construction, nested snapshots, reset, and readonly public types have separate checks.
Register loads and INC/DEC cover every byte and all 64 incoming flag patterns, including half carry/borrow, signed overflow, flag preservation, pair views, and unchanged alternate state. The transfer matrix checks every encoding and byte, cycling through all incoming flag patterns; memory cases also check address-space boundaries, opcode overlap, and H/L destination aliasing. Pair loads check each selector with boundary words, all flag patterns, and wrapped operand fetches. JR conditions cover all flag patterns; DJNZ covers every B value and flag pattern. Every relative displacement is checked on each available path, including page/address-space crossings and instruction overlap. All supported opcodes are checked with every R value. Further checks cover live arithmetic flags, current registers/pointers/operands, and retained records across execution, reset, and caller edits.
Paired 8080/Z80 programs check common instruction bytes and data effects while asserting each CPU's own flags: arithmetic overflow versus parity, subtraction half-borrow versus AC, and AND's distinct half-carry behavior. The checksum example checks 38 complete records, actual RAM calls, carry propagation, both comparison failure paths, bounded running, snapshot resumption, reset, full memory images, and fresh factories.
All 1,000 SingleStepTests Z80 cases for each of the 72 unprefixed ALU forms were also checked (72,000 cases total). These supplementary checks compare every modeled field, final RAM, fetched bytes, and ordered memory accesses. Undocumented F bits 3/5 and internal latches are omitted; bus samples are reduced to memory transactions, excluding refresh activity. This is an independent emulator comparison, not a claim of hardware or cycle-accuracy testing. Repository tests remain self-contained.
CB tests cover all 248 encodings with every byte and both incoming carry values, checking complete state and actual RAM calls. A further run exhausts all byte and raw F combinations for each CB operation/bit through POP AF; expected bit results use character movement independently of the core's shifts and masks. Checks include all R values, memory read/write overlap with either opcode byte, wrapped fetches, same-value writes, live HL and RAM, and atomic unsupported CB attempts. PUSH/POP cover every flag pattern, boundary words and SP wrapping; CALL/RET cover every condition and flag pattern with wrapped PC/SP and code aliasing. Each unprefixed encoding in that group checks all R values. The bit-count example checks all 151 records, full RAM, three nested call levels, fresh factories, reset preservation, and resumption from every instruction boundary.
All 1,000 SingleStepTests cases for each of these forms also passed: 248 CB forms and 26 unprefixed stack/call/return forms, 274,000 cases in total. Comparison covers every modeled state field, final RAM, fetched bytes, and ordered memory transactions. For PUSH AF only, the expected written F byte is projected to the same six modeled bits; its omitted bits 5/3 are checked as zero in repository tests. The earlier ALU comparison and these checks use the same exclusions for internal latches and bus refresh activity. They do not establish cycle accuracy.
The remaining ordinary unprefixed instructions have further checks for every A/flag combination in DAA, accumulator rotates, CPL, SCF, and CCF; every valid decimal operand pair and incoming carry/borrow in ADC/SBC followed by DAA; word values and boundaries in pair INC/DEC, ADD HL, word loads, and register jumps/transfers; and every stack address in EX (SP),HL. Bank exchanges cover every pair of flag patterns, repeated exchanges, and snapshot resumption. Absolute jumps and RST cover every condition/vector, flag pattern, PC/SP wrapping, code overlap, and refresh value. The decimal-total example verifies all 46 records, full RAM, preserved caller state, both carry paths, and resumption from every boundary, with edited inputs spanning zero through 396.
All 1,000 independent cases for each of these 53 forms also passed: 53,000 cases from SingleStepTests Z80 at revision ebe1875. The comparison checks modeled state, final RAM, fetched bytes, and ordered memory transactions under the same exclusions above. In particular, it checks arbitrary DAA states and EX (SP),HL's read/write order. These are independent emulator cases; they do not establish hardware or cycle accuracy.
Indexed tests cover both registers, every signed displacement, byte value, and carry input, real H/L transfers, word/stack wrapping, and overlap with every encoding byte. All 171 indexed/ED forms check every initial R value. ED checks cover word arithmetic boundaries with every flag pattern, word-transfer aliasing, every NEG input/flag combination, IFF1/IFF2 disagreement, live R transfers, and every A/memory pair for RLD/RRD. Block checks cover all eight forms, both pointer directions and wraps, zero/final/nonzero counts, every comparison byte pair, overlapping copies, rewritten code, a full 65,536-byte copy, and early comparison matches. The indexed-buffer example checks 24 full records, every match position, failure, bounded running, snapshot resumption, reset, complete memory images, and fresh factories.
All 1,000 independent cases for each of the 171 indexed/ED additions also passed: 171,000 cases from SingleStepTests Z80 at revision ebe1875. They compare every modeled state field, final RAM, fetched bytes, and ordered memory transactions, excluding undocumented F bits/internal latches and bus refresh activity as above. Repeating block cases describe one iteration. These supplementary emulator comparisons do not establish cycle accuracy; repository tests remain self-contained.
I/O checks cover every register input/output byte and all incoming flag patterns, all immediate low-address bytes, old-A/BC address selection, and every block count/data byte. They verify actual interleaving, H/PV repeat-phase boundaries, PC/HL/R wrapping, zero-count 256-iteration blocks, code overlap, live code/data, missing or failing connections, invalid input, and non-reentrant execution. The runner test reads a device buffer with INIR and writes it back in reverse with OTDR, comparing records, full RAM, and separately restored device state at every step boundary.
All 24,000 independent cases for the 24 I/O forms also passed, from SingleStepTests Z80 at revision ebe1875. They compare every modeled state field, final RAM, instruction bytes, full port addresses/values, and interleaved memory/port transactions. Bus samples are reduced to transfers, excluding idle/refresh clocks; repeating cases describe one iteration. This is an independent emulator comparison with the same omissions above, not hardware or cycle-accuracy testing. The external corpus is supplementary; repository tests remain self-contained.
Interrupt checks cover all IFF/flag combinations, mode selectors, enable and NMI inhibition, wrapped stacks/vectors, all vector bytes, mode-0 execution of every documented encoding, rejection, callback failures, and reentrancy. The runner programs cover EI/HALT, each IRQ mode, nested NMI, port output, EI/RETI and RETN, restoring CPU, RAM, and device state at every instruction or entry boundary.
All 7,000 independent instruction cases for DI/EI, IM 0/1/2, RETI and RETN passed from SingleStepTests Z80 at revision ebe1875. They compare all modeled hardware fields, final RAM, fetched bytes, and ordered memory transactions. Boundary inhibition and device notifications have separate repository tests; these supplementary instruction vectors do not test external interrupt recognition or establish cycle accuracy.
All documented forms are implemented. The coverage inventory tracks their count separately from the recognition and timing limits above.
References
The CPU manual supplies the architectural contract. Where it leaves modeled flags unspecified, the qualifications above and independent references under checks describe the chosen behavior.
- Zilog Z80 CPU User Manual, UM008011-0816: registers, instruction encodings, flags, and bus/interrupt operation. This later manual also covers variants; the declared Dromaios model targets the original Z80's documented instruction forms, not every later extension.
- Zilog oral history panel, Computer History Museum, 2007: Faggin, Shima, and Ungermann discuss the company's founding and the Z80's development.
- 8080 guide: a comparison of the shared register organization and encoding patterns, with the 8080's distinct arithmetic flag rules.