tinyboot-protocol
Part of the tinyboot project — see the main README to get started.
Wire protocol for tinyboot. Defines the frame format used between host and device over UART/RS-485.
Frame format
A single Frame struct is used for both requests (host to device) and responses (device to host), so we keep code size tiny.
0 1 2 3 4 5 6 7 8 9 10 10+len 10+len+2
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+- - - -+-------+-------+
| SYNC0 | SYNC1 | CMD |STATUS | ADDR (u32 LE) | LEN_LO LEN_HI | DATA... | CRC_LO CRC_HI |
| 0xAA | 0x55 | | | | | | |
+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+- - - -+-------+-------+
|<--------------------- header (10 bytes) --------------------->|<- payload ->|<--- CRC --->|
Total frame size = 12 bytes overhead + payload. Maximum payload is 64 bytes (MAX_PAYLOAD).
| Field | Size | Description |
|---|---|---|
| SYNC | 2 bytes | Preamble 0xAA 0x55 for frame synchronization |
| CMD | 1 byte | Command code |
| STATUS | 1 byte | Request (0x00) for requests, result status for responses |
| ADDR | 4 bytes | Flash address (u32 LE). Echoed in responses |
| LEN | 2 bytes | Data payload length (u16 LE, 0..64) |
| DATA | 0..64 | Payload bytes |
| CRC | 2 bytes | CRC16-CCITT (LE) over SYNC + CMD + STATUS + ADDR + LEN + DATA |
Commands
| Code | Name | Direction | Description |
|---|---|---|---|
| 0x00 | Info | Host to Device | Query device info (capacity, erase size, versions, mode) |
| 0x01 | Erase | Host to Device | Erase byte_count bytes at addr (first erase transitions Idle → Updating) |
| 0x02 | Write | Host to Device | Write data at address |
| 0x03 | Verify | Host to Device | Compute CRC16 over addr bytes of app, store checksum + state in metadata |
| 0x04 | Reset | Host to Device | Reset the device |
| 0x05 | Flush | Host to Device | Flush buffered writes to storage |
Info response
Returns 12 bytes via the InfoData struct:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 bytes | capacity | App region capacity in bytes (u32 LE) |
| 4 | 2 bytes | erase_size | Erase page size in bytes (u16 LE) |
| 6 | 2 bytes | boot_version | Boot version (packed u16 LE, 0xFFFF=none) |
| 8 | 2 bytes | app_version | App version (packed u16 LE, 0xFFFF=none) |
| 10 | 2 bytes | mode | 0 = bootloader, 1 = app |
Versions are packed as (major << 11) | (minor << 6) | patch and read from the last 2 bytes of each binary (boot and app).
Erase
Erases byte_count bytes starting at addr. Both addr and byte_count must be aligned to the device's erase size.
Request payload (2 bytes via EraseData):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 2 bytes | byte_count | Number of bytes to erase (u16 LE) |
Flush
The host must send a Flush command to commit any partial page still buffered on the device. Flush is required:
- After the final Write — otherwise the last partial page may not be written to flash, causing Verify to fail.
- Before skipping an address range — if the host advances the write address (e.g. skipping a gap between segments), it must Flush first to commit the buffered data at the previous address.
Write alignment
Write payloads must be padded to a 4-byte boundary. The device writes to flash 4 bytes at a time, so unaligned payloads will fail.
Verify
The addr field carries the application size in bytes. The device computes CRC16 over the first addr bytes of flash (the actual firmware, not the full region), stores the checksum and app size in boot metadata, and transitions to Validating state.
If Verify returns CrcMismatch, check that all Write payloads were padded to 4 bytes and that Flush was sent after the last Write (and before any address skips).
Response returns 2 bytes via the VerifyData struct:
| Offset | Size | Description |
|---|---|---|
| 0 | 2 bytes | CRC16 of app firmware bytes (u16 LE) |
Status codes
| Code | Name | Description |
|---|---|---|
| 0x00 | Request | Frame is a request (not a response) |
| 0x01 | Ok | Success |
| 0x02 | WriteError | Flash write/erase failed |
| 0x03 | CrcMismatch | CRC verification failed |
| 0x04 | AddrOutOfBounds | Address or length out of range |
| 0x05 | Unsupported | Command not valid in current state |
| 0x06 | PayloadOverflow | Frame payload exceeds maximum size |
CRC
CRC16-CCITT with polynomial 0x1021 and initial value 0xFFFF. Computed over the entire frame body (SYNC through DATA, excluding the CRC field itself). Bit-bang implementation with no lookup table for minimal flash footprint.
Protocol flow
- Host sends a request frame with
status = Request (0x00) - Device reads the frame, processes the command
- Device sends a response frame with
cmdandaddrechoed from the request,statusset to the result
The same Frame struct is reused: after read(), the device modifies status, len, and data, then calls send(). The cmd and addr fields carry over automatically.
Data union
The data field is a #[repr(C)] union with typed variants for structured payloads:
pub union Data
Data starts at offset 10 (even-aligned), so u16 fields in the union variants are naturally aligned.
Example: flash sequence
Host -> Info request
Device -> Info response (capacity=16384, erase_size=64, mode=0)
Host -> Erase addr=0x0000 byte_count=16384
Device -> Ok
...
Host -> Write addr=0x0000 data=[64 bytes]
Device -> Ok
Host -> Write addr=0x0040 data=[64 bytes]
Device -> Ok
...
Host -> Flush
Device -> Ok
Host -> Verify addr=5110 (app_size)
Device -> Ok crc=0x1234
Host -> Reset
Device -> Ok (then resets)