m68k-rs
A safe, pure Rust implementation of the Motorola 68000 family CPU emulator.
One core for transaction-accurate hardware emulation and high-throughput high-level emulation (HLE).
Features
- Complete CPU family support: M68000 through M68060, including EC/LC variants and the SCC68070
- Two explicit execution contracts: transaction-accurate cycle scheduling for hardware emulators, and an instruction-budgeted fast path for HLE
- Bus-visible accuracy: 68000/68010 two-word prefetch, model-specific access ordering, and internal clock synchronization through
AddressBus::sync - Memory-safe core: The interpreter — instruction semantics, decode, exceptions, MMU, FPU — is 100% safe Rust. The optional fast paths (fastmem batch execution and the trace JIT) use a small, contract-documented
unsafeperimeter, fenced by step-vs-batch equivalence tests - FPU emulation: Software 80-bit extended precision, packed decimal, and model-specific 68881/68882/68040/68060 behavior
- MMU emulation: 68030/68040/68060 translation, ATCs, transparent translation,
PTEST, fault frames, and writeback - HLE-ready: Built-in trap interception for High-Level Emulation
- Save-state ready: Optional
serdesupport serializes architectural state while rebuilding runtime caches on load - Extensively tested: Validated against multiple industry-standard test suites
Quick Start
Add to your Cargo.toml:
[]
= "0.14.3"
The default build has no JIT compiler dependency. Native applications that use
run_batch() can enable Cranelift compilation explicitly:
[]
= { = "0.14.3", = ["jit"] }
With jit enabled on x86-64, eligible indirect-dispatch loops and short
return arms are combined in the background by default, including on Windows,
Linux and Intel macOS. Set M68K_NATIVE_REGIONS=off before starting the process
to use ordinary trace execution without the extra combination IR or compiler
worker. 0, empty and unrecognized values also disable combining; public,
on and 1 explicitly enable it. Configuration is read when the thread-local
JIT is created, so restart the process to change it. Other architectures,
including native Apple Silicon, keep their existing execution path.
The architecture limit is implemented in code, not just a lack of performance measurements: combining requires checked trace wrappers and retained inline IR currently generated only on x86-64, and worker startup is also gated to x86-64. Setting the override on ARM64 does not activate this tier. This does not disable the ordinary JIT or imply that the combining algorithm cannot be ported to ARM64; that path needs implementation and validation separately.
This tier preserves exact run_batch() instruction limits and observable
memory, watch, trap and fault behavior. As already documented for
run_batch(), it does not provide cycle accounting; precise execution APIs
are unchanged. Promotion compiles immutable IR snapshots on a background
worker while existing native traces continue executing. Publication rechecks
the captured trace identities; stale or failed results keep the old tier.
The worker owns its executable memory, with leases keeping installed code
alive. Submission, polling and shutdown do not wait for compilation.
Thread creation, snapshot capture and publication still require foreground
work, and ordinary first-tier compilation remains synchronous. The
trace-profile feature retains the original per-head instrumentation and
does not start this worker.
Basic Usage
use ;
// Implement your memory bus
High-Level Emulation (HLE)
Intercept traps for OS emulation or debugger integration with CPU/bus access:
use ;
;
// All methods in HleHandler are optional (default return is false).
// Return `true` to indicate the HLE handled the trap (suppressing the hardware exception).
// Return `false` to let the CPU take the standard hardware exception.
Choosing an Approach
| Method | Budget | Execution contract | Host-visible exit |
|---|---|---|---|
step() |
One instruction | Transaction-accurate AddressBus accesses and cycles |
Surfaces A-line, F-line, TRAP, BKPT, and illegal instructions without taking their exception |
step_with_hle_handler() |
One instruction | Same precise path as step() |
Offers traps to HleHandler; unhandled traps take the hardware exception |
execute() |
CPU cycles | Precise path; whole instructions may overshoot the requested cycles | Takes traps as hardware exceptions and returns consumed cycles |
run_for_cycles() |
CPU cycles | Precise path with actual cycle and instruction totals | Surfaces traps, STOP, and bus-requested instruction boundaries separately |
run_for_cycles_with_hook() |
CPU cycles | Same precise path, with host synchronization after each normally completed instruction | The hook can continue or request an instruction-boundary return |
run_for_cycles_with_boundary_hook() |
CPU cycles | Same precise path, with separate instruction and interrupt-entry events | The hook can synchronize or return before the first handler instruction |
run_batch() |
Instructions | Throughput path using decoded-op caching, optional direct RAM, and portable or jit-enabled native hot-loop traces |
Surfaces traps, STOP, watched PCs, or budget exhaustion |
Use step() for debugger-style control. Use run_for_cycles() when a
machine scheduler needs to advance the CPU by a clock budget without losing
bus ordering or trap state:
use CycleBatchExit;
let result = cpu.run_for_cycles;
match result.exit
An AddressBus can return true from take_boundary_request() when a bus
access discovers host work that must run after the current instruction or
interrupt entry and before another instruction executes. The request is
checked after a normally completed instruction and after an interrupt serviced
on batch entry. Completed cycles are included; interrupt entry does not add to
the retirement count. The request takes precedence over cycle budget
exhaustion. Implementations should consume the request while retaining the
associated work until the host handles the boundary exit.
The 68000 and 68010 may already have instruction words in their hardware
prefetch queue at this boundary. If the host work changes instruction-visible
memory or mapping and those queued words must not be used, call
cpu.invalidate_prefetch() before resuming.
Use run_for_cycles_with_hook() when devices or IRQ lines must be updated
between instructions rather than after an aggregate batch:
use CycleBatchControl;
let result = cpu.run_for_cycles_with_hook;
The hook receives the cycles for the just-completed instruction and can inspect
cpu.ppc and cpu.pc. Its CPU and bus updates are applied before another
instruction is fetched. A hook-requested return uses
CycleBatchExit::BoundaryRequested, includes the completed instruction exactly
once, and resumes at the following instruction. Interrupt-entry cycles are
included in result.cycles but are not reported as instruction cycles to the
hook. The original run_for_cycles() path has no runtime hook check.
Use run_for_cycles_with_boundary_hook() when the host must also advance
devices for interrupt-entry cycles before the first handler instruction:
use ;
let result = cpu.run_for_cycles_with_boundary_hook;
Interrupt entry contributes no retired instruction. Returning from its event stops before the first handler instruction, and resuming executes that instruction normally.
Use step_with_hle_handler() when patching selected guest OS calls while
allowing every unhandled trap to follow hardware behavior. Use
run_batch() for HLE workloads where host-call latency and instruction
throughput matter more than observing the physical prefetch bus.
Direct RAM and observed writes
run_batch() prefers AddressBus::fast_mem(), whose FastMem window permits
side-effect-free direct reads and writes. With native JIT enabled on 68020 and
later CPUs, a bus that needs write bookkeeping can instead return TrackedMem
from AddressBus::tracked_mem(). Compiled code reads RAM directly and calls
write_byte/word/long for stores; memory-to-memory MOVE also retains
begin_memory_copy/end_memory_copy notifications. This supports metadata such
as retained graphics coverage and write journals without interpreting the loop.
Both capabilities are opt-in. TrackedMem requires stable, side-effect-free
reads and infallible writes with the restrictions documented on the type;
faulting, relocating, MMIO, and read-observing buses must decline it. Active MMU
translation and pre-68020 store sequencing retain the ordinary path. A compiled
memory trace is specialized for its write mode and re-recorded if that mode
changes between batches, keeping notification checks out of direct stores.
Supported CPU Types
| CPU | Description |
|---|---|
M68000 |
Original 68000 (24-bit address bus) |
M68010 |
68010 with virtual memory support |
M68EC020 |
68020 embedded controller (no MMU) |
M68020 |
Full 68020 with 32-bit address bus |
M68EC030 |
68030 embedded controller (no MMU) |
M68030 |
Full 68030 with on-chip MMU |
M68EC040 |
68040 embedded controller (no FPU/MMU) |
M68LC040 |
68040 lite (no FPU) |
M68040 |
Full 68040 with FPU and MMU |
M68060 |
Superscalar 68060 with FPU and MMU |
SCC68070 |
Philips SCC68070 variant |
Validation & Testing
This emulator has been rigorously validated against multiple industry-standard test suites to ensure correctness:
SingleStepTests (m68000)
The SingleStepTests project provides exhaustive per-instruction fixtures from MAME's microcoded 68000 core. The suite covers every supplied instruction file and 261,894 cases, including:
- All addressing modes and operand sizes
- Edge cases for condition codes (CCR/SR)
- BCD arithmetic (ABCD, SBCD, NBCD)
- Multiply/divide overflow handling
- Exception frame generation
- Cycle and transaction auditing: all 261,894 cases match their fixture cycle and bus-access totals, with separate ignored audit tools for access sequence and per-access timing analysis
Musashi Reference Implementation
We also run binaries from Musashi, a widely deployed M68000 emulator. The integration tests:
- Execute complete Musashi test binaries
- Verify register state, memory contents, and exception handling
- Cover 68000 through 68040 instruction sets
- Explicitly exclude legacy cases whose undefined BCD flags or illegal 68000 encodings conflict with the hardware-oriented SingleStepTests model
Cross-CPU Verification
Additional test suites verify behavior across CPU generations:
- FPU tests: 80-bit arithmetic, transcendental functions, packed decimal, memory operands, rounding modes, and save/restore
- MMU translation tests: 68030/68040/68060 table walks, ATCs, TTR matching, page crossings, fault frames, and writeback
- Privilege tests: User/supervisor mode transitions, TRAP behavior
- Exception tests: Per-model frames, resumable bus faults, double faults, and address errors
- Execution-path differential tests: Cold/warm cache state, self-modifying code, cycle batches, fast batches, and native traces
Test Coverage
tests/
├── singlestep_m68000_v1_tests.rs # Exhaustive 68000 fixture suite
├── musashi_tests.rs # Musashi integration binaries
├── m68020_tests.rs ... # Generation-specific behavior
├── m68060_tests.rs # 68060 integer, FPU, and MMU behavior
├── fpu_accuracy.rs # Extended-precision differential tests
├── run_for_cycles_tests.rs # Precise cycle-batch boundary contract
├── run_batch_tests.rs # HLE fast-path equivalence and exits
└── fixtures/
├── m68000/ # External SingleStepTests checkout
└── Musashi/ # Musashi reference binaries
Architecture
m68k/
├── core/ # CPU state, interpreter, timing, caches, and trace JIT
├── dasm/ # Disassembler
├── fpu/ # 80-bit FPU, packed decimal, and transcendental operations
└── mmu/ # 68030/68040/68060 translation and ATCs
Key Types
| Type | Description |
|---|---|
CpuCore |
Main CPU state and execution APIs |
CpuType |
CPU model selection enum |
AddressBus |
Extensible memory, device, fetch-cache, timing, and fast-RAM contract |
LinearMemoryBus / FastMem |
Ready-made flat memory and optional direct-RAM window |
HleHandler |
Trap interception callbacks |
StepResult |
Single-instruction result |
CycleBatchResult / CycleBatchExit / CycleBatchControl / CycleBoundaryEvent |
Precise cycle-scheduled execution result, hook control, and boundary kind |
BatchResult / BatchExit |
High-throughput instruction-batch result |
CpuCore::is_stopped() |
STOP state check |
CpuCore::is_halted() |
Double-fault halt check |
Performance
Accuracy and throughput are separate, deliberate contracts:
- Precise execution —
step,step_with_hle_handler,execute, andrun_for_cyclesuse the interpreter and ordinaryAddressBuscalls. These paths preserve bus-visible prefetch, access ordering, internal-clock synchronization, fault state, and model-specific cycle accounting. - Throughput execution —
run_batchreuses decoded operations and executes eligible hot backward-branch traces through a portable micro-op loop. The opt-injitfeature compiles those traces with Cranelift on native targets. A bus may expose a contiguousFastMemwindow to keep eligible RAM operands inside the trace. Guarded exits and self-modifying-code checks fall back without partially committing an instruction.
The two paths share instruction semantics and are continuously compared in cold-cache, warm-cache, memory, control-flow, and fault tests. This keeps the hardware contract simple for machine emulators while allowing HLE systems to opt into lower dispatch overhead explicitly.
License
MIT License - see LICENSE for details.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md
Acknowledgments
- Musashi - Reference implementation and test fixtures
- SingleStepTests - Exhaustive instruction test vectors
- The M68000 Programmer's Reference Manual