Skip to content

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:

target_link_libraries(my_target PRIVATE tero::interfaces)

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.
  • IPortResolverresolve(peer, peer_slot) → IPort*, handed to peripherals during the connect_ports phase.

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 (ILogger is the only logging surface).
  • No utility functions on Result<T> beyond tl::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