Hardware-in-the-loop development platform for the Commodore 64.
ultimate-tool docs MAILBOX_PROTOCOL.md
8.4 kB
Markdown
at main

Ultimate Tool C64 Protocols #

Ultimate Tool currently uses three independent fixed-memory protocols:

  • UTD2: C64-to-host diagnostic events plus cooperative liveness in 256 bytes
  • UTX2: C64-to-host cooperative crash context and optional exact site in 32 bytes
  • UTC1: one host-to-C64 startup command in 64 bytes

The project manifest declares their addresses. The Rust-MOS fixture reserves $C000-$C0FF, $C100-$C13F, and $C140-$C15F as NOLOAD linker regions. All multibyte integers are little-endian.

Diagnostic mailbox (UTD2 version 2) #

The host continues to parse legacy UTD1 version 1 mailboxes. UTD1 has no heartbeat or crash terminal/event, so it cannot support opt-in hang detection.

Offset Size Meaning
0 4 ASCII magic UTD2
4 1 Protocol version, 2
5 1 Header size, 16
6 1 Record size, 60
7 1 Record count, 4
8 2 Latest published sequence; zero means no event
10 1 Terminal state: 0 running, 1 pass, 2 fail, 3 crash
11 2 Wrapping cooperative heartbeat counter
13 3 Reserved and zero

Event records #

Four records begin at offset 16.

Record offset Size Meaning
0 2 Event sequence
2 1 Event kind
3 1 Flags
4 2 Project-defined numeric code
6 1 Payload length, at most 52
7 1 Reserved and zero
8 52 Printable ASCII payload

Event kinds are ready, log, assertion pass, assertion fail, test pass, test fail, and crash (1 through 7 respectively). A crash terminal requires exactly one crash event and no pass/fail terminal event; every other terminal rejects crash events.

Sequences use 1…65535, skip reserved sequence zero, and wrap to 1. The C64 advances the ring slot independently from the numeric sequence so all four records remain available across that wrap.

The publisher clears a selected record sequence, writes its metadata and payload, publishes the record sequence, writes a terminal state when applicable, and publishes the header sequence last. No event may follow a terminal event.

DeveloperSession::heartbeat() advances the 16-bit heartbeat independently of event publication and wraps through zero. The host begins monitoring only after ready and startup-command acknowledgement. A heartbeat may advance between the two stability reads without invalidating an otherwise stable event publication; the host uses the heartbeat from the second read.

DeveloperSession::crash(code, label) publishes an explicit terminal crash without an exact origin. The panic_halt helper reclaims an initialized UTD2 mailbox on the terminal panic path, publishes code 0x0001 and the stable label panic, then halts. crash_here!(&mut session, code, label) is the opt-in exact-site path described below.

The host reads the complete 256-byte mailbox twice and compares all publication state. UTD2 heartbeat bytes are excluded from that equality check because they are independently published liveness state. A stable snapshot must expose a contiguous ring ending at the published sequence. The host accumulates and deduplicates events, but any missing sequence is a hard protocol failure. After the first valid mailbox, three consecutive malformed stable reads fail the run and preserve the raw read pair.

Crash context (UTX2 version 2) #

Manifest schema 4 may opt into a fixed crash context plus bounded memory captures. The SDK clears the 32-byte context during initialization. A cooperative crash records the context before publishing its terminal event. The host continues to accept UTX1; those contexts have no exact site.

Offset Size Meaning
0 4 ASCII magic UTX2
4 1 Protocol version, 2
5 1 Context size, 32
6 1 Publication state: 1 writing, 2 complete
7 1 Register semantics: 1 capture-helper entry
8 1 Accumulator
9 1 X register
10 1 Y register
11 1 Processor status as pushed by PHP
12 1 Hardware stack pointer before the capture helper call
13 2 Crash code, which must match the crash event
15 1 Site semantics: 0 unavailable, 1 instrumented callsite
16 2 Runtime address after the instrumentation helper call, or zero
18 14 Reserved and zero

The register values are the values available on entry to the SDK's assembly capture helper. They are deliberately labeled capture_entry; they are not represented as an exact snapshot of the instruction that caused a Rust panic. As on a 6510, PHP forces the break and unused bits in the saved status representation.

crash_here! invokes a dedicated 6510 assembly helper directly from the instrumented function. The helper reads only its own JSR return slot, adds one to normalize the 6510 return address to the following instruction, and records that address before normal crash publication. It never scans arbitrary stack words. DeveloperSession::crash and panic_halt publish UTX2 with site semantics zero and address zero.

UTX1 uses the same bytes through offset 14 and requires bytes 15–31 to be zero.

After a valid crash terminal/event, the host reads the context twice and requires identical, complete publications. It then reads only:

  • The configured number of hardware-stack bytes beginning at $0100 + SP + 1, truncated at the end of the stack page
  • Explicit manifest-declared zero-page ranges
  • Explicit manifest-declared global ranges

Configuration limits the stack to 64 bytes, each named range to 64 bytes, eight ranges of each kind, and 256 captured bytes in total. Zero-page ranges must remain below $0100; selected globals cannot overlap C64 I/O or protocol regions. This slice does not perform arbitrary memory reads, source-address resolution, stack walking, or register reconstruction.

Raw context, stack, zero-page, and global bytes are preserved alongside schema-2 crash-state.json. Schema-1 crash-state evidence remains readable. Each memory record includes its address, length, evidence filename, and SHA-256.

Startup command (UTC1 version 1) #

The one-shot command slot has a 12-byte header followed by 52 combined bytes of command name and payload.

Offset Size Meaning
0 4 ASCII magic UTC1
4 1 Protocol version, 1
5 1 Slot size, 64
6 1 State: 1 waiting, 2 submitted, 3 accepted, 4 rejected
7 1 Reserved and zero
8 2 Request sequence
10 1 Command-name length; nonzero when submitted
11 1 Payload length
12 52 Printable ASCII command name followed by payload

The device initializes the slot and publishes waiting after the diagnostic mailbox is ready. The host requires both the ready event and two identical waiting-slot reads before writing.

For VICE, the binary monitor stops execution while Memory Set writes the full slot. For an Ultimate, the runner pauses the CPU at the advertised application safe point, posts the 64 bytes through DMA, and resumes even when the write fails. Neither adapter retries an ambiguously accepted write.

The SDK validates the complete submitted slot, changes its state to accepted, and returns an owned command to the application. Unknown application-level commands are reported through normal diagnostic failure events. The current fixture accepts command name scenario with payload pass, fail, crash, panic, hang, timeout, or malformed.

Timeout classification #

Manifest schema 3 adds optional per-test heartbeat_timeout_ms. It must be positive, shorter than the overall timeout, and at least three times the largest configured target polling interval.

  • A valid crash terminal/event is classified crash.
  • A configured heartbeat that stops advancing is classified hang.
  • An advancing heartbeat whose overall deadline expires is classified timeout.
  • A missing ready event remains a readiness-stage timeout.

Without heartbeat_timeout_ms, the runner retains generic deadline behavior and never relabels an uninstrumented program as hung.

DMA boundary #

The fixture performs no UCI transfers. The ready/waiting state is therefore a valid quiescent checkpoint for the single coordinated write. Future transports must add explicit application-level safe-point negotiation before mixing UCI activity with host DMA.