lince_bus¶
Physical-address routing. This module owns the Ram block(s) and the
SystemBus that maps physical addresses to either a backing RAM or a
peripheral's MMIO range.
It depends on lince_interfaces only — no awareness of the CPU.
Source layout¶
src/bus/
├── include/lince/bus/
│ ├── ram.hpp ← raw byte storage
│ └── system_bus.hpp ← address router + IBusMaster
└── src/
├── ram.cpp
└── system_bus.cpp
Ram¶
Ram is a thin wrapper around std::vector<std::byte>:
class Ram {
public:
Ram(PhysAddr base, std::size_t size);
PhysAddr base() const noexcept;
std::size_t size() const noexcept;
Result<void> read (PhysAddr, std::span<std::byte>);
Result<void> write(PhysAddr, std::span<const std::byte>);
private:
PhysAddr base_;
std::vector<std::byte> bytes_;
};
It deliberately knows nothing about endianness — the bytes are
stored exactly as they appear on the wire. Big-endian byte-swap is the
job of SystemBus.
Ram is movable but not copyable; a copy would silently double the
backing storage and is almost never what you want.
SystemBus¶
SystemBus is the central physical-address router. It owns:
- An ordered list of
Ramblocks (byunique_ptr<Ram>). - A non-owning list of
IPeripheral*mappings.
And exposes:
- Untyped accesses:
read_physical(PhysAddr, std::span<std::byte>)/write_physical(PhysAddr, std::span<const std::byte>). - Typed accesses with big-endian byte-swap:
read_physical_u32,write_physical_u32,read_physical_u16,write_physical_u16,read_physical_u8,write_physical_u8. IBusMaster— DMA-side interface for peripherals. The same byte fabric.
The bus is non-copyable and non-movable because peripherals cache raw pointers into it (Decision 1 in design decisions).
Routing rules¶
- Walk the routing table in registration order.
- The first range whose
[base, base + size)contains the access wins. - If no range matches, return
ErrorCode::InvalidAddress. - If an access straddles two regions, return
ErrorCode::BusError. Real hardware latches one transaction against one target.
Big-endian semantics¶
The typed accessors use encode_be / decode_be:
inline uint32_t decode_be(std::span<const std::byte, 4> bytes) noexcept {
return (uint32_t(std::to_integer<uint8_t>(bytes[0])) << 24)
| (uint32_t(std::to_integer<uint8_t>(bytes[1])) << 16)
| (uint32_t(std::to_integer<uint8_t>(bytes[2])) << 8)
| uint32_t(std::to_integer<uint8_t>(bytes[3]));
}
Untyped read_physical / write_physical move bytes verbatim — they
are the right tool for byte-level memcpy from a host buffer.
MMIO access constraints¶
- Naturally aligned 1 / 2 / 4-byte accesses only (alignment errors are
ErrorCode::AlignmentError). - Single-region per access (no straddle, returns
BusError).
These constraints are enforced in SystemBus, before the
peripheral is even called. That means a peripheral handler can assume
its mmio_read / mmio_write arguments are valid.
Adding RAM¶
The runtime adds RAM during Emulator::initialize() from the
EmulatorConfig fields. You typically do not call add_ram from
application code — but it is available if you need a multi-region
memory map.
Adding peripherals¶
This is what Emulator::add_peripheral does internally. The bus does
not own the peripheral; it caches the raw pointer for routing only.
DMA¶
Peripherals that implement IBusMaster consumers (their
PeripheralContext::bus field is the bus itself) can do:
ctx_.bus->dma_read(PhysAddr{src}, span<byte>);
ctx_.bus->dma_write(PhysAddr{dst}, span<const byte>);
DMA shares the exact same address map as CPU accesses. Including your own peripherals — a peripheral can DMA into another peripheral's MMIO range if you want it to.
What is intentionally not in lince_bus¶
- No CPU-side code (the
ICpuBusadapter lives inlince_runtime). - No virtual addresses (those come with the SRMMU in Phase 7).
- No cache.
- No notion of "cycle" or "tick" — the bus is a pure routing function.