Python API Reference¶
Auto-generated from module docstrings.
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:
ExceptionRaised when a byte sequence contains an undefined 65C02 opcode.
- exception romulan.build_rom.SkippedInstructionError(message)[source]¶
Bases:
ExceptionRaised when a required instruction address is missing from the ROM dump.
- 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_addris outside$8000–$FFFF.- Return type:
int
- 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
0x0000–0x7FFFand are mapped to CPU addresses by addingROM_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 opcode bytes and append errors instead of raising immediately.
Distinguishes undefined opcodes from immediate or address operands that follow a valid instruction byte. Errors are appended to
error_listandERROR_COUNTERis incremented.- Parameters:
data (List[int]) – Byte values parsed from the ROM image.
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)[source]¶
Parse a hex dump file and write a 32 KB ROM binary.
Runs opcode and address-order validation before writing. Unused bytes are filled with
$EA(NOP). The reset and IRQ/BRK vectors at$FFFC–$FFFFmust be present in the input or the build fails.- Side effects:
Resets
ERROR_COUNTER, may print errors and callsys.exit(1), creates parent directories foroutput_path, writes the binary file, and prints a summary to stdout.
- Parameters:
input_path (Path) – Path to the annotated hex dump file.
output_path (Path) – Destination path for the 32 KB
.binfile.
- Raises:
ValueError – If parsing fails or required vectors are missing.
- Return type:
None
Plain-text upload¶
Plain-text ROM upload for the Pico-as-ROM 65C02 firmware.
Uploads a 32 KB ROM image over USB serial using the firmware’s loadbin
handshake: the host sends loadbin, the Pico responds with
OK send 32768 bytes, the host streams 32,768 raw bytes, and the Pico
acknowledges with loaded 32768 bytes.
Also handles CPU reset sequencing and ROM-emulator toggling so the CPU restarts cleanly on the new image.
- 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
- romulan.upload_rom.read_until(ser, needle, timeout=3.0)[source]¶
Read from a serial port until a substring appears or time runs out.
Non-empty lines received are echoed to stdout prefixed with
<<.- Parameters:
ser (Serial) – An open
serial.Serialconnection.needle (str) – Substring to search for in the accumulated receive buffer.
timeout (float) – Maximum wait time in seconds. Defaults to
3.0.
- Returns:
The full receive buffer once
needleis found.- Raises:
TimeoutError – If
needleis not seen before the deadline.- Return type:
str
- romulan.upload_rom.upload(port, path)[source]¶
Upload a 32 KB ROM image via the plain-text
loadbinprotocol.- Side effects:
Opens the serial port at 115200 baud, asserts CPU reset (
r0), disables ROM emulation (roms), streams the binary, re-enables ROM and clock (rom,c100), releases reset (r1), captures ~5 s of live output, then closes the port.
- Parameters:
port (str) – Serial device path for the Pico.
path (Path) – Path to the ROM binary; must be exactly
ROM_SIZEbytes.
- Raises:
SystemExit – If
pathis not exactly 32 KB (viasys.exit).TimeoutError – If the firmware does not respond during handshake.
- Return type:
None
- romulan.upload_rom.main()[source]¶
CLI entry point for standalone
upload-rom.pyusage.Accepts optional
[PORT] [BIN]arguments; auto-detects the port and defaults tobin/rom.binwhen omitted.- Side effects:
Delegates to
upload(), which opens the serial port and drives the Pico firmware.
- Return type:
None
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:
ExceptionInvalid 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:
objectA 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 (1 = read, 0 = write).
- Type:
int
- class romulan.protocol_v1.DoneEvent(ok, reason, cycles, addr)[source]¶
Bases:
objectTerminating 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:
objectAggregated 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
CycleEventitems in order.- Type:
- stopped_addr¶
Address at which the capture stopped, as a hex string.
- Type:
str
- class romulan.protocol_v1.StatusResponse(phi2_hz, rom_active, reset_asserted, last_addr, read_active, monitor_enabled, upload_active=False)[source]¶
Bases:
objectSnapshot of firmware/hardware state returned by the
statuscommand.- Parameters:
phi2_hz (float)
rom_active (bool)
reset_asserted (bool)
last_addr (str)
read_active (bool)
monitor_enabled (bool)
upload_active (bool)
- 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
- read_active¶
Whether a bus-capture read is currently running.
- Type:
bool
- monitor_enabled¶
Whether the ASCII monitor output is enabled.
- Type:
bool
- upload_active¶
Whether a ROM upload is in progress.
- Type:
bool
- class romulan.protocol_v1.UploadProgress(action, received, expected=32768, offset=None, reset_vector=None)[source]¶
Bases:
objectProgress 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_resetkeyword to the wire fieldassert.- 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) orstr.- 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
okfalse, or declares an unsupported protocol version.- Return type:
dict[str, Any]
- romulan.protocol_v1.parse_cycle_event(msg)[source]¶
Parse a
cycleevent frame into aCycleEvent.- Parameters:
msg (dict[str, Any]) – A parsed frame expected to be a
cycleevent.- Returns:
The captured bus cycle.
- Raises:
ProtocolV1Error – If the frame is not a
cycleevent or the version is unsupported.- Return type:
- romulan.protocol_v1.parse_done_event(msg)[source]¶
Parse a
doneevent frame into aDoneEvent.- Parameters:
msg (dict[str, Any]) – A parsed frame expected to be a
doneevent.- Returns:
The terminating capture event.
- Raises:
ProtocolV1Error – If the frame is not a
doneevent or the version is unsupported.- Return type:
- romulan.protocol_v1.parse_status(msg)[source]¶
Parse a
statuscommand response into aStatusResponse.- Parameters:
msg (dict[str, Any]) – A parsed frame expected to be a successful
statusresponse.- Returns:
The decoded status snapshot.
- Raises:
ProtocolV1Error – If the frame reports an error or the version is unsupported.
- Return type:
- romulan.protocol_v1.parse_upload_response(msg)[source]¶
Parse an
upload_romcommand response into anUploadProgress.- Parameters:
msg (dict[str, Any]) – A parsed frame expected to be a successful
upload_romreply (begin, chunk, or commit).- Returns:
The decoded upload progress.
- Raises:
ProtocolV1Error – If the frame reports an error or the version is unsupported.
- Return type:
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 ASCII monitor, reading status, uploading a ROM image, 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:
ExceptionRaised when the Pico responds with NACK or a frame error occurs.
- class romulan.hardware_api.CaptureResult(reason, cycles=<factory>)[source]¶
Bases:
objectResult 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, andrwkeys.- 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
CaptureResultfrom a protocolReadResult.- Parameters:
result (ReadResult) – The parsed read result returned by the capture loop.
- Returns:
A
CaptureResultwith each cycle flattened into a plain dict.- Return type:
- class romulan.hardware_api.HardwareAPI(port, baudrate=115200, timeout=3.0, verbose=False)[source]¶
Bases:
objectContext-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 awithblock.- 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¶
Default per-read timeout in seconds.
- 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
- __exit__(exc_type, exc_val, exc_tb)[source]¶
Exit a
withblock, closing the serial port.- Parameters:
exc_type (Any)
exc_val (Any)
exc_tb (Any)
- Return type:
None
- property ser: Serial¶
The live
serial.Serialconnection.- 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
addrfield or the firmware reports an error.TimeoutError – If the Pico does not respond in time.
- Return type:
int
- 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) –
Trueto hold the CPU in reset,Falseto 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 unstructured ASCII monitor output.
The ASCII monitor must be disabled before framed operations such as
upload_rom()andread_until_stp(), otherwise its free-form text would corrupt the framed protocol stream.- Parameters:
enable (bool) –
Trueto turn the monitor on,Falseto 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
StatusResponsedescribing the clock frequency, ROM/reset/monitor state, and last bus address.- Raises:
HardwareAPIError – If the firmware reports an error.
TimeoutError – If the Pico does not respond in time.
- Return type:
- 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 ASCII 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, andexpected(expected total size).- Raises:
ValueError – If
datais not exactlyROM_SIZEbytes.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=12.0)[source]¶
Capture CPU bus cycles until the CPU executes STP or a limit is hit.
Streams
cycleevents from the firmware and stops on the terminatingdoneevent (reached when the CPU halts via STP ormax_cyclesis reached).- Side effects:
Disables the ASCII monitor and flushes serial input before starting the capture.
- Parameters:
max_cycles (int) – Maximum number of bus cycles to capture before the firmware stops. Defaults to
10000.frame_timeout (float) – Per-frame read timeout in seconds while waiting for cycle/done events. Defaults to
READ_FRAME_TIMEOUT.
- Returns:
A
CaptureResultwith the stop reason and captured cycles.- Raises:
HardwareAPIError – If the read is rejected or an unexpected frame arrives.
TimeoutError – If the Pico stops sending frames before
done.
- Return type:
- 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 withfind_pico_port().- Returns:
A connected
HardwareAPIinstance.- Raises:
HardwareAPIError – If no Pico serial port can be found.
- Return type:
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.main.create_parser()[source]¶
Build the argument parser for the default build/upload workflow.
- Returns:
A configured
argparse.ArgumentParseraccepting the input file plus the--build,--upload,--output, and--portoptions.- Return type:
ArgumentParser
- romulan.main.main()[source]¶
Entry point for the
romulancommand-line tool.Routes to the
hardwaresub-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 viasys.exit()(orparser.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 and communicates with the Raspberry Pi Pico firmware over USB serial. Submodules:
romulan.build_rom— hex dump parser and ROM builderromulan.upload_rom— plain-text upload protocol and port detectionromulan.protocol_v1— v1 JSON envelope helpersromulan.hardware_api— framed serial protocol clientromulan.main— CLI entry point