Skip to content

tero_bus

Physical-address routing. It owns the Ram block(s) and the SystemBus that maps a physical address to either a backing RAM region or a peripheral's MMIO window, applying big-endian encode/decode for typed accesses. It is also the DMA fabric (IBusMaster).

# src/bus/CMakeLists.txt — "the bus does not depend on the core"
target_link_libraries(tero_bus
    PUBLIC  tero::interfaces
    PRIVATE tero::warnings)

Responsibility

Route every physical access — CPU-initiated or DMA — to the one region that contains it, with big-endian semantics, and reject unmapped / straddling / misaligned-MMIO accesses as bus errors.

The bus does not depend on tero_core

Its only upstream dependency is tero_interfaces. The bus is a pure physical-address router with no notion of a CPU, an instruction, a cycle, or a tick. This lets the same bus instance be the DMA target for peripherals (it implements IBusMaster) with no circular dependency — a peripheral DMAs through exactly the memory map the CPU sees. The CPU-side ICpuBus adapter (CpuBusBridge) lives in tero_runtime.

Source layout

src/bus/
├── include/tero/bus/
│   ├── ram.hpp          ← raw byte storage + BE word + atomics
│   ├── system_bus.hpp   ← physical-address router + IBusMaster + IEntity
│   ├── endian.hpp       ← MemEndian (bus byte-order decision)
│   ├── amba_bus.hpp     ← AmbaBus (AHB/APB topology overlay entity)
│   └── memory_space.hpp ← MemorySpace + MmioMemoryAccess (uniform slave view)
└── src/
    ├── ram.cpp
    ├── system_bus.cpp
    ├── amba_bus.cpp
    └── memory_space.cpp

Ram

