retro-silicon

Components

debug/test-shell

UART command engine giving a host framed, CRC-checked access to a core's bus

Portable Sim: passing (12 passed). Used by Nintendo Entertainment System, shell-ram, shell-sdram. Source.

BoardStateRun
Sipeed Tang Nano 20Kverified2026-10-28, at ac84cec
Sipeed Tang Console 138Kverified2026-10-25, at f2536e6
TUL PYNQ-Z2verified2026-10-28, at ac84cec

Verified: the latest run on that board of a system using it passed every test, and the component hasn't changed since. Stale: it has changed since that run.

README

A UART command engine that gives a host framed, CRC-checked access to a core: it reads and writes the core’s bus, and reports a build ID so every result ties to the exact RTL that produced it. Later blocks (ROM loader, run control, video and audio CRCs, frame dump, trace buffer) are reached through the same commands and bus.

The host side is rig::shell::Client in harness.

Protocol

The host sends one command and waits for its response before sending the next. Frames in both directions:

BytesField
1Sync: A5 for a command, 5A for a response
1Command code, or the response’s status
1Sequence number, copied into the response
2Payload length, little-endian
lengthPayload
4CRC-32 (as zlib) of everything after the sync byte, little-endian

All multi-byte values are little-endian.

Versions: 1 had PING, INFO, PEEK and POKE; 2 adds CRC.

Recovery: the shell drops a command frame without answering when its CRC fails, when its length is over the payload buffer (it resyncs straight away), or when no byte arrives for IDLE_CLKS clocks in the middle of it (10 ms by default). The host resends after a silence longer than that, with the same sequence number. Every command is idempotent (addresses are absolute), so running one twice is harmless. The host ignores responses with another sequence number. Because commands and responses use different sync bytes, an echo or loopback never passes for a shell.

Commands

CodeCommandPayloadResponse payload
01PINGAny bytes, up to the payload limitThe same bytes
02INFONone16 bytes: "RSTS", then the protocol version (2 bytes, now 2), BUILD_ID (4 bytes), the payload limit (2 bytes), and counts of corrupt and of stalled frames dropped (2 bytes each, saturating)
10PEEKAddress (4), count (2)count bytes read from the bus, from the address up
11POKEAddress (4), then the dataNone; writes the data to the bus, from the address up
12CRCAddress (4), count (4)CRC-32 (as zlib, 4 bytes) of count bytes read from the bus, from the address up; the address wraps at the top. The response comes once every byte is read, so the host waits longer for a big count

Status codes

CodeMeaning
00OK
01Unknown command
02Wrong payload length for the command

An error response has no payload.

Ports

PortDirWidthMeaning
clk, rstin1Clock; synchronous reset
uart_rxin1Serial input from the host, idle high
uart_txout1Serial output to the host
bus_addrout32Byte address; the system decides what it maps to
bus_reqout1Held high, with the address and data, until bus_ack
bus_weout1Write when high, read when low
bus_wdataout8Byte to write
bus_ackin1One-cycle pulse: the access is done, and on a read bus_rdata is valid
bus_rdatain8Byte read
crc_errorout1One-cycle pulse: a command frame was dropped as corrupt
timeoutout1One-cycle pulse: a command frame stalled and was dropped

Parameters

ParameterDefaultMeaning
CLKS_PER_BIT18Clocks per bit; 27 MHz / 18 = 1.5 Mbaud
MAX_PAYLOAD_LOG211Command payloads up to 2 KB, buffered in one block RAM
IDLE_CLKS270,000Clocks of silence that drop a partial frame; 10 ms at 27 MHz
BUILD_ID0Reported by INFO; CMake’s rs_build_id derives it from the RTL

Built from rtl/ts_rx.sv (frame receiver), rtl/ts_tx.sv (frame transmitter, CRC computed as bytes go out, so a response of any length needs no buffer) and rtl/test_shell.sv (dispatcher), with common/uart and common/crc32. About 900 LUTs and one block RAM on a GW2A-18C.

Status

Sim tests against a bus model with a random 0-4 clock acknowledge delay:

In systems/shell-ram on a Tang Nano 20K at 1.5 Mbaud: