This specification is the complete instruction-level 6800 model: its cpu fences
generate stored state, all 197 documented instruction forms, reset, execution,
IRQ/NMI entry, and the public TypeScript interface. Prose explains the chip and
the model's contracts and limitations alongside those definitions.
CPU tests · Public type checks · Coverage · Examples
Historical background
Motorola introduced the MC6800 in 1974, its first eight-bit microprocessor. It anchored a family of companion memory and peripheral parts; the original 6800 is the subject here, excluding later-family additions and undocumented opcodes. NXP's company history records that introduction.
The chip in a system
The programmer has two independent byte accumulators, A and B, a sixteen-bit index register X, a sixteen-bit stack pointer SP, and a sixteen-bit program counter PC. A and B do not form the D register of later Motorola processors. An eight-bit data bus transfers bytes; sixteen address lines select 64 KiB. Words occupy two consecutive bytes, high byte first.
The original family paired the processor with MCM6830 ROM, MCM6810 RAM, MC6820 parallel interfaces, and MC6850 serial interfaces. Memory and device registers share the address space, so ordinary loads and stores also perform I/O. The family uses a single +5 V supply. The processor takes two external, non-overlapping clock phases; VMA marks valid memory addresses and R/W selects the direction of a transfer. IRQ, NMI, and RESET provide external control; HALT and three-state controls allow bus sharing. Motorola describes these connections in its 1975 systems reference.
This emulator accepts byte memory and explicit interrupt offers at instruction boundaries. It does not model clock phases, bus arbitration, pins, or attached devices. The distinction matters for WAI: this specification defines saved frames and wake-up effects, while a machine must decide when to deliver an external event.
Addresses and values below use $ for hexadecimal; # in an instruction name
marks an immediate value rather than a memory address. Executable statements
specify ordered instruction effects, without cycles or dummy accesses. A failed
memory access prevents later effects while retaining earlier ones. Capturing a
value makes its timing explicit, including what memory callbacks can observe.
Stored state
The complete declaration supplies the schema used by construction, snapshots, machine parsing, generated instructions, and the execution runtime. It preserves the existing public field names and their read order. Initial values are supplied by the caller; storage declarations imply no reset defaults.
H records half-carry for decimal adjustment, I masks ordinary IRQ offers, N/Z
describe sign and zero, V records signed overflow, and C records carry or
borrow. The waiting latch records WAI; it is not a condition-code bit.
cpu "6800"
state {
register A: 8
register B: 8
register X: 16
register SP: 16
register PC: 16
flag H
flag I
flag N
flag Z
flag V
flag C
latch WAITING = waiting
}
Cpu6800State requires all these fields:
| Fields | Range | Meaning |
|---|---|---|
| A, B | 00–FF |
Byte accumulators |
| X | 0000–FFFF |
Index register |
| SP | 0000–FFFF |
Stack pointer |
| PC | 0000–FFFF |
Program counter |
H, I, N, Z, V, C in flags |
Boolean | Half carry, interrupt mask, negative, zero, overflow, carry |
| waiting | Boolean | WAI has saved a frame and suspended instruction execution |
TypeScript fields are lowercase; .machine definitions conventionally use
uppercase register and flag names. Snapshots expose this same state. H/I/N/Z/V/C
correspond to condition-code bits 5–0. TPA packs those flags into A with bits
7–6 set; TAP replaces the flags from A's low six bits. The upper bits are not
mutable flags, and snapshots do not duplicate the flags in a packed field.
waiting is required, including false for an ordinary initial state. A snapshot
with waiting: true assumes its saved frame is already in the accompanying RAM;
construction neither creates nor validates that frame. External HALT remains
unmodeled and is distinct from this WAI latch.
Construction and inspection
Memory uses the byte-connection contract, with RAM as the usual connection.
new Cpu6800(ram, initialState) requires a 64 KiB byte-memory address space. It copies declared
registers, flags, and the waiting latch, then validates numeric ranges and Boolean values.
Each declared field is read once, including non-enumerable properties; extra
metadata is ignored. Invalid register values or address-space size throw RangeError;
non-Boolean flags or waiting values throw TypeError. Construction neither resets the CPU nor
reads or writes RAM. All initial state must be supplied explicitly.
snapshot() returns detached registers and flags without RAM accesses. Public
snapshots are recursively readonly in TypeScript; bypassing that typing cannot
change the CPU or another snapshot. A snapshot can initialize a fresh CPU.
The CPU retains no execution history.
Instruction steps
step() attempts one instruction and returns a Cpu6800StepRecord containing
independent before and after snapshots, the instruction's start address and
fetched bytes, ordered accesses, and an outcome: executed, waiting, or
unsupported. Only unsupported carries reason: "opcode". WAI returns
waiting with its fetched instruction and frame writes. An already waiting CPU
returns waiting with instruction: null, no accesses, and unchanged state.
No ordinary instruction executes until an accepted interrupt or reset releases WAI.
Supported instructions fetch the opcode and then their operands, advancing PC
after each byte with wrap from FFFF to 0000. Accumulator instructions use
the addressing forms below. Immediate forms fetch a value without a separate
data read; memory forms fetch their address bytes before reading or writing data.
Transfers and accumulator unary operations fetch only their opcode. Branches
always fetch a displacement byte, whether taken or untaken. Immediate word
operands and extended addresses are high byte first. Stack pushes,
pulls, and RTS fetch only their opcode; BSR fetches a displacement byte.
Stack data reads appear in accesses, but not in the fetched instruction
bytes, and do not advance PC. All instruction bytes are fetched before
stack reads or writes. STAA and STAB write the selected accumulator without
reading the destination. Writes are recorded even when the value is
unchanged. Stores can overwrite instruction or vector bytes; retained records
keep the values fetched at the time, while subsequent operations read current RAM.
Unsupported attempts read only the opcode and leave all CPU state and RAM unchanged. Repeating the attempt repeats that read. Preserving PC despite a recorded fetch is an explicit rejection policy of this model.
These records describe instruction-level accesses. Cycle counts, internal cycles, dummy bus accesses, pin transitions, and electrical behavior are omitted.
Condition codes as a byte
The packed condition-code register is 11HINZVC. Bits 7 and 6 read as one and
are ignored on restoration; they are not stored flags. The CC view captures
H/I/N/Z/V/C in that order, then assembles the byte. Restoration replaces the
complete flag object from a captured byte. The stack and interrupt
definitions below reuse this same view and policy, so TAP/TPA and saved frames agree.
view CC "packed condition codes": 8 {
h = flag H
i = flag I
n = flag N
z = flag Z
v = flag V
c = flag C
return pack<8>(1, 1, h, i, n, z, v, c)
}
policy CCFLAGS "restore condition codes" (status: 8) {
H = bit(status, 5)
I = bit(status, 4)
N = bit(status, 3)
Z = bit(status, 2)
V = bit(status, 1)
C = bit(status, 0)
}
Capture A and replace all six flags, ignoring bits 7/6. Preserve every register and the waiting latch; no memory access occurs after the opcode fetch.
family TAP "0000011 0" {
status = register A
replace CCFLAGS(status)
}
Capture all six flags and write A, setting reserved bits 7/6. Preserve the flags and all other state; no memory access occurs after the opcode fetch.
family TPA "0000011 1" {
status = source CC
A <- status
}
Addressing and operand selection
The main byte families use 1 r mm oooo: r=0/1 selects A/B, mm selects
immediate/direct/indexed/extended in binary order, and oooo selects the
operation. An address source does not read the final data byte. This lets the
same mode serve a load, store, or comparison without inventing destination reads.
Direct addressing zero-extends one instruction byte to page zero. Indexed
addressing fetches an unsigned byte offset before reading live X, then adds
with sixteen-bit wrap. For example, X=$FFF0, offset=$80 selects $0070.
Extended addressing fetches the high address byte before the low byte.
source direct "direct, page zero": 16 {
offset = fetch
return extend(offset, 16)
}
source indexed "indexed by X with unsigned offset": 16 {
offset = fetch
index = register X
return add(index, extend(offset, 16))
}
source extended "extended, high byte first": 16 {
high = fetch
low = fetch
return concat(high, low)
}
source immediateByte "immediate byte": 8 {
byte = fetch
return byte
}
source immediateWord "immediate word, high byte first": 16 {
high = fetch
low = fetch
return concat(high, low)
}
operands bytes {
00 "#byte" = value immediateByte
01 "direct" = memory direct
10 "indexed" = memory indexed
11 "extended" = memory extended
}
operands accumulators {
0 "A" = register A
1 "B" = register B
}
Word reads resolve their address once, then read high and low data bytes.
The second address wraps at $FFFF, including for indexed and extended words.
A direct word at $00FF continues at $0100; there is no page-zero wrap.
The word catalogue provides values, while bytes.address supplies the same
three memory addresses for word stores and omits the immediate selector.
source directWord "direct word": 16 {
address = source direct
high = memory(address)
low = memory(add(address, u16($0001)))
return concat(high, low)
}
source indexedWord "indexed word": 16 {
address = source indexed
high = memory(address)
low = memory(add(address, u16($0001)))
return concat(high, low)
}
source extendedWord "extended word": 16 {
address = source extended
high = memory(address)
low = memory(add(address, u16($0001)))
return concat(high, low)
}
operands words {
00 "#word" = value immediateWord
01 "direct" = value directWord
10 "indexed" = value indexedWord
11 "extended" = value extendedWord
}
operands wordRegisters {
0 "S" = register SP
1 "X" = register X
}
Loads, stores, and transfers
Loads, stores, transfers, and logic set N/Z from the captured result, clear V, and preserve H/I/C. One policy covers both byte and word results. Flags are explicit effects, not automatic consequences of a register write.
policy NZV<bits: 8, 16> "6800 result" (result: bits) {
N = negative(result)
Z = zero(result)
V = 0
}
Fetch or read the complete byte before writing A/B, then set N/Z and clear V. Preserve H/I/C and all other state. A failed read prevents register and flag updates; completed instruction fetches remain.
family LDA "1 r mm 0110" for r in accumulators, m in bytes.read named "LDA{r} {m}" {
result = source m
operand r <- result
apply NZV<8>(result)
}
Resolve the address before capturing A/B. Do not read the destination; write the captured byte once, even if unchanged. Only after a successful write, set N/Z and clear V, preserving H/I/C. A failed write leaves flags unchanged; completed fetches and addressing effects remain. There is no immediate store.
family STA "1 r mm 0111" for r in accumulators, m in bytes.address named "STA{r} {m}" {
address = source m
result = operand r
memory(address) <- result
apply NZV<8>(result)
}
The word-transfer pattern is 1 r mm 111t: here r=0/1 selects SP/X, and
t=0/1 selects load/store. For indexed LDX, the old X supplies the address;
only after both data reads succeed does the new word replace X.
Fetch or read the complete word high byte first, then write SP/X and set N/Z from that captured word, clearing V. Preserve H/I/C and the waiting latch. A failed read prevents register and flag updates.
family LD "1 r mm 1110" for r in wordRegisters, m in words.read named "LD{r} {m}" {
result = source m
operand r <- result
apply NZV<16>(result)
}
Resolve the address, then capture SP/X. Do not read the destination; write high
byte then low byte, wrapping at $FFFF, even if unchanged. Only after both writes
succeed, set N/Z and clear V, preserving H/I/C. A failed write leaves flags
unchanged; completed writes, fetches, and addressing effects remain.
family ST "1 r mm 1111" for r in wordRegisters, m in bytes.address named "ST{r} {m}" {
address = source m
result = operand r
memory(address) <- highByte(result)
memory(add(address, u16($0001))) <- lowByte(result)
apply NZV<16>(result)
}
Transfers use 0001011 d, with d=0 selecting A to B and d=1 B to A.
Capture A and write B, then set N/Z from that byte and clear V. Preserve H/I/C and all other state. No data-memory access occurs.
family TAB "0001011 0" {
result = register A
B <- result
apply NZV<8>(result)
}
Capture B and write A, then set N/Z from that byte and clear V. Preserve H/I/C and all other state. No data-memory access occurs.
family TBA "0001011 1" {
result = register B
A <- result
apply NZV<8>(result)
}
Logic
In 1 r mm oooo, operation values 0100/0101/1000/1010 select AND/BIT/EOR/ORA.
The original mnemonics ORAA/ORAB contain the operation name ORA followed by the
accumulator letter. BIT tests the AND result without replacing the accumulator;
its flags describe that result, not individual bits copied from memory.
Fetch or read the operand before capturing A/B. Combine the captured bytes by AND, write the result, then set N/Z and clear V. Preserve H/I/C. A failed read prevents register and flag updates.
family AND "1 r mm 0100" for r in accumulators, m in bytes.read named "AND{r} {m}" {
right = source m
accumulator = operand r
result = and(accumulator, right)
operand r <- result
apply NZV<8>(result)
}
Fetch or read the operand before capturing A/B. Test their AND result without writing either operand. Set N/Z and clear V, preserving H/I/C. A failed read prevents flag updates.
family BIT "1 r mm 0101" for r in accumulators, m in bytes.read named "BIT{r} {m}" {
right = source m
accumulator = operand r
result = and(accumulator, right)
apply NZV<8>(result)
}
Fetch or read the operand before capturing A/B. Combine the captured bytes by exclusive OR, write the result, then set N/Z and clear V. Preserve H/I/C. A failed read prevents register and flag updates.
family EOR "1 r mm 1000" for r in accumulators, m in bytes.read named "EOR{r} {m}" {
right = source m
accumulator = operand r
result = xor(accumulator, right)
operand r <- result
apply NZV<8>(result)
}
Fetch or read the operand before capturing A/B. Combine the captured bytes by inclusive OR, write the result, then set N/Z and clear V. Preserve H/I/C. A failed read prevents register and flag updates.
family ORA "1 r mm 1010" for r in accumulators, m in bytes.read named "ORA{r} {m}" {
right = source m
accumulator = operand r
result = or(accumulator, right)
operand r <- result
apply NZV<8>(result)
}
Comparisons
Byte comparisons subtract without storing the result. C means borrow, N/Z
describe the wrapped byte, and V reports signed subtraction overflow. Incoming
C is ignored; H/I are preserved. The pattern 1 r mm 0001 selects CMPA/CMPB.
policy COMPARE8 "6800 byte comparison" (left: 8, right: 8, result: 8) {
N = negative(result)
Z = zero(result)
V = overflow(left, right)
C = borrow(left, right)
}
Fetch or read the source before capturing A/B. Subtract it, set N/Z/V/C, and preserve H/I. Do not write a result. A failed read leaves flags unchanged; completed fetches and addressing effects remain.
family CMP "1 r mm 0001" for r in accumulators, m in bytes.read named "CMP{r} {m}" {
right = source m
left = operand r
result = subtract(left, right)
apply COMPARE8(left, right, result)
}
Compare A with B without writing either register. Capture B before A, ignore incoming C, and set N/Z/V/C from byte subtraction, preserving H/I. No data-memory access occurs.
family CBA "0001000 1" {
right = register B
left = register A
result = subtract(left, right)
apply COMPARE8(left, right, result)
}
The original 6800's CPX (10 mm 1100) is not a full sixteen-bit signed compare.
Z tests the whole word, but N/V describe high-byte subtraction without the
low-byte borrow. C is preserved. For X=$1200, operand=$1201, the word result
is $FFFF but the high-byte result is $00: N/V/Z are all clear. Motorola
cautions against using N/V for CPX conditional branches; later-family CPX
behaves differently.
policy COMPAREX "6800 CPX high-byte N/V and whole-word Z" (left: 16, right: 16, result: 16) {
N = negative(subtract(highByte(left), highByte(right)))
Z = zero(result)
V = overflow(highByte(left), highByte(right))
}
Fetch or read the complete high-first word before capturing X. N/V describe high-byte subtraction without low-byte borrow; Z tests whole-word equality. Preserve H/I/C and X. A failed read leaves flags unchanged; completed fetches and addressing effects remain.
family CPX "10 mm 1100" for m in words.read {
right = source m
left = register X
result = subtract(left, right)
apply COMPAREX(left, right, result)
}
Arithmetic and decimal adjustment
The arithmetic selectors in 1 r mm oooo are 0000 SUB, 0010 SBC,
1001 ADC, and 1011 ADD. Read the complete operand before the selected
accumulator. ADC/SBC then read C as incoming carry/borrow. Flags change before
accumulator writeback, unlike loads and logic. All operations preserve I;
subtraction preserves H, while addition updates it from the low-nibble carry.
policy ADD8 "6800 addition" (left: 8, right: 8, result: 8, incoming: flag) {
N = negative(result)
Z = zero(result)
V = addOverflow(left, right, incoming)
C = carry(left, right, incoming)
H = halfCarry(left, right, incoming)
}
policy SUB8 "6800 subtraction" (left: 8, right: 8, result: 8, incoming: flag) {
N = negative(result)
Z = zero(result)
V = overflow(left, right, incoming)
C = borrow(left, right, incoming)
}
Fetch or read the operand, then A/B; ignore incoming C. Add with byte wrap, set N/Z/V/C/H, and write A/B after flags. Preserve I and other state. A failed source read prevents arithmetic, flags, and writeback.
family ADD "1 r mm 1011" for r in accumulators, m in bytes.read named "ADD{r} {m}" {
right = source m
left = operand r
result = add(left, right)
apply ADD8(left, right, result, 0)
operand r <- result
}
Fetch or read the operand, then A/B, then incoming C. Add all three, set N/Z/V/C/H, and write A/B after flags. Preserve I and other state; failed reads prevent later effects.
family ADC "1 r mm 1001" for r in accumulators, m in bytes.read named "ADC{r} {m}" {
right = source m
left = operand r
carry = flag C
result = add(left, right, carry)
apply ADD8(left, right, result, carry)
operand r <- result
}
Fetch or read the operand, then A/B; ignore incoming C. Subtract with byte wrap, set N/Z/V/C with C meaning borrow, and write A/B after flags. Preserve H/I and other state; failed reads prevent later effects.
family SUB "1 r mm 0000" for r in accumulators, m in bytes.read named "SUB{r} {m}" {
right = source m
left = operand r
result = subtract(left, right)
apply SUB8(left, right, result, 0)
operand r <- result
}
Fetch or read the operand, then A/B, then incoming C as borrow. Subtract the operand and borrow, set N/Z/V/C, and write A/B after flags. Preserve H/I and other state; failed reads prevent later effects.
family SBC "1 r mm 0010" for r in accumulators, m in bytes.read named "SBC{r} {m}" {
right = source m
left = operand r
carry = flag C
result = subtract(left, right, carry)
apply SUB8(left, right, result, carry)
operand r <- result
}
ABA and SBA use B directly, with no operand bytes. Unlike the memory/immediate families, their declared capture order is A then B. CBA, defined earlier, compares without writeback and retains its B-before-A capture order.
Read A then B and add, ignoring incoming C. Update N/Z/V/C/H before writing A; preserve B, I, and every other register. No data-memory access occurs.
family ABA "0001 1011" {
left = register A
right = register B
result = add(left, right)
apply ADD8(left, right, result, 0)
A <- result
}
Read A then B and subtract, ignoring incoming C. Update N/Z/V/C before writing A; preserve B, H/I, and every other register. No data-memory access occurs.
family SBA "0001 0000" {
left = register A
right = register B
result = subtract(left, right)
apply SUB8(left, right, result, 0)
A <- result
}
Decimal adjustment
DAA corrects a packed-decimal addition in A after ABA, ADDA, or ADCA. Select $06 when the low nibble
exceeds nine or H is set, and $60 when the original byte exceeds $99 or C
is set. Both choices use the original A. The model defines this calculation
for invalid decimal digits too, without claiming a documented decimal result
for those inputs. The manual specifies reachable results after valid BCD
addition and leaves V undefined; the model clears V. H and I remain unchanged.
DAA does not implement BCD subtraction or a decimal-mode latch.
policy DECIMAL "6800 DAA" (original: 8, correction: 8, result: 8, incoming: flag) {
N = negative(result)
Z = zero(result)
V = 0
C = or(incoming, carry(original, correction))
}
Capture A, H, then C; choose both corrections from those captures. Add them to A with byte wrap. Set N/Z, clear V, and retain or set C before writing A. Preserve H/I and other state. No instruction or data-memory access occurs.
family DAA "0001 1001" {
original = register A
half = flag H
carry = flag C
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($00)), select(adjustHigh, u8($60), u8($00)))
result = add(original, correction)
apply DECIMAL(original, correction, result, carry)
A <- result
}
Unary byte operations
Unary instructions use 01 tt oooo: tt=00/01 selects A/B, while tt=10/11
selects indexed/extended memory. oooo selects NEG=0000, COM=0011,
LSR=0100, ROR=0110, ASR=0111, ASL=1000, ROL=1001, DEC=1010,
INC=1100, TST=1101, or CLR=1111. JMP=1110 uses only the memory
address and is defined with control flow below. Other selectors remain undefined.
Register and memory forms share their flag policies. Memory forms capture one address for both read and write, so an operand callback changing X cannot move the destination. Flags change before writeback. TST never writes; the original 6800 CLR never reads the destination. These distinctions differ from the 6809.
operands unaryMemory {
0 "indexed" = memory indexed
1 "extended" = memory extended
}
policy NEGFLAGS "6800 NEG" (original: 8, result: 8) {
N = negative(result)
Z = zero(result)
V = overflow(u8($00), original)
C = borrow(u8($00), original)
}
policy COMFLAGS "6800 COM" (result: 8) {
N = negative(result)
Z = zero(result)
V = 0
C = 1
}
policy SHIFTFLAGS "6800 shift" (result: 8, outgoing: flag) {
N = negative(result)
Z = zero(result)
C = outgoing
V = xor(negative(result), outgoing)
}
policy DECFLAGS "6800 DEC" (original: 8, result: 8) {
N = negative(result)
Z = zero(result)
V = equal(original, u8($80))
}
policy INCFLAGS "6800 INC" (original: 8, result: 8) {
N = negative(result)
Z = zero(result)
V = equal(original, u8($7F))
}
policy TESTFLAGS "6800 TST" (result: 8) {
N = negative(result)
Z = zero(result)
V = 0
C = 0
}
policy CLEARFLAGS "6800 CLR" () {
N = 0
Z = 1
C = 0
V = 0
}
NEG
Negate modulo 256. V marks original $80; C marks a nonzero original. Set N/Z from the result; preserve H/I.
No register other than the selected destination changes.
family NEGregister "010 r 0000" for r in accumulators named "NEG{r}" {
original = operand r
result = subtract(u8($00), original)
apply NEGFLAGS(original, result)
operand r <- result
}
Resolve the address once. Negate modulo 256. V marks original $80; C marks a nonzero original. Set N/Z from the result; preserve H/I.
Apply flags before any write; a failed write retains them and completed addressing effects.
family NEGmemory "011 m 0000" for m in unaryMemory.address named "NEG {m}" {
address = source m
original = memory(address)
result = subtract(u8($00), original)
apply NEGFLAGS(original, result)
memory(address) <- result
}
COM
Take the ones' complement, clearing V and setting C. Set N/Z from the result; preserve H/I. No register other than the selected destination changes.
family COMregister "010 r 0011" for r in accumulators named "COM{r}" {
original = operand r
result = subtract(u8($FF), original)
apply COMFLAGS(result)
operand r <- result
}
Resolve the address once. Take the ones' complement, clearing V and setting C. Set N/Z from the result; preserve H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family COMmemory "011 m 0011" for m in unaryMemory.address named "COM {m}" {
address = source m
original = memory(address)
result = subtract(u8($FF), original)
apply COMFLAGS(result)
memory(address) <- result
}
LSR
Shift right, inserting zero. C is the outgoing low bit; V is N XOR C. Set N/Z from the result; preserve H/I. No register other than the selected destination changes.
family LSRregister "010 r 0100" for r in accumulators named "LSR{r}" {
original = operand r
result = shiftRight(original, 0)
apply SHIFTFLAGS(result, lowBit(original))
operand r <- result
}
Resolve the address once. Shift right, inserting zero. C is the outgoing low bit; V is N XOR C. Set N/Z from the result; preserve H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family LSRmemory "011 m 0100" for m in unaryMemory.address named "LSR {m}" {
address = source m
original = memory(address)
result = shiftRight(original, 0)
apply SHIFTFLAGS(result, lowBit(original))
memory(address) <- result
}
ROR
Capture incoming C after the operand; rotate it into the top bit. Set C from the outgoing low bit and V to N XOR C. Set N/Z from the result; preserve H/I. No register other than the selected destination changes.
family RORregister "010 r 0110" for r in accumulators named "ROR{r}" {
original = operand r
carry = flag C
result = shiftRight(original, carry)
apply SHIFTFLAGS(result, lowBit(original))
operand r <- result
}
Resolve the address once. Capture incoming C after the operand; rotate it into the top bit. Set C from the outgoing low bit and V to N XOR C. Set N/Z from the result; preserve H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family RORmemory "011 m 0110" for m in unaryMemory.address named "ROR {m}" {
address = source m
original = memory(address)
carry = flag C
result = shiftRight(original, carry)
apply SHIFTFLAGS(result, lowBit(original))
memory(address) <- result
}
ASR
Shift right, retaining the original sign. Set C from the outgoing low bit and V to N XOR C. Set N/Z from the result; preserve H/I. No register other than the selected destination changes.
family ASRregister "010 r 0111" for r in accumulators named "ASR{r}" {
original = operand r
result = shiftRight(original, negative(original))
apply SHIFTFLAGS(result, lowBit(original))
operand r <- result
}
Resolve the address once. Shift right, retaining the original sign. Set C from the outgoing low bit and V to N XOR C. Set N/Z from the result; preserve H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family ASRmemory "011 m 0111" for m in unaryMemory.address named "ASR {m}" {
address = source m
original = memory(address)
result = shiftRight(original, negative(original))
apply SHIFTFLAGS(result, lowBit(original))
memory(address) <- result
}
ASL
Shift left, inserting zero. Set C from the original sign bit and V to N XOR C. Set N/Z from the result; preserve H/I. No register other than the selected destination changes.
family ASLregister "010 r 1000" for r in accumulators named "ASL{r}" {
original = operand r
result = shiftLeft(original, 0)
apply SHIFTFLAGS(result, negative(original))
operand r <- result
}
Resolve the address once. Shift left, inserting zero. Set C from the original sign bit and V to N XOR C. Set N/Z from the result; preserve H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family ASLmemory "011 m 1000" for m in unaryMemory.address named "ASL {m}" {
address = source m
original = memory(address)
result = shiftLeft(original, 0)
apply SHIFTFLAGS(result, negative(original))
memory(address) <- result
}
ROL
Capture incoming C after the operand; rotate it into the low bit. Set C from the original sign bit and V to N XOR C. Set N/Z from the result; preserve H/I. No register other than the selected destination changes.
family ROLregister "010 r 1001" for r in accumulators named "ROL{r}" {
original = operand r
carry = flag C
result = shiftLeft(original, carry)
apply SHIFTFLAGS(result, negative(original))
operand r <- result
}
Resolve the address once. Capture incoming C after the operand; rotate it into the low bit. Set C from the original sign bit and V to N XOR C. Set N/Z from the result; preserve H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family ROLmemory "011 m 1001" for m in unaryMemory.address named "ROL {m}" {
address = source m
original = memory(address)
carry = flag C
result = shiftLeft(original, carry)
apply SHIFTFLAGS(result, negative(original))
memory(address) <- result
}
DEC
Decrement modulo 256. Set V only for original $80; preserve C. Set N/Z from the result; preserve H/I.
No register other than the selected destination changes.
family DECregister "010 r 1010" for r in accumulators named "DEC{r}" {
original = operand r
result = subtract(original, u8($01))
apply DECFLAGS(original, result)
operand r <- result
}
Resolve the address once. Decrement modulo 256. Set V only for original $80; preserve C. Set N/Z from the result; preserve H/I.
Apply flags before any write; a failed write retains them and completed addressing effects.
family DECmemory "011 m 1010" for m in unaryMemory.address named "DEC {m}" {
address = source m
original = memory(address)
result = subtract(original, u8($01))
apply DECFLAGS(original, result)
memory(address) <- result
}
INC
Increment modulo 256. Set V only for original $7F; preserve C. Set N/Z from the result; preserve H/I.
No register other than the selected destination changes.
family INCregister "010 r 1100" for r in accumulators named "INC{r}" {
original = operand r
result = add(original, u8($01))
apply INCFLAGS(original, result)
operand r <- result
}
Resolve the address once. Increment modulo 256. Set V only for original $7F; preserve C. Set N/Z from the result; preserve H/I.
Apply flags before any write; a failed write retains them and completed addressing effects.
family INCmemory "011 m 1100" for m in unaryMemory.address named "INC {m}" {
address = source m
original = memory(address)
result = add(original, u8($01))
apply INCFLAGS(original, result)
memory(address) <- result
}
TST
Test the byte, clearing V and C without writing a result. Set N/Z from the result; preserve H/I. No register other than the selected destination changes.
family TSTregister "010 r 1101" for r in accumulators named "TST{r}" {
original = operand r
result = original
apply TESTFLAGS(result)
}
Resolve the address once. Test the byte, clearing V and C without writing a result. Set N/Z from the result; preserve H/I. A failed read prevents flag updates.
family TSTmemory "011 m 1101" for m in unaryMemory.address named "TST {m}" {
address = source m
original = memory(address)
result = original
apply TESTFLAGS(result)
}
CLR
Do not read the destination. Set Z and clear N/C/V, then write zero; preserve H/I. No register other than the selected destination changes.
family CLRregister "010 r 1111" for r in accumulators named "CLR{r}" {
result = u8($00)
apply CLEARFLAGS()
operand r <- result
}
Resolve the address once. Do not read the destination. Set Z and clear N/C/V, then write zero; preserve H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family CLRmemory "011 m 1111" for m in unaryMemory.address named "CLR {m}" {
address = source m
result = u8($00)
apply CLEARFLAGS()
memory(address) <- result
}
Flag controls and word adjustments
00001 ff v uses ff=01/10/11 for V/C/I and v=0/1 to clear/set it.
These are individual flag writes; they do not replace the flag object or inspect
other flags. ff=00 instead selects INX/DEX, whose only flag effect is Z.
policy VFLAG "set V" (value: flag) {
V = value
}
policy CFLAG "set C" (value: flag) {
C = value
}
policy IFLAG "set I" (value: flag) {
I = value
}
policy ZWORD "index Z" (result: 16) {
Z = zero(result)
}
Clear V; preserve every other flag and register.
family CLV "00001 01 0" {
apply VFLAG(0)
}
Set V; preserve every other flag and register.
family SEV "00001 01 1" {
apply VFLAG(1)
}
Clear C; preserve every other flag and register.
family CLC "00001 10 0" {
apply CFLAG(0)
}
Set C; preserve every other flag and register.
family SEC "00001 10 1" {
apply CFLAG(1)
}
Clear I; preserve every other flag and register.
family CLI "00001 11 0" {
apply IFLAG(0)
}
Set I; preserve every other flag and register.
family SEI "00001 11 1" {
apply IFLAG(1)
}
Capture X, add one with word wrap, and write X. Then read back X and update Z only.
family INX "0000 1000" {
original = register X
X <- add(original, u16($0001))
adjusted = register X
apply ZWORD(adjusted)
}
Capture X, subtract one with word wrap, and write X. Then read back X and update Z only.
family DEX "0000 1001" {
original = register X
X <- subtract(original, u16($0001))
adjusted = register X
apply ZWORD(adjusted)
}
Capture SP, add one with word wrap, and write SP. Preserve all flags.
family INS "0011 0001" {
original = register SP
SP <- add(original, u16($0001))
}
Capture SP, subtract one with word wrap, and write SP. Preserve all flags.
family DES "0011 0100" {
original = register SP
SP <- subtract(original, u16($0001))
}
Capture SP, add one with word wrap, and write X. Preserve all flags.
family TSX "0011 0000" {
original = register SP
X <- add(original, u16($0001))
}
Capture X, subtract one with word wrap, and write SP. Preserve all flags.
family TXS "0011 0101" {
original = register X
SP <- subtract(original, u16($0001))
}
NOP has no effects after opcode fetching. Preserve all stored state.
family NOP "0000 0001" {
}
Branches and jumps
Short branches use 0010 ttt p: ttt selects always/HI/CC/NE/VC/PL/GE/GT,
and p inverts the test. $20 is BRA; $21 is undefined on the original 6800,
which has no BRN. The signed byte displacement is added to PC after its
fetch. Conditional branches read their flags after fetching, and access PC
only if taken. No flags change.
Fetch the displacement, read the post-fetch PC, then add the signed offset with word wrapping. Preserve all flags and other registers.
family BRA "0010 0000" {
offset = fetch
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
codes branchHI: 8 {
0 "BHI"
1 "BLS"
}
Fetch the signed displacement, then capture C/Z in that order. BHI tests and(not(c), not(z)); BLS inverts it. Only a taken branch reads and writes PC.
Preserve flags and other state; failed fetching prevents the test.
family branchHIPair "0010 001 p" for p in branchHI.read named "{p}" {
offset = fetch
c = flag C
z = flag Z
inverse = source p
when xor(and(not(c), not(z)), lowBit(inverse)) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
}
codes branchCC: 8 {
0 "BCC"
1 "BCS"
}
Fetch the signed displacement, then capture C in that order. BCC tests not(c); BCS inverts it. Only a taken branch reads and writes PC.
Preserve flags and other state; failed fetching prevents the test.
family branchCCPair "0010 010 p" for p in branchCC.read named "{p}" {
offset = fetch
c = flag C
inverse = source p
when xor(not(c), lowBit(inverse)) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
}
codes branchNE: 8 {
0 "BNE"
1 "BEQ"
}
Fetch the signed displacement, then capture Z in that order. BNE tests not(z); BEQ inverts it. Only a taken branch reads and writes PC.
Preserve flags and other state; failed fetching prevents the test.
family branchNEPair "0010 011 p" for p in branchNE.read named "{p}" {
offset = fetch
z = flag Z
inverse = source p
when xor(not(z), lowBit(inverse)) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
}
codes branchVC: 8 {
0 "BVC"
1 "BVS"
}
Fetch the signed displacement, then capture V in that order. BVC tests not(v); BVS inverts it. Only a taken branch reads and writes PC.
Preserve flags and other state; failed fetching prevents the test.
family branchVCPair "0010 100 p" for p in branchVC.read named "{p}" {
offset = fetch
v = flag V
inverse = source p
when xor(not(v), lowBit(inverse)) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
}
codes branchPL: 8 {
0 "BPL"
1 "BMI"
}
Fetch the signed displacement, then capture N in that order. BPL tests not(n); BMI inverts it. Only a taken branch reads and writes PC.
Preserve flags and other state; failed fetching prevents the test.
family branchPLPair "0010 101 p" for p in branchPL.read named "{p}" {
offset = fetch
n = flag N
inverse = source p
when xor(not(n), lowBit(inverse)) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
}
codes branchGE: 8 {
0 "BGE"
1 "BLT"
}
Fetch the signed displacement, then capture N/V in that order. BGE tests not(xor(n, v)); BLT inverts it. Only a taken branch reads and writes PC.
Preserve flags and other state; failed fetching prevents the test.
family branchGEPair "0010 110 p" for p in branchGE.read named "{p}" {
offset = fetch
n = flag N
v = flag V
inverse = source p
when xor(not(xor(n, v)), lowBit(inverse)) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
}
codes branchGT: 8 {
0 "BGT"
1 "BLE"
}
Fetch the signed displacement, then capture N/V/Z in that order. BGT tests and(not(z), not(xor(n, v))); BLE inverts it. Only a taken branch reads and writes PC.
Preserve flags and other state; failed fetching prevents the test.
family branchGTPair "0010 111 p" for p in branchGT.read named "{p}" {
offset = fetch
n = flag N
v = flag V
z = flag Z
inverse = source p
when xor(and(not(z), not(xor(n, v))), lowBit(inverse)) {
pc = register PC
PC <- add(pc, signExtend(offset, 16))
}
}
JMP uses 011 m 1110, where m=0/1 selects indexed/extended. It resolves the
address without reading the target byte, then replaces PC. Preserve flags and
all other state; failed address fetching leaves PC at the completed fetch boundary.
family JMP "011 m 1110" for m in unaryMemory.address {
target = source m
PC <- target
}
The descending stack
Stack accesses use ordinary current RAM and the current SP. Saved accumulator values and return addresses share the same stack; there is no hidden call history, frame type, depth limit, or underflow check. RTS can consume bytes placed in RAM without a preceding call. Editing stack memory changes what the next pull or return reads, and stack writes may overwrite code or vectors.
SP names the next free byte. A push writes at SP and only then decrements the live SP. A pull increments live SP before reading at the resulting live address. Each step wraps at sixteen bits. A memory callback may change SP; the next declared read observes that change. Failed pushes retain earlier writes and decrements; failed pulls retain their preceding increments.
A perform expands an earlier action with its own captures. Arguments are
captured before the action begins, and no local name leaks back to its caller.
This lets all stacks share the explicit byte-transfer rule. Sources return
values; actions perform effects.
action pushByte "push byte through SP" (byte: 8) using memory {
address = register SP
memory(address) <- byte
pointer = register SP
SP <- subtract(pointer, u16($0001))
}
action pushWord "push high-first word through SP" (word: 16) using memory {
perform pushByte(lowByte(word))
perform pushByte(highByte(word))
}
source popByte "pop byte through SP": 8 {
pointer = register SP
SP <- add(pointer, u16($0001))
address = register SP
byte = memory(address)
return byte
}
source popWord "pop high-first word through SP": 16 {
high = source popByte
low = source popByte
return concat(high, low)
}
Words reside high byte first at ascending addresses. A descending stack must therefore push low then high; pulls read high then low. TSX copies SP+1 into X, pointing at the top occupied byte; TXS sets SP to X-1, restoring the free-byte convention. These adjustments were defined above without memory accesses.
Capture A/B before reading SP, then push that byte. Decrement live SP only after the write succeeds. Preserve all flags and every other register.
family PSH "0011 011 r" for r in accumulators named "PSH{r}" {
original = operand r
perform pushByte(original)
}
Increment SP before reading, then replace A/B only after the read succeeds. Preserve all flags and every other register; a failed read retains the increment.
family PUL "0011 001 r" for r in accumulators named "PUL{r}" {
result = source popByte
operand r <- result
}
Calls and returns
BSR is $8D; JSR uses $AD/$BD for indexed/extended targets. $9D is undefined:
the original chip has no direct JSR. Resolve the complete target first, capture
the current PC as the return address, push low/high, and only then write PC.
The target remains captured even if stack writes overlap the instruction bytes.
source relativeTarget "relative call target": 16 {
offset = fetch
pc = register PC
return add(pc, signExtend(offset, 16))
}
action call "save return address and call" (target: 16) using memory {
returnPC = register PC
perform pushWord(returnPC)
PC <- target
}
Fetch a signed displacement and calculate its target from post-fetch PC. Capture PC again for the return address, push it low/high, and write the target only after both writes succeed. Preserve all flags and other registers.
family BSR "1000 1101" {
target = source relativeTarget
perform call(target)
}
Resolve the indexed or extended target before capturing the return PC. Push that PC low/high and write the target only after both writes succeed. Preserve all flags and other registers; failed writes retain completed stack effects.
family JSR "10 1m 1101" for m in unaryMemory.address {
target = source m
perform call(target)
}
Pull the complete high-first return address, then replace PC. Preserve every flag and other register. A failed read retains completed SP increments and prevents PC replacement.
family RTS "0011 1001" {
returnPC = source popWord
PC <- returnPC
}
Interrupt frames, WAI, SWI, and RTI
| Entry | Saved PC | Vector |
|---|---|---|
SWI (3F) |
Address after the opcode, wrapping at sixteen bits | FFFA / FFFB |
| IRQ | Current PC at the offered boundary | FFF8 / FFF9 |
| NMI | Current PC at the offered boundary | FFFC / FFFD |
SWI executes regardless of I. Entry preserves A/B/X and H/N/Z/V/C. Vector reads use current RAM after stack writes, so overlapping stack and vector locations affect the target. No handler instruction is prefetched. RTI uses ordinary RAM: its frame may be supplied or edited by the caller without a preceding interrupt.
The saved frame contains PC, X, A, B, and CC in push order. Capture each field only when its turn arrives. Word pushes transfer low/high, and CC is captured last, with the old I flag. The action below serves WAI, SWI, and external entry. Frame saving itself neither masks interrupts nor changes waiting state.
action saveFrame "save the complete interrupt frame" using memory {
pc = register PC
perform pushWord(pc)
index = register X
perform pushWord(index)
a = register A
perform pushByte(a)
b = register B
perform pushByte(b)
status = source CC
perform pushByte(status)
}
External entry and SWI skip frame saving when WAI already saved it. After any required frame, release waiting, set I, and read both vector bytes high first before changing PC. A failed vector read retains those state and stack effects.
action enterInterrupt "external interrupt entry" (vector: 16) using memory {
waiting = latch WAITING
when not(waiting) {
perform saveFrame()
}
WAITING <- 0
apply IFLAG(1)
high = memory(vector)
low = memory(add(vector, u16($0001)))
PC <- concat(high, low)
}
0011111 s selects WAI=s=0 or SWI=s=1. WAI saves the frame before waiting;
SWI enters the software vector at $FFFA. The execution contract below selects
IRQ/NMI recognition and the waiting-step policy.
Save the complete frame, then set WAITING. Preserve I until wake-up. Failed stack writes prevent waiting and retain only completed frame bytes and SP changes.
family WAI "0011111 0" {
perform saveFrame()
WAITING <- 1
}
Save a frame unless WAI already supplied it, release waiting, set I, then read
the high-first $FFFA vector before replacing PC. Preserve H/N/Z/V/C and other
registers; failed accesses retain completed effects.
family SWI "0011111 1" {
perform enterInterrupt(u16($FFFA))
}
RTI pulls CC, B, A, X, and PC in that order. Replace all six flags immediately after CC is read; commit each later register only after its complete read. Preserve waiting. Each pull increments live SP before reading; a failed read retains earlier increments and restorations.
family RTI "0011 1011" {
status = source popByte
replace CCFLAGS(status)
b = source popByte
B <- b
a = source popByte
A <- a
index = source popWord
X <- index
pc = source popWord
PC <- pc
}
Reset and instruction boundaries
The counter view and writer connect instruction fetching to stored PC. Both are state-only operations; the writer accepts a wrapped sixteen-bit address. Reset reads its vector high first, then writes PC, sets I, and releases waiting. No register or flag changes until both vector reads succeed; other state is preserved.
view NEXT "next instruction address": 16 {
address = register PC
return address
}
action setPC "write the program counter" (address: 16) {
PC <- address
}
action reset "read the reset vector, set I, and release waiting" using memory {
high = memory(u16($FFFE))
low = memory(u16($FFFF))
PC <- concat(high, low)
apply IFLAG(1)
WAITING <- 0
}
CPU reset
reset() reads FFFE followed by FFFF, loads their high-byte-first address
into PC, sets I, and clears waiting. It performs no instruction fetch or RAM write. A/B/X/SP,
the other five flags, and RAM are preserved. Preserving state for which the
reset specification does not establish values is a deterministic model policy;
it does not claim defined power-on values for those registers.
The Cpu6800ResetRecord has detached before/after snapshots and the two vector
reads, without an instruction or step outcome. Every reset rereads the current
vector. A later step executes from the resulting PC. Reset inside a subroutine
preserves the current SP and saved stack bytes; it does not unwind calls.
Reset does not restore an example's original registers or memory image. Creating a fresh example performs that lesson restart. A caller completion address belongs to the runner; the CPU itself does not stop at that address.
Instruction boundary policy
stopped WAITING as waiting makes an already-waiting step return without a
fetch. A successful WAI reports its fetched instruction and waiting; later
waiting steps have no instruction. Ignored IRQ offers preserve the saved frame
and waiting state. Accepted entry uses the action above to reuse that frame,
release waiting, mask IRQ, and read the selected vector. The runtime neither
clears WAITING nor saves a frame implicitly.
An unsupported opcode records its read but does not advance PC. Supported opcode fetches advance PC at dispatch; operand reads advance PC only after success. Failures retain completed effects and return no record. Step, reset, and interrupt share a reentrancy guard; snapshots remain available inside callbacks.
execution {
memory 16
counter NEXT write setPC
stopped WAITING as waiting
word big
opcode advance on dispatch
operand advance after read
failure retain
reset action reset
retire none
interrupt vectors {
source irq unless flag I with enterInterrupt($FFF8)
source nmi always with enterInterrupt($FFFC)
}
}
interface Cpu6800 "Instruction-level Motorola 6800 with explicit boundary IRQ/NMI delivery." {
}
The interface generates the public class, concrete state and record types, and
detached snapshots. The source catalogue defines the interrupt argument type;
only IRQ can yield ignored / masked. Construction copies and validates the
initial state without resetting or accessing RAM.
Waiting and external interrupt delivery
WAI (3E) advances PC, saves the same frame, then sets waiting: true.
It preserves every flag, including I. A masked IRQ leaves it waiting; an
accepted IRQ or NMI reuses the existing frame, clears waiting, sets I, and
reads the selected vector without further pushes. RTI consequently restores
the state saved by WAI. Reset also releases waiting, without unwinding the frame.
interrupt("irq" | "nmi") offers a selected request at the current instruction
boundary. IRQ is ignored when current I is set; NMI is always accepted.
The caller owns pending IRQ levels, NMI edge detection, and priority when
multiple sources need service. Offers are neither queued nor sampled by
step(). A repeated NMI offer represents another selected event and can nest.
Hardware look-ahead, cycle timing, and pin-level recognition are unmodeled.
Cpu6800InterruptRecord contains detached before/after snapshots,
source, instruction: null, ordered memory accesses, and either
outcome: "accepted" or outcome: "ignored", reason: "masked". Only IRQ can
be ignored; ignored entry makes no accesses or changes, including while waiting.
Invalid source values throw RangeError before accessing RAM or changing state.
The runner returns stopReason: "waiting" as soon
as a step reports it. The caller delivers an interrupt separately and can run
again. A waiting snapshot plus its RAM can initialize a fresh CPU and resume
through the same delivery path, without hidden saved frames or pending signals.
Transition boundaries and host failures
step(), interrupt(), and reset() share a per-instance execution guard.
RAM callbacks may inspect snapshots; nested mutating calls throw before they
access RAM or change state. The guard is released even when a transition throws.
A RAM error propagates without a result record or rollback. Completed accesses and register changes remain visible: instruction fetch advances PC after a successful read, a push decrements SP after a successful write, and a pull increments SP before its read. RTI retains each completed byte register/flag restore; a partly read word leaves X or PC unchanged. WAI sets waiting only after the complete frame. Entry sets I and releases waiting before the vector reads, retaining those changes if a read fails. Reset commits its PC/I/wait changes only after both reads succeed.
This is host-error behavior, not a modeled hardware memory exception. A failed transition has no automatic retry or continuation; callers can restore CPU/RAM or reset explicitly.
Checks and examples
Independent CPU tests cover every addition operand pair, all incoming flags
at arithmetic boundaries, every load/store/transfer/increment/decrement byte
and flag pattern, every store destination, every PC, and every reset-vector
value. Literal branch truth tables cover every flag combination; displacement
checks cover every byte, taken and untaken paths, and boundary wrapping.
Unary tests cover every byte and incoming flag combination in all 44 forms,
using arithmetic ranges and bit-string shifts for independent expectations.
Addressing checks cover every indexed displacement, wrapping, overlapping
instruction bytes, unchanged-value writes, and the distinct TST/CLR records.
Complete records and observed RAM calls check byte order, preserved state,
unchanged-value writes, self-modifying code, unsupported attempts (including
21), repeated reset, and detached snapshots.
Stack checks cover every LDS word, accumulator byte and incoming flag pattern, every push/pull SP, every BSR displacement, and every JSR/RTS target. Boundary cases cover word-wide LDS flags, PC/SP wrapping, overlapping code and stack bytes, low-first call writes, high-first return reads, unchanged flags, and edited stack RAM without hidden return state.
Logic tests use independent per-bit truth tables for every operand pair in both accumulators, with incoming flags all clear and all set. Further checks cover all flag patterns at byte boundaries and with alternating-bit operands, ordinary and wrapped PC, exact fetch records, BIT preserving its accumulator, current operands, and resumption after replacing an unassigned opcode.
All eighty accumulator-read encodings have literal-opcode tests across all incoming flag patterns and arithmetic boundaries. Independent signed-range calculations check every immediate arithmetic operand pair in both accumulators, including both incoming bits for ADC/SBC. The six stores check all byte values and flag patterns. Address tests cover every direct address byte, every unsigned indexed displacement at boundary X values, PC wrapping, and operands or stores overlapping fetched instructions. Actual RAM calls verify complete fetch/data ordering, preserved registers, and unchanged-value writes.
The generated arithmetic, counted-loop, stack, logic, and addressing examples check full initial/final memory images, explicit state, complete execution records, bounded running, caller completion, reset, and fresh restart. The loop also checks pause/resume and an edited displacement that repeats BNE until the step budget expires. The stack example nests BSR inside JSR, saves/restores both accumulators, resumes from snapshots and RAM at different call depths, and checks residual stack bytes and reset during a call. The logic example exercises all eight immediate forms inside a subroutine, branches on BIT results, preserves the caller's B on the stack, adds to A, and stores the answer. It checks snapshot resumption after either bit test, as well as edited masks that select the early-return and fallback paths. The addressing example propagates carry and borrow across A/B, wraps indexed addresses into page zero, and branches on a memory comparison. It checks snapshot resumption across the arithmetic chain and an edited addend that changes the borrow and selects the fallback path. The word-transformation example checks the full trace and memory image, the low-byte wrap path, resumption between ASR and ROR, and a bounded loop after a code edit. Shared shift tests cover every byte and incoming bit; the unchanged 6809 instruction tests verify its own flag policies. Parser, generator, and type checks preserve CPU-specific state and record contracts.
Word-transfer tests cover every loaded/stored word, all forms and flag patterns at boundaries, every direct address and indexed offset, and data/code overlap. CPX checks every high-byte pair with equal and unequal low bytes, including low-byte borrowing. Pointer operations check every 16-bit value and all flags at boundaries. TAP/TPA cover every A byte and flag pattern; subsequent arithmetic checks that restored flags remain live. ABA/SBA/CBA cover every byte pair. DAA checks every row of the adjustment table and every BCD operand pair after ABA/ADDA/ADCA, plus explicit policies for undefined inputs and V. JMP/JSR tests check target and stack wrapping, overlapping writes, and absence of target reads. The decimal example combines an indexed call, stack inspection, decimal arithmetic, packed flags, and a word comparison. It verifies complete records, RAM images, snapshot resumption, and a bounded failure loop.
Interrupt checks cover every flag pattern, all packed RTI status bytes, stack/code/vector overlaps and wrapping, current-mask offers, masked waits, frame reuse on wake, and live RAM edits. Failure injection checks each entry, return, and reset access; callbacks check reentrancy and inspection. A runner program combines WAI, IRQ, nested NMIs, RTI, and SWI, comparing complete traces and RAM after resuming a waiting snapshot. Parser/generator/type checks include the required waiting field and the waiting/interrupt record unions.
All 197 documented forms are complete; the opcode audit rejects the 59 undefined encodings. Mapped devices, external HALT, look-ahead, and cycle timing remain deferred. The 6800 has no separate port-I/O instruction forms.
Lifecycle specification tests edit reset addresses and effects, the waiting policy, wake-up actions, masks, vectors, source names, and the public interface, then execute the generated CPU. They check saved-frame reuse after snapshot resumption, failures at every reset and entry access, reentrant calls, and Markdown diagnostics. The shared runtime contains no 6800-specific vectors, flag choices, frame layout, or wake-up effects.
References
Hardware references are Motorola's M6800 Programming Reference Manual, November 1976, sections 1, 3.2–3.5, 4.6–4.7 and Appendix A's ADD, ADC, SUB, SBC, CMP, LDA, STA, TAB, TBA, INC, DEC, NEG, COM, ASL, ASR, LSR, ROL, ROR, TST, CLR, branch, LDS, PSH, PUL, JSR, RTS, AND, BIT, EOR, ORA, CPX, DAA, LDX, STX, STS, TSX, TXS, TAP, TPA, CLI, SEI, WAI, SWI, and RTI definitions; and the MC6800 data sheet in M6800 Systems Reference and Data Sheets, reset/interrupt descriptions and flow chart on pages 13–15 and instruction tables on pages 18–21. The MAME 6800 core also corroborates IRQ/NMI masking and reuse of the WAI frame. The supported encodings are for the original 6800; later-family additions and undocumented opcodes are outside this model.