Python API Reference

Auto-generated from module docstrings.

Assembler

Minimal two-pass 6502/65C02 assembler.

Parses mnemonic assembly source into a mapping of CPU address -> byte — the same shape produced by romulan.build_rom.parse_hex_file() — so romulan.build_rom.build_rom() can consume either input format. The input format is detected by romulan.build_rom.detect_format().

Supported syntax (all origins are CPU addresses, not file offsets):

; comment
label:                      ; alone on a line or before an instruction
.org $8000                  ; set origin (must be in $8000-$FFFF)
.byte $EA, 5, label         ; raw bytes, comma separated
.word $8000, label          ; little-endian words (interrupt vectors)
CLC                         ; implied
ASL / ASL A                 ; accumulator
LDA #$05                    ; immediate
LDA $10                     ; zero page (numeric operand < $100)
STA $4000                   ; absolute (numeric operand >= $100)
LDA $10,X                   ; zero page,X   LDA $1234,Y  absolute,Y
JMP ($1234)                 ; indirect (JMP only)
LDA ($10,X) / ($10),Y       ; (zp,X) and (zp),Y
LDA ($10)                   ; 65C02 (zp) indirect
BNE loop                    ; relative branch to a label or address

Numbers may be written as $hex, 0xhex or decimal. When an operand is a label it is always encoded in absolute (16-bit) form, because labels can only refer to ROM addresses ($8000-$FFFF). A numeric operand narrower than $100 uses the zero-page form when the mnemonic provides one, and the absolute form otherwise (so JMP $0000 encodes as 4C 00 00).

Instruction set: all 56 official NMOS 6502 mnemonics plus the W65C02 additions used in the course (BRA, PHX, PHY, PLX, PLY, STZ, TRB, TSB, STP, WAI, accumulator INC/DEC, BIT immediate/indexed, (zp) indirect).

If no .org precedes the first byte, assembly starts at $8000. All emitted bytes must land in the ROM region $8000-$FFFF; operand values (e.g. the target of JMP $0000) are unrestricted.

Errors raise ValueError with a line number, mirroring romulan.build_rom.parse_hex_file().

romulan.assemble.parse_asm_file(path)[source]

Assemble a 6502 assembly source file into a CPU address -> byte map.

Two passes: the first collects labels and computes statement addresses; the second emits bytes, resolving label operands and checking ranges.

Parameters:

path (Path) – Path to the assembly source file.

Returns:

A mapping from CPU address ($8000-$FFFF) to byte value.

Raises:

ValueError – On syntax errors, unknown mnemonics, unsupported addressing modes, undefined or duplicate labels, branches out of range, or bytes emitted outside the ROM region.

Return type:

dict[int, int]

ROM builder

Build a 32 KB ROM image for the Pico-as-ROM 65C02 system.

The 32 KB image maps to CPU addresses $8000-$FFFF. File offset $0000 = CPU address $8000 File offset $7FFC = CPU address $FFFC (reset vector low byte) File offset $7FFF = CPU address $FFFF

exception romulan.build_rom.InvalidInstructionError(message)[source]

Bases: Exception

Raised when a byte sequence contains an undefined 65C02 opcode.

__str__()[source]

Return the error message.

Returns:

The message passed to the constructor.

exception romulan.build_rom.SkippedInstructionError(message)[source]

Bases: Exception

Raised when a required instruction address is missing from the ROM dump.

__str__()[source]

Return the error message.

Returns:

The message passed to the constructor.

romulan.build_rom.cpu_to_offset(cpu_addr)[source]

Convert a CPU address ($8000-$FFFF) to a file offset (0-$7FFF).

Parameters:

cpu_addr (int) – A 65C02 address in the ROM region.

Returns:

The corresponding byte offset in a 32 KB ROM file.

Raises:

ValueError – If cpu_addr is outside $8000$FFFF.

Return type:

int

romulan.build_rom.detect_format(path)[source]

Detect the input format of a ROM source file.

