Systems and scenes
Firmware rarely runs alone. A system wires microcontrollers and parts together; a scene runs them, each with its own firmware, in lock-step virtual time.
The system editor
In a project, open Systems and choose New system. The editor is a canvas:
- Drag in microcontrollers and parts, and wire their pins.
- Each microcontroller says which links it can join. The canvas refuses a wire whose link its models cannot run, and says why.
- Pick the firmware for each microcontroller: one you upload, or a starter for that part.
- Run shows every console and a bus monitor of UART bytes, CAN frames, SPI words and I2C transfers. Pause, change speed or stop, then read the report and verdict.
A system can hold other systems. To put a system in a 3D world, use the Proving Ground.
What runs today
| Link | Scene section | Notes |
|---|---|---|
| UART | uartLinks | Every UART model |
| SPI and I2C | spiLinks, i2cBuses | The SPI slave or I2C target needs a model with that mode: the STM32 SPI and I2C models and the Nordic SPIS and TWIS today |
| CAN and CAN FD | canBuses | Through transceivers in a system |
| RS-485 (UARTs through half-duplex transceivers) | rs485Buses | Through transceivers in a system |
| Ethernet | media with kind: ethernet | STM32 Ethernet MACs; write it in the scene, the system editor does not draw it |
Every link between microcontrollers is provisional: it carries bytes and frames at the level the firmware observes, not line levels or electrical timing.
A system file: two boards on a CAN bus
The editor stores systems as ct.system/2. The same document works from the CLI: parts, the wires between their pins, and buses. Older ct.system/1 files still compile.
{
"schemaVersion": "ct.system/2",
"id": "can-two-nucleo",
"parts": [
{ "name": "a", "kind": "mcu", "board": "nucleo-f767zi", "firmware": "firmware/can-ping.elf" },
{ "name": "b", "kind": "mcu", "board": "nucleo-f767zi", "firmware": "firmware/can-pong.elf" },
{ "name": "xa", "part": "SN65HVD230" },
{ "name": "xb", "part": "SN65HVD230" }
],
"buses": [{ "name": "can0", "kind": "can", "bitRateBps": 500000 }],
"wires": [
{ "id": "a-tx", "from": { "part": "a", "pin": "PD1" }, "to": { "part": "xa", "pin": "D" } },
{ "id": "a-rx", "from": { "part": "a", "pin": "PD0" }, "to": { "part": "xa", "pin": "R" } },
{ "id": "a-gnd", "from": { "part": "a", "pin": "GND" }, "to": { "part": "xa", "pin": "GND" } },
{ "id": "xa-h", "from": { "part": "xa", "pin": "CANH" }, "to": { "bus": "can0", "pin": "CANH" } },
{ "id": "xa-l", "from": { "part": "xa", "pin": "CANL" }, "to": { "bus": "can0", "pin": "CANL" } },
{ "id": "b-tx", "from": { "part": "b", "pin": "PD1" }, "to": { "part": "xb", "pin": "D" } },
{ "id": "b-rx", "from": { "part": "b", "pin": "PD0" }, "to": { "part": "xb", "pin": "R" } },
{ "id": "b-gnd", "from": { "part": "b", "pin": "GND" }, "to": { "part": "xb", "pin": "GND" } },
{ "id": "xb-h", "from": { "part": "xb", "pin": "CANH" }, "to": { "bus": "can0", "pin": "CANH" } },
{ "id": "xb-l", "from": { "part": "xb", "pin": "CANL" }, "to": { "bus": "can0", "pin": "CANL" } },
{ "id": "gnd", "from": { "part": "a", "pin": "GND" }, "to": { "part": "b", "pin": "GND" } }
],
"until": { "machine": "a", "marker": "A DONE" },
"maxMs": 400
}ct system compile can-two-nucleo.system.json --output can-two-nucleo.scene.json
ct scene run can-two-nucleo.scene.json
ct system convert old.system.json --output new.system.json # ct.system/1 or a board, as ct.system/2ct system compile reads the pin multiplexing of each microcontroller twin, works out which controller every wire belongs to, and writes the scene. It explains every wire it cannot use. Exit codes: 0 without errors, 1 with errors, 2 for an unreadable document. Problems carry a severity (error, warning, info), the wires they concern and a code such as system.wire.same-direction (TX wired to TX) or system.wire.no-ground (two boards share no ground).
A scene: two boards on a UART link
You can also write a scene by hand:
{
"schemaVersion": "ct.scene/1",
"id": "uart-two-nucleo",
"machines": [
{ "name": "a", "board": "nucleo-f767zi", "firmware": "firmware/uart-a.elf" },
{ "name": "b", "board": "nucleo-f767zi", "firmware": "firmware/uart-b.elf" }
],
"uartLinks": [
{
"name": "usart2",
"baudBps": 115200,
"ends": [
{ "machine": "a", "uart": "USART2" },
{ "machine": "b", "uart": "USART2" }
]
}
],
"until": { "machine": "a", "marker": "A DONE" },
"maxMs": 200
}ct scene run uart-two-nucleo.scene.json --junit scene.xml
ct scene run uart-two-nucleo.scene.json --cloud # run it on the hosted serviceThe report lists every machine and every bus record. A run of the same scene gives the same report every time.
Scene fields
| Field | Meaning |
|---|---|
machines[] | name, board (built-in id, board file or MCU twin bundle) and firmware |
uartLinks[] | name, baudBps, ends (machine, uart) |
spiLinks[] | name, clockHz, a master and its slaves (machine, spi) |
i2cBuses[] | name, a controller and its targets (machine, i2c) |
canBuses[] | name, nodes (machine, controller such as CAN1 or FDCAN1), timing (nominalBps, and dataBps for CAN FD) |
rs485Buses[] | name, baudBps, frameBits (default 8N1), nodes (machine, uart, optional driverEnable and receiverEnable pins) |
media[] | name, kind (ethernet), nodes (machine, peripheral such as ETH) |
circuits[] | Analog circuits solved beside a machine; their node voltages reach the machine’s converters |
until, maxMs, quantumNs | Stop when a machine prints a marker; the virtual time budget; the lock-step quantum |
registers[] | Peripheral registers to report after the run, read without side effects |