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 thespanround-trip ofread()+ byte decode into a singlememcpy-into-register plus__builtin_bswap32, which compile to oneMOV+BSWAPon x86-64 at-O2.- Atomics (
ram.hpp:83-99): the SWAP/CASA/LDSTUB backing. They reinterpret an aligned word/byte throughstd::atomic_refwithseq_cstordering — correct for SPARC TSO, lowering to alock-prefixed instruction on x86-64, and behaviourally identical to a separate read+write in SingleThread. A misaligned word offset returnsAlignmentError.
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¶
- Look up the region that contains the target address (RAM first, then MMIO).
- The matching range's
[base, base + size)wins. - No match →
ErrorCode::InvalidAddress. - An access that straddles two regions →
ErrorCode::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 aSystemBusmember to a caller-owned value so each core'sCpuBusBridgeowns its own — a single shared cache would tear under thread-per-core. A stale entry is self-correcting (the range check misses and refreshes). Passnullptrfor 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 containingaddr(orhost == nullptrif 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
ICpuBusadapter is intero_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¶
- Architecture: memory and bus — the memory map, big-endian rationale, straddle/alignment policy.
- Runtime —
CpuBusBridge, RAM/PnP mapping at init. - Peripherals — the MMIO slaves and
IMemoryRegion.