Sniffs the first meaningful line (skipping blank lines and full-line ; or @ comments): if it matches the annotated hex dump shape (0xADDR 0xBYTE) the file is a hex dump, otherwise it is treated as 6502 assembly. File extensions are not consulted — either format may live in a .txt or .s file.

Parameters:

path (Path) – Path to the input file.

Returns:

"hex" for an annotated hex dump or "asm" for 6502 assembly.

Raises:

ValueError – If the file contains no data lines.

Return type:

str

romulan.build_rom.parse_hex_file(path)[source]

Parse an annotated hex dump file into a dict of CPU address -> byte.

Expected line format:

0x0000   0x18   @ CLC
0x0001   0xA9   @ LDA 0x5

File addresses are in the range 0x00000x7FFF and are mapped to CPU addresses by adding ROM_BASE_ADDR (0x8000). Everything after @ on a line is treated as a comment and ignored.

Parameters:

path (Path) – Path to the annotated hex dump file.

Returns:

A mapping from CPU address to byte value.

Raises:

ValueError – If a line cannot be parsed or an address is out of range.

Return type:

dict[int, int]

romulan.build_rom.verify_instructions(data, error_list)[source]

Validate a contiguous instruction stream and append errors.

Walks opcode lengths so operand bytes are never treated as opcodes. Errors are appended to error_list and ERROR_COUNTER is incremented.

Parameters:
  • data (List[int]) – Contiguous program bytes (not including reset/IRQ vector slots).

  • error_list (List) – List that receives human-readable error messages.

Return type:

None

romulan.build_rom.verify_instruction_order(data, error_list)[source]

Check that file addresses are contiguous (no gaps before the vectors).

Parameters:
  • data (List[int]) – File-offset addresses from the parsed ROM dump.

  • error_list (List) – List that receives human-readable error messages.

Return type:

None

romulan.build_rom.error_processing(data_dict)[source]

Run opcode and address-order validation on a parsed ROM dump.

Parameters:

data_dict (Dict[int, int]) – Mapping of CPU address to byte value from parse_hex_file().

Returns:

A list of human-readable error messages (empty when validation passes).

Return type:

List

romulan.build_rom.build_rom(input_path, output_path, verbose=False)[source]

Parse a hex dump file and write a 32 KB ROM binary.

The input may be an annotated hex dump or 6502 assembly (auto-detected by detect_format()). Hex dumps get opcode and address-order validation before writing; assembled input is valid by construction and may contain gaps from .org directives, so that validation is skipped for it. Unused bytes are filled with $EA (NOP). The reset and IRQ/BRK vectors at $FFFC$FFFF must be present in the input or the build fails.

Side effects:

Resets ERROR_COUNTER, may print errors and call sys.exit(1), creates parent directories for output_path, writes the binary file, and prints a summary to stdout. When verbose is True, structured NDJSON events are emitted to stderr.

Parameters:
  • input_path (Path) – Path to the annotated hex dump or 6502 assembly file.

  • output_path (Path) – Destination path for the 32 KB .bin file.

  • verbose (bool) – If True, emit structured build progress to stderr.

Raises:

ValueError – If parsing fails or required vectors are missing.

Return type:

None

Plain-text upload

Serial port discovery for the Pico-as-ROM 65C02 firmware.

ROM upload uses the framed v1 Hardware API (romulan.hardware_api.HardwareAPI.upload_rom()). This module only auto-detects the Pico USB-CDC port for the CLI and client helpers.

romulan.upload_rom.find_pico_port()[source]

Auto-detect a Raspberry Pi Pico serial port.

Tries USB vendor ID 0x2E8A (Raspberry Pi) first, then falls back to common port name patterns on Linux, macOS, and Windows.

Returns:

The device path of the detected Pico (e.g. /dev/ttyACM0).

Raises:

RuntimeError – If zero or more than one matching port is found.

Return type:

str

Protocol v1 helpers

Wire-format helpers for the Pico-as-ROM hardware API (protocol v1).

