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

LinkScene sectionNotes
UARTuartLinksEvery UART model
SPI and I2CspiLinks, i2cBusesThe 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 FDcanBusesThrough transceivers in a system
RS-485 (UARTs through half-duplex transceivers)rs485BusesThrough transceivers in a system
Ethernetmedia with kind: ethernetSTM32 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.

can-two-nucleo.system.json
{
  "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
}
terminal
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/2

ct 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:

uart-two-nucleo.scene.json
{
  "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
}
terminal
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 service

The report lists every machine and every bus record. A run of the same scene gives the same report every time.

Scene fields

FieldMeaning
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, quantumNsStop 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