Skip to content

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 IBusMasterSystemBus 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); IMmio itself is a plain capability interface like ICanBus, not an IEntity (immio.hpp:24-26).
  • Bus media are passive: no MMIO, no IPeripheral lifecycle; nodes hang off them via connection edges (peripheral_spec.hpp:102-107). A controller peripheral re-exposes its joined medium through a provider capability (ICanBusProvider at ican.hpp:211, ISpiBusProvider at ispi.hpp:222, IMilStdBusProvider at imilstd1553.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-owned core::CpuState and the shared ir::IArchitecture; Leon3 / Leon4 differ only in model identity (cpu.hpp:104,110); a non-SPARC frontend gets a GenericCpu exposing ICpu only (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_entity namespace: they are reached positionally via Emulator::cpu(idx) (src/runtime/include/tero/runtime/emulator.hpp:213), then navigated with get_interface<ICpu>() or get_interface<core::ISparc>().
  • Emulator implements IEntityRegistry and delegates to Soc::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, the Emulator facade).
  • Peripheral system — the IPeripheral lifecycle, 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.