Skip to content

lince_interfaces

A header-only vocabulary library that defines the strong types and contracts shared by every other module. It produces no .a file — add_library(... INTERFACE) — and depends on nothing.

target_link_libraries(my_target PRIVATE lince_interfaces)

Headers

Located under src/interfaces/include/lince/:

Header What it defines
types.hpp PhysAddr, VirtAddr, CoreId, SimTimeNs, IrqLine, AccessSize, ErrorCode, Result<T>, make_error()
address_range.hpp AddressRange — half-open [base, base + size) for MMIO dispatch
ilogger.hpp ILogger, LogLevel
itime_source.hpp ITimeSource — pull-only sim-time source
ipublisher.hpp IPublisher — SMP2-style observable field registration
ifault_injector.hpp IFaultInjector — Level-2 FT hook surface
icharacter_device.hpp ICharacterDevice — UART TX/RX byte path
ientry_point.hpp IEntryPoint — named callable, maps to Smp::IEntryPoint
iperipheral.hpp IPeripheral — unified contract for every device
peripheral_context.hpp PeripheralContext — services injected via attach()
ibus_master.hpp IBusMaster — DMA-side interface
iinterrupt_source.hpp IInterruptSourceraise() / lower()
ischeduler.hpp ISchedulerschedule_event(when, IEvent*)
ievent.hpp IEvent — callable scheduled event
icpu_bus.hpp ICpuBus — virtual-address CPU-side data bus

The strong-type contract

The strong types in types.hpp are enum class aliases over fixed-width unsigned integers:

enum class PhysAddr : std::uint32_t {};
enum class VirtAddr : std::uint32_t {};
enum class CoreId   : std::uint8_t  {};
enum class SimTimeNs: std::uint64_t {};
enum class IrqLine  : std::uint8_t  {};
enum class AccessSize : std::uint8_t {
    Byte = 1, Half = 2, Word = 4
};

They are zero-overhead (the underlying integer is preserved) and not implicitly convertible to each other or to raw int. The header provides arithmetic operators only where they make physical sense:

  • PhysAddr + uint32_t → PhysAddr (offset)
  • PhysAddr - PhysAddr → uint32_t (distance)
  • SimTimeNs + SimTimeNs → SimTimeNs
  • No CoreId + CoreId (a core ID is an identifier, not a number).

The to_underlying(x) helper extracts the raw integer when you really need it (encoders, hex formatters). Prefer that to static_cast.

Result and ErrorCode

template <typename T>
using Result = tl::expected<T, ErrorCode>;

enum class ErrorCode : std::uint8_t {
    Ok = 0,
    BusError,
    InvalidAddress,
    AlignmentError,
    TrapGenerated,
    InvalidConfig,
    ElfLoadError,
};

[[nodiscard]] inline tl::unexpected<ErrorCode> make_error(ErrorCode c);

Result<T> is the canonical return type at every public API boundary. tl::expected is used in place of std::expected so the project can stay on C++20; semantics match. When the project moves to C++23 this will be a one-line using change.

IPeripheral contract

Every peripheral, no matter how trivial, implements the same interface:

class IPeripheral {
public:
    virtual std::string_view name() const = 0;
    virtual AddressRange     mmio_range() const = 0;
    virtual void             attach(const PeripheralContext& ctx) = 0;
    virtual void             reset() = 0;
    virtual Result<uint32_t> mmio_read (PhysAddr, AccessSize) = 0;
    virtual Result<void>     mmio_write(PhysAddr, AccessSize, uint32_t) = 0;
    virtual void             tick(SimTimeNs now) = 0;
    virtual void             publish(IPublisher&) {}
    virtual ~IPeripheral() = default;
};

PeripheralContext carries everything a device might need: IBusMaster* (DMA), IInterruptSource* (IRQ), IScheduler* (timed events), ILogger*, ITimeSource*, and ICharacterDevice* (only APBUart uses the last one).

The runtime injects this context through attach() once, then never mutates it. A peripheral that needs DMA or IRQs simply caches the relevant pointer.

Custom peripheral walkthrough

What the module deliberately does not include

  • No log macros (ILogger is the only logging surface).
  • No utility functions on Result<T> beyond what tl::expected provides.
  • No global allocator, no global registry, no thread-local state.

Anything that is not strictly required by the type system or the contracts lives in a downstream module.