Entity object model¶
Everything modelled in a Tero machine — peripherals, host-facing plugins, comms bus media, CPUs, the memory fabric, the AMBA topology overlay — is a tero::IEntity; capabilities are reached by typed query (get_interface<T>()), and wiring is declared as data in EmulatorConfig and resolved by one generic pass at assembly time.
| Concept | Mechanism | Where |
|---|---|---|
| Identity | IEntity::name() |
src/interfaces/include/tero/ientity.hpp:29 |
| Capability query | get_interface<T>() / interface_cast<T>() |
src/interfaces/include/tero/ientity.hpp:38,60 |
| Name resolution | IEntityRegistry::find_entity |
src/interfaces/include/tero/ientity_registry.hpp:36 |
| Declarative wiring | runtime::Connection{from_slot, peer, peer_slot} |
src/runtime/include/tero/runtime/peripheral_spec.hpp:38 |
| Generic connect pass | Soc::connect_peripheral_ports |
src/runtime/src/soc.cpp:416 |
The model is inspired by TEMU's object system and SMP2 (ECSS-E-ST-40) but stays Tero-idiomatic: typed instead of string-keyed, config-by-struct instead of reflective properties, static inheritance instead of runtime class registration. IEntity maps conceptually to Smp::IModel; get_interface<T>() maps to Smp::IComponent::GetInterface (ientity.hpp:17-18).
The IEntity contract¶
The base interface has exactly two responsibilities: stable identity and capability navigation (src/interfaces/include/tero/ientity.hpp:24-53).
class IEntity {
public:
virtual ~IEntity() = default;
/// Stable instance identity (for naming, logging, connection resolution).
[[nodiscard]] virtual std::string_view name() const = 0;
/// Navigate to a capability interface this entity exposes, or `nullptr`.
template <class T>
[[nodiscard]] T* get_interface() noexcept {
return dynamic_cast<T*>(this);
}
};
/// Null-safe pointer form.
template <class T>
[[nodiscard]] T* interface_cast(IEntity* e) noexcept {
return e != nullptr ? e->template get_interface<T>() : nullptr;
}
v1 resolves capabilities by direct inheritance (dynamic_cast); composed interfaces are a later extension (ientity.hpp:36). The interface is the connection point — there is no separate "port object" indirection for capabilities (ientity.hpp:33-34).
Capability queries¶
Two spellings exist for the same query.
| Form | Signature | Use when |
|---|---|---|
| Member | entity.get_interface<ICanBus>() |
You hold a non-null reference or pointer you have already checked. |
| Free function | interface_cast<ICanBus>(maybe_null) |
The pointer may be null (e.g. the result of find_entity), or the call site is inside an entity's own methods. |
Both exist for a concrete C++ reason: inside an entity's member functions, the get_interface member shadows any same-named free function, so an unqualified call on a peer pointer would resolve to the wrong name. interface_cast is named distinctly so it is unambiguous from inside an entity's own methods (ientity.hpp:55-58). The free function also adds the null check, which makes it the natural partner of find_entity — see Decision 61 (decisions.md).
// Typical consumer pattern (a sniffer plugin, soc.cpp connect pass, ...):
IEntity* e = registry->find_entity("can0");
if (auto* bus = interface_cast<ICanBus>(e)) { /* use the capability */ }
Identity¶
| Rule | Source |
|---|---|
name() is the registry identity: find_entity(x)->name() == x for every entity kind. |
src/interfaces/include/tero/iperipheral.hpp:31-35 |
For peripherals, name() is final: it returns the runtime-injected PeripheralSpec::instance_name, falling back to device_class(). |
iperipheral.hpp:36-38 |
The runtime injects the instance name via set_instance_name() during Soc assembly, immediately after the factory runs. |
src/runtime/src/soc.cpp:363-367 |
device_class() is the IP-core kind ("apbuart"), shared by every instance of that model. |
iperipheral.hpp:40-42 |
The fallback covers direct construction outside an Emulator (a unit test, a post-initialize() add_peripheral). |
iperipheral.hpp:33-35, soc.cpp:607-611 |
Instance names live in one flat namespace across peripherals, plugins, and bus media; uniqueness is enforced by validate_emulator_config (soc.cpp:624-627). Three names are reserved by the runtime: "system_bus" (the memory fabric, soc.cpp:641-645) and "ahb" / "apb" (the AMBA overlay segments, soc.cpp:633-639). CPU entities name themselves "<model>_<index>", e.g. "leon4_2" (src/runtime/src/cpu.cpp:16,101-105). See Decision 70 (decisions.md).
Entity kinds and their capabilities¶
| Kind | Defining capability | Owner | Enters the graph via |
|---|---|---|---|
| Peripheral | IMmio — it occupies an address window (iperipheral.hpp:24-28) |
Soc::peripherals_ |
PeripheralSpec in EmulatorConfig::peripherals |
| Host-facing plugin | None — exposing no IMmio is what makes it not a peripheral; its capabilities are the observer/connector interfaces it exposes (iplugin.hpp:58-60) |
Soc::plugins_ |
PluginSpec in EmulatorConfig::plugins (src/runtime/include/tero/runtime/plugin_spec.hpp:25) |
| Comms bus medium | ICanBus / ISpiBus / IMilStdBus — each derives IEntity directly (ican.hpp:155, ispi.hpp:131, imilstd1553.hpp:199) |
Soc::buses_ |
BusSpec in EmulatorConfig::buses (peripheral_spec.hpp:108) |
| CPU | ICpu (icpu.hpp:24); a SPARC core additionally exposes core::ISparc (src/core/include/tero/core/isparc.hpp:22) and IGdbRegisters (igdb_registers.hpp:35) |
ExecutionEngine::cpu_entities_ |
Built by the engine at initialize() (src/runtime/src/execution_engine.cpp:172-185) |
| Memory fabric | IBusMaster — SystemBus is IBusMaster + IEntity (src/bus/include/tero/bus/system_bus.hpp:48) |
Soc::bus_ (member) |
Built by the Soc |
| AMBA overlay segment | Topology queries: slaves(), masters(), layer() — AmbaBus : IEntity (src/bus/include/tero/bus/amba_bus.hpp:35) |
Soc::ahb_ / Soc::apb_ (members) |
Built by the Soc |
Notes on the table:
- A peripheral is
IPeripheral : IEntity, IMmio(iperipheral.hpp:29);IMmioitself is a plain capability interface likeICanBus, not anIEntity(immio.hpp:24-26). - Bus media are passive: no MMIO, no
IPeripherallifecycle; nodes hang off them via connection edges (peripheral_spec.hpp:102-107). A controller peripheral re-exposes its joined medium through a provider capability (ICanBusProvideratican.hpp:211,ISpiBusProvideratispi.hpp:222,IMilStdBusProvideratimilstd1553.hpp:263), so an observer reaches the medium through the peripheral that owns the link, never through a protocol registry. - CPU entities are thin facades:
SparcCpu(src/runtime/include/tero/runtime/cpu.hpp:45) references the engine-ownedcore::CpuStateand the sharedir::IArchitecture;Leon3/Leon4differ only in model identity (cpu.hpp:104,110); a non-SPARC frontend gets aGenericCpuexposingICpuonly (cpu.hpp:128). The hot loop never consults these entities — they are a pure inspection/control surface (execution_engine.cpp:165-171). - CPUs are not in the
find_entitynamespace: they are reached positionally viaEmulator::cpu(idx)(src/runtime/include/tero/runtime/emulator.hpp:213), then navigated withget_interface<ICpu>()orget_interface<core::ISparc>(). EmulatorimplementsIEntityRegistryand delegates toSoc::find_entity(emulator.hpp:237,src/runtime/src/emulator.cpp:188-192), which resolves peripherals first, then plugins, then bus media, then the reserved fabric names (soc.cpp:607-649).
Connection shapes¶
Four wiring mechanisms exist; each is the typed shorthand for one physical reality.
MMIO window¶
The entity exposes IMmio (immio.hpp:27): mmio_range() declares the physical window, mmio_read/mmio_write react to ½/4-byte accesses, and the optional memory_region() capability serves bulk spans (debugger reads, DMA) (immio.hpp:52-54). The runtime's bus routes every physical access to the entity whose mmio_range() contains the address, reaching it through the capability — it never needs the concrete type (immio.hpp:20-22). Mapping happens at assembly (bus_.map_mmio, soc.cpp:189 for spec-built peripherals, soc.cpp:594 for add_peripheral); no Connection edge is involved.
IRQ lines¶
PeripheralSpec::irqs is the IRQ connection shape: a typed shorthand for an edge to the interrupt-controller entity (peripheral_spec.hpp:73-81). Each entry lowers to an IrqBridge with mask 1u << irq, allocated before the factory runs and handed to the peripheral as PeripheralContext::irqs (soc.cpp:303-336). This is deliberately not a post-attach Connection edge: peripherals need their IRQ source during attach(), so the shorthand resolves earlier than the connect pass — see Decision 64 (decisions.md).
Shared-bus membership¶
A controller joins a bus medium through the IConnectable capability (src/interfaces/include/tero/iconnectable.hpp:24):
[[nodiscard]] virtual Result<void> connect(std::string_view from_slot,
IEntity& peer,
std::string_view peer_slot) = 0;
The consuming entity owns the binding: its connect implementation does the typed interface_cast<IFooBus>(&peer) and wires itself through its existing internals (iconnectable.hpp:19-23). Slot names are the entity's own typed constants — role and addressing live in the identity of the slot, not in separate config fields (iconnectable.hpp:32-35).
| Implementer | Slot constants | Header |
|---|---|---|
OcCan |
SlotBus = "can" |
src/peripherals/include/tero/peripherals/occan.hpp:147 |
GrCan |
SlotBus = "can" |
src/peripherals/include/tero/peripherals/grcan.hpp:154 |
SpiCtrl |
SlotBus = "spi" (chip-select as peer_slot) |
src/peripherals/include/tero/peripherals/spictrl.hpp:101 |
Gr1553b |
SlotBc = "mil_bc", SlotRt = "mil_rt" |
src/peripherals/include/tero/peripherals/gr1553b.hpp:131-133 |
B1553Brm |
SlotBc = "mil_bc", SlotRt = "mil_rt" |
src/peripherals/include/tero/peripherals/b1553brm.hpp:189-191 |
Protocol invariants live in the bus entity, not the runtime: the one-BC-per-bus rule is enforced by IMilStdBus::set_controller (soc.cpp:421-422).
Point-to-point ports¶
A peripheral exposes named ports via IPeripheral::find_port(name) (iperipheral.hpp:71); a port is typed by IPort::type_id() (iport.hpp:25-32). Two port types exist: ISignalPort (one-bit level with edge callbacks, iport.hpp:47) and ISpaceWirePort (symmetric link endpoint, src/interfaces/include/tero/ispacewire.hpp:127). Three peripherals implement find_port: GrGpio (per-pin signals), GrSpw2, and SpwRouter (src/peripherals/include/tero/peripherals/{grgpio,grspw2,spwrouter}.hpp).
The one generic resolution pass¶
Soc::connect_peripheral_ports (soc.cpp:416) resolves every declared edge {from_slot, peer, peer_slot} with one rule — the consumer's own slot picks the shape:
IEntity* peer = find_entity(cn.peer); // flat namespace: peripheral or bus
IPort* my_port = me->find_port(cn.from_slot);
if (my_port == nullptr) {
// Shared-bus slot: the consumer owns the typed binding.
auto* connectable = me->get_interface<IConnectable>();
...
connectable->connect(cn.from_slot, *peer, cn.peer_slot);
} else {
// Port slot: resolve the peer's named port and bind it.
// SpaceWire ports bind both ends (symmetric link); signal ports
// chain peer.on_change -> my.set.
}
(soc.cpp:443-523.) SpaceWire links bind both directions, so declaring the edge on either side wires the link (soc.cpp:493-508). A second pass calls the optional IPeripheral::connect_ports(IPortResolver&) hook for wiring richer than "peer signal drives my signal" (soc.cpp:526-531, iperipheral.hpp:76-83). The runtime knows no protocol: it knows find_port, IConnectable, and the two port types — nothing about CAN, SPI, or 1553.
Rejected alternatives¶
| Alternative | Rejected because |
|---|---|
| TEMU-style string-keyed reflective properties | Config-by-struct is a frozen principle; capabilities resolve by typed query, introspection stays read-only via IPublisher. Decision 62 (decisions.md). |
IPort as the universal wiring mechanism |
A port is one capability (a signal pin, a SpaceWire endpoint), not the mechanism; shared-bus joins and IRQ lines have different shapes and resolve through IConnectable and the spec shorthand. Decision 63 (decisions.md). |
One flat owning vector<IEntity> in the Soc |
The three typed lists encode real lifecycle roles: peripherals_ ticks every round, plugins_ has start/stop and must destruct before the buses it observes, buses_ is passive; declaration order makes the destruction guarantee a compile-time invariant. Decision 72 (decisions.md). |
Naming the base IObject / IModel |
Collides with Smp::IObject / Smp::IModel; the separate-repo SMP2 wrapper bridges between the Tero type and the SMP2 type, and identical names would make the adapter ambiguous (ientity.hpp:14-18). Decision 60 (decisions.md). |
Pointers¶
- Runtime decomposition — who owns the entity graph (
Soc,ExecutionEngine,DebugServer, theEmulatorfacade). - Peripheral system — the
IPeripherallifecycle,PeripheralSpec, and the assembly order. - Plugin system — host-facing instruments and the seams they consume.
- Interfaces module — the full header-by-header reference for
src/interfaces.