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:
- Cores / family / clock / pacing — CPU objects set
num_coresandsoc_family; theclockscalar is mandatory (machine.cpp:267-274);coresoverrides the CPU count;pacingacceptsturbo/realtime. - AMBA controllers → PnP bridges — exactly one
AhbCtrlwith apnpscratch base is required (machine.cpp:311-313); eachApbCtrlcontributes aPnpBridgeand an AHB-slave record. - Memories and register peripherals — a mapped
Ramis required (machine.cpp:546);Promis optional; eachPeripheralruns itsMakePeripheralfactory at its mapped base and becomes aPeripheralSpec. Interrupt controllers are emitted first incfg.peripherals; PnP slots follow creation order withpnp_slotpinning. - Comms-bus media —
CanBus/SpiBus/MilStdBusobjects lower tocfg.busesentries. - Connections — a console ↔ UART edge binds
chardev_index; a controller ↔ bus edge becomes aConnectionon the controller's spec; the CPU↔memory and CPU↔IRQ edges are structural declarations and change nothing (machine.cpp:589-591). - Placement + validation —
cfg.pnp_placementis set andvalidate_emulator_configruns 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)¶
2× 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 |
apbuart1–apbuart5 |
ApbUart |
0x80100100–0x80100500 |
17–21 |
grgpio0, grgpio1 |
GrGpio |
0x80000900, 0x80000A00 |
1–15 (per pin) |
occan0, occan1 |
OcCan |
0xFFF30000, 0xFFF30100 |
5, 6 |
canmux |
CanMux |
0x80000500 |
— |
grspw0–grspw5 |
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)¶
4× 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) |
gptimer1–gptimer4 |
GPTimer |
0xFF909000–0xFF90C000 |
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.