Ram (ram.hpp:29) is a byte-addressable block of simulated physical memory — a thin wrapper over std::vector<std::byte>, intentionally endianness-agnostic (bytes are stored exactly as they appear on the wire; multi-byte decoding is SystemBus's job). It implements IMemoryAccess, so it can also sit as a slave in a MemorySpace.

explicit Ram(std::uint32_t size_bytes,
             MemEndian endian = MemEndian::Big);    // ram.hpp:33 — zero-initialized
std::uint32_t size() const noexcept;
std::span<std::byte>       span() noexcept;
std::span<const std::byte> span() const noexcept;
Result<void> read (std::uint32_t offset, std::span<std::byte>) const;
Result<void> write(std::uint32_t offset, std::span<const std::byte>);
Result<std::uint32_t> read_u32_be (std::uint32_t offset) const;   // fast path
Result<void>          write_u32_be(std::uint32_t offset, std::uint32_t);
// Atomic RMW for SPARC SWAP / CASA / LDSTUB (Phase 13):
Result<std::uint32_t> atomic_swap_u32_be(std::uint32_t offset, std::uint32_t);
Result<std::uint32_t> atomic_cas_u32_be (std::uint32_t offset, std::uint32_t compare, std::uint32_t);
Result<std::uint8_t>  atomic_ldstub     (std::uint32_t offset);
void fill(std::byte) noexcept;  void zero() noexcept;

Ram knows no base address

Ram is constructed with a size only; the guest base address is held by SystemBus (in its RamRegion.range). Earlier docs showed a Ram(PhysAddr base, size) constructor — that does not exist. Ram::read/write take a region-relative offset, and out-of-range accesses return ErrorCode::InvalidAddress.

Two performance levers live on Ram:

  • read_u32_be / write_u32_be (ram.hpp:66,69): the Phase 10 P1 fast path. Folds the span round-trip of read() + byte decode into a single memcpy-into-register plus __builtin_bswap32, which compile to one MOV+BSWAP on x86-64 at -O2.
  • Atomics (ram.hpp:83-99): the SWAP/CASA/LDSTUB backing. They reinterpret an aligned word/byte through std::atomic_ref with seq_cst ordering — correct for SPARC TSO, lowering to a lock-prefixed instruction on x86-64, and behaviourally identical to a separate read+write in SingleThread. A misaligned word offset returns AlignmentError.

SystemBus

SystemBus (system_bus.hpp:48) is the central physical-address router. It owns one or more Ram regions (RamRegion = {AddressRange, unique_ptr<Ram>}) and references one or more MMIO regions (MmioRegion = {AddressRange, IMmio*, unique_ptr<GatedMutex>}non-owning of the slave; the Emulator owns those). It derives from IBusMaster, so it is the DMA fabric, and from IEntity — the memory fabric is a first-class entity with the reserved name "system_bus" (system_bus.hpp:57-59), resolvable via find_entity("system_bus"). The byte order of the typed accessors is a constructor parameter (MemEndian, endian.hpp), defaulting to big-endian (SPARC).

Non-copyable and non-movable (Decision 1)

SystemBus deletes its copy and move operations because peripherals (and each core's CpuBusBridge) cache raw pointers into it. Own it by value inside the Emulator, never by unique_ptr that you move.

Region management

Result<void> map_ram(PhysAddr base, std::uint32_t size);           // allocate + map
Result<void> map_ram(PhysAddr base, std::unique_ptr<Ram> ram);     // map a provided Ram
Result<void> map_mmio(IMmio* slave);                               // at its mmio_range()
void         set_thread_safe(bool active) noexcept;                // engage MMIO locks (MT)

map_ram fails with InvalidConfig if the new range overlaps an existing region. map_mmio dispatches by capability — anything exposing IMmio (every peripheral does) can be mapped at its own mmio_range(); there is no separate base argument, and overlap is reported here (config validation cannot check MMIO overlap, since addresses only exist after the factory runs). The runtime calls these during Soc::build; application code rarely needs them directly.

Access surface

Group Methods
Untyped (byte-granular, verbatim) read_physical(addr, span<byte>), write_physical(addr, span<const byte>)
Typed big-endian read_physical_u{8,16,32}, write_physical_u{8,16,32}
Atomic RMW atomic_swap_u32, atomic_cas_u32, atomic_ldstub
DMA (IBusMaster) dma_read(addr, span<byte>), dma_write(addr, span<const byte>)
Introspection ram_region_count(), mmio_region_count(), ram_view_at(addr)

The typed read_physical_u32/write_physical_u32 take an optional RamFastCache* (see below). Atomic RMW on a RAM target uses a true atomic on the backing store (correct under MultiThread); on an MMIO target it falls back to plain read+write (atomics on MMIO are not meaningful and RTEMS never issues them) — the per-region lock still serialises the access.

Routing rules

  1. Look up the region that contains the target address (RAM first, then MMIO).
  2. The matching range's [base, base + size) wins.
  3. No match → ErrorCode::InvalidAddress.
  4. An access that straddles two regionsErrorCode::BusError. Real hardware latches one transaction against one target.

MMIO access constraints

Enforced in SystemBus, before the peripheral's mmio_read/mmio_write is called, so a peripheral handler can assume valid arguments:

  • Naturally aligned ½/4-byte accesses only (else ErrorCode::AlignmentError).
  • Single region per access (no straddle, else BusError).

When an access cannot be served by the strict ½/4-byte path (bulk debugger reads, DMA spans), the bus consults the peripheral's memory_region() capability (IMemoryRegion) for side-effect-free bulk access — that is how PROM and descriptor areas serve read_physical(span).

Big-endian semantics

The typed accessors encode/decode SPARC's MSB-first layout regardless of host endianness; the untyped read_physical/write_physical move bytes verbatim (the right tool for memcpy from a host buffer).

Performance: the RAM fast paths

Two caches collapse the common RAM hot path to a native access:

  • RamFastCache (system_bus.hpp:113): a one-entry "last RAM region that satisfied a u32 access" cache. Phase 13 Inc 5b relocated it from a SystemBus member to a caller-owned value so each core's CpuBusBridge owns its own — a single shared cache would tear under thread-per-core. A stale entry is self-correcting (the range check misses and refreshes). Pass nullptr for the uncached cold path (DMA, external API, atomics).
  • RamView ram_view_at(addr) (system_bus.hpp:160-165): exposes the host backing pointer + guest base + size of the RAM region containing addr (or host == nullptr if none). The JIT uses it to inline RAM loads/stores as native accesses with a runtime bounds check, instead of an out-of-line bus call. RAM regions are mapped once and never moved, so the pointer is stable for the bus's lifetime.

DMA

Peripherals whose PeripheralContext::bus is this SystemBus call:

ctx_.bus->dma_read (PhysAddr{src}, span<std::byte>);
ctx_.bus->dma_write(PhysAddr{dst}, span<const std::byte>);

DMA shares the exact same address map as CPU accesses — a peripheral can even DMA into another peripheral's MMIO range. See examples/demo-dma/ and the custom peripheral guide.

AMBA overlay

The module also models the AMBA hierarchy as descriptive entities (Decision 73). The hot dispatch path is unchanged: every access still routes through the flat SystemBus (RAM fast paths + per-slave IMmio linear dispatch); the overlay is an additive structural view for introspection.

AmbaBus (amba_bus.hpp)

A modelled AMBA bus segment (AHB or APB) as a first-class IEntity (amba_bus.hpp:35). It describes topology only — which slaves occupy which address windows, which masters drive the bus:

Member Purpose
layer() AmbaLayer::Ahb or Apb (the enum lives in interfaces, shared with the IAmbaPnp capability)
attach_slave(range, entity) one slave at its address window
attach_master(entity) a bus master (CPU core, DMA-capable peripheral)
slaves() / masters() / slave_at(addr) descriptive lookups — not the hot dispatch

The hierarchy nests through the same primitive: the APB bus is itself an AHB slave (the APB bridge maps its window), so ahb.attach_slave(apb_window, apb). The Soc builds the overlay at the end of build() from each peripheral's mmio_range() and its declared IAmbaPnp layer — a register-mapped peripheral that declares no layer defaults to the APB (src/runtime/src/soc.cpp:223-259). The segments are resolvable by the reserved entity names "ahb" and "apb", and the flat fabric by "system_bus", all via find_entity (src/runtime/src/soc.cpp:633-647).

MemorySpace + IMemoryAccess (memory_space.hpp)

MemorySpace (memory_space.hpp:53) is a structured physical address space of uniform slaves: free-standing IMemoryAccess implementations (src/interfaces/include/tero/imemory_access.hpp — offset-relative mem_read/mem_write) mapped at [base, base + size). An access dispatches to the slave whose range contains the address, with the offset taken relative to that slave's base. MmioMemoryAccess (memory_space.hpp:24) adapts an IMmio peripheral to that slave interface, so register-mapped devices and RAM/ROM sit in one uniform view. It is an additive substrate — built and tested alongside the live SystemBus, not on the hot path; dispatch is a linear range scan.

Thread safety

set_thread_safe(true) engages the per-peripheral MMIO GatedMutex locks for MultiThread execution; in SingleThread (the common case) every MMIO dispatch lock is a no-op. Call it once at initialize(), before any core thread starts; regions mapped afterwards inherit the state. RAM accesses are not per-region-locked — the atomic RMW path provides the ordering SPARC TSO needs.

What is intentionally not in tero_bus

  • No CPU-side code (the ICpuBus adapter is in tero_runtime).
  • No virtual addresses (SRMMU deferred — see the roadmap).
  • No cache model.
  • No "cycle" or "tick" — the bus is a pure routing function.

See also