This module defines the version-1 JSON envelope used to talk to the Pico firmware: constants describing size limits, the dataclasses that model firmware responses and events, a request builder, and parsers that validate and unpack each kind of frame. It contains no I/O; romulan.hardware_api uses these helpers to build commands and interpret replies.

exception romulan.protocol_v1.ProtocolV1Error(message, *, error=None, detail=None)[source]

Bases: Exception

Invalid frame or firmware error response.

Parameters:
  • message (str)

  • error (str | None)

  • detail (str | None)

Return type:

None

class romulan.protocol_v1.CycleEvent(seq, addr, data, rw)[source]

Bases: object

A single captured 65C02 bus cycle.

Parameters:
  • seq (int)

  • addr (str)

  • data (str)

  • rw (int)

seq

Monotonic sequence number assigned by the firmware.

Type:

int

addr

Address on the bus, as a hex string.

Type:

str

data

Data byte on the bus, as a hex string.

Type:

str

rw

Read/write flag (0 = read, 1 = write).

Type:

int

class romulan.protocol_v1.DoneEvent(ok, reason, cycles, addr)[source]

Bases: object

Terminating event that ends a bus-capture stream.

Parameters:
  • ok (bool)

  • reason (str)

  • cycles (int)

  • addr (str)

ok

Whether the capture completed successfully.

Type:

bool

reason

Why capture stopped (e.g. "stp" or "max_cycles").

Type:

str

cycles

Total number of cycles the firmware reports it captured.

Type:

int

addr

Address at which the capture stopped, as a hex string.

Type:

str

class romulan.protocol_v1.ReadResult(ok, reason, cycles=<factory>, stopped_addr='')[source]

Bases: object

Aggregated result of a capture: the done status plus all cycles.

Parameters:
  • ok (bool)

  • reason (str)

  • cycles (list[CycleEvent])

  • stopped_addr (str)

ok

Whether the capture completed successfully.

Type:

bool

reason

Why capture stopped.

Type:

str

cycles

The captured CycleEvent items in order.

Type:

list[romulan.protocol_v1.CycleEvent]

stopped_addr

Address at which the capture stopped, as a hex string.

Type:

str

class romulan.protocol_v1.PeekResult(addr, data)[source]

Bases: object

Result of a live bus/RAM peek (CPU read cycle, not ROM-image offset).

Parameters:
  • addr (int)

  • data (int)

addr

CPU address that was peeked (0–0xFFFF).

Type:

int

data

Data byte sampled on the matching bus cycle.

Type:

int

class romulan.protocol_v1.StatusResponse(phi2_hz, rom_active, reset_asserted, last_addr, read_active, monitor_enabled, upload_active=False, last_data='00', last_rw=0, resb=0, rwb=0, a15=0, phi2=0)[source]

Bases: object

Snapshot of firmware/hardware state returned by the status command.

Parameters:
  • phi2_hz (float)

  • rom_active (bool)

  • reset_asserted (bool)

  • last_addr (str)

  • read_active (bool)

  • monitor_enabled (bool)

  • upload_active (bool)

  • last_data (str)

  • last_rw (int)

  • resb (int)

  • rwb (int)

  • a15 (int)

  • phi2 (int)

phi2_hz

Current CPU clock (PHI2) frequency in hertz.

Type:

float

rom_active

Whether the ROM emulator is driving the bus.

Type:

bool

reset_asserted

Whether the CPU RESET line is asserted.

Type:

bool

last_addr

Last address seen on the bus, as a hex string.

Type:

str

last_data

Data byte from the last bus sample, as a hex string.

Type:

str

last_rw

Read/write flag from the last bus sample (0 = read, 1 = write).

Type:

int

read_active

Whether a bus-capture read is currently running.

Type:

bool

monitor_enabled

Whether the JSON monitor output is enabled.

Type:

bool

upload_active

Whether a ROM upload is in progress.

Type:

bool

resb

Raw RESB (reset) input level (0 = low/asserted, 1 = high/released).

Type:

