CM0
Get the packages first - they all live in one repo:
git clone https://github.com/freewili/onewili
cd onewili
The CM0 is the Linux host beside MAIN on the same board. This package reaches MAIN's menu system from there over the FPGA mailbox console link, without an external PC or USB connection. The generated commands use the same menu API as the host libraries.
Connect from C or C++
It needs the fwcm0 driver (fwcm0::fwcm0_core) for the mailbox and router; build the package against it:
cmake -S ./cm0 -B ./cm0/build
cmake --build ./cm0/build
This path owns the CM0 hardware directly. Stop the bridge daemon before using it. With the driver and onewili_cm0.h included, open a device and send a first command in C++:
fwcm0::LinuxTransport io(fwcm0::LinuxConfig{});
fwcm0::SramRouter router(io);
router.open();
fwcm0::MboxMux mux(router);
fwcm0::ConsoleClient client(mux);
fwcm0::OneWiliLink link(client); // confirms the API-mode handshake
ow_transport t = link.transport();
ow_device dev;
ow_open(&dev, &t);
/* ow_status ow_gui_set_led_color(ow_device* dev, int32_t ledindex, int32_t red, int32_t green, int32_t blue, int32_t duration, ow_ow_led_manager_led_mode mode); */
ow_gui_set_led_color(&dev, /* args */);
ow_close(&dev);
Connect from Python
Python pipes the same command bytes through fwcm0 api and the running bridge daemon. Install the onewili package from ./python, add ./cm0/python to PYTHONPATH for the adapter, and put fwcm0 on PATH:
from onewili_cm0 import connect_cm0
dev = connect_cm0()
dev.gui.set_led_color(...)
dev.close()
The CM0's own SRAM/PSRAM router is not part of this API - it is a local capability reached directly through fwcm0::SramRouter.
Mailbox architecture
The FPGA provides two independent, single-message buffers. Inbox and outbox are named from MAIN's perspective:
CM0 Linux FPGA MAIN
| -- SPI0: OP_MBOX ----------> inbox (CM0 -> MAIN) -- read --> |
| <-- UART: FT_MBOX ---------- outbox (MAIN -> CM0) <- write -- |
^ |
+-- MAIN register interface --+
SPI1 or I2C
Each buffer holds a mailbox body of 1 to 32 bytes. Its first byte is a message type, leaving at most 31 bytes for that type's data. The FPGA moves the body without interpreting it; CM0 and MAIN implement its meaning. OneWili commands and replies can span many bodies. Body boundaries do not mark command or response boundaries.
CM0-facing wire formats
CM0 submits a body in one SPI command, with chip select held for the command:
[OP_MBOX = 07] [body_length: 1 byte] [body: 1..32 bytes]
For example, a HELLO request is 07 02 01 02: opcode, length, CONTROL type,
HELLO sub-operation. This SPI command has no per-body acknowledgement or CRC.
In the reverse direction, the router wraps MAIN's body in its UART framing:
Decoded frame:
[FT_MBOX = 05] [sequence = 00] [length_hi] [length_lo] [body] [CRC-8]
UART bytes:
COBS(decoded frame) [00 delimiter]
The length is a big-endian 16-bit body length. CRC-8 covers the decoded
header and body, excluding the CRC byte itself: polynomial 07, initial
value 00, MSB first, no reflection or final XOR. The receiver COBS-decodes
the frame and validates its length and CRC before dispatching it. Mailbox
UART frames currently use sequence zero; this is separate from the sequence
field in a OneWili response. The outer FT_MBOX identifies a router frame;
the first byte inside its body identifies console, control, or shell data.
MAIN-facing register interface
MAIN accesses the same buffers through the FPGA register window. The register
addresses below use the mailbox's default base at 0x08.
| Address | Register | Meaning |
|---|---|---|
0x08..0x27 | Inbox data | Up to 32 bytes received from CM0. |
0x28 | Inbox status | Bit 7: ready; bit 6: overflow; bits 5..0: body length. |
0x29 | Inbox consume | Any write releases the inbox and clears its length and overflow flag. |
0x30..0x4F | Outbox data | MAIN writes the next body here. |
0x50 | Outbox commit | Write the body length in bits 5..0 to publish it; zero is ignored. |
0x51 | Outbox status | Bit 0: free; bit 1: overflow. |
MAIN checks inbox-ready, reads the indicated bytes, then writes consume. Reading data alone does not release the inbox. To reply, MAIN waits for the outbox to be free, writes the body, then commits its length. The FPGA releases the outbox after transmitting its UART frame. Do not write outbox data while it is busy.
The MAIN SPI1 path shares pins with other functions and must access these
registers inside the IO_CONFIG_ENABLE window. The I2C path does not require
that SPI window. fwProgIODir supplies the register-access helpers, while
rpCM0Comm services the inbox and outbox from the firmware loop.
Body types and controls
All byte values in these tables and examples are hexadecimal.
| First byte | Type | Remaining bytes |
|---|---|---|
00 | CONSOLE | Up to 31 bytes of console or OneWili API text. |
01 | CONTROL | Sub-operation followed by its arguments. |
02 | SHELL | Up to 31 raw bytes for the CM0 bash session. |
API mode still uses CONSOLE bodies. The selected stream mode tells MAIN whether to treat them as interactive console input or isolated API commands.
| Control body | Direction | Meaning |
|---|---|---|
01 02 | CM0 -> MAIN | HELLO: request the mailbox protocol version. |
01 02 major minor | MAIN -> CM0 | HELLO reply; current version is 01 02 (1.2). |
01 01 mode | CM0 -> MAIN | SET_STREAM: 00 off, 01 interactive console, 02 OneWili API. |
01 01 mode | MAIN -> CM0 | Acknowledge that the selected mode has been applied. |
01 03 | CM0 -> MAIN | RESET: inject Ctrl-C into the input stream; this is not a board reset. |
01 04 enable | MAIN -> CM0 | SHELL_ATTACH: 01 starts bash, 00 stops it. |
01 05 | CM0 -> MAIN | SHELL_EXIT: report that bash exited. |
Control replies can be sent with console streaming disabled. MAIN reference counts shell attachments so multiple local surfaces can share one bash session. Shell data has its own queues and is not OneWili command data.
Opening an API session
- CM0 sends HELLO (
01 02) and receives01 02 01 02from current MAIN firmware. - The client checks for mailbox protocol 1.2 or later, which supports API mode.
- CM0 sends SET_STREAM with mode 2 (
01 01 02). - MAIN switches modes, clears the console queues when the mode changes, and
replies
01 01 02. The client waits for this acknowledgement before sending API commands. - The Python adapter then sends the Device State command (
h\a\g) and requires a valid response beforeconnect_cm0()returns a connected device.
SET_STREAM acknowledgements were added in protocol 1.1; isolated API mode was
added in 1.2. An interactive console connection to older firmware does not
establish that the OneWili API is usable. The C++ OneWiliLink constructor
performs the HELLO and API-mode acknowledgement steps; the Python adapter adds
the end-to-end Device State probe.
OneWili command stream
The generated libraries write the usual menu API command as bytes:
[Ctrl-B = 02] path[ arguments] [LF = 0A]
For example, a GPIO state query is 02 followed by the ASCII text i\g\u
and a newline. Its complete SPI mailbox command fits in one body:
07 08 00 02 69 5C 67 5C 75 0A
^^ CONSOLE body type
MboxMux splits longer byte streams into bodies with at most 31 data bytes.
MAIN reassembles the command until a line ending; Ctrl-B or Ctrl-C resets the
partially assembled command. Replies use the normal menu API envelope:
[path timestamp sequence response success]\n
Here timestamp is the hexadecimal timestamp field, sequence is the menu
response sequence, and success is 1 or 0. Response text may contain
spaces and newlines. The SDK parses the response envelope after reassembling
CONSOLE data; neither one mailbox body nor one UART frame necessarily contains
a complete reply.
How MAIN isolates API execution
rpCM0Comm routes mode-2 input to a dedicated API reader instead of shared
console input, and disables the normal console-output tee to CM0.
fwMenuMain::processCM0API() assembles one command in a private buffer and
runs it through executeLocalCommand(), capturing its reply for the mailbox.
The local command path saves and restores menu navigation and input state,
so a USB terminal can remain partway through its own command.
For commands that finish asynchronously, fwMenuX retains the response sink
and the originating menu route until completion. A session-generation check
prevents a deferred reply from an earlier session entering a new one. MAIN
drains pending API output and deferred work before executing the next command.
Linux implementation and ownership
The Python route is:
onewili -> Cm0Transport -> fwcm0 api -> Unix socket -> bridge daemon
-> ConsoleClient -> MboxMux -> SramRouter -> LinuxTransport
The default socket is /run/fwcm0-bridge.sock. The CLI opens an API connection
with a little-endian 32-bit payload length of one, followed by socket operation
OP_API = 07: 01 00 00 00 07. After this request the socket carries a raw,
bidirectional command/reply byte stream. This socket opcode and SPI OP_MBOX
happen to share a value but belong to different protocol layers.
The daemon owns SPI, UART, and the router receive thread. MboxMux owns the
router's mailbox callback, dispatches bodies by type/control sub-operation,
and serializes console and shell sends. The daemon permits one console/API
owner at a time and rejects a competing attachment. USB can remain connected
because MAIN handles API mode separately. Closing the Python transport closes
the CLI input, lets the daemon release the session, and reaps the subprocess.
The C/C++ route uses the same lower layers directly. OneWiliLink adapts a
ConsoleClient to the C library's ow_transport read/write interface and
detaches its receive callback and disables the stream on destruction. Keep
the transport, router, mux, client, and link alive while ow_device uses them.
Buffering and limits
Each hardware direction holds only one body. A new CM0 body arriving while
the inbox is occupied is dropped and sets overflow; the existing body remains.
MAIN leaves data in the inbox when its software receive queues cannot accept
it, and checks outbox-free before publishing a reply. CM0 sends must pass
through the shared MboxMux sender. The hardware offers no per-data-body
acknowledgement or automatic retransmission, so it must not be treated as an
unbounded, lossless queue. HELLO and SET_STREAM replies acknowledge those
control operations, not every chunk of subsequent data.
MAIN currently has 4096-byte console rings (4095 usable bytes), a 512-byte command buffer (511 text bytes plus terminator), and a 4096-byte captured-reply buffer (4095 text bytes plus terminator). Commands or captured replies that exceed their buffers fail. Raw file transfers and binary event streaming are not supported by this adapter; ordinary menu commands and polling use the text API. SRAM/PSRAM access remains a separate router operation.
Source map
Implementation paths below are relative to the firmware repository:
| Layer | Source |
|---|---|
| FPGA buffers and register window | freewilifpga/fpga/src/dsn/cm0_mailbox.v |
| FPGA command/frame routing | freewilifpga/fpga/src/dsn/cm0_router_ctrl.v, cm0_router_defs.vh |
| CM0 wire definitions and framing | freewilicm0/include/fwcm0/protocol.h, freewilicm0/src/commands.cpp, frame_parser.cpp, crc8.cpp |
| CM0 dispatch, handshake, and socket bridge | freewilicm0/src/mbox_mux.cpp, console_client.cpp, bridge_daemon.cpp |
| MAIN register access and mailbox service | freewilimain/fwProgIODir.cpp, freewilimain/rmpLib/rpCM0Comm.cpp |
| MAIN API execution and deferred replies | freewilimain/fwMenuMain.cpp, freewilimain/rmpLib/fwMenuX.cpp |
| Generated OneWili adapters | freewilimain/testprojects/onewili/cm0/ |
| Adapter and documentation generators | freewilimain/testprojects/menutool/core/Cm0ApiEmitter.cpp, DocusaurusEmitter.cpp |
Full command list: GUI Functions - or start at the API index.