|
ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
|
This section specifies every first-party binary format this firmware produces or consumes. It exists because the code alone is a poor teacher: you can read jof_parse() and learn what the bytes are without ever learning why they are arranged that way, and the "why" is where all the device constraints live.
Each specification is written for a competent engineer who is new to that particular format. Every page follows the same progression, so you can stop at whatever depth you need:
| Section | What it answers | Stop here if... |
|---|---|---|
| 1. Synopsis | What is it, what problem does it solve? | you just need to hold a conversation about it |
| 2. Design rationale | Why this and not an off-the-shelf format? | you are reviewing a design decision |
| 3. Wire format | Exact bytes: offsets, widths, ranges | you are writing a parser |
| 4. Algorithms | How producing and consuming actually work | you are changing the producer or reader |
| 5. Memory behaviour | What is resident, what streams, what it costs | you are budgeting SRAM |
| 6. Worked example | Real bytes from a real file, annotated | you are debugging a specific file |
| 7. Failure modes | What a malformed or hostile file can do | you are threat-modelling |
| 8. Versioning | How revisions are detected and rejected | you are adding a field |
Confirmed by sweeping the tree for four-byte magic constants. The table separates content formats (a file or blob that carries data across a trust or time boundary, and therefore earns a full specification) from internal markers (a tag that identifies an in-memory or build-time artifact, and only earns an entry here).
| Magic | Format | Home | Producer | Specification |
|---|---|---|---|---|
| JOF1 / JOFE | Jump-Offset band-tile atlas | apps/shared_libs/jof | jof_produce(), rabook_imagepack convert | JOF – The Jump-Offset Band-Tile Atlas |
| RBKC | Chunked .rabook container | apps/shared_libs/book | tools/epub_compile/src/epub_compile.py | RBKC – The Chunked .rabook Container |
| NPU1 | .npub Ethos-U55 model container | libs/ra8_hal | tools/vela/src/vela_gen.py | NPU1 – The .npub Ethos-U55 Model Container |
| ROT1 | Root-of-trust signed-image trailer | libs/ra8_dfu | scripts/secrets/rot_sign.py | ROT1 – The Root-of-Trust Signed-Image Trailer |
| NSR1 | Non-Secure image RoT header | libs/ra8_tz_secure_boot | examples/ek_ra8d2/hw_pending/secure_boot_ns_hil/scripts/sign_and_merge.py, examples/ek_ra8d2/hw_pending/secure_boot_ns_hil/ns_image.ld | NSR1 – The Non-Secure Image RoT Header |
Each has its own page in this section; they are also listed in the navigation sidebar under "Binary format specifications".
They are not independent, and reading them in dependency order costs less effort than the alphabetical sidebar suggests:
These are deliberately excluded from the full treatment. Each is a tag on an artifact that never crosses a trust boundary as a standalone file, so there is nothing to parse defensively and no wire contract to pin down. Documenting them as "formats" would imply a stability guarantee none of them has.
| Magic | What it actually is | Why no spec |
|---|---|---|
| RBK1 | A four-byte stamp (k_rabook_import_stamp_magic, 0x52424B31) the importer writes to mark a blob as having passed import | Not a container. It is one word inside a structure the same library owns end to end – there is no independent reader, so there is no wire contract. Covered where it is used, in rabook_import.h. |
| NPUQ | A four-character tag on the NPU quantisation helper (s_tag in ra8_npu_quant.c) | A logging/identification string in one translation unit. It is never serialised to a file. |
| SE55 | "Emu-Ethos-U55" marker in the high bits of a emulated NPU command word (ra8_npu_fake_cmd.h) | A register-level convention between the firmware and ra8_emulator, not an on-disk format. It exists so a host test can distinguish a emulated command from noise. Specified where it belongs, in ra8_npu_fake_cmd.h. |
Two more four-character strings turn up in a naive grep and are not magics at all, recorded here so the next person does not re-investigate them: RAIO is a FAT volume label used by the ra8_io demo apps, and BLEN is the backlight-enable signal name in the EK-RA8D2 board pin table.
Every page in this section has to answer the same challenge, and it is the first thing a reviewer asks: a standard format already does this – why is there a bespoke one instead? The challenge is fair, and for each format there is a specific competitor a knowledgeable reader will name.
| Format | The off-the-shelf competitor | Does it already solve seeking? | What actually decided it |
|---|---|---|---|
| JOF1 | Tiled TIFF – TileOffsets / TileByteCounts, COMPRESSION_ADOBE_DEFLATE | Yes. That pair genuinely is a jump-offset index over independently-compressed tiles | Parser attack surface, and a producer that must never seek |
| RBKC | Blocked DEFLATE – bgzf, dictzip, or .xz with its block index | Yes. Independent blocks plus an index is exactly this pattern | One chunk == one ra8_vmem cache frame == one inflate |
| RABOOK1 | EPUB, read directly | Not the question – the cost here is parsing, not seeking | Pre-resolving the CSS cascade and pre-transcoding images to panel-native 4 bpp |
Two of those three competitors already solve random access. That is worth saying plainly, because "the standard format cannot seek" is the argument these pages are most likely to be assumed to make, and for two of them it would be false.
What every one of these formats does is move a variable, input-dependent cost off the device permanently, and pay it once on a host with an operating system and gigabytes of RAM.
The fixed-offset wire layouts they share are a consequence, not the goal. Once the device is no longer parsing content, the only thing left on it to parse is a table of offsets – and a table can be fixed-width, bounded, and validated with arithmetic. The bounded parser falls out of moving the work, which is why the same shape appears four times.
The binding constraint behind all of it, stated once: a bare-metal reader with zero dynamic allocation (NASA P10 Rule 3) and a 1.6 MB SRAM budget, facing a file that arrived on a removable card and is therefore untrusted. A format that must allocate in order to be read is not a candidate, however well specified it is.
A rationale that only lists wins is not trustworthy. The bill for owning four bespoke formats is real and is paid continuously:
The trade is accepted because the alternative is worse on this specific device, not because the standard formats are bad. On a machine with a heap and an MMU, most of these decisions would go the other way.
The three cases are not equally strong, and flattening them into one story would misrepresent one of them.
Every multi-byte integer in every first-party format is little-endian. The Cortex-M85 runs little-endian, so this is a zero-cost choice on device and the producers (Python and C host tools) match it explicitly. There is no byte-swapping anywhere in the read paths.
This trips people up in a hexdump, so it is worth being explicit. The formats split into two camps:
| Style | Formats | Declared as | Bytes on disk |
|---|---|---|---|
| Byte string | JOF1, JOFE, RBKC | a 4-byte array, compared with memcmp | read left-to-right: 4a 4f 46 31 = JOF1 |
| uint32 constant | NPU1, NSR1, ROT1, RBK1 | a uint32_t enum, compared with == | stored little-endian, so whether they read forwards depends on how the constant was chosen – see below |
A uint32 magic reads forwards in a hexdump only if whoever picked the constant wrote it "backwards" on purpose. Half of ours did and half did not:
| Magic | Constant | Little-endian bytes | ASCII column | Reads |
|---|---|---|---|---|
| NPU1 | 0x3155504E | 4e 50 55 31 | NPU1 | forwards |
| NSR1 | 0x3152534E | 4e 53 52 31 | NSR1 | forwards |
| ROT1 | 0x524F5431 | 31 54 4f 52 | 1TOR | reversed |
| RBK1 | 0x52424B31 | 31 4b 42 52 | 1KBR | reversed |
NPU1 and NSR1 were deliberately spelled so the flashed image reads its own marker in order – convenient when you are staring at a hexdump of an NS image or a .npub blob. ROT1 and RBK1 were written the "natural" way, so they appear reversed on disk. Neither is a bug; both compare identically with ==. Just do not assume a uint32 magic reads forwards, and do not "fix" 1TOR when you see it.
Every magic ends in a character that identifies the revision or role of the structure, not just the family:
This makes rejection cheap and unambiguous. A reader compares four bytes; if they do not match exactly, it refuses the file. It never tries to parse a structure it does not fully recognise, so an unknown future revision produces a clean "I do not understand this file" rather than a plausible-looking misparse. Adding an incompatible revision means spending a new fourth byte (JOF2), and every existing reader rejects it automatically with no code change.
Two formats additionally carry an explicit version field alongside the magic (NPU1, ROT1). Those use the magic for family identification and the field for revision, which is the better design when the format expects to revise often – but the discriminator byte is still checked first.
Content formats arrive from untrusted media – an SD card the user filled, an EPUB downloaded from anywhere. Every reader in this tree is fail-closed: a field is validated before it is used, never after, and any failed check aborts the whole operation with a ra8_err_t rather than clamping and continuing.
The threat being defended against is not remote code execution from an SD card. It is the far more likely failure that actually matters in practice: the application or an EPUB crashing because a file was malformed or hostile. So the bar every reader must clear is that no input, however corrupt, causes an out-of-bounds access, an unbounded loop, an unbounded allocation, or a hang.
Three mechanisms do most of that work:
NASA Power of 10 Rule 3 holds throughout: no reader in this section allocates. The caller supplies every buffer at open time (offset tables, metadata tables, staging, scratch, output pixels). This is why the readers take so many buffer arguments – it is a deliberate contract, not an ergonomic oversight, and it is what makes the working set statically knowable.
Every worked example in this section was produced with tools/rabook_imagepack, the in-tree inspector. It exposes three verbs over a format registry, and the distinction between them matters:
| Verb | Input | Question it answers |
|---|---|---|
| convert | a source file (PNG/JPEG) | produce the first-party container |
| inspect | a container | is this structurally sound, and what is in it? Verbose mode adds header/footer hexdumps and a per-record table |
| verify | a source file | does the transcode round-trip losslessly, byte for byte? |
inspect sniffs the magic itself, so rabook_imagepack inspect foo.bin identifies the container without being told which format it is; for JOF it also proves the tile grid covers the image exactly once, with no gaps and no duplicates. Note that verify takes the source, not the container – it re-runs the transcode and diffs the result against a reference decode, which is how the no-quality-loss rule is checked mechanically rather than asserted.
Using the real tool rather than hand-written examples is a deliberate policy: invented hexdumps drift silently from the code, and a reader who cannot reproduce the example cannot trust it. Every byte quoted in these pages was generated by running these commands.
Every diagram on these pages is a Graphviz @dot block written inline in the Markdown source. That is a deliberate choice, and worth recording so the next person extending this section does the same thing:
**Do not use @startuml anywhere in the tree.** PLANTUML_JAR_PATH is not configured and no JVM is provisioned, so doxygen ignores every @startuml block – they render nowhere. Use @dot, which does. scripts/checks/check_doc_diagrams.py enforces both halves: it rejects @startuml outright, and it fails when the number of authored @dot blocks does not match the number of diagrams that actually reached the generated HTML.
—
## Where the authority lives
For each format the wire contract is owned by this specification section; the header keeps the enum of byte offsets, which is the machine-checkable truth the compiler enforces. Where a header still carries a summary field table, it is a convenience copy and says so, pointing here for the full contract. When you change a format:
The shape above is the reason this section exists at all: all the expensive, unbounded, untrusted parsing happens on the host, once, and the device only ever reads a format designed for it – seekable, bounded, and checkable. Every page in this section is a variation on that one idea.