ull65

Crates.ioull65
lib.rsull65
version0.3.0
created_at2025-11-13 21:02:28.928135+00
updated_at2025-11-20 20:05:03.041439+00
descriptionull65 is a no_std CPU emulator for the MOS 6502 and WDC 65C02.
homepagehttps://github.com/patricktcoakley/ull
repositoryhttps://github.com/patricktcoakley/ull
max_upload_size
id1931860
size233,595
Patrick T Coakley (patricktcoakley)

documentation

https://docs.rs/ull65

README

ull65

ull65 is a no_std CPU core for the MOS 6502 and WDC 65C02 (with plans for more).

Core concepts

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:

  • Pluggable buses
    • ull65 leverages the ull::Bus abstraction, so memory maps and peripherals live in your bus implementation in a consistent API across all systems.
  • Instruction-sets
    • swap or patch opcode tables with the InstructionSet trait instead of duplicating the CPU core for each variant, making it easy to implement new CPU cores without much effort.
  • Deterministic stepping
    • drive the CPU with run, run_until, or single-cycle tick calls and keep DMA/peripheral time in lockstep with the processor.

Architecture overview

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 accesses
    • on_tick to let the bus advance its own clocks
    • request_dma/poll_dma_cycle to model DMA bursts
  • InstructionSet 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.

Error handling

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.

Basic usage

The general workflow for ull65 is:

  1. Implement a Bus that mirrors your target machine.
  2. Choose or define an InstructionSet.
  3. Instantiate 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.
  4. Drive it via 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}");
} 

Customizing instruction sets

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.

Toggle feature flags

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;
}

Patch specific opcodes

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.

Examples

The examples directory (crates/ull65/examples) contains runnable snippets that double as API demonstrations. Run them with cargo run --example <name>.

Commit count: 0

cargo fmt