int

rwb

Raw RWB input level (0 = write, 1 = read).

Type:

int

a15

Raw A15 input level (0 = RAM space, 1 = ROM space).

Type:

int

phi2

Raw PHI2 clock input level (0 = low, 1 = high).

Type:

int

class romulan.protocol_v1.PeekResponse(offset, count, data)[source]

Bases: object

Response from the peek command returning ROM image bytes.

Parameters:
  • offset (int)

  • count (int)

  • data (bytes)

offset

Byte offset within rom_image[] that was read.

Type:

int

count

Number of bytes returned (may be clipped to ROM bounds).

Type:

int

data

The returned bytes.

Type:

bytes

class romulan.protocol_v1.DriveResponse(enabled, value='00')[source]

Bases: object

Response from the drive diagnostic command.

Parameters:
  • enabled (bool)

  • value (str)

enabled

Whether the Pico is currently forcing D0-D7 as outputs.

Type:

bool

value

The forced byte as a 2-digit hex string, or "00" when disabled.

Type:

str

class romulan.protocol_v1.UploadProgress(action, received, expected=32768, offset=None, reset_vector=None)[source]

Bases: object

Progress reported by the firmware during a ROM upload.

Parameters:
  • action (str)

  • received (int)

  • expected (int)

  • offset (int | None)

  • reset_vector (str | None)

action

Which upload phase this reply corresponds to ("begin", "chunk", or "commit").

Type:

str

received

Total number of bytes received so far.

Type:

int

expected

Total number of bytes expected (defaults to ROM_SIZE).

Type:

int

offset

Byte offset of the acknowledged chunk, if reported.

Type:

int | None

reset_vector

Reset vector read back after commit, if reported.

Type:

str | None

romulan.protocol_v1.build_request(cmd, *, req_id=None, **fields)[source]

Build a v1 request envelope for a firmware command.

Adds the protocol version and command name, and (as a convenience) renames the reserved assert_reset keyword to the wire field assert.

Parameters:
  • cmd (str) – The command name (e.g. "status" or "upload_rom").

  • req_id (str | None) – Optional request id echoed back by the firmware.

  • **fields (Any) – Extra command-specific fields to include in the envelope.

Returns:

A dict ready to be serialized to JSON and sent to the Pico.

Return type:

dict[str, Any]

romulan.protocol_v1.parse_frame(raw)[source]

Parse a JSON frame and validate the v1 envelope.

Parameters:

raw (bytes | str) – The raw frame payload as bytes (UTF-8) or str.

Returns:

The decoded frame as a dict.

Raises:

ProtocolV1Error – If the payload is not valid JSON, is not a JSON object, or declares an unsupported protocol version.

Return type:

dict[str, Any]

romulan.protocol_v1.parse_command_response(msg)[source]

Return a command acknowledgement, or raise on a firmware error.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a command response (not an event).

Returns:

The same frame, unchanged, when it represents a successful ack.

Raises:

ProtocolV1Error – If the frame is an event, reports ok false, or declares an unsupported protocol version.

Return type:

dict[str, Any]

romulan.protocol_v1.parse_cycle_event(msg)[source]

Parse a cycle event frame into a CycleEvent.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a cycle event.

Returns:

The captured bus cycle.

Raises:

ProtocolV1Error – If the frame is not a cycle event or the version is unsupported.

Return type:

CycleEvent

romulan.protocol_v1.parse_done_event(msg)[source]

Parse a done event frame into a DoneEvent.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a done event.

Returns:

The terminating capture event.

Raises:

ProtocolV1Error – If the frame is not a done event or the version is unsupported.

Return type:

DoneEvent

romulan.protocol_v1.parse_cycles_event(msg)[source]

Parse a batched cycles event frame into a list of CycleEvent.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a cycles event.

Returns:

The captured bus cycles in order.

Raises:

ProtocolV1Error – If the frame is not a cycles event, the version is unsupported, or a cycle entry is malformed.

Return type:

list[CycleEvent]

