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/$CDprefix 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:
- Flag / arithmetic self-test — hand-checked sequences for the tricky cases
(
MULC-bit,ASLD/LSRD,DAA,ADDD/SUBDoverflow & carry,NEG/COMcarry, signed branches,ABX/XGDX). - 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. - The
call()harness demonstrably running self-contained subroutines that read inputs from registers/memory and return results inD/X/memory with cycles reported.
Run them with:
pip install -e ".[test]"
pytest
Notes on a couple of documented edge cases
DAAleaves theVflag unchanged (the reference manual documentsVas undefined afterDAA).IDIV/FDIVleaveNandHunaffected; divide-by-zero setsCand forces the quotient (X) to$FFFF.TAPcan clear theX(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/$CDpages, lengths, and cycle counts. - A device data sheet (e.g. MC68HC11A8 / MC68HC11F1) for the
$1000register block and the$FFC0–$FFFFvector 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef22f906d40c207fe5a17c713bbabf1b872409e72c1587cd3f712677ebe8bcd4
|
|
| MD5 |
2ad082c6348941ed2c82d624a914fada
|
|
| BLAKE2b-256 |
a4ec35512a4e37b85b5f9204e4ed9dc636cd0d85b6f9d844703b610bf79d418e
|
Provenance
The following attestation bundles were made for m68hc11-0.1.0.tar.gz:
Publisher:
publish.yml on anarkiwi/m68hc11
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
m68hc11-0.1.0.tar.gz -
Subject digest:
ef22f906d40c207fe5a17c713bbabf1b872409e72c1587cd3f712677ebe8bcd4 - Sigstore transparency entry: 1817635829
- Sigstore integration time:
-
Permalink:
anarkiwi/m68hc11@8f41c56155f27ddbd735ebf0e25d0ea37f4d0316 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/anarkiwi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8f41c56155f27ddbd735ebf0e25d0ea37f4d0316 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93e75c3c8e8f0a974fe70bcc55c76477e4353a966f5fb0f0592f16141436e730
|
|
| MD5 |
1e591b6700d3f4b2c4d04c993c6d65f6
|
|
| BLAKE2b-256 |
6aef28245c9b48c41d3a5ee257cb0a4254584f0b349e6824b7d965f864ffc9c6
|
Provenance
The following attestation bundles were made for m68hc11-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on anarkiwi/m68hc11
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
m68hc11-0.1.0-py3-none-any.whl -
Subject digest:
93e75c3c8e8f0a974fe70bcc55c76477e4353a966f5fb0f0592f16141436e730 - Sigstore transparency entry: 1817635964
- Sigstore integration time:
-
Permalink:
anarkiwi/m68hc11@8f41c56155f27ddbd735ebf0e25d0ea37f4d0316 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/anarkiwi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8f41c56155f27ddbd735ebf0e25d0ea37f4d0316 -
Trigger Event:
release
-
Statement type: