Skip to content

tero_compose

Board composition above the runtime: a typed object graph (Machine) that lowers to a runtime::EmulatorConfig, the GR712RC/GR740 kits, the .tero script front-end, and the dlopen loader for component libraries. The module owns assembly only — it produces a config and stops. Execution, the entity graph at runtime, and peripheral lifecycle belong to the Soc inside tero_runtime (runtime decomposition).

# src/compose/CMakeLists.txt
target_link_libraries(tero_compose
    PUBLIC  tero::runtime tero::interfaces
    PRIVATE tero::peripherals tero::defaults
            ${CMAKE_DL_LIBS}
            $<BUILD_INTERFACE:tero::warnings>
)

Responsibility

Turn a description of a board — C++ verb calls, a .tero script, or a kit — into a validated EmulatorConfig, including the generative AMBA Plug&Play placement. Resolve component type names through a registry that shared objects can extend at runtime.

Sits above the runtime

tero_compose depends on tero::runtime (it builds the config the runtime consumes) and instantiates concrete peripherals from tero::peripherals inside the built-in factories. Nothing below the runtime depends on it; the core never sees it.

Source layout

src/compose/
├── include/tero/compose/
│   ├── machine.hpp            # Machine + Object: imperative graph builder
│   ├── kits.hpp               # gr712rc_/gr740_ machine() and _config() kits
│   ├── script.hpp             # .tero parser/loader (parse_/load_machine_script)
│   ├── param_bag.hpp          # ParamBag string-property store + parse_u32
│   ├── component_registry.hpp # ComponentRegistry, ComponentType, ComponentKind
│   ├── component_plugin.hpp   # ABI contract + TERO_COMPONENT_LIBRARY macro
│   └── plugin_loader.hpp      # ComponentLibrary RAII + load_component_library
└── src/
    ├── machine.cpp            # Machine::build() — graph → EmulatorConfig
    ├── kits.cpp               # the two silicon kits as Machine compositions
    ├── script.cpp             # line/verb tokenizer driving the Machine verbs
    ├── param_bag.cpp          # typed getters, no-defaults enforcement
    ├── component_registry.cpp # builtin_registry(): every in-tree type
    └── plugin_loader.cpp      # dlopen / handshake / register

Key types

Type Header Role Lifetime / owner
Machine machine.hpp:52 Imperative builder: set / create / write / map / connect / set_time_source, then build() Caller-owned, move-only. Owns the object records and any Console chardev — must outlive an Emulator built from its config (machine.hpp:46-50)
Object machine.hpp:32 Opaque handle returned by Machine::create, passed to the other verbs Non-owning index into the Machine
ParamBag param_bag.hpp:22 String-property store per object; typed getters parse on read; require_u32 enforces the no-defaults rule Owned by the Machine's object records
ComponentRegistry component_registry.hpp:73 Maps type names ("ApbUart", "Leon3", …) to ComponentType Caller-owned; must outlive every Machine that references it
ComponentType component_registry.hpp:50 One registered type: ComponentKind, AMBA layer, PnP identity, MakePeripheral factory Value inside the registry
ComponentKind component_registry.hpp:22 Role during assembly: Cpu, MemorySpace, Ram, Prom, Peripheral, Console, AhbCtrl, ApbCtrl, CanBus, SpiBus, MilStdBus
ComponentLibrary plugin_loader.hpp:21 Move-only RAII over a dlopen handle Caller-owned; code stays resident regardless (RTLD_NODELETE)

Machine::build() — lowering stages

build() (src/compose/src/machine.cpp:229) folds the accumulated graph into a fresh EmulatorConfig in fixed stages; the first error wins and nothing is defaulted from the bare struct:

  1. Cores / family / clock / pacing — CPU objects set num_cores and soc_family; the clock scalar is mandatory (machine.cpp:267-274); cores overrides the CPU count; pacing accepts turbo / realtime.
  2. AMBA controllers → PnP bridges — exactly one AhbCtrl with a pnp scratch base is required (machine.cpp:311-313); each ApbCtrl contributes a PnpBridge and an AHB-slave record.
  3. Memories and register peripherals — a mapped Ram is required (machine.cpp:546); Prom is optional; each Peripheral runs its MakePeripheral factory at its mapped base and becomes a PeripheralSpec. Interrupt controllers are emitted first in cfg.peripherals; PnP slots follow creation order with pnp_slot pinning.
  4. Comms-bus mediaCanBus / SpiBus / MilStdBus objects lower to cfg.buses entries.
  5. Connections — a console ↔ UART edge binds chardev_index; a controller ↔ bus edge becomes a Connection on the controller's spec; the CPU↔memory and CPU↔IRQ edges are structural declarations and change nothing (machine.cpp:589-591).
  6. Placement + validationcfg.pnp_placement is set and validate_emulator_config runs before the config is returned (machine.cpp:659-663).

Machine::dump() serialises the graph back to .tero text; parse(dump(m)) reproduces the same graph (machine.hpp:107-112).

Kits

gr712rc_machine() / gr740_machine() (src/compose/src/kits.cpp) are the silicon boards expressed through the same verbs a user board uses; gr712rc_config() / gr740_config() are machine.build() plus the historic chardev contract (six / two caller-populated character_devices slots bound to apbuart{N}, kits.cpp:26-46,369-389). The old hand-written runtime recipes were deleted; the kits are their replacement, byte-identical at the PnP-table level (the identity gate is tests/integration/test_compose_kits.cpp) — Decision 71 (../architecture/decisions.md). Instance names live in one flat namespace; duplicates are rejected at create — Decision 70 (../architecture/decisions.md).

