Processor guides / Introduced 1978

Motorola 6809

Guide and executable specificationView source ↗
Explore the model’s stored state
Motorola 6809 stored-state schema: registers, arrays, flags, and control fields grouped by type.
Generated by Sauvignon from this processor’s compiled state declarations. Labels are stored field names; register aliases and physical wiring are not shown. Select the diagram to view it at full size.

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

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.