Skip to main content

Instruction-level emulator for the Motorola 68HC11 microcontroller core

Project description

m68hc11

An instruction-level emulator for the Motorola 68HC11 8-bit microcontroller core, in a single self-contained Python module with no third-party runtime dependencies.

It exists to execute real ROM machine code and read back exactly what it computes — register values, memory writes, and elapsed bus cycles — so that arithmetic and timing done in firmware can be derived by running it rather than by squinting at a disassembly.

  • Full 68HC11 instruction set: all addressing modes, all four opcode pages (un-prefixed plus the $18 / $1A / $CD prefix pages), bit-manipulation (BSET/BCLR/BRSET/BRCLR), and the fiddly ALU ops (MUL, IDIV, FDIV, DAA, the shifts/rotates, ABA/SBA/CBA, ADDD/SUBD, …) with correct condition-code flag effects.
  • A documented per-instruction cycle count accumulated into a running total, so elapsed time follows from a known bus-clock frequency.
  • A flat 64 KB memory with I/O hooks for memory-mapped peripheral registers / open bus.
  • A call() harness that runs a subroutine like a function and hands back the full post-state.
  • An optional, layered main-timer + interrupt model (output compares, timer overflow, vectored interrupt delivery).
  • Illegal opcodes raise — the emulator never silently mis-decodes.

Determinism: pure functions only, no wall-clock, no RNG, no global state. The same inputs always produce the same outputs.

Install

pip install m68hc11

The import name is m68hc11:

import m68hc11
cpu = m68hc11.HC11()

Requires Python 3.9+.

Quick start

import m68hc11

cpu = m68hc11.HC11()

# LDAA #$07 ; LDAB #$06 ; MUL ; RTS   ->  D = 7*6 = 42
cpu.load(bytes([0x86, 0x07, 0xC6, 0x06, 0x3D, 0x39]), 0x2000)

state = cpu.call(0x2000)
print(state.d)        # 42
print(state.cycles)   # 2 + 2 + 10 + 5 = 19 bus cycles

The call() harness

call() pushes a sentinel return address, sets PC and any argument registers, then single-steps until the routine's final RTS pops the sentinel (or until max_steps). It returns a State snapshot of every register, flag, and the cycles elapsed during the call.

cpu = m68hc11.HC11()

# Double the byte at $00C0 in place and also return it in A.
#   LDAA $C0 ; ASLA ; STAA $C0 ; RTS
cpu.load(bytes([0x96, 0xC0, 0x48, 0x97, 0xC0, 0x39]), 0x2000)
cpu.write(0x00C0, 0x09)

st = cpu.call(0x2000)
assert cpu.read(0x00C0) == b"\x12"
assert st.a == 0x12
assert st.cycles == 13

Pass inputs via keyword: call(addr, a=…, b=…, d=…, x=…, y=…). Set the stack pointer first (it defaults to $01FF) if your routine pushes.

API

class HC11:
    def __init__(self, *, ram_fill: int = 0x00) -> None: ...

    # memory
    def load(self, data: bytes, base: int) -> None      # raw blob copy (no hooks)
    def read(self, addr: int, n: int = 1) -> bytes
    def write(self, addr: int, data: bytes | int) -> None
    def read16(self, addr: int) -> int                  # big-endian
    def write16(self, addr: int, value: int) -> None
    def read8(self, addr: int) -> int                   # hook-aware
    def write8(self, addr: int, val: int) -> None       # hook-aware

    # registers (plain attributes)
    a; b; x; y; sp; pc; ccr
    d                                                   # property (a<<8)|b, settable
    s; x_irq; h; i; n; z; v; c                          # flag bool properties
    def set_regs(self, *, a=None, b=None, d=None, x=None,
                 y=None, sp=None, pc=None, ccr=None) -> None

    # I/O hooks (inclusive ranges)
    def on_read(self, start: int, end: int, fn) -> None   # fn(addr) -> int
    def on_write(self, start: int, end: int, fn) -> None  # fn(addr, value)

    # execution
    cycles: int                                         # cumulative bus cycles
    def reset_cycles(self) -> None
    def step(self) -> Step                               # execute ONE instruction
    def run(self, *, max_steps=None, until_pc=None,
                  until_cycles=None) -> str              # returns stop reason
    def call(self, addr, *, a=None, b=None, d=None, x=None, y=None,
             max_steps=1_000_000, sentinel=0xFFFE) -> State

    # tracing / debug
    def set_trace(self, fn) -> None                     # fn(Step) before each instruction
    def disassemble(self, addr: int) -> Step            # decode without executing

    # optional timer + interrupts (spec section 6)
    def enable_timer(self, base: int = 0x1000) -> None
    def run_until_irq(self, vector, count=1, max_cycles=10_000_000) -> str

Step (returned by step, passed to the trace fn): pc, opcode bytes, a mnemonic disassembly string, cycles (this instruction), and the resolved ea (effective address) if any.

State (returned by call): all registers + individual flags + cycles elapsed during the call.

run() stop reasons: "max_steps", "until_pc", "until_cycles", "wai", "stop", "illegal".

