tero_interfaces¶
A header-only vocabulary library that defines the strong types and
the injected I* contracts shared by every other module. It is the root
of the dependency graph: everything links it, and it depends on nothing
but tl::expected.
# src/interfaces/CMakeLists.txt
add_library(tero_interfaces INTERFACE)
target_include_directories(tero_interfaces INTERFACE include)
target_link_libraries(tero_interfaces INTERFACE tl::expected)
target_compile_features(tero_interfaces INTERFACE cxx_std_20)
INTERFACE means it produces no .a — only #include paths and the
transitive tl::expected dependency. Consume it with:
Responsibility
Define, in one place: (1) the strong types (PhysAddr, VirtAddr,
CoreId, SimTimeNs, IrqLine, AccessSize), (2) Result<T> and
ErrorCode, (3) the entity-model base (IEntity and the capability
interfaces — see the
Entity object model), (4) every
interface the core is decoupled from its environment by, and (5) a
small set of value types (AddressRange, PeripheralContext,
BreakpointSet, GatedMutex, CoreControl). No logic, no global
state, no I/O.
Header inventory¶
All under src/interfaces/include/tero/.
| Header | Defines | Role |
|---|---|---|
types.hpp |
PhysAddr, VirtAddr, CoreId, SimTimeNs, IrqLine, AccessSize, ErrorCode, Result<T>, make_error(), to_underlying(), bytes() |
The vocabulary every module speaks |
address_range.hpp |
AddressRange |
Half-open [base, base+size) for MMIO dispatch |
entity_context.hpp |
EntityContext |
Services injected into a host-facing entity at attach() time (entity registry, observer, scheduler, logger, time source, core count) |
ientity.hpp |
IEntity, interface_cast<T>() |
Base of everything modelled; capability query via get_interface<T>(); host-facing lifecycle (attach(EntityContext), start(), stop()) with no-op defaults — see Entity object model |
ientity_registry.hpp |
IEntityRegistry |
By-name entity lookup (find_entity); implemented by Emulator (delegating to Soc) |
iconnectable.hpp |
IConnectable |
Named-slot wiring capability (connect(from_slot, peer, peer_slot)); consumed by Soc::connect_peripheral_ports |
immio.hpp |
IMmio |
The MMIO capability (mmio_range/mmio_read/mmio_write/memory_region); what makes an entity a peripheral; the bus dispatches by it |
icpu.hpp |
ICpu |
Arch-neutral CPU inspection/control capability; exposed by the CPU entities (SparcCpu, GenericCpu) |
igdb_registers.hpp |
IGdbRegisters, GdbFaultClass |
CPU-entity capability: the architecture's GDB g-packet register block; consumed by the GDB stub |
core_control.hpp |
CoreControl |
Arch-neutral per-core run latches (power-down, error mode); owned by the ExecutionEngine |
imemory_access.hpp |
IMemoryAccess |
Uniform memory-slave interface (offset-relative read/write); slaves of bus::MemorySpace |
iamba_pnp.hpp |
IAmbaPnp, PnpIdentity, AmbaLayer |
AMBA plug-and-play identity + layer capability; consumed by the PnP table builder and the AMBA topology overlay |
ilogger.hpp |
ILogger, LogLevel |
Logging sink (the only logging surface) |
itime_source.hpp |
ITimeSource |
Pull-only "now" in simulated time |
ipublisher.hpp |
IPublisher |
SMP2-style observable-field registration |
ifault_injector.hpp |
IFaultInjector |
Level-2 fault-tolerance hook surface |
iemulator_observer.hpp |
IEmulatorObserver |
Diagnostic hooks (IRQ/trap/attach/per-instruction) |
icharacter_device.hpp |
ICharacterDevice |
UART byte TX/RX path |
ientry_point.hpp |
IEntryPoint |
Named parameterless callable (Smp::IEntryPoint) |
iperipheral.hpp |
IPeripheral |
The unified device contract (IEntity + IMmio) |
peripheral_context.hpp |
PeripheralContext |
Services injected into a peripheral via attach() |
ibus_master.hpp |
IBusMaster |
DMA-side bus access |
iinterrupt_source.hpp |
IInterruptSource |
A raise()/lower() IRQ line |
iinterrupt_controller.hpp |
IInterruptController |
Common IRQMP/IRQAMP control surface |
iinterrupt_bus.hpp |
IInterruptBus, IInterruptBusObserver |
Observe interrupt-line assertions on the bus; implemented by IrqMP/IrqAMP, consumed by the GRTIMER time-latch |
iwatchdog_sink.hpp |
IWatchdogSink |
On-chip watchdog input into an interrupt controller (GR740 GPTIMER timer 4 → IrqAMP) |
ischeduler.hpp |
IScheduler |
schedule_event(when, IEvent*) |
ievent.hpp |
IEvent |
A schedulable execute() action |
icpu_bus.hpp |
ICpuBus |
CPU-side virtual-address data bus (+ atomics) |
iport.hpp |
IPort, ISignalPort, IPortResolver |
Typed peripheral-to-peripheral wiring |
ican.hpp |
ICanBus, ICanNode, CanFrame, ICanBusObserver, ICanBusProvider |
Per-protocol CAN 2.0B seam; OCCAN/GRCAN nodes join a CanBus entity |
ispi.hpp |
ISpiBus, ISpiDevice, ISpiMaster, SpiWord, ISpiBusObserver |
Per-protocol SPI seam (master-driven, full-duplex); SPICTRL drives an SpiBus entity |
imilstd1553.hpp |
IMilStdBus, IMilStdTerminal, IMilStdController, command/status/data structs |
Per-protocol MIL-STD-1553B seam; GR1553B (GR740) and B1553BRM (GR712RC) terminals |
ispacewire.hpp |
ISpaceWirePort, SpwPacket, SpwTimeCode, ISpaceWireObserver |
Point-to-point SpaceWire link port (ECSS-E-ST-50-12C); GRSPW2 and the SpW router |
imemory_region.hpp |
IMemoryRegion |
Side-effect-free bulk memory capability |
breakpoint_set.hpp |
BreakpointSet |
Shared software-breakpoint registry |
gated_mutex.hpp |
GatedMutex |
Runtime-gated std::mutex for MultiThread |
Strong types and Result<T>¶
The strong types in types.hpp are enum class aliases over fixed-width
unsigned integers — zero-overhead, not implicitly convertible to each
other or to raw int:
enum class PhysAddr : std::uint32_t {}; // types.hpp:23
enum class VirtAddr : std::uint32_t {}; // types.hpp:26
enum class CoreId : std::uint8_t {}; // types.hpp:29
enum class SimTimeNs : std::uint64_t {}; // types.hpp:32
enum class IrqLine : std::uint8_t {}; // types.hpp:35
enum class AccessSize : std::uint8_t { Byte = 1, Half = 2, Word = 4 }; // types.hpp:38
Operators are provided only where they make physical sense (types.hpp):
| Operation | Result | Rationale |
|---|---|---|
PhysAddr + uint32_t / VirtAddr + uint32_t |
same address type | offset arithmetic |
PhysAddr - PhysAddr / VirtAddr - VirtAddr |
uint32_t |
distance |
SimTimeNs + SimTimeNs, SimTimeNs - SimTimeNs |
SimTimeNs |
time math |
CoreId + CoreId, IrqLine + IrqLine |
not provided | an identifier is not a number |
Use to_underlying(x) to extract the raw integer (one overload per type)
rather than static_cast. bytes(AccessSize) returns ½/4.
template <typename T>
using Result = tl::expected<T, ErrorCode>; // types.hpp:67
enum class ErrorCode : std::uint8_t { // types.hpp:49
Ok = 0, BusError, InvalidAddress, AlignmentError,
TrapGenerated, InvalidConfig, ElfLoadError,
IoError, ///< socket / file / OS API failure (outside the sim bus)
JitError, ///< LLVM JIT compile / symbol-lookup / ORCv2 error
};
[[nodiscard]] inline tl::unexpected<ErrorCode> make_error(ErrorCode) noexcept; // types.hpp:71
Why tl::expected
tl::expected stands in for std::expected (C++23) so the project
can stay on C++20; semantics match. Moving to C++23 is a one-line
using change. Result<T> is the canonical return type at every
public API boundary — exceptions never cross the library edge.
The injected interfaces¶
These are the seams that decouple tero_core (and the peripherals) from
their environment. In standalone mode the tero_defaults
implementations are used; an external (SMP2) wrapper substitutes adapters
that delegate to the host environment.
Services consumed by the core / runtime¶
| Interface | Key methods | Default | SMP2 mapping |
|---|---|---|---|
ILogger |
log(LogLevel, category, message) (ilogger.hpp:30) |
StdoutLogger |
Smp::Services::ILogger |
ITimeSource |
now() → SimTimeNs (itime_source.hpp:17) |
internal counter | Smp::Services::ITimeKeeper |
IPublisher |
publish_field(name, T*) for T ∈ {u8,u16,u32,u64,bool} (ipublisher.hpp:19-23) |
DebugPublisher |
Smp::IPublication |
IFaultInjector |
should_corrupt_read(addr, size), corrupt(span<byte>) (ifault_injector.hpp:20-23) |
NullFaultInjector |
custom SMP2 ops |
ICharacterDevice |
write_char(c), read_char() → optional<char>, has_input() (icharacter_device.hpp:17-22) |
StdoutCharDevice |
SMP2 output field / linked model |
IEntryPoint |
name(), execute() (ientry_point.hpp:17-20) |
direct calls | Smp::IEntryPoint |
The ILogger::log signature has three arguments
log(LogLevel level, std::string_view category, std::string_view
message) — the category (a short free-form tag: "cpu",
"irqmp", "bus") sits between level and message. Implementations
must not retain either string view past the call.
Bus / interrupt / scheduling¶
| Interface | Key methods | Notes |
|---|---|---|
ICpuBus |
typed read_u{8,16,32} / write_u{8,16,32} over VirtAddr; atomic atomic_swap_u32, atomic_cas_u32, atomic_ldstub |
The CPU's data path. See "Atomics" below. |
IBusMaster |
dma_read(PhysAddr, span<byte>), dma_write(PhysAddr, span<const byte>) (ibus_master.hpp:18-21) |
The DMA path; SystemBus implements it |
IInterruptSource |
raise(), lower() (iinterrupt_source.hpp:13-16) |
A single level-sensitive IRQ line |
IInterruptController |
external_assert/clear(bits), pending_mask(cpu), acknowledge(cpu, bit), raw_pending(), consume_core_release_wake(cpu), set_thread_safe(bool) |
The shared IRQMP/IRQAMP surface; see below |
IScheduler |
schedule_event(SimTimeNs when, IEvent*) (ischeduler.hpp:19) |
Time-ordered queue; peripherals never spin on the clock |
IEvent |
execute() (ievent.hpp:13) |
A schedulable action; may re-schedule itself |
IInterruptController (iinterrupt_controller.hpp) is the SoC-agnostic
control surface implemented by both peripherals::IrqMP (GR712RC) and
peripherals::IrqAMP (GR740). It exposes only what the two GRLIB IP cores
share; GR740-specific extended-IRQ delivery is wrapped internally.
consume_core_release_wake(cpu) (iinterrupt_controller.hpp:60) is the
generic "one core wakes another" SMP-boot seam — the GRLIB variant drives it
from an MPSTAT[i]=1 write, kept inside the implementation.
raw_pending() (iinterrupt_controller.hpp:52) is the single-load fast
path: a result of 0 proves pending_mask(cpu) == 0 for every cpu, so the
SingleThread dispatcher skips the per-CPU scan. set_thread_safe(bool)
engages internal locking for MultiThread (call once at initialize(),
before threads start).
Atomics in ICpuBus¶
atomic_swap_u32 / atomic_cas_u32 / atomic_ldstub
(icpu_bus.hpp:53-93) back the SPARC SWAP, CASA, and LDSTUB
instructions. They are non-pure virtual with default implementations
that do a plain read-then-write — correct for single-threaded callers
(test mocks, the oracle-lockstep overlay). The runtime's
CpuBusBridge overrides them with a true
atomic RMW on the backing store, which is what makes the SPARC atomics
safe once cores run on separate host threads (Phase 13). Keeping them
non-pure means other ICpuBus implementations need no change.
Why the CPU speaks VirtAddr, not PhysAddr
ICpuBus is virtual-address-typed even though there is no MMU today
(virtual ≡ physical). Keeping architectural typing honest means an
SRMMU slots in between ICpuBus and the physical SystemBus without
changing the handler signatures (icpu_bus.hpp:7-15). Alignment is
the handler's job — the bus never sees an alignment error.
Peripheral system¶
class IPeripheral : public IEntity, public IMmio { // iperipheral.hpp:29
// IEntity identity — final: the instance name injected by the Soc,
// falling back to device_class() outside an Emulator.
std::string_view name() const final;
virtual std::string_view device_class() const = 0; // the IP-core kind
// IMmio: mmio_range(), mmio_read(), mmio_write(), memory_region()
virtual void attach(const PeripheralContext&) = 0;
virtual void reset() = 0;
virtual void tick(SimTimeNs now) = 0;
virtual void publish(IPublisher&) = 0;
// Capability hooks (default null / no-op):
virtual IPort* find_port(std::string_view) noexcept { return nullptr; }
virtual void connect_ports(IPortResolver&) {}
};
Every device — built-in and custom — implements this one interface. A
peripheral is an IEntity whose defining capability is IMmio; authors
implement device_class() (the IP-core kind, "apbuart"), while name()
is final and returns the PeripheralSpec::instance_name the runtime
injects at assembly (iperipheral.hpp:36-42). The capability hooks let a
device opt into richer behaviour without bloating the base contract:
| Hook | When to override | Consumer |
|---|---|---|
memory_region() (on IMmio, immio.hpp:53) |
the device owns a memory bank with side-effect-free reads (PROM, descriptor RAM, packet buffers) | SystemBus for bulk/span access — debugger reads, DMA spans |
find_port(name) |
the device exposes named typed ports for peer wiring | runtime's connect_ports lifecycle phase |
connect_ports(resolver) |
the wiring is richer than "peer signal drives mine" | runtime, after simple ISignalPort edges are resolved |
get_interface<T>() (from IEntity) |
the device exposes a protocol capability (ICanBus, IConnectable, IAmbaPnp, …) |
the connection pass, the PnP builder, plugins |
PeripheralContext (peripheral_context.hpp) is the aggregate injected
once via attach(); pointers stay valid for the peripheral's lifetime:
| Field | Type | Purpose |
|---|---|---|
bus |
IBusMaster* |
DMA into physical memory |
irqs |
std::span<IInterruptSource* const> |
one bridge per PeripheralSpec::irqs[i]; multi-line devices read ctx.irqs[k]->raise() |
irq |
IInterruptSource* |
legacy alias for irqs.empty() ? nullptr : irqs[0] |
chardev |
ICharacterDevice* |
character I/O (set when chardev_index given) |
scheduler |
IScheduler* |
future-event queue |
logger |
ILogger* |
diagnostic sink |
time |
ITimeSource* |
read current simulated time |
observer |
IEmulatorObserver* |
optional event hooks; null = no-op |
num_cores |
std::uint32_t |
sizing for per-core register banks (IRQMP/IRQAMP) |
ns_per_cycle |
std::uint64_t |
sim-ns per system-clock cycle (GPTIMER prescaler) |
Peripheral details · Custom peripheral walkthrough
Ports (iport.hpp)¶
The port system wires one peripheral's output to another's input without RTTI dependencies on the consumer:
IPort— tag-only base;type_id()returns a stable namespaced string constant (e.g."tero::ISignalPort").ISignalPort— one-bit, level-sensitive signal:set(bool),get(),on_change(std::function<void(bool)>). A self-edge (set(get())) is not a transition and fires no callbacks.IPortResolver—resolve(peer, peer_slot) → IPort*, handed to peripherals during theconnect_portsphase.
Tanda 3 ships only ISignalPort (SignalPort is the concrete impl in
tero_peripherals); data/FIFO/bus port types arrive
with the first peripheral that needs them.
Observation (iemulator_observer.hpp)¶
IEmulatorObserver is the extension seam for reacting to runtime events
without touching the core or a peripheral. Every method has an empty
default; install one via Emulator::set_observer(). The runtime guards
each call with a null check, so the no-observer default costs one branch
per site.
| Hook | Fires when |
|---|---|
on_irq_raised(mask) / on_irq_lowered(mask) |
a source asserts / de-asserts IRQ lines (after pending updates) |
on_irq_acknowledged(cpu, bit_mask, eid) |
the CPU acks an IRQ at trap entry |
on_trap_taken(cpu, level, tt, pc) |
the emulator decides to deliver a trap (before enter_trap) |
on_peripheral_attached(name, base) |
a peripheral is attached to the bus |
on_instruction(cpu, pc) |
immediately before each instruction (per-PC probes; the --trace CLI flag and GDB use this) |
Installing any observer forces the Switch path
The per-instruction on_instruction hook is only reachable from the
core::step interpreter, so installing an observer (e.g. the
--trace observer in main.cpp) deterministically routes
execution through the Switch interpreter rather than the JIT.
Value types in the interface layer¶
AddressRange (address_range.hpp)¶
Half-open [base, base + size). contains(addr) and
overlaps(other) are constexpr and treat size == 0 as empty. Used
for every MMIO routing decision in SystemBus.
BreakpointSet (breakpoint_set.hpp)¶
An std::unordered_set<uint32_t> of guest PCs: add, remove,
contains (O(1) average, hot path), list (O(N), debugger UI only),
size, clear. Owned by the Emulator, referenced non-owning by
GdbStub, so future consumers (a CLI, scripted breakpoints) can share
one registry. Keys are raw uint32_t (not strong-typed) because the RSP
parser and the should_break hot path already deal in raw PCs.
GatedMutex (gated_mutex.hpp)¶
A std::mutex whose locking is gated by a runtime flag (ADR-001). In
SingleThread mode the gate is inactive and lock/unlock/try_lock
are no-ops (one predictable branch → near-zero overhead); in
MultiThread mode it behaves exactly like a std::mutex. It satisfies
the BasicLockable / Lockable requirements, so it drops into
std::scoped_lock and friends.
Threading contract
set_active() is meant to be called exactly once, during
Emulator::initialize(), before any worker thread starts. The gate
flag is read-only for the rest of the run, so it never races. Flipping
it while threads run is a usage error. This is the runtime-gated
reconciliation of ADR-001's original compile-time NullMutex idea
with the project's no-compile-flags-for-behaviour rule.
What this module deliberately omits¶
- No log macros (
ILoggeris the only logging surface). - No utility functions on
Result<T>beyondtl::expected's own. - No global allocator, no global registry, no thread-local state.
Anything not strictly required by the type system or the contracts lives in a downstream module.
See also¶
- Modules overview — the dependency rules and graph.
- Core —
ICpuBusconsumer; trap/interrupt semantics. - Peripherals —
IPeripheral/PeripheralContextimplementers. - Defaults — the standalone implementations of these interfaces.
- Architecture: traps and interrupts
— how
IInterruptControllerand the observer hooks fit the trap path.