Skip to main content

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.

AddressRegisterMeaning
0x08..0x27Inbox dataUp to 32 bytes received from CM0.
0x28Inbox statusBit 7: ready; bit 6: overflow; bits 5..0: body length.
0x29Inbox consumeAny write releases the inbox and clears its length and overflow flag.
0x30..0x4FOutbox dataMAIN writes the next body here.
0x50Outbox commitWrite the body length in bits 5..0 to publish it; zero is ignored.
0x51Outbox statusBit 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 byteTypeRemaining bytes
00CONSOLEUp to 31 bytes of console or OneWili API text.
01CONTROLSub-operation followed by its arguments.
02SHELLUp 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 bodyDirectionMeaning
01 02CM0 -> MAINHELLO: request the mailbox protocol version.
01 02 major minorMAIN -> CM0HELLO reply; current version is 01 02 (1.2).
01 01 modeCM0 -> MAINSET_STREAM: 00 off, 01 interactive console, 02 OneWili API.
01 01 modeMAIN -> CM0Acknowledge that the selected mode has been applied.
01 03CM0 -> MAINRESET: inject Ctrl-C into the input stream; this is not a board reset.
01 04 enableMAIN -> CM0SHELL_ATTACH: 01 starts bash, 00 stops it.
01 05CM0 -> MAINSHELL_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​

  1. CM0 sends HELLO (01 02) and receives 01 02 01 02 from current MAIN firmware.
  2. The client checks for mailbox protocol 1.2 or later, which supports API mode.
  3. CM0 sends SET_STREAM with mode 2 (01 01 02).
  4. 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.
  5. The Python adapter then sends the Device State command (h\a\g) and requires a valid response before connect_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:

LayerSource
FPGA buffers and register windowfreewilifpga/fpga/src/dsn/cm0_mailbox.v
FPGA command/frame routingfreewilifpga/fpga/src/dsn/cm0_router_ctrl.v, cm0_router_defs.vh
CM0 wire definitions and framingfreewilicm0/include/fwcm0/protocol.h, freewilicm0/src/commands.cpp, frame_parser.cpp, crc8.cpp
CM0 dispatch, handshake, and socket bridgefreewilicm0/src/mbox_mux.cpp, console_client.cpp, bridge_daemon.cpp
MAIN register access and mailbox servicefreewilimain/fwProgIODir.cpp, freewilimain/rmpLib/rpCM0Comm.cpp
MAIN API execution and deferred repliesfreewilimain/fwMenuMain.cpp, freewilimain/rmpLib/fwMenuX.cpp
Generated OneWili adaptersfreewilimain/testprojects/onewili/cm0/
Adapter and documentation generatorsfreewilimain/testprojects/menutool/core/Cm0ApiEmitter.cpp, DocusaurusEmitter.cpp

Full command list: GUI Functions - or start at the API index.