GR712RC kit (gr712rc_machine, kits.cpp:49-200)

Leon3, clock 80 MHz (kits.cpp:56), 16 MiB RAM @ 0x40000000, 32 MiB PROM @ 0x0, two APB bridges (apb0 @ 0x80000000, apb1 @ 0x80100000), buses can_bus_a, can_bus_b, spi0, mil1553_0.

Instance Type MMIO base IRQ
irqmp IrqMP 0x80000200 — (controller)
memctrl MemCtrl 0x80000000
gptimer0 GPTimer 0x80000300 8
apbuart0 ApbUart 0x80000100 2
apbuart1apbuart5 ApbUart 0x801001000x80100500 17–21
grgpio0, grgpio1 GrGpio 0x80000900, 0x80000A00 1–15 (per pin)
occan0, occan1 OcCan 0xFFF30000, 0xFFF30100 5, 6
canmux CanMux 0x80000500
grspw0grspw5 GrSpw2 0x80100800 + n·0x100 22–27
spictrl SpiCtrl 0x80000400 13
b1553brm B1553Brm 0xFFF00000 14
ahbstat AhbStat 0x80000F00 1
grclkgate GrClkGate 0x80000D00
grgpreg GrGpReg 0x80000600
grtimer GrTimer 0x80100600 7

GR740 kit (gr740_machine, kits.cpp:202-367)

Leon4, clock 250 MHz (kits.cpp:208), 256 MiB RAM @ 0x0, no PROM, one APB bridge (apb0 @ 0xFF900000), buses can_bus0, spi0, mil1553_0.

Instance Type MMIO base IRQ
irqamp IrqAMP 0xFF904000 — (controller)
memctrl MemCtrl 0xFFE00000
gptimer0 GPTimer 0xFF908000 1–5, 15 (watchdog/NMI)
gptimer1gptimer4 GPTimer 0xFF9090000xFF90C000 6–9
apbuart0, apbuart1 ApbUart 0xFF900000, 0xFF901000 29, 30
grgpio0, grgpio1 GrGpio 0xFF902000, 0xFFA08000 16–19 (routed)
grcan0, grcan1 GrCan 0xFFA01000, 0xFFA02000 16, 17
spwrouter SpwRouter 0xFF880000
spictrl SpiCtrl 0xFFA03000 19
gr1553b Gr1553b 0xFFA05000 26
ahbstat0, ahbstat1 AhbStat 0xFFA06000, 0xFFA07000 27
memscrub MemScrub 0xFFE01000 28
grclkgate GrClkGate 0xFFA04000
grgpreg GrGpReg 0xFFA09000
tempsensor TempSensor 0xFFA0A000 27
grgprbank GrGprBank 0xFFA0B000
l4stat L4Stat 0xFFA0D000
grspwtdp GrSpwTdp 0xFFA0C000 31
grpci2 GrPci2 0xFFA00000 11
l2cache L2Cache 0xF0000000 28
griommu Griommu 0xFF840000 31

The .tero script front-end

parse_machine_script / load_machine_script (script.hpp) drive the same Machine verbs from a flat text format — one statement per line, # comments, six verbs (set, create, write, map, connect, time_source; src/compose/src/script.cpp:97-167). The grammar and a complete example live in the user guide: Assembling a machine.

Component libraries (dlopen)

A component library is a shared object that adds types to a ComponentRegistry at load time, so custom devices become instantiable from scripts and the Machine API without rebuilding Tero. Authoring is covered in Custom components; the contract is:

Element Definition
tero_component_abi_version C symbol returning the ABI version the library was built against (component_plugin.hpp:52)
tero_register_components C symbol called once after the handshake; receives the ComponentRegistry* (component_plugin.hpp:54)
TERO_COMPONENT_LIBRARY(fn) Macro that defines both symbols from one void (ComponentRegistry&) function (component_plugin.hpp:70)
ComponentAbiVersion = 2 Current contract version (component_plugin.hpp:48). v2 is the IPeripheral identity split: name() is final (instance name, runtime-injected) and authors implement device_class(). The loader rejects any other reported version (plugin_loader.cpp:88-96), so v1 libraries fail at load with a diagnostic.

The loader (load_component_library, plugin_loader.cpp:51) opens the library with RTLD_NOW | RTLD_LOCAL | RTLD_NODELETE (plugin_loader.cpp:69). RTLD_NODELETE keeps the code mapped after the ComponentLibrary handle closes: the registered factories (std::function manager code, peripheral vtables) outlive any destruction-order guarantee a caller could uphold, so component code stays resident for the process lifetime by design (plugin_loader.cpp:61-68). Failures return ErrorCode::PluginLoadError with a human-readable diagnostic; non-POSIX hosts report the same error.

PnP table derivation

The composed graph carries its own AMBA Plug&Play placement instead of a hardcoded per-SoC layout. Machine::build() derives a runtime::PnpPlacement — bridge records from the ApbCtrl objects, one slave record per published device (BAR from the map base/size, IRQ from the irq property, slot from creation order or a pnp_slot pin, identity from the live device's IAmbaPnp) — and stores it in EmulatorConfig::pnp_placement. The kits pin the historic slots (pnp_slot writes throughout kits.cpp) and suppress devices the historic table did not publish (pnp_publish=0). At initialize() the runtime's generative builder (build_writes_generative, src/runtime/src/pnp_table.cpp:103) turns the placement into the GRLIB PnP table bytes RTEMS scans; a config without pnp_placement takes the historic hardcoded path, byte-identical.