romulan.protocol_v1.parse_status(msg)[source]

Parse a status command response into a StatusResponse.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a successful status response.

Returns:

The decoded status snapshot.

Raises:

ProtocolV1Error – If the frame reports an error or the version is unsupported.

Return type:

StatusResponse

romulan.protocol_v1.parse_peek_response(msg)[source]

Parse a peek command response into a PeekResponse.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a successful peek response.

Returns:

The decoded offset, count, and bytes.

Raises:

ProtocolV1Error – If the frame reports an error, the version is unsupported, or the hex data is malformed.

Return type:

PeekResponse

romulan.protocol_v1.parse_drive_response(msg)[source]

Parse a drive command response into a DriveResponse.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a successful drive response.

Returns:

The decoded drive force state.

Raises:

ProtocolV1Error – If the frame reports an error or the version is unsupported.

Return type:

DriveResponse

romulan.protocol_v1.parse_live_peek_response(msg)[source]

Parse a live peek command response into a PeekResult.

Distinct from any ROM-image offset peek: fields are CPU addr and sampled bus data (single byte).

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a successful peek response.

Returns:

The decoded address and data byte.

Raises:

ProtocolV1Error – If the frame reports an error, the version is unsupported, or required fields are missing/invalid.

Return type:

PeekResult

romulan.protocol_v1.parse_upload_response(msg)[source]

Parse an upload_rom command response into an UploadProgress.

Parameters:

msg (dict[str, Any]) – A parsed frame expected to be a successful upload_rom reply (begin, chunk, or commit).

Returns:

The decoded upload progress.

Raises:

ProtocolV1Error – If the frame reports an error or the version is unsupported.

Return type:

UploadProgress

Hardware API client

High-level client for the Pico-as-ROM 65C02 hardware over the v1 serial protocol.

This module provides HardwareAPI, a client-side wrapper around the framed JSON protocol implemented by the Pico firmware. It handles the low-level ENQ/STX/ACK/EOT framing, encodes commands built by romulan.protocol_v1, and exposes friendly methods for the common operations: querying the current CPU address, asserting/releasing reset, toggling the JSON monitor, reading status, uploading a ROM image, live-peeking a CPU bus address, and capturing bus cycles.

Opening a HardwareAPI immediately opens the underlying serial port. The class supports the context-manager protocol so the port is always closed:

with HardwareAPI("/dev/ttyACM0") as api:
    api.upload_rom(rom_bytes)
exception romulan.hardware_api.HardwareAPIError[source]

Bases: Exception

Raised when the Pico responds with NACK or a frame error occurs.

class romulan.hardware_api.CaptureResult(reason, cycles=<factory>)[source]

Bases: object

Result of a bus capture (read until STP).

Parameters:
  • reason (str)

  • cycles (list[dict[str, Any]])

reason

Why the capture stopped (e.g. "stp" or "max_cycles").

Type:

str

cycles

One dict per captured bus cycle, each with seq, addr, data, and rw keys.

Type:

list[dict[str, Any]]

__repr__()[source]

Return a concise debug representation.

Returns:

A string showing the stop reason and cycle count.

Return type:

str

classmethod from_read_result(result)[source]

Build a CaptureResult from a protocol ReadResult.

Parameters:

result (ReadResult) – The parsed read result returned by the capture loop.

Returns:

A CaptureResult with each cycle flattened into a plain dict.

Return type:

CaptureResult

class romulan.hardware_api.HardwareAPI(port, baudrate=115200, timeout=30.0, verbose=False)[source]

Bases: object

Context-manager compatible hardware API for Pico-as-ROM firmware v1.

Each instance owns a single serial connection to the Pico. The connection is opened as soon as the object is constructed, and closed by close() or on exit from a with block.

Parameters:
  • port (str)

  • baudrate (int)

  • timeout (float)

  • verbose (bool)

port

The serial device path the client is connected to.

baudrate

The serial baud rate in use.

timeout

Idle / activity timeout in seconds (no useful framing progress).

