| Crates.io | ull65 |
| lib.rs | ull65 |
| version | 0.3.0 |
| created_at | 2025-11-13 21:02:28.928135+00 |
| updated_at | 2025-11-20 20:05:03.041439+00 |
| description | ull65 is a no_std CPU emulator for the MOS 6502 and WDC 65C02. |
| homepage | https://github.com/patricktcoakley/ull |
| repository | https://github.com/patricktcoakley/ull |
| max_upload_size | |
| id | 1931860 |
| size | 233,595 |
ull65 is a no_std CPU core for the MOS 6502 and WDC 65C02 (with plans for more).
The primary goal of ull65 is to be easy to use and extend, with a secondary focus on being portable and (eventually)
cycle-accurate. To that end, the public API is mostly focused on ergonomics for the end-user, with a few key
features that make it easy to customize the core for specific use cases:
ull65 leverages the ull::Bus abstraction, so memory maps and peripherals live in your bus
implementation in a consistent API across all systems.InstructionSet trait instead of duplicating the CPU core for each variant,
making it easy to implement new CPU cores without much effort.run, run_until, or single-cycle tick calls and keep DMA/peripheral time in lockstep with
the processor.ull65 is organized around a few composable building blocks:
Cpu<B> is the core execution engine parameterized over a Bus
implementation. It owns the registers/flags, exposes helpers like
with_program, with_reset_vector, run, run_until, and single-cycle
tick, and keeps track of elapsed cycles.Bus is a trait you implement to wire memory and peripherals. The CPU uses it
for every instruction fetch/data access plus timing hooks:
read/write for memory accesseson_tick to let the bus advance its own clocksrequest_dma/poll_dma_cycle to model DMA burstsInstructionSet is a high-level description of a CPU flavor. Implement this
trait to tell the core which opcode table to run, whether decimal mode is
available, and so on.InstructionTable is a dense array of 256 Instruction entries. You usually
get one by calling Mos6502::base_table() or Wdc65c02s::base_table() and
optionally patching a few slots.Instruction is the executable payload stored in each table slot. It contains
the cycle count and a function pointer (fn(&mut Cpu<B>, &mut B)) that
performs the opcode’s work.RunConfig/RunPredicate are control structures for run_until, letting you
stop on BRK, on predicates (e.g., “PC reached $C000”), or after a cycle limit.Nibble/Byte/Word are tiny newtypes to handle things like wrapping addition or subtraction and added conveniences
for working with the various types of the 6502 without having to use as or ::from calls everywhere.The original 6502 has no concept of traps or recoverable faults because addresses wrap at
16 bits, arithmetic wraps at 8/16 bits, and undefined memory just returns
whatever the bus supplies. ull65 mirrors that behavior by leveraging wrapped arithemtic internally, so the CPU always
continues executing unless you halt it explictly via RunConfig options, BRK handlers, or your own Bus logic.
If you need to detect error conditions (e.g., illegal opcodes, out-of-range accesses), add the checks inside your Bus
implementation or a patched InstructionSet so the behavior stays explicit.
The general workflow for ull65 is:
Bus that mirrors your target machine.InstructionSet.Cpu::<YourBus> and load code using whichever constructor fits:
with_program::<YourInstructionSet> for ad-hoc buffers, or
with_reset_vector::<YourInstructionSet> when you want the ROM’s reset
vector to run.run, run_until, or tick, letting the bus handle timing and optional
DMA callbacks.use ull::Word;
use ull65::instruction::mos6502::Mos6502;
use ull65::processor::run::RunConfig;
use ull65::{AccessType, Cpu, SimpleBus};
fn main() {
let mut bus = SimpleBus::default();
let program = vec![0xA9, 0x01, 0x8D, 0x00, 0x02, 0x00]; // LDA #$01; STA $0200; BRK
let mut cpu = Cpu::with_program::<Mos6502>(&mut bus, Word(0x8000), &program, Word(0x8000));
let summary = cpu.run_until(&mut bus, RunConfig { stop_on_brk: true, ..RunConfig::default() });
let value: u8 = bus.read(Word(0x0200), AccessType::DataRead).into();
println!("Summary: {summary:?}, memory[$0200]={value:02X}");
}
InstructionSet is the abstraction that tells the CPU which opcode table to
execute. Each instruction table is just an array of 256 Instructions:
pub struct Instruction<B: Bus> {
pub cycles: u8,
pub execute: fn(&mut Cpu<B>, &mut B),
}
The stock tables (Mos6502 and Wdc65c02s) cover the common CPU variants.
Start from whichever base table matches your target (Mos6502::base_table()
or Wdc65c02s::base_table()) and then patch it further if needed, or construct
an entirely custom ISA.
Many 6502 derivatives only differ by feature flags (e.g., BCD mode). Set the
associated constants on your InstructionSet to opt in/out without touching the
table itself:
impl InstructionSet for Ricoh2a03 {
fn instruction_table<B: Bus + 'static>() -> InstructionTable<B> {
Mos6502::base_table()
}
const SUPPORTS_DECIMAL_MODE: bool = false;
}
When you need to replace individual opcodes you can use with on the instruction table to patch in a new
Instruction. This lets you intercept undocumented opcodes, add tracing, or emulate chips that diverge in only a
handful of instructions:
impl InstructionSet for MyCustomCpu {
fn instruction_table<B: Bus + 'static>() -> InstructionTable<B> {
Mos6502::base_table::<B>().with(
0x00,
Instruction {
cycles: 7,
execute: custom_brk::<B>,
},
)
}
}
Here we keep the MOS behavior and only replace BRK with a custom trap handler.
The examples directory (crates/ull65/examples) contains runnable snippets that
double as API demonstrations. Run them with cargo run --example <name>.