I/O hooks

Plain reads/writes hit the backing array unless an address is covered by a hook, in which case the hook is called instead — letting peripheral registers be stubbed or modelled without touching the core:

regs = {0x1004: 0x37}
cpu.on_read(0x1000, 0x103F, lambda addr: regs.get(addr, 0xFF))
cpu.on_write(0x1000, 0x103F, lambda addr, val: regs.__setitem__(addr, val))

Tracing

cpu.set_trace(lambda step: print(f"{step.pc:04X}: {step.mnemonic}"))
cpu.run(max_steps=20)

Timer + interrupts (optional)

Layered on top of the core — the core works without ever calling enable_timer(). When enabled, a 16-bit free-running counter TCNT advances by the prescale selected in TMSK2, output compares latch flags in TFLG1, and enabled+unmasked sources deliver vectored interrupts (full HC11 context stacked). The on-chip register block base defaults to $1000 and is parameterizable.

cpu = m68hc11.HC11()
cpu.enable_timer()                 # registers in the $1000 block
cpu.set_regs(pc=0x0100, sp=0x01FF, ccr=0x00)  # I clear -> interrupts enabled
cpu.write16(0x1018, 0x000A)        # TOC2 compare value
cpu.write8(0x1022, 0x40)           # TMSK1: enable the OC2 interrupt
cpu.write16(0xFFE6, 0x3000)        # OC2 interrupt vector -> handler
cpu.load(bytes([0x20, 0xFE]), 0x0100)  # BRA * (spin)

reason = cpu.run_until_irq(0xFFE6, count=1)
assert reason == "irq" and cpu.pc == 0x3000

Correctness & validation

The test suite (tests/) encodes the spec's acceptance criteria:

  1. Flag / arithmetic self-test — hand-checked sequences for the tricky cases (MUL C-bit, ASLD/LSRD, DAA, ADDD/SUBD overflow & carry, NEG/COM carry, signed branches, ABX/XGDX).
  2. Cross-validation routines — small hand-assembled routines whose final registers, CCR, and exact cycle counts are computed by hand from the M68HC11 reference manual. These are good candidates to diff against sim/m68hc11 (GNU binutils-gdb) or MAME.
  3. The call() harness demonstrably running self-contained subroutines that read inputs from registers/memory and return results in D/X/memory with cycles reported.

Run them with:

pip install -e ".[test]"
pytest

Notes on a couple of documented edge cases

  • DAA leaves the V flag unchanged (the reference manual documents V as undefined after DAA).
  • IDIV/FDIV leave N and H unaffected; divide-by-zero sets C and forces the quotient (X) to $FFFF.
  • TAP can clear the X (XIRQ) mask but, per the hardware, cannot set it.

Reference material

  • M68HC11 Reference Manual (M68HC11RM) — instruction set, cycle-by-cycle operation, exact CCR effects.
  • M68HC11 Programming Reference Guide (M68HC11PG) — compact opcode map for the page-1 / $18 / $1A / $CD pages, lengths, and cycle counts.
  • A device data sheet (e.g. MC68HC11A8 / MC68HC11F1) for the $1000 register block and the $FFC0–$FFFF vector table.

Releasing (maintainers)

Tagged releases publish to PyPI automatically via trusted publishing (OIDC, no API tokens). See .github/workflows/publish.yml. To wire it up the first time, on PyPI add a pending publisher (or, after the first manual upload, a trusted publisher) for project m68hc11 with:

Field Value
Owner anarkiwi
Repository m68hc11
Workflow name publish.yml
Environment name pypi

Then publishing a GitHub Release builds the sdist + wheel and uploads them with no secrets stored in the repo.

License

Apache-2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

m68hc11-0.1.0.tar.gz (33.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

m68hc11-0.1.0-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

Details for the file m68hc11-0.1.0.tar.gz.

File metadata

  • Download URL: m68hc11-0.1.0.tar.gz
  • Upload date:
  • Size: 33.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for m68hc11-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ef22f906d40c207fe5a17c713bbabf1b872409e72c1587cd3f712677ebe8bcd4
MD5 2ad082c6348941ed2c82d624a914fada
BLAKE2b-256 a4ec35512a4e37b85b5f9204e4ed9dc636cd0d85b6f9d844703b610bf79d418e

See more details on using hashes here.

Provenance

The following attestation bundles were made for m68hc11-0.1.0.tar.gz:

Publisher: publish.yml on anarkiwi/m68hc11

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file m68hc11-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: m68hc11-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for m68hc11-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 93e75c3c8e8f0a974fe70bcc55c76477e4353a966f5fb0f0592f16141436e730
MD5 1e591b6700d3f4b2c4d04c993c6d65f6
BLAKE2b-256 6aef28245c9b48c41d3a5ee257cb0a4254584f0b349e6824b7d965f864ffc9c6

See more details on using hashes here.

Provenance

The following attestation bundles were made for m68hc11-0.1.0-py3-none-any.whl:

Publisher: publish.yml on anarkiwi/m68hc11

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page