verbose

When True, protocol traffic is logged to stderr.

close()[source]

Close the serial port if it is open.

Safe to call multiple times; subsequent calls are no-ops.

Return type:

None

__enter__()[source]

Enter a with block and return this client.

Return type:

HardwareAPI

__exit__(exc_type, exc_val, exc_tb)[source]

Exit a with block, closing the serial port.

Parameters:
  • exc_type (Any)

  • exc_val (Any)

  • exc_tb (Any)

Return type:

None

property ser: Serial

The live serial.Serial connection.

Returns:

The open serial connection.

Raises:

HardwareAPIError – If the port has already been closed.

request_addr()[source]

Ask the firmware for the address currently on the CPU bus.

Returns:

The current CPU address as an integer.

Raises:
  • HardwareAPIError – If the response is missing the addr field or the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

int

live_peek(addr)[source]

Live-peek one byte by running a short LDA absolute / STP stub on the CPU.

The firmware briefly resets the 65C02, patches LDA $addr / STP at $8000, samples the data byte on the bus cycle whose address matches addr, then restores the previous ROM bytes. This reads live RAM (or ROM) contents — not a host-side ROM-image offset (see peek() for that). Requires firmware with live-peek support.

Parameters:

addr (int) – CPU address to read (00xFFFF).

Returns:

A PeekResult with addr and data.

Raises:
  • ValueError – If addr is outside 00xFFFF.

  • HardwareAPIError – If the firmware reports an error (timeout, no matching cycle, busy, etc.).

  • TimeoutError – If the Pico does not respond in time.

Return type:

PeekResult

reset(assert_reset)[source]

Assert or release the 65C02 RESET line.

Side effects:

Changes the CPU run state: asserting reset halts the CPU, while releasing it lets the CPU start executing from its reset vector.

Parameters:

assert_reset (bool) – True to hold the CPU in reset, False to release it.

Raises:
  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

None

monitor(enable)[source]

Enable or disable the firmware’s unframed JSON monitor output.

The monitor must be disabled before framed operations such as upload_rom() and read_until_stp(), otherwise its free-form text would corrupt the framed protocol stream.

Parameters:

enable (bool) – True to turn the monitor on, False to turn it off.

Raises:
  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

None

status()[source]

Query the firmware for its current status.

Returns:

A StatusResponse describing the clock frequency, ROM/reset/monitor state, and last bus sample.

Raises:
  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

StatusResponse

peek(offset, count=16)[source]

Read back bytes from the loaded rom_image[].

This is useful for verifying that an upload landed at the expected offsets before releasing RESET.

Parameters:
  • offset (int) – Byte offset within the 32 KB ROM image.

  • count (int) – Number of bytes to read (1-64; firmware caps at 64).

Returns:

A PeekResponse with the offset, count, and returned bytes.

Raises:
  • ValueError – If offset or count is out of range.

  • HardwareAPIError – If the firmware reports an error or returns malformed data.

  • TimeoutError – If the Pico does not respond in time.

Return type:

PeekResponse

set_clock(hz)[source]

Set the 65C02 PHI2 clock frequency.

Parameters:

hz (float) – Target frequency in hertz. The firmware accepts 0.1..1000 Hz.

Raises:
  • ValueError – If hz is outside the supported range.

  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

None

drive(value=None)[source]

Force the Pico to drive D0-D7 with a byte, or release the bus.

This is a diagnostic command. The CPU should be in reset or removed before forcing the data bus, otherwise the Pico and CPU contend.

Parameters:

value (int | str | None) – Byte to drive on D0-D7. Pass None (or omit) to release the bus and return to normal ROM emulation.

Returns:

A DriveResponse with the new state.

Raises:
  • ValueError – If value is outside 0..255.

  • HardwareAPIError – If the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

DriveResponse

upload_rom(data)[source]

Upload a full 32 KB ROM image to the Pico in a begin/chunk/commit sequence.

The image is sent as base64-encoded chunks and committed at the end. The reset vector is reported back by the firmware after commit.

