The 6809 extends Motorola's accumulator-oriented programming model with a combined sixteen-bit D register, a movable direct page, two stack pointers, and richer indexed addressing. D is A:B, not separate storage. Its two bytes remain independently accessible as A and B.
This specification is the sole authored implementation and model contract for the
6809. Its cpu fences generate state, views, all 268 documented instruction
forms, indexed and page decoding, reset, ordinary execution, IRQ/FIRQ/NMI entry,
and the public class and record types. The surrounding prose explains the
hardware and distinguishes its behavior from the model's deliberate limits.
Historical and hardware background
The 6809 belongs to Motorola's late-1970s extension of the 6800 family. Motorola's 1978 company report placed initial sampling in the last quarter of that year. The instruction set retained the A/B accumulator model while adding facilities for larger programs: Y, a second stack, a movable direct page, word arithmetic, and richer indexed addressing. Motorola offered a macro assembler to translate 6800 source; compatibility depended on reassembly, not unchanged opcode bytes. The programming manual describes that migration and its limits.
A sixteen-bit address bus selects 64 KiB; the data bus carries eight bits at a time, so words travel as ordered byte pairs. S serves calls and interrupts, while U can serve program data and local storage independently. PC-relative addressing supports relocatable code. FIRQ reduces entry work by saving PC/CC alone; software can save additional registers as needed. These choices explain many of the patterns in the executable definitions below. Motorola manual
The MC6809 develops E/Q clocks from its oscillator connection; the MC6809E receives externally generated E/Q clocks. Both expose the same programming model, while differing in clock and bus-control connections. The E clock signal is distinct from the E flag in CC. Timing, bus arbitration, refresh, and physical pin sampling are hardware background rather than effects simulated here. Motorola signal descriptions
Tandy's Color Computer service manual identifies the 6809E as its CPU. A complete Color Computer also needs its memory map, display circuitry, ROM, and I/O devices. This specification supplies the processor model for that longer term direction; earlier CoCo implementation comparisons record evidence and ideas for the surrounding machine.
Model boundary
Cpu6809 models the original Motorola MC6809 instruction set, also used by
the MC6809E, connected to flat
64 KiB RAM. Clock and
pin differences between those parts are outside this instruction-level model.
This is not an HD6309 model or a complete Color Computer.
One step() attempts one instruction or reports an existing wait without
fetching. The caller owns completion addresses and execution budgets. All 268
documented forms are implemented, including SYNC, CWAI, RTI, and SWI/SWI2/SWI3.
External IRQ/FIRQ/NMI requests are explicit instruction-boundary offers.
Timing, pin sampling, dummy bus accesses, devices, and browser controls remain
outside the model. The 6809 has no separate port-I/O instructions.
Stored state
A and B hold bytes; X/Y are index registers, S/U are independent stack pointers, and PC selects the next instruction. DP supplies the high byte for direct addressing. All addresses wrap at sixteen bits. Initial state is explicit: construction does not reset registers, read vectors, or fabricate stack frames.
The eight CC flags are E/F/H/I/N/Z/V/C. E records the saved interrupt-frame shape; F and I mask FIRQ and IRQ. H supports decimal adjustment; N/Z/V/C describe arithmetic and logical results. NMI recognition also depends on the separate arming latch. The wait choice distinguishes ordinary execution, SYNC, and CWAI; a Boolean cannot preserve their different wake-up and saved-frame rules. The declaration defines the public stored fields and their copy order.
cpu "6809"
state {
register A: 8
register B: 8
register DP: 8
register X: 16
register Y: 16
register S: 16
register U: 16
register PC: 16
choice WAIT: "none", "sync", "cwai" = waitMode
latch NMIARMED = nmiArmed
flag E
flag F
flag H
flag I
flag N
flag Z
flag V
flag C
}
Opcode pages
Most instructions use one opcode byte. $10 and $11 select a second opcode
page: fetch exactly one more opcode byte, then decode it only within that page.
A repeated prefix or an unassigned second byte is unsupported; neither is
reinterpreted as a base-page opcode. Operand fetching starts after both opcode
bytes and retains ordinary PC wrapping and access-failure behavior.
page secondary = $10
page tertiary = $11
The page names belong to this specification; their prefix bytes determine generated
dispatch. Encodings below mark their page with on secondary or on tertiary.
SWI2/SWI3 use the same page declarations as word comparisons and transfers.
Register views and writes
Read A before B to assemble D. Writing D captures a complete word, then replaces A followed by B. Instructions, snapshots, and indexed D offsets use this same view; transfers, stacks, multiplication, and arithmetic use the same write action. Neither operation changes CC or touches memory.
view D "D from A:B": 16 {
high = register A
low = register B
return concat(high, low)
}
action writeD "write D as A then B" (word: 16) {
A <- highByte(word)
B <- lowByte(word)
}
CC is EFHINZVC; every bit is stored. Packing reads flags from bit seven to
bit zero. Restoring CC replaces the entire flag object. Register transfers,
stack frames, software interrupts, and external entry all use these definitions.
The mask action captures packed CC before applying the supplied mask; the
execution contract below selects that mask and decides when entry is allowed.
view CC "packed condition codes": 8 {
e = flag E
f = flag F
h = flag H
i = flag I
n = flag N
z = flag Z
v = flag V
c = flag C
return pack<8>(e, f, h, i, n, z, v, c)
}
policy CCFLAGS "restore all condition codes" (status: 8) {
E = bit(status, 7)
F = bit(status, 6)
H = bit(status, 5)
I = bit(status, 4)
N = bit(status, 3)
Z = bit(status, 2)
V = bit(status, 1)
C = bit(status, 0)
}
action maskCC "set interrupt masks in CC" (mask: 8) {
status = source CC
replace CCFLAGS(or(status, mask))
}
Register transfers and exchanges
TFR ($1F) and EXG ($1E) fetch a ssss dddd postbyte. The high nibble
selects the source, the low nibble the destination. Codes $0–$5 name
D/X/Y/U/S/PC; $8–$B name A/B/CC/DP. Only same-width pairs are modeled.
Unlisted or mixed-width pairs stop as unsupported after the postbyte fetch,
before reading or changing any selected register. PC therefore means the
post-fetch PC, including sixteen-bit wrapping.
Writable views keep the special register rules beside their definitions. D splits A then B; CC replaces all flags; an explicit S write arms NMI. Even TFR reads both originals before its first write. EXG writes the destination first, then the original destination to the source. This matters for aliases and for observing a partial write failure.
action writeCC "replace packed CC" (status: 8) {
replace CCFLAGS(status)
}
view STACK "system stack pointer": 16 {
pointer = register S
return pointer
}
action writeStack "write S and arm NMI" (pointer: 16) {
S <- pointer
NMIARMED <- 1
}
operands transferWords {
000 "D" = view D write writeD
001 "X" = register X
010 "Y" = register Y
011 "U" = register U
100 "S" = view STACK write writeStack
101 "PC" = register PC
110 "reserved" = unsupported
111 "reserved" = unsupported
}
operands transferBytes {
00 "A" = register A
01 "B" = register B
10 "CC" = view CC write writeCC
11 "DP" = register DP
}
codes transferOperation: 8 {
0 "EXG" = 1
1 "TFR" = 0
}
family registerTransfer "0001 111 t" for t in transferOperation named "{t}" {
exchange = operand t
postbyte = fetch
match postbyte {
case "0 sss 0 ddd" for s in transferWords, d in transferWords {
originalSource = operand s
originalTarget = operand d
operand d <- originalSource
when lowBit(exchange) {
operand s <- originalTarget
}
}
case "10 ss 10 dd" for s in transferBytes, d in transferBytes {
originalSource = operand s
originalTarget = operand d
operand d <- originalSource
when lowBit(exchange) {
operand s <- originalTarget
}
}
otherwise unsupported
}
}
Addressing and encodings
The base-page accumulator pattern is 1 r mm oooo: r=0/1 selects A/B,
mm=00/01/10/11 selects immediate/direct/indexed/extended, and oooo selects
the operation. The specification owns all four modes. Its memory-only pattern
1 r m1 oooo groups direct/extended with m=0/1; a separate mm=10 encoding
binds the indexed source to the same body. Indexed addressing can update its
base register and arm NMI before the instruction applies its own effects.
Immediate bytes are instruction fetches. Immediate words fetch high then low.
A direct operand fetches its offset before reading DP; the address is DP:offset.
For example, DP=$12 and offset=$34 select $1234. Extended addressing fetches
an absolute high/low pair. Captures preserve the resolved address even when a
subsequent memory callback changes live registers.
source immediateByte "#byte": 8 {
byte = fetch
return byte
}
source immediateWord "#word": 16 {
high = fetch
low = fetch
return concat(high, low)
}
source direct "direct": 16 {
offset = fetch
page = register DP
return concat(page, offset)
}
source extended "extended": 16 {
high = fetch
low = fetch
return concat(high, low)
}
Indexed postbytes
One captured postbyte describes the entire indexed operation. In 0 rr nnnnn,
rr=00/01/10/11 selects X/Y/U/S and nnnnn is a signed five-bit offset.
In 1 rr i mmmm, the low four bits select a mode and i=1 requests an indirect
high-first pointer read. PC-relative modes ignore rr; extended indirect uses
exactly $9F. Unlisted patterns are unsupported, after fetching the postbyte
but before further fetches, pointer updates, or data accesses.
The outer match captures the selected base before decoding the mode, including
PC-relative and invalid forms. Auto-updates use that captured base. Updating S
arms NMI before any later indirect read; an ordinary offset does not. The
five-bit offset is extended with $FFE0 when bit four is set. Byte offsets
use signed extension; adding an entire word modulo 65536 also gives its signed
interpretation. Every addition and pointer read wraps at sixteen bits.
operands indexedRegisters {
00 "X" = register X
01 "Y" = register Y
10 "U" = register U
11 "S" = register S
}
action armIndexedStack "arm NMI after indexed S update" (postbyte: 8) {
when equal(and(postbyte, u8($60)), u8($60)) {
NMIARMED <- 1
}
}
source indexed "indexed postbyte": 16 {
postbyte = fetch
address = match postbyte: 16 {
case "x rr xxxxx" for r in indexedRegisters {
base = operand r
effective = match postbyte: 16 {
case "0 xx xxxxx" {
offset = and(postbyte, u8($1F))
signed = select(lessThan(offset, u8($10), unsigned), extend(offset, 16), or(extend(offset, 16), u16($FFE0)))
return add(base, signed)
}
case "1 xx 0 0000" { // ,R+ (no indirect form)
operand r <- add(base, u16($0001))
perform armIndexedStack(postbyte)
return base
}
case "1 xx x 0001" { // ,R++
operand r <- add(base, u16($0002))
perform armIndexedStack(postbyte)
return base
}
case "1 xx 0 0010" { // ,-R (no indirect form)
updated = subtract(base, u16($0001))
operand r <- updated
perform armIndexedStack(postbyte)
return updated
}
case "1 xx x 0011" { // ,--R
updated = subtract(base, u16($0002))
operand r <- updated
perform armIndexedStack(postbyte)
return updated
}
case "1 xx x 0100" { // ,R
return base
}
case "1 xx x 0101" { // B,R
offset = register B
return add(base, signExtend(offset, 16))
}
case "1 xx x 0110" { // A,R
offset = register A
return add(base, signExtend(offset, 16))
}
case "1 xx x 1000" { // signed byte offset,R
offset = fetch
return add(base, signExtend(offset, 16))
}
case "1 xx x 1001" { // word offset,R
offset = source immediateWord
return add(base, offset)
}
case "1 xx x 1011" { // D,R
offset = source D
return add(base, offset)
}
case "1 xx x 1100" { // signed byte offset,PC
offset = fetch
pc = register PC
return add(pc, signExtend(offset, 16))
}
case "1 xx x 1101" { // word offset,PC
offset = source immediateWord
pc = register PC
return add(pc, offset)
}
case "1001 1111" { // extended indirect
absolute = source immediateWord
return absolute
}
otherwise unsupported
}
target = match postbyte: 16 {
case "0 xx xxxxx" {
return effective
}
case "1 xx 0 xxxx" {
return effective
}
case "1 xx 1 xxxx" {
high = memory(effective)
low = memory(add(effective, u16($0001)))
return concat(high, low)
}
otherwise unsupported
}
return target
}
otherwise unsupported
}
return address
}
source indexedByte "indexed byte": 8 {
address = source indexed
byte = memory(address)
return byte
}
Indirect reads happen only after mode-specific fetching and auto-updates. A failed second pointer read retains the first access and any earlier update. The resulting address is captured once for the complete instruction, even if subsequent memory callbacks change registers. Prefixed word instructions use the same generated source and specification-owned opcode bindings.
operands accumulators {
0 "A" = register A
1 "B" = register B
}
operands memoryModes {
0 "direct" = memory direct
1 "extended" = memory extended
}
Word data reads use the captured address, high byte first, then the following
address with sixteen-bit wrap. A direct word at $12FF continues at $1300,
not $1200; a word at $FFFF continues at $0000. The same addressing sources
serve stores, which never read the destination.
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 wordMemory {
0 "direct" = value directWord
1 "extended" = value extendedWord
}
operands indexRegisters {
0 "X" = register X
1 "U" = register U
}
Loads and stores
Loads and stores set N/Z from the complete byte or word, clear V, and preserve E/F/H/I/C. Loads write their register before flags. Stores apply flags only after every write succeeds; a failed second word write retains the first byte and old flags. Repeated writes are observable even if the value is unchanged.
policy RESULT<bits: 8, 16> "result" (result: bits) {
N = negative(result)
Z = zero(result)
V = 0
}
LDA/LDB use operation 0110. Fetch or read the complete operand, write the
selected accumulator, then set N/Z and clear V. Preserve the other accumulator
and E/F/H/I/C. A failed operand read prevents accumulator and flag updates;
completed addressing effects remain.
family LD {
encoding "1 r 00 0110" for r in accumulators with m = immediateByte named "LD{r} #byte"
encoding "1 r m1 0110" for r in accumulators, m in memoryModes.read named "LD{r} {m}"
encoding "1 r 10 0110" for r in accumulators with m = indexedByte named "LD{r} indexed"
result = source m
operand r <- result
apply RESULT<8>(result)
}
STA/STB use operation 0111, with no immediate encoding. Resolve the whole
address before reading A/B, write that byte once, then set N/Z and clear V.
Preserve E/F/H/I/C; a failed write retains old flags and completed addressing effects.
family ST {
encoding "1 r m1 0111" for r in accumulators, m in memoryModes.address named "ST{r} {m}"
encoding "1 r 10 0111" for r in accumulators with m = indexed named "ST{r} indexed"
address = source m
result = operand r
memory(address) <- result
apply RESULT<8>(result)
}
1 r mm 1110 selects LDX when r=0, LDU when r=1. Read or fetch high/low,
then replace the selected pointer and apply word-wide N/Z with V clear. Preserve
all other state left by addressing. A partial read prevents destination writeback;
an indexed auto-update of that same pointer still remains.
family LDindex {
encoding "1 r 00 1110" for r in indexRegisters with m = immediateWord named "LD{r} #word"
encoding "1 r m1 1110" for r in indexRegisters, m in wordMemory.read named "LD{r} {m}"
encoding "1 r 10 1110" for r in indexRegisters with m = indexedWord named "LD{r} indexed"
result = source m
operand r <- result
apply RESULT<16>(result)
}
1 r mm 1111 selects STX/STU and has no immediate form. Resolve the address,
capture the selected word, write high/low with sixteen-bit address wrap, then
only after both writes succeed, apply word-wide N/Z and clear V. Preserve
E/F/H/I/C and the source pointer as left by addressing. If the second write
fails, the first write remains and flags are unchanged.
family STindex {
encoding "1 r m1 1111" for r in indexRegisters, m in memoryModes.address named "ST{r} {m}"
encoding "1 r 10 1111" for r in indexRegisters with m = indexed named "ST{r} indexed"
address = source m
result = operand r
memory(address) <- highByte(result)
memory(add(address, u16($0001))) <- lowByte(result)
apply RESULT<16>(result)
}
LDD uses 11 mm 1100. Read or fetch both bytes before writing D as A then B;
then set word-wide N/Z and clear V. Preserve E/F/H/I/C and all pointers as
left by addressing.
A failed read leaves both accumulators and CC unchanged.
family LDD {
encoding "11 00 1100" with m = immediateWord named "LDD #word"
encoding "11 m1 1100" for m in wordMemory.read named "LDD {m}"
encoding "11 10 1100" with m = indexedWord named "LDD indexed"
result = source m
perform writeD(result)
apply RESULT<16>(result)
}
STD uses 11 mm 1101, excluding the immediate slot. Resolve the address,
capture A then B through D and write high/low. Only after both writes succeed,
set word-wide N/Z and clear V. Capture both source bytes before either write;
memory callbacks cannot change this stored word. Preserve E/F/H/I/C and registers
as left by addressing. If the second write fails, the first write remains and
flags are unchanged.
family STD {
encoding "11 m1 1101" for m in memoryModes.address named "STD {m}"
encoding "11 10 1101" with m = indexed named "STD indexed"
address = source m
result = source D
memory(address) <- highByte(result)
memory(add(address, u16($0001))) <- lowByte(result)
apply RESULT<16>(result)
}
Logic and bit tests
AND, BIT, EOR, and OR use operations 0100, 0101, 1000, and 1010.
They read the operand before A/B. N/Z describe the combined byte and V clears;
E/F/H/I/C are preserved. BIT differs from AND only by omitting writeback.
AND reads or fetches the operand, then captures the selected accumulator. Compute bitwise AND. Write the result before applying flags. Set N/Z and clear V; preserve E/F/H/I/C. A failed operand read prevents all register and flag effects.
family AND {
encoding "1 r 00 0100" for r in accumulators with m = immediateByte named "AND{r} #byte"
encoding "1 r m1 0100" for r in accumulators, m in memoryModes.read named "AND{r} {m}"
encoding "1 r 10 0100" for r in accumulators with m = indexedByte named "AND{r} indexed"
right = source m
left = operand r
result = and(left, right)
operand r <- result
apply RESULT<8>(result)
}
BIT reads or fetches the operand, then captures the selected accumulator. Compute bitwise AND. Preserve both accumulators without writing a result. Set N/Z and clear V; preserve E/F/H/I/C. A failed operand read prevents all register and flag effects.
family BIT {
encoding "1 r 00 0101" for r in accumulators with m = immediateByte named "BIT{r} #byte"
encoding "1 r m1 0101" for r in accumulators, m in memoryModes.read named "BIT{r} {m}"
encoding "1 r 10 0101" for r in accumulators with m = indexedByte named "BIT{r} indexed"
right = source m
left = operand r
result = and(left, right)
apply RESULT<8>(result)
}
EOR reads or fetches the operand, then captures the selected accumulator. Compute bitwise XOR. Write the result before applying flags. Set N/Z and clear V; preserve E/F/H/I/C. A failed operand read prevents all register and flag effects.
family EOR {
encoding "1 r 00 1000" for r in accumulators with m = immediateByte named "EOR{r} #byte"
encoding "1 r m1 1000" for r in accumulators, m in memoryModes.read named "EOR{r} {m}"
encoding "1 r 10 1000" for r in accumulators with m = indexedByte named "EOR{r} indexed"
right = source m
left = operand r
result = xor(left, right)
operand r <- result
apply RESULT<8>(result)
}
OR reads or fetches the operand, then captures the selected accumulator. Compute bitwise OR. Write the result before applying flags. Set N/Z and clear V; preserve E/F/H/I/C. A failed operand read prevents all register and flag effects.
family OR {
encoding "1 r 00 1010" for r in accumulators with m = immediateByte named "OR{r} #byte"
encoding "1 r m1 1010" for r in accumulators, m in memoryModes.read named "OR{r} {m}"
encoding "1 r 10 1010" for r in accumulators with m = indexedByte named "OR{r} indexed"
right = source m
left = operand r
result = or(left, right)
operand r <- result
apply RESULT<8>(result)
}
Comparison
CMPA/CMPB use byte operation 0001; CMPX uses 10 mm 1100. All three read
the operand first and preserve the compared register as left by addressing. Subtraction sets N/Z/V/C,
with C meaning unsigned borrow, while E/F/H/I remain unchanged. Unlike the
original 6800 CPX, CMPX uses a full sixteen-bit subtraction, including low-byte
borrow in N/V. X=$1200 compared with $1201 therefore gives N=1, Z=0, V=0, C=1.
policy COMPARE<bits: 8, 16> "comparison" (left: bits, right: bits, result: bits) {
N = negative(result)
Z = zero(result)
V = overflow(left, right)
C = borrow(left, right)
}
CMPA/CMPB read or fetch a byte before capturing A/B. Subtract without using incoming C; set N/Z/V/C and preserve E/F/H/I. Neither accumulator is written, and no flag changes if the operand read fails.
family CMP {
encoding "1 r 00 0001" for r in accumulators with m = immediateByte named "CMP{r} #byte"
encoding "1 r m1 0001" for r in accumulators, m in memoryModes.read named "CMP{r} {m}"
encoding "1 r 10 0001" for r in accumulators with m = indexedByte named "CMP{r} indexed"
right = source m
left = operand r
result = subtract(left, right)
apply COMPARE<8>(left, right, result)
}
CMPX reads or fetches a high-first word, then captures X. Subtract at sixteen bits without incoming C; set N/Z/V/C while preserving E/F/H/I and every register as left by addressing. A failed first or second byte read prevents flag updates; an indexed auto-update of X remains.
family CMPX {
encoding "10 00 1100" with m = immediateWord named "CMPX #word"
encoding "10 m1 1100" for m in wordMemory.read named "CMPX {m}"
encoding "10 10 1100" with m = indexedWord named "CMPX indexed"
right = source m
left = register X
result = subtract(left, right)
apply COMPARE<16>(left, right, result)
}
Prefixed word comparisons and transfers
The prefixed pages reuse all four operand modes: mm=00/01/10/11 selects
immediate/direct/indexed/extended. Words are fetched or read high byte first;
the compared register is captured only after the complete operand succeeds.
Indexed auto-updates, including updates of the compared register itself, remain
visible. A failed operand read prevents comparison flags or load writeback.
operands wordOperands {
00 "#word" = value immediateWord
01 "direct" = value directWord
10 "indexed" = value indexedWord
11 "extended" = value extendedWord
}
CMPD reads its complete operand before D. Apply full-word N/Z/V/C from subtraction, with C meaning borrow, preserving E/F/H/I. Do not write a result; completed addressing effects remain if an operand access fails.
family CMPD "10 mm 0011" on secondary for m in wordOperands.read named "CMPD {m}" {
right = source m
left = source D
result = subtract(left, right)
apply COMPARE<16>(left, right, result)
}
CMPY reads its complete operand before Y. Apply full-word N/Z/V/C from subtraction, with C meaning borrow, preserving E/F/H/I. Do not write a result; completed addressing effects remain if an operand access fails.
family CMPY "10 mm 1100" on secondary for m in wordOperands.read named "CMPY {m}" {
right = source m
left = register Y
result = subtract(left, right)
apply COMPARE<16>(left, right, result)
}
CMPU reads its complete operand before U. Apply full-word N/Z/V/C from subtraction, with C meaning borrow, preserving E/F/H/I. Do not write a result; completed addressing effects remain if an operand access fails.
family CMPU "10 mm 0011" on tertiary for m in wordOperands.read named "CMPU {m}" {
right = source m
left = register U
result = subtract(left, right)
apply COMPARE<16>(left, right, result)
}
CMPS reads its complete operand before S. Apply full-word N/Z/V/C from subtraction, with C meaning borrow, preserving E/F/H/I. Do not write a result; completed addressing effects remain if an operand access fails.
family CMPS "10 mm 1100" on tertiary for m in wordOperands.read named "CMPS {m}" {
right = source m
left = register S
result = subtract(left, right)
apply COMPARE<16>(left, right, result)
}
LDY reads or fetches the complete word, then writes Y before setting N/Z and clearing V. Preserve E/F/H/I/C. A failed read prevents these updates; earlier indexed updates and their NMI arming remain.
family LDY "10 mm 1110" on secondary for m in wordOperands.read named "LDY {m}" {
result = source m
Y <- result
apply RESULT<16>(result)
}
LDS reads or fetches the complete word, then writes S and arms NMI before setting N/Z and clearing V. Preserve E/F/H/I/C. A failed read prevents these updates; earlier indexed updates and their NMI arming remain.
family LDS "11 mm 1110" on secondary for m in wordOperands.read named "LDS {m}" {
result = source m
S <- result
NMIARMED <- 1
apply RESULT<16>(result)
}
STY resolves the address before capturing Y; it never reads the destination. Write high then low with sixteen-bit wrap. Only after both writes succeed, set N/Z and clear V, preserving E/F/H/I/C and the NMI arming left by addressing. A failed write leaves flags unchanged; completed writes, fetches, and addressing effects remain.
family STY {
encoding "10 m1 1111" on secondary for m in memoryModes.address named "STY {m}"
encoding "10 10 1111" on secondary with m = indexed named "STY indexed"
address = source m
result = register Y
memory(address) <- highByte(result)
memory(add(address, u16($0001))) <- lowByte(result)
apply RESULT<16>(result)
}
STS resolves the address before capturing S; it never reads the destination. Write high then low with sixteen-bit wrap. Only after both writes succeed, set N/Z and clear V, preserving E/F/H/I/C and the NMI arming left by addressing. A failed write leaves flags unchanged; completed writes, fetches, and addressing effects remain.
family STS {
encoding "11 m1 1111" on secondary for m in memoryModes.address named "STS {m}"
encoding "11 10 1111" on secondary with m = indexed named "STS indexed"
address = source m
result = register S
memory(address) <- highByte(result)
memory(add(address, u16($0001))) <- lowByte(result)
apply RESULT<16>(result)
}
Arithmetic
Byte SUB/SBC/ADC/ADD use operations 0000, 0010, 1001, and 1011.
SUBD/ADDD use 1 r mm 0011, with r=0 subtracting and r=1 adding.
Read the operand, then the accumulator, then any incoming C. Calculate with
byte or word wrap; replace arithmetic flags before register writeback.
Addition records unsigned carry and signed overflow; byte addition also sets H
from bit-three carry. Subtraction records borrow and signed overflow while
preserving H. Word arithmetic preserves H too. The manual leaves H undefined
for subtraction; preserving it is the model's deterministic policy. E/F/I never change. Subtracting
$FF with incoming borrow from a byte subtracts 256, not zero; the wrapped
result and carry/borrow tests deliberately use the original operands.
policy ADD8 "8-bit add" (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 "8-bit sub" (left: 8, right: 8, result: 8, incoming: flag) {
N = negative(result)
Z = zero(result)
V = overflow(left, right, incoming)
C = borrow(left, right, incoming)
}
policy ADD16 "16-bit add" (left: 16, right: 16, result: 16) {
N = negative(result)
Z = zero(result)
V = addOverflow(left, right)
C = carry(left, right)
}
policy SUB16 "16-bit sub" (left: 16, right: 16, result: 16) {
N = negative(result)
Z = zero(result)
V = overflow(left, right)
C = borrow(left, right)
}
SUB reads or fetches the operand, then captures A/B. Ignore incoming C. Subtract with byte wrap, update N/Z/V/C, preserving H, then write the selected accumulator. Preserve E/F/I and all other registers. Failed reads prevent flags and writeback; completed instruction fetches remain visible.
family SUB {
encoding "1 r 00 0000" for r in accumulators with m = immediateByte named "SUB{r} #byte"
encoding "1 r m1 0000" for r in accumulators, m in memoryModes.read named "SUB{r} {m}"
encoding "1 r 10 0000" for r in accumulators with m = indexedByte named "SUB{r} indexed"
right = source m
left = operand r
result = subtract(left, right)
apply SUB8(left, right, result, 0)
operand r <- result
}
SBC reads or fetches the operand, then captures A/B. Capture C as incoming borrow. Subtract with byte wrap, update N/Z/V/C, preserving H, then write the selected accumulator. Preserve E/F/I and all other registers. Failed reads prevent flags and writeback; completed instruction fetches remain visible.
family SBC {
encoding "1 r 00 0010" for r in accumulators with m = immediateByte named "SBC{r} #byte"
encoding "1 r m1 0010" for r in accumulators, m in memoryModes.read named "SBC{r} {m}"
encoding "1 r 10 0010" for r in accumulators with m = indexedByte named "SBC{r} indexed"
right = source m
left = operand r
incoming = flag C
result = subtract(left, right, incoming)
apply SUB8(left, right, result, incoming)
operand r <- result
}
ADC reads or fetches the operand, then captures A/B. Capture C as incoming carry. Add with byte wrap, update N/Z/V/C/H, then write the selected accumulator. Preserve E/F/I and all other registers. Failed reads prevent flags and writeback; completed instruction fetches remain visible.
family ADC {
encoding "1 r 00 1001" for r in accumulators with m = immediateByte named "ADC{r} #byte"
encoding "1 r m1 1001" for r in accumulators, m in memoryModes.read named "ADC{r} {m}"
encoding "1 r 10 1001" for r in accumulators with m = indexedByte named "ADC{r} indexed"
right = source m
left = operand r
incoming = flag C
result = add(left, right, incoming)
apply ADD8(left, right, result, incoming)
operand r <- result
}
ADD reads or fetches the operand, then captures A/B. Ignore incoming C. Add with byte wrap, update N/Z/V/C/H, then write the selected accumulator. Preserve E/F/I and all other registers. Failed reads prevent flags and writeback; completed instruction fetches remain visible.
family ADD {
encoding "1 r 00 1011" for r in accumulators with m = immediateByte named "ADD{r} #byte"
encoding "1 r m1 1011" for r in accumulators, m in memoryModes.read named "ADD{r} {m}"
encoding "1 r 10 1011" for r in accumulators with m = indexedByte named "ADD{r} indexed"
right = source m
left = operand r
result = add(left, right)
apply ADD8(left, right, result, 0)
operand r <- result
}
SUBD reads or fetches both operand bytes before capturing A then B through D. Subtract with sixteen-bit wrap, ignoring incoming C. Apply N/Z/V/C, then write D as A followed by B. Preserve E/F/H/I and all pointers. A partial operand read leaves the complete destination and flags unchanged.
family SUBD {
encoding "1 0 00 0011" with m = immediateWord named "SUBD #word"
encoding "1 0 m1 0011" for m in wordMemory.read named "SUBD {m}"
encoding "1 0 10 0011" with m = indexedWord named "SUBD indexed"
right = source m
left = source D
result = subtract(left, right)
apply SUB16(left, right, result)
perform writeD(result)
}
ADDD reads or fetches both operand bytes before capturing A then B through D. Add with sixteen-bit wrap, ignoring incoming C. Apply N/Z/V/C, then write D as A followed by B. Preserve E/F/H/I and all pointers. A partial operand read leaves the complete destination and flags unchanged.
family ADDD {
encoding "1 1 00 0011" with m = immediateWord named "ADDD #word"
encoding "1 1 m1 0011" for m in wordMemory.read named "ADDD {m}"
encoding "1 1 10 0011" with m = indexedWord named "ADDD indexed"
right = source m
left = source D
result = add(left, right)
apply ADD16(left, right, result)
perform writeD(result)
}
Decimal adjustment
DAA corrects a packed-decimal addition in A after 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. E/F/H/I remain unchanged.
DAA does not implement BCD subtraction or a decimal-mode latch.
policy DECIMAL "6809 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 E/F/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
oooo selects NEG=0000, COM=0011, LSR=0100, ROR=0110, ASR=0111,
ASL=1000, ROL=1001, DEC=1010, INC=1100, TST=1101, and CLR=1111.
010 r oooo selects A/B. 0000 oooo is direct memory and 0111 oooo is
extended memory; indexed uses 0110 oooo. All three encodings share one body
with a bound address source. JMP=1110 uses the address
without reading the destination and appears with control flow below.
Capture the destination once, then apply flags before writeback. A failed write retains those flags; a failed read prevents them. TST never writes. The 6809 reads even a CLR destination, preserves C on TST, and preserves V on right shifts. These rules differ from the 6800. All these operations preserve E/F/H/I.
policy NEGFLAGS "6809 NEG" (original: 8, result: 8) {
N = negative(result)
Z = zero(result)
V = overflow(u8($00), original)
C = borrow(u8($00), original)
}
policy COMFLAGS "6809 COM" (result: 8) {
N = negative(result)
Z = zero(result)
V = 0
C = 1
}
policy SHIFTFLAGS "6809 shift" (result: 8, outgoing: flag) {
N = negative(result)
Z = zero(result)
C = outgoing
V = xor(negative(result), outgoing)
}
policy RIGHTFLAGS "6809 right shift" (result: 8, outgoing: flag) {
N = negative(result)
Z = zero(result)
C = outgoing
}
policy DECFLAGS "6809 DEC" (original: 8, result: 8) {
N = negative(result)
Z = zero(result)
V = equal(original, u8($80))
}
policy INCFLAGS "6809 INC" (original: 8, result: 8) {
N = negative(result)
Z = zero(result)
V = equal(original, u8($7F))
}
policy TESTFLAGS "6809 TST" (result: 8) {
N = negative(result)
Z = zero(result)
V = 0
}
policy CLEARFLAGS "6809 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 E/F/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 E/F/H/I.
Apply flags before any write; a failed write retains them and completed addressing effects.
family NEGmemory {
encoding "0000 0000" with m = direct named "NEG direct"
encoding "0110 0000" with m = indexed named "NEG indexed"
encoding "0111 0000" with m = extended named "NEG extended"
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 E/F/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 E/F/H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family COMmemory {
encoding "0000 0011" with m = direct named "COM direct"
encoding "0110 0011" with m = indexed named "COM indexed"
encoding "0111 0011" with m = extended named "COM extended"
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 preserved. Set N/Z from the result; preserve E/F/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 RIGHTFLAGS(result, lowBit(original))
operand r <- result
}
Resolve the address once. Shift right, inserting zero. C is the outgoing low bit; V is preserved. Set N/Z from the result; preserve E/F/H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family LSRmemory {
encoding "0000 0100" with m = direct named "LSR direct"
encoding "0110 0100" with m = indexed named "LSR indexed"
encoding "0111 0100" with m = extended named "LSR extended"
address = source m
original = memory(address)
result = shiftRight(original, 0)
apply RIGHTFLAGS(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 preserve V. Set N/Z from the result; preserve E/F/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 RIGHTFLAGS(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 preserve V. Set N/Z from the result; preserve E/F/H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family RORmemory {
encoding "0000 0110" with m = direct named "ROR direct"
encoding "0110 0110" with m = indexed named "ROR indexed"
encoding "0111 0110" with m = extended named "ROR extended"
address = source m
original = memory(address)
carry = flag C
result = shiftRight(original, carry)
apply RIGHTFLAGS(result, lowBit(original))
memory(address) <- result
}
ASR
Shift right, retaining the original sign. Set C from the outgoing low bit and preserve V. Set N/Z from the result; preserve E/F/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 RIGHTFLAGS(result, lowBit(original))
operand r <- result
}
Resolve the address once. Shift right, retaining the original sign. Set C from the outgoing low bit and preserve V. Set N/Z from the result; preserve E/F/H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family ASRmemory {
encoding "0000 0111" with m = direct named "ASR direct"
encoding "0110 0111" with m = indexed named "ASR indexed"
encoding "0111 0111" with m = extended named "ASR extended"
address = source m
original = memory(address)
result = shiftRight(original, negative(original))
apply RIGHTFLAGS(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 E/F/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 E/F/H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family ASLmemory {
encoding "0000 1000" with m = direct named "ASL direct"
encoding "0110 1000" with m = indexed named "ASL indexed"
encoding "0111 1000" with m = extended named "ASL extended"
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 E/F/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 E/F/H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family ROLmemory {
encoding "0000 1001" with m = direct named "ROL direct"
encoding "0110 1001" with m = indexed named "ROL indexed"
encoding "0111 1001" with m = extended named "ROL extended"
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 E/F/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 E/F/H/I.
Apply flags before any write; a failed write retains them and completed addressing effects.
family DECmemory {
encoding "0000 1010" with m = direct named "DEC direct"
encoding "0110 1010" with m = indexed named "DEC indexed"
encoding "0111 1010" with m = extended named "DEC extended"
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 E/F/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 E/F/H/I.
Apply flags before any write; a failed write retains them and completed addressing effects.
family INCmemory {
encoding "0000 1100" with m = direct named "INC direct"
encoding "0110 1100" with m = indexed named "INC indexed"
encoding "0111 1100" with m = extended named "INC extended"
address = source m
original = memory(address)
result = add(original, u8($01))
apply INCFLAGS(original, result)
memory(address) <- result
}
TST
Test the byte, clearing V while preserving C, without writing a result. Set N/Z from the byte; preserve E/F/H/I and all registers.
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 while preserving C, without writing a result. Set N/Z from the result; preserve E/F/H/I. A failed read prevents flag updates.
family TSTmemory {
encoding "0000 1101" with m = direct named "TST direct"
encoding "0110 1101" with m = indexed named "TST indexed"
encoding "0111 1101" with m = extended named "TST extended"
address = source m
original = memory(address)
result = original
apply TESTFLAGS(result)
}
CLR
Read the destination even though its value is discarded. Set Z and clear N/C/V, then write zero; preserve E/F/H/I. No register other than the selected destination changes.
family CLRregister "010 r 1111" for r in accumulators named "CLR{r}" {
original = operand r
result = u8($00)
apply CLEARFLAGS()
operand r <- result
}
Resolve the address once. Read the destination even though its value is discarded. Set Z and clear N/C/V, then write zero; preserve E/F/H/I. Apply flags before any write; a failed write retains them and completed addressing effects.
family CLRmemory {
encoding "0000 1111" with m = direct named "CLR direct"
encoding "0110 1111" with m = indexed named "CLR indexed"
encoding "0111 1111" with m = extended named "CLR extended"
address = source m
original = memory(address)
result = u8($00)
apply CLEARFLAGS()
memory(address) <- result
}
Register and flag operations
NOP ($12) has no effects after opcode fetching. It does not read stored
registers or flags and makes no further memory access.
family NOP "0001 0010" {
}
ORCC ($1A) and ANDCC ($1C) capture the complete packed CC before fetching
the mask. Replace all eight flags with the combined byte. This is an ordinary
flag operation: it neither recognizes an interrupt nor reads a vector. A failed
mask fetch leaves CC unchanged; changes during a successful fetch do not alter
the captured status used by the operation.
Capture CC, fetch the mask, then replace all flags with their bitwise OR. Preserve every register and control field.
family ORCC "0001 1010" {
status = source CC
mask = fetch
replace CCFLAGS(or(status, mask))
}
Capture CC, fetch the mask, then replace all flags with their bitwise AND. Preserve every register and control field.
family ANDCC "0001 1100" {
status = source CC
mask = fetch
replace CCFLAGS(and(status, mask))
}
SEX ($1D) sign-extends B into D by replacing A with $00 or $FF.
N/Z describe the original B; V and every other flag are preserved. In particular,
SEX does not use the load-result policy, which would clear V.
policy SIGNEXTEND "SEX N/Z" (byte: 8) {
N = negative(byte)
Z = zero(byte)
}
Capture B, write its extended sign byte into A, then set N/Z. Preserve B, E/F/H/I/V/C, all pointers, and control state. No memory access occurs.
family SEX "0001 1101" {
byte = register B
A <- highByte(signExtend(byte, 16))
apply SIGNEXTEND(byte)
}
ABX ($3A) reads X then unsigned B, adds with sixteen-bit wrapping, and
replaces X. It neither reads nor writes flags; B and all other state remain intact.
family ABX "0011 1010" {
index = register X
byte = register B
X <- add(index, extend(byte, 16))
}
MUL ($3D) multiplies unsigned A and B into D. The byte-by-byte multiplication
produces a sixteen-bit result. C records product bit 7, which permits rounding
when only the high byte is retained; it is not an overflow or bit-15 test.
Only Z/C change, after writing A then B. N/V and the control flags are preserved.
policy PRODUCT "MUL Z/C" (product: 16) {
Z = zero(product)
C = negative(lowByte(product))
}
Read A then B and multiply their captured unsigned bytes. Write the complete product into D as A then B, then apply Z and C. Preserve E/F/H/I/N/V and all pointers and control state; no memory access occurs.
family MUL "0011 1101" {
left = register A
right = register B
product = multiply(left, right)
perform writeD(product)
apply PRODUCT(product)
}
Branches and jumps
0010 cccc, with cccc=tttp, selects T/HI/CC/NE/VC/PL/GE/GT via ttt;
p=1 inverts that test. The first pair is BRA/BRN. Each instruction fetches a
signed byte displacement before testing flags, and only a taken branch reads
the post-fetch PC. Addition wraps at sixteen bits. No branch changes flags.
Page $10 repeats 0010 cccc for LBRN/LBcc with high-first word displacements;
$10 $20 is undefined because LBRA has its own base-page opcode. Both lengths
share the same flag-read order and branch effects. The byte source widens its
signed offset; a word already has the required modulo-65536 representation.
source byteDisplacement "signed byte displacement": 16 {
offset = fetch
return signExtend(offset, 16)
}
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))
}
BRN ($21) and LBRN ($10 $21) consume byte/word displacements without reading
flags or PC and without writing any state. Displacement fetching can still fail
normally, retaining any earlier fetch.
family BRN {
encoding "0010 0001" with d = byteDisplacement named "BRN"
encoding "0010 0001" on secondary with d = immediateWord named "LBRN"
offset = source d
}
codes branchHI: 8 {
0 "BHI"
1 "BLS"
}
Fetch the complete 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 {
encoding "0010 001 p" for p in branchHI.read with d = byteDisplacement named "{p}"
encoding "0010 001 p" on secondary for p in branchHI.read with d = immediateWord named "L{p}"
offset = source d
c = flag C
z = flag Z
inverse = source p
when xor(and(not(c), not(z)), lowBit(inverse)) {
pc = register PC
PC <- add(pc, offset)
}
}
codes branchCC: 8 {
0 "BCC"
1 "BCS"
}
Fetch the complete displacement, then capture C. 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 {
encoding "0010 010 p" for p in branchCC.read with d = byteDisplacement named "{p}"
encoding "0010 010 p" on secondary for p in branchCC.read with d = immediateWord named "L{p}"
offset = source d
c = flag C
inverse = source p
when xor(not(c), lowBit(inverse)) {
pc = register PC
PC <- add(pc, offset)
}
}
codes branchNE: 8 {
0 "BNE"
1 "BEQ"
}
Fetch the complete displacement, then capture Z. 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 {
encoding "0010 011 p" for p in branchNE.read with d = byteDisplacement named "{p}"
encoding "0010 011 p" on secondary for p in branchNE.read with d = immediateWord named "L{p}"
offset = source d
z = flag Z
inverse = source p
when xor(not(z), lowBit(inverse)) {
pc = register PC
PC <- add(pc, offset)
}
}
codes branchVC: 8 {
0 "BVC"
1 "BVS"
}
Fetch the complete displacement, then capture V. 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 {
encoding "0010 100 p" for p in branchVC.read with d = byteDisplacement named "{p}"
encoding "0010 100 p" on secondary for p in branchVC.read with d = immediateWord named "L{p}"
offset = source d
v = flag V
inverse = source p
when xor(not(v), lowBit(inverse)) {
pc = register PC
PC <- add(pc, offset)
}
}
codes branchPL: 8 {
0 "BPL"
1 "BMI"
}
Fetch the complete displacement, then capture N. 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 {
encoding "0010 101 p" for p in branchPL.read with d = byteDisplacement named "{p}"
encoding "0010 101 p" on secondary for p in branchPL.read with d = immediateWord named "L{p}"
offset = source d
n = flag N
inverse = source p
when xor(not(n), lowBit(inverse)) {
pc = register PC
PC <- add(pc, offset)
}
}
codes branchGE: 8 {
0 "BGE"
1 "BLT"
}
Fetch the complete 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 {
encoding "0010 110 p" for p in branchGE.read with d = byteDisplacement named "{p}"
encoding "0010 110 p" on secondary for p in branchGE.read with d = immediateWord named "L{p}"
offset = source d
n = flag N
v = flag V
inverse = source p
when xor(not(xor(n, v)), lowBit(inverse)) {
pc = register PC
PC <- add(pc, offset)
}
}
codes branchGT: 8 {
0 "BGT"
1 "BLE"
}
Fetch the complete 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 {
encoding "0010 111 p" for p in branchGT.read with d = byteDisplacement named "{p}"
encoding "0010 111 p" on secondary for p in branchGT.read with d = immediateWord named "L{p}"
offset = source d
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, offset)
}
}
LBRA ($16) fetches a high-first word displacement before reading PC. Add that
captured word to the post-fetch PC with sixteen-bit wrapping, which also gives
the signed displacement result. Preserve flags and every other register.
family LBRA "0001 0110" {
offset = source immediateWord
pc = register PC
PC <- add(pc, offset)
}
JMP ($0E/$6E/$7E) resolves the direct/indexed/extended target, then replaces
PC without reading target memory. Indexed decoding may read an indirect pointer
or auto-update its base register before the jump. The jump itself changes only PC.
action jump "jump to a resolved address" (target: 16) {
PC <- target
}
Resolve the complete target, then replace PC. Do not read the destination; preserve flags and the state left by address decoding. A failed operand or pointer read retains completed addressing effects and prevents the jump.
family JMP {
encoding "0000 1110" with m = direct named "JMP direct"
encoding "0110 1110" with m = indexed named "JMP indexed"
encoding "0111 1110" with m = extended named "JMP extended"
target = source m
perform jump(target)
}
Subroutine stack
S points at the top occupied byte, unlike the 6800's next-free convention. Push decrements S before each write; pop reads at S, then increments the live S only after that read succeeds. All addresses and adjustments wrap at sixteen bits. Each adjustment uses live S, so a memory callback can affect the next access. Failed accesses retain completed writes and pointer changes.
Words occupy high/low order at ascending addresses. A descending stack therefore pushes low then high and pulls high then low. Ordinary calls and RTS preserve the NMI-arming latch, even though they adjust S. Explicit S-register writes and indexed auto-updates have their separate arming policies.
action pushByte "push byte through S" (byte: 8) using memory {
pointer = register S
S <- subtract(pointer, u16($0001))
address = register S
memory(address) <- byte
}
action pushWord "push high-first word through S" (word: 16) using memory {
perform pushByte(lowByte(word))
perform pushByte(highByte(word))
}
source popByte "pop byte through S": 8 {
address = register S
byte = memory(address)
pointer = register S
S <- add(pointer, u16($0001))
return byte
}
source popWord "pop high-first word through S": 16 {
high = source popByte
low = source popByte
return concat(high, low)
}
action pushUserByte "push byte through U" (byte: 8) using memory {
pointer = register U
U <- subtract(pointer, u16($0001))
address = register U
memory(address) <- byte
}
action pushUserWord "push high-first word through U" (word: 16) using memory {
perform pushUserByte(lowByte(word))
perform pushUserByte(highByte(word))
}
source popUserByte "pop byte through U": 8 {
address = register U
byte = memory(address)
pointer = register U
U <- add(pointer, u16($0001))
return byte
}
source popUserWord "pop high-first word through U": 16 {
high = source popUserByte
low = source popUserByte
return concat(high, low)
}
action call "save return address and call" (target: 16) using memory {
returnPC = register PC
perform pushWord(returnPC)
PC <- target
}
Calls resolve their complete target before capturing the return PC. Both values remain captured across stack writes, including when the stack overlaps code. The shared call action also serves indexed JSR after the specification decoder has completed its address effects. RTS consumes ordinary RAM; it needs no hidden call history and can return through a frame supplied or changed by the caller.
BSR ($8D) fetches a signed byte displacement, then captures post-fetch PC to
calculate the target. Capture PC again as the return address, push low/high,
and write the target only after both writes succeed. Preserve flags, other
registers, and NMI arming; failed writes retain completed stack effects.
family BSR "1000 1101" {
offset = fetch
pc = register PC
target = add(pc, signExtend(offset, 16))
perform call(target)
}
LBSR ($17) fetches a high-first word displacement, then reads PC to calculate
the wrapped target. Capture PC again as the return address, push low/high,
and write the target only after both writes succeed. Preserve flags, other
registers, and NMI arming; failed accesses retain completed fetch/stack effects.
family LBSR "0001 0111" {
offset = source immediateWord
pc = register PC
target = add(pc, offset)
perform call(target)
}
JSR ($9D/$AD/$BD) resolves a direct/indexed/extended target, then captures the
return PC. Push low/high and replace PC only after both writes succeed. Preserve
flags and the other registers and NMI arming left by address decoding; failed
accesses retain completed effects, including indexed S updates.
family JSR {
encoding "10 m1 1101" for m in memoryModes.address named "JSR {m}"
encoding "10 10 1101" with m = indexed named "JSR indexed"
target = source m
perform call(target)
}
RTS ($39) pulls a complete high-first return address and writes PC without
incrementing that address. Preserve flags, other registers, and NMI arming.
A failed second read retains the first successful S increment and prevents PC
replacement; a failed first read makes no stack adjustment.
family RTS "0011 1001" {
returnPC = source popWord
PC <- returnPC
}
Effective addresses
LEAX/LEAY/LEAS/LEAU use 001100 rr, with rr=00/01/10/11 selecting X/Y/S/U.
They run the complete indexed decoder, including indirection, before writing
the destination. A destination that is also the indexed base overwrites any
completed auto-update. X/Y replace only Z; S arms NMI, while U preserves flags.
A failed or unsupported decode prevents the destination write.
policy ADDRESSZERO "LEA Z" (address: 16) {
Z = zero(address)
}
Resolve the indexed address, then write X. Set Z from the address and preserve every other flag.
family LEAX "001100 00" {
address = source indexed
X <- address
apply ADDRESSZERO(address)
}
Resolve the indexed address, then write Y. Set Z from the address and preserve every other flag.
family LEAY "001100 01" {
address = source indexed
Y <- address
apply ADDRESSZERO(address)
}
Resolve the indexed address, then write S. Arm NMI and preserve every flag.
family LEAS "001100 10" {
address = source indexed
S <- address
NMIARMED <- 1
}
Resolve the indexed address, then write U. The destination write preserves every flag and the NMI arming left by decoding.
family LEAU "001100 11" {
address = source indexed
U <- address
}
Register masks and interrupt frames
PSHS/PULS ($34/$35) use S; PSHU/PULU ($36/$37) use U.
The fetched mask selects CC/A/B/DP/X/Y/the other stack pointer/PC in bits 0–7.
Pushes visit selected fields from bit seven down to zero; pulls visit bit zero
up to seven. Each push captures a field only when its turn arrives. Each pull
commits a field only after its complete byte or word read. Thus callbacks and
failed accesses can observe earlier transfers and pointer adjustments.
The four actions spell out that order using the byte/word primitives above. An empty mask does nothing. Ordinary PSHS/PULS arm NMI after a nonempty mask completes; a PULU write to S arms it immediately at that field's turn. Saving an interrupt frame uses the same S push action without ordinary PSHS arming.
action pushSystemRegisters "pushSystemRegisters" (mask: 8) using memory {
when bit(mask, 7) {
contents = register PC
perform pushWord(contents)
}
when bit(mask, 6) {
contents = register U
perform pushWord(contents)
}
when bit(mask, 5) {
contents = register Y
perform pushWord(contents)
}
when bit(mask, 4) {
contents = register X
perform pushWord(contents)
}
when bit(mask, 3) {
contents = register DP
perform pushByte(contents)
}
when bit(mask, 2) {
contents = register B
perform pushByte(contents)
}
when bit(mask, 1) {
contents = register A
perform pushByte(contents)
}
when bit(mask, 0) {
contents = source CC
perform pushByte(contents)
}
}
action pullSystemRegisters "pullSystemRegisters" (mask: 8) using memory {
when bit(mask, 0) {
contents = source popByte
perform writeCC(contents)
}
when bit(mask, 1) {
contents = source popByte
A <- contents
}
when bit(mask, 2) {
contents = source popByte
B <- contents
}
when bit(mask, 3) {
contents = source popByte
DP <- contents
}
when bit(mask, 4) {
contents = source popWord
X <- contents
}
when bit(mask, 5) {
contents = source popWord
Y <- contents
}
when bit(mask, 6) {
contents = source popWord
U <- contents
}
when bit(mask, 7) {
contents = source popWord
PC <- contents
}
}
action pushUserRegisters "pushUserRegisters" (mask: 8) using memory {
when bit(mask, 7) {
contents = register PC
perform pushUserWord(contents)
}
when bit(mask, 6) {
contents = register S
perform pushUserWord(contents)
}
when bit(mask, 5) {
contents = register Y
perform pushUserWord(contents)
}
when bit(mask, 4) {
contents = register X
perform pushUserWord(contents)
}
when bit(mask, 3) {
contents = register DP
perform pushUserByte(contents)
}
when bit(mask, 2) {
contents = register B
perform pushUserByte(contents)
}
when bit(mask, 1) {
contents = register A
perform pushUserByte(contents)
}
when bit(mask, 0) {
contents = source CC
perform pushUserByte(contents)
}
}
action pullUserRegisters "pullUserRegisters" (mask: 8) using memory {
when bit(mask, 0) {
contents = source popUserByte
perform writeCC(contents)
}
when bit(mask, 1) {
contents = source popUserByte
A <- contents
}
when bit(mask, 2) {
contents = source popUserByte
B <- contents
}
when bit(mask, 3) {
contents = source popUserByte
DP <- contents
}
when bit(mask, 4) {
contents = source popUserWord
X <- contents
}
when bit(mask, 5) {
contents = source popUserWord
Y <- contents
}
when bit(mask, 6) {
contents = source popUserWord
perform writeStack(contents)
}
when bit(mask, 7) {
contents = source popUserWord
PC <- contents
}
}
Fetch the mask before any register or stack effects. Apply ordinary system-stack NMI arming only after the selected action returns successfully.
family pshs "00110100" named "PSHS" {
mask = fetch
perform pushSystemRegisters(mask)
when not(zero(mask)) {
NMIARMED <- 1
}
}
family puls "00110101" named "PULS" {
mask = fetch
perform pullSystemRegisters(mask)
when not(zero(mask)) {
NMIARMED <- 1
}
}
family pshu "00110110" named "PSHU" {
mask = fetch
perform pushUserRegisters(mask)
}
family pulu "00110111" named "PULU" {
mask = fetch
perform pullUserRegisters(mask)
}
Waiting and software interrupts
SYNC ($13) enters its own wait mode without memory access. CWAI ($3C)
captures CC before fetching its mask, replaces CC with the masked value, sets E,
and saves the entire frame. It enters CWAI only after the final push succeeds.
The execution contract below distinguishes waking from accepting an interrupt.
A full frame sets E before saving PC/U/Y/X/DP/B/A/CC. Its action is shared by CWAI and software entry. The lower-level masked action also serves external FIRQ's short PC/CC frame. Neither action implicitly arms NMI.
policy ENTIRE "save an entire interrupt frame" () {
E = 1
}
action saveEntireFrame "set E and save all interrupt registers" using memory {
apply ENTIRE()
perform pushSystemRegisters(u8($FF))
}
action readVector "mask CC, leave waiting, and read a high-first vector" (vector: 16, masks: 8) using memory {
perform maskCC(masks)
WAIT <- "none"
high = memory(vector)
low = memory(add(vector, u16($0001)))
PC <- concat(high, low)
}
action softwareEntry "save frame and read software vector" (vector: 16, masks: 8) using memory {
waiting = choice WAIT = "cwai"
when not(waiting) {
perform saveEntireFrame()
}
perform readVector(vector, masks)
}
family sync "0001 0011" named "SYNC" {
WAIT <- "sync"
}
family cwai "0011 1100" named "CWAI" {
status = source CC
mask = fetch
replace CCFLAGS(and(status, mask))
perform saveEntireFrame()
WAIT <- "cwai"
}
SWI is base-page $3F, SWI2 is $10 $3F, and SWI3 is $11 $3F.
All use a full frame unless CWAI already saved one. SWI sets I/F after saving
the original CC; SWI2/SWI3 preserve them. Replace CC and leave waiting before
reading the vector high then low; write PC only after both reads succeed.
The vectors are $FFFA, $FFF4, and $FFF2 respectively.
family swi "0011 1111" named "SWI" {
perform softwareEntry(u16($FFFA), u8($50))
}
family swi2 "0011 1111" on secondary named "SWI2" {
perform softwareEntry(u16($FFF4), u8($00))
}
family swi3 "0011 1111" on tertiary named "SWI3" {
perform softwareEntry(u16($FFF2), u8($00))
}
RTI ($3B) restores CC first. The captured restored E then selects the remaining
full frame or PC alone. A full return pulls A/B/DP/X/Y/U/PC; a short return pulls
only PC. Arm NMI after the complete return succeeds. Failed reads preserve all
earlier restored fields and successful stack increments.
family rti "0011 1011" named "RTI" {
status = source popByte
perform writeCC(status)
entire = flag E
when entire {
perform pullSystemRegisters(u8($FE))
}
when not(entire) {
perform pullSystemRegisters(u8($80))
}
NMIARMED <- 1
}
Reset and external entry
Each vector contains the destination PC high byte first. Entry performs stack writes before vector reads, so overlapping stack writes can change the vector. There is no opcode fetch or prefetch for an external offer.
| Source | Vector | Frame | Masks set after saving CC |
|---|---|---|---|
SWI3 (11 3F) |
FFF2 |
Entire | None |
SWI2 (10 3F) |
FFF4 |
Entire | None |
| FIRQ | FFF6 |
Short, except after CWAI | F and I |
| IRQ | FFF8 |
Entire | I |
SWI (3F) |
FFFA |
Entire | F and I |
| NMI | FFFC |
Entire | F and I |
Reset reads $FFFE and $FFFF before changing any state. It then commits PC,
clears DP, sets F/I, leaves waiting, and disarms NMI. A failed vector read retains
all original state. Reset preserves A/B/X/Y/S/U and E/H/N/Z/V/C; it neither clears
RAM nor saves a frame. The explicit flag policies preserve those untouched bits.
view NEXT "next instruction address": 16 {
pc = register PC
return pc
}
action setPC "write the program counter" (address: 16) {
PC <- address
}
policy RESETMASKS "mask FIRQ and IRQ on reset" () {
F = 1
I = 1
}
action resetState "read reset vector then reset control state" using memory {
high = memory(u16($FFFE))
low = memory(u16($FFFF))
PC <- concat(high, low)
DP <- u8($00)
apply RESETMASKS()
WAIT <- "none"
NMIARMED <- 0
}
reset() returns a Cpu6809ResetRecord with detached readonly before,
after, and accesses; it has no instruction, outcome, or reason fields.
Motorola's RESTART entry specifies DP and the F/I masks but
leaves other CC bits unspecified. Their preservation here is a deterministic
model policy, not a promise about power-on values. Reset rereads the live vector
every time and performs no dummy reads or opcode prefetch. Restarting an example
instead creates fresh CPU and RAM from its definition, restoring all supplied
values and memory; see the machine guide.
IRQ and NMI save the twelve-byte entire frame. FIRQ clears E and saves only PC and CC: a three-byte frame. Each frame saves CC before changing interrupt masks. IRQ then sets I; FIRQ and NMI set F/I. The vector action releases waiting before reading its high and low bytes, committing PC only after both succeed. CWAI already saved an entire frame, so every accepted source reuses it without changing E or writing another frame. In particular, FIRQ after CWAI returns through the full frame. These actions share the same stack and vector effects as software entry; implicit pushes never arm NMI.
policy FRAME "select the interrupt frame shape" (entire: 8) {
E = lowBit(entire)
}
action enterInterrupt "save a hardware frame unless CWAI already did" (vector: 16, masks: 8, entire: 8) using memory {
waiting = choice WAIT = "cwai"
when not(waiting) {
apply FRAME(entire)
perform pushSystemRegisters(select(lowBit(entire), u8($FF), u8($81)))
}
perform readVector(vector, masks)
}
RTI follows the E bit actually stored in RAM. There is no hidden frame stack: edited or caller-created frames determine the return. E describes that saved frame; entry/return need not preserve its pre-entry value. Restoring a snapshot requires restoring the corresponding RAM as well.
Execution and public interface
One step() attempts an instruction unless WAIT is SYNC or CWAI. Waiting steps
have no instruction and no memory accesses. Executing SYNC/CWAI still records
its fetched bytes and returns waiting. Named interrupt offers occur between
steps. The caller supplies priority and edge detection; hardware priority is
NMI, FIRQ, then IRQ. This model does not queue offers or sample physical pins.
Invalid source names throw RangeError without changing state. Ignored and
resumed offers have no memory accesses; their instruction is null. An accepted
record has no reason. Masked/unarmed offers are not retained, so the caller must
offer them again if appropriate. The runner
stops on waiting; a caller may offer an interrupt and run again.
An unarmed NMI is ignored and cannot release SYNC. A masked IRQ/FIRQ releases
SYNC without stacking or reading a vector, returning resumed with reason
masked. The same offer during ordinary execution or CWAI is ignored. An
accepted source runs its entry action. The declaration makes the distinction
between a flag mask, an arming latch, and a masked wake-up visible:
action resumeSync "leave SYNC without taking an interrupt" {
WAIT <- "none"
}
The boundary binds state views and actions to byte dispatch and named entry. Its interface adds the derived D snapshot field without storing another register.
execution {
memory 16
counter NEXT write setPC
stopped choice WAIT unless "none" as waiting
word big
opcode advance on dispatch
operand advance after read
failure retain
reset action resetState
retire none
interrupt vectors {
source irq unless flag I with enterInterrupt($FFF8, $10, $01) resume when choice WAIT = "sync" with resumeSync()
source firq unless flag F with enterInterrupt($FFF6, $50, $00) resume when choice WAIT = "sync" with resumeSync()
source nmi when latch NMIARMED otherwise "unarmed" with enterInterrupt($FFFC, $50, $01)
}
}
interface Cpu6809 "Instruction-level Motorola 6809 with explicit IRQ, FIRQ, and NMI entry." {
snapshot d = D
}
The generated public class exposes snapshot(), step(), reset(), and
interrupt(source). Its records preserve the source/outcome/reason relationships
in the execution declaration. The following sections define its host contract.
State and initialization
Cpu6809State contains a, b, dp, x, y, s, u, pc, and
flags, waitMode, and nmiArmed. Cpu6809Flags contains eight booleans: e, f, h, i, n,
z, v, and c, corresponding to the condition-code register's bit order.
Memory uses the byte-connection contract, with RAM as the usual connection.
new Cpu6809(ram, initialState: Omit<Cpu6809Snapshot, "d">) requires exactly
64 KiB of byte-memory address space. A, B, and DP must be integers in 00–FF; X, Y, S, U, and PC
must be integers in 0000–FFFF. Invalid numeric values or address-space sizes throw
RangeError; non-boolean flags or nmiArmed throw TypeError.
waitMode must be exactly "none", "sync", or "cwai"; other values throw
RangeError. Both control fields are required and copied into snapshots. The constructor copies only
declared stored fields, including inherited getters and non-enumerable fields,
once before validation. It retains neither the caller's state object nor its
nested flags object. Construction performs no reset, vector read, or
instruction fetch.
Initial registers and flags are explicit caller choices, not power-on or reset defaults. Flags are supplied state, not inferred from the initial A value. Register values exposed in records are numbers; hexadecimal formatting belongs to presentation. PC and operand fetches wrap to 16 bits, while RAM validates host addresses and values instead of wrapping them.
F and I are stored interrupt-mask bits; E is a stored stacking indicator. S is the hardware stack pointer used by calls and interrupts; U is a separate programmer-controlled stack pointer. See the register descriptions. The stack example defines packing and unpacking CC for stack transfers; CC is not separately stored public state. NMI arming is separate from S's numeric value; see NMI arming.
D is not a separate initialization input. Passing an existing snapshot is
structurally permitted; its d is ignored and recomputed from the copied A
and B. Extra properties, including a supplied d getter, must not be read.
There is no public setter for D. Word arithmetic, LDD, MUL, and register
transfers update A/B together; STD reads the combined word without storing
another copy.
Snapshots and ownership
snapshot() returns a detached Cpu6809Snapshot without accessing RAM.
Before/after snapshots, instruction bytes, and access entries are recursively
readonly to TypeScript and independent of later CPU or RAM changes. JavaScript
edits to returned objects cannot affect live state or other records. No runtime
freezing is required. D is consistent with A/B when a snapshot is produced;
bypassing readonly checks does not make the returned copy a live register view.
Snapshots copy CPU state, not RAM.
Step records
Cpu6809MemoryAccess has readonly kind: "read" | "write", address: number,
and value: number. Cpu6809Instruction has readonly address: number and
bytes: readonly number[]. The step record is:
export type Cpu6809StepRecord =
| InstructionStep<Cpu6809Snapshot>
| WaitingStep<Cpu6809Snapshot>;
The shared record types
provide readonly before, after, instruction, and accesses fields.
An executed or unsupported attempt has a non-null instruction; unsupported
records also carry reason: "opcode". Executing SYNC or CWAI returns
outcome: "waiting" with its fetched instruction and accesses. Later step()
calls while waiting return waiting, a null instruction, no accesses, and
unchanged state. Executed and waiting records have no reason.
The CPU has no halted or complete outcome and retains no record history.
Instruction bytes come from actual opcode and operand fetches; data reads and
writes appear only in accesses. Do not reread RAM to construct a record.
These accesses describe the instruction-level model, not every electrical bus operation or idle cycle. Records have no cycle-count or elapsed-time field.
NMI arming
Reset clears nmiArmed, regardless of S. Construction instead uses the explicit
supplied latch, allowing a snapshot of initialized execution to resume.
Instruction effects are checked against Motorola's S-initialization discussion
and XRoar's MC6809 core (version 1.12.1):
- LDS, LEAS, TFR to S, and EXG involving S arm NMI, even when the value is unchanged.
- PULU arms when it successfully restores S; nonempty PSHS/PULS and completed RTI also arm NMI.
- Indexed increment/decrement of S arms NMI when that address update occurs.
- Merely reading S, empty stack masks, implicit call/RTS stack updates, and interrupt/CWAI stacking do not arm it. Rejected encodings do not arm it.
The latch stays armed until reset. An interrupted load cannot arm before the value is complete; an indexed S update already performed survives a later memory failure. An explicit request after the arming instruction can be accepted; a prior unarmed offer is not automatically redelivered.
Host failures and reentrancy
RAM errors propagate immediately, without a fabricated record or transaction rollback. Completed fetches advance PC, completed reads/writes retain their effects, and S predecrements before each attempted push. A failed pull does not increment S; a word register changes only after both bytes are read. Entry selects E before stacking and applies masks/releases a wait only after its frame is complete. CWAI applies its CC mask before stacking and enters its wait only after all writes succeed. RTI exposes successfully restored registers even if a later read fails. Vector PC commits only after both reads; reset commits all of its state changes at that point.
A shared execution guard rejects nested step(), reset(), or interrupt()
calls during any of those transitions, including calls through RAM callbacks.
snapshot() remains available to inspect the current partial state. The guard
is released on success or failure. After a host error the caller must inspect
or restore state before deciding how to continue; repeating the operation is
not an automatic retry of an atomic instruction.
Unsupported instructions and prefixes
For an unsupported first byte, record one opcode read and unchanged state and RAM. A repeated attempt repeats the same read and leaves PC in place. The coverage tracker lists the current supported forms.
For an undefined indexed or transfer/exchange postbyte, record the opcode
(including its prefix, if present) and postbyte reads,
return reason opcode, and leave all state (including PC) and RAM unchanged.
Do not fetch offset bytes, read an indirect pointer, update an index register,
or execute the operation. Replacing the postbyte in RAM allows the next attempt
to proceed normally. Rejection is a model boundary, not an emulation of the
hardware's undefined behavior.
Prefix policy: 10 and 11 select additional opcode pages in the
hardware opcode map. A step fetches the prefix and exactly one
following opcode, then any operands of that page's supported instruction.
Unsupported page entries record [prefix, opcode], reason opcode, and
unchanged state/RAM, with PC restored to the prefix. They fetch no operands.
Repeated prefixes do not nest or fall back to the base page. Fetching can
wrap at FFFF; each attempt consults current RAM and retains no prefix latch.
The caller must stop on unsupported results and use a bounded instruction budget when running programs.
Checks and examples
The CPU tests retain independent
opcode, arithmetic, addressing, interrupt, and failure expectations. The
specification tests
check the exact 268-form inventory, state and view ownership, formal edits, live
read ordering, wrapping, and failed accesses. Unary tests distinguish preserved
V/C and CLR read failures; call/return tests retain live S changes and partial
stack effects. Formal multiplication checks also exercise full-width word
products and width diagnostics. Integration tests
show state and action edits reaching construction, snapshots, .machine parsing,
and indexed calls through the public CPU. Indexed specification tests
check all 256 postbytes, signed offsets, captured bases, live PC-relative reads,
indirection failures, and formal pattern/update edits. Integration edits also
change indexed behavior and prefix bytes in the public CPU, including the
specification-owned SWI2/SWI3 forms. Page tests
check isolated opcode spaces, aliases, collisions, exact fetch counts, unsupported
results, and failures without relying on a 6809-specific dispatcher.
Register-transfer probes check all legal and unsupported postbytes, both-original capture order, aliases, CC replacement, and S arming. Mask probes check every mask, live-pointer changes, and failure at each access. Interrupt probes retain independently written frame and vector schedules. Formal edits to the S write action, wait choice, and software-entry vector reach the generated bodies.
Lifecycle specification tests edit reset vectors and effects, interrupt frame arguments, gates and wake-up rules, names, and derived snapshot fields through the generated public class. A differently named CPU with renamed storage and sources uses the same runtime. The public type checks retain constructor, readonly ownership, and outcome narrowing guarantees.
The coverage tracker records support and source footprint. Its linked examples supply complete programs and acceptance traces rather than additional CPU rules.
Reference comparisons
Recorded comparisons used dromaios-coco at 099aeb7 with flat
observed RAM. They supplement independent manual-derived tests; they are not
hardware traces or a certification of that emulator.
| Comparison | Cases | Checked observations |
|---|---|---|
| 137 base-page opcodes × all 256 initial CC values | 35,072 | Stored registers, derived D, flags, and ordered instruction-level RAM accesses, with varied data and PC/S boundaries |
| 41 indexed opcodes × 217 documented postbytes × two CC patterns | 17,794 | State, flags, ordered accesses, and written memory, including wrapping |
| 15 immediate/direct/extended word transfers × all CC values | 3,840 | The same observations; the six indexed word forms are in the preceding row |
The comparisons agreed after accounting for the reference's omitted memory read in 512 direct/extended and 434 indexed CLR cases. Motorola's CLR entry requires that read, which Dromaios independently tests, including code/data overlap and unchanged writes. Indexed word stores resolve and update their address before reading the source; loads can replace an auto-updated destination.
The reference accepts undefined indexed forms, including indirect one-byte
increment/decrement and aliases of extended indirect. Dromaios follows
Motorola Table 2-1: PC-relative forms ignore rr, but extended indirect
is exactly 9F. All 39 undefined postbytes have independent rejection tests.
The reference also clears general registers on reset and advances PC on
unsupported instructions; this chapter's reset and rejection policies differ.
The pinned CoCo architecture note assigns BSR/JSR to U, but both the manual and its instruction implementation use S. That prose error must not enter lessons about the two stacks. Its memory watchpoints also motivate inspection without device reads.
References
- Motorola company background report (1978): planned 6809 sampling.
- Tandy Color Computer service manual: 6809E system context.
- Motorola MC6809–MC6809E programming manual, sections 1–3: register relationships, reset, NMI arming, and vector byte order.
- Motorola instruction details, Appendix A: accumulator operations, word arithmetic, addressing, register transfers, decimal adjustment, multiply, sign extension, branch conditions, RESTART, and calls.
- Appendix F opcode map: prefixes and opcode pages.
- XRoar source release 1.12.1,
src/mc6809/mc6809.c: cross-check for the instruction-specific NMI arming effects, including stack and indexed updates beyond the manual's explicit LDS/TFR/EXG examples.
The Motorola manual links are HTML transcriptions. Explicit initialization, preservation of unspecified state on reset, prefix rejection, record ownership, and omitted accesses are deliberate model choices.