Side effects:

Disables the JSON monitor and flushes serial input before transferring, so the framed protocol is not corrupted.

Parameters:

data (bytes) – The ROM image; must be exactly ROM_SIZE (32 KB) bytes.

Returns:

A dict with keys ok, bytes (bytes committed), reset_vector, and expected (expected total size).

Raises:
  • ValueError – If data is not exactly ROM_SIZE bytes.

  • HardwareAPIError – If a chunk stalls or the firmware reports an error.

  • TimeoutError – If the Pico does not respond in time.

Return type:

dict[str, Any]

read_until_stp(max_cycles=10000, frame_timeout=None, on_cycle=None, batch_size=32, phi2_hz=None)[source]

Capture CPU bus cycles until the CPU executes STP or a limit is hit.

Holds reset, arms read, then releases reset so capture starts from the reset vector (typically $8000). Polls read_event for batched cycle and done frames.

Side effects:

Disables the JSON monitor, asserts then releases CPU reset. The current PHI2 clock is preserved unless phi2_hz is provided.

Parameters:
  • max_cycles (int) – Maximum number of bus cycles to capture before the firmware stops. Defaults to 10000.

  • frame_timeout (float | None) – Idle timeout in seconds with no new cycle/done event. none polls do not extend the deadline. Defaults to timeout.

  • on_cycle (Callable[[CycleEvent], None] | None) – Optional callback invoked for each captured cycle (e.g. for live CLI output).

  • batch_size (int) – Number of cycles to request per read_event poll. Defaults to READ_EVENT_BATCH_SIZE.

  • phi2_hz (float | None) – Optional clock frequency to set when arming capture. If omitted, the current clock speed is preserved.

Returns:

A CaptureResult with the stop reason and captured cycles.

Raises:
  • HardwareAPIError – If the read is rejected or an unexpected frame arrives.

  • TimeoutError – If no cycle/done arrives within the idle timeout.

Return type:

CaptureResult

romulan.hardware_api.open_hardware_api(port=None)[source]

Open a HardwareAPI, auto-detecting the Pico port if needed.

Side effects:

Opens the serial port via HardwareAPI.

Parameters:

port (str | None) – Explicit serial device path. If None, the port is auto-detected with find_pico_port().

Returns:

A connected HardwareAPI instance.

Raises:

HardwareAPIError – If no Pico serial port can be found.

Return type:

HardwareAPI

CLI entry point

Romulan CLI entry point.

Usage:

romulan input.txt –build –upload [–port PORT] romulan hardware <subcommand> …

Examples

romulan program.txt –build # Build bin/rom.bin only romulan program.txt –build –upload # Build and upload romulan –upload # Upload existing bin/rom.bin romulan program.txt –upload –port /dev/ttyACM0 romulan hardware upload bin/rom.bin –port /dev/ttyACM0 romulan hardware capture –max-cycles 500 romulan hardware monitor –disable romulan hardware reset –assert romulan hardware request-addr romulan hardware peek –offset 0x7000 –count 16 romulan hardware clock –hz 100 romulan hardware status

romulan.main.create_parser()[source]

Build the argument parser for the default build/upload workflow.

Returns:

A configured argparse.ArgumentParser accepting the input file plus the --build, --upload, --output, and --port options.

Return type:

ArgumentParser

romulan.main.main()[source]

Entry point for the romulan command-line tool.

Routes to the hardware sub-command when it is the first argument; otherwise runs the default workflow, which builds a ROM image from an input file (--build) and/or uploads it to the Pico (--upload).

Side effects:

Parses sys.argv, may read/write files, and may open the serial port to talk to the Pico. Exits via sys.exit() (or parser.error) on invalid arguments or failures.

Return type:

None

Package

Romulan — host client for the Piclone 65C02 system.

Romulan assembles 32 KB ROM images from annotated hex dumps or 6502 assembly source and communicates with the Raspberry Pi Pico firmware over USB serial. Submodules: