Skip to content

Embedding as a library

lince_runtime is a static library that exposes the lince::runtime::Emulator class. Linking against it gives your application full programmatic control over the SoC: load images, run simulated time, inspect registers, install custom peripherals.

CMake integration

If your project uses find_package / add_subdirectory, the typical shape is:

add_subdirectory(third-party/lince)

add_executable(my_simulator main.cpp)
target_link_libraries(my_simulator PRIVATE lince_runtime)

lince_runtime transitively pulls lince_core, lince_bus, lince_peripherals, lince_defaults, and lince_interfaces. You do not need to list them by hand.

Minimal flow

#include "lince/runtime/emulator.hpp"
#include "lince/runtime/emulator_config.hpp"

int main() {
    auto cfg          = lince::runtime::gr712rc_config();
    cfg.num_cores     = 1;
    cfg.ram_size      = 16 * 1024 * 1024;

    auto emu_res = lince::runtime::Emulator::create(cfg);
    if (!emu_res) return 1;
    auto emu = std::move(emu_res.value());

    if (auto r = emu->initialize();          !r) return 2;
    if (auto r = emu->load_elf("hello.elf"); !r) return 3;

    auto result = emu->run_for(lince::SimTimeNs{5'000'000'000ULL});
    return result.reason == lince::runtime::HaltReason::ErrorMode ? 4 : 0;
}

Three things to note:

  • Emulator::create returns a Result<std::unique_ptr<Emulator>>. Each step at the public API boundary returns Result<T>; check it.
  • The lifecycle is fixed: create → set_* (optional) → initializeload_elf (or load_binary) → run_for / run_until.
  • The Emulator is not copyable or movable. Always own it through a std::unique_ptr.

Lifecycle

stateDiagram-v2
    [*] --> Created : Emulator::create(cfg)
    Created --> Configured : set_logger / set_character_device
    Configured --> Initialized : initialize()
    Initialized --> Running : run_for / run_until
    Running --> Initialized : returns HaltReason
    Running --> Stopped : ErrorMode / Breakpoint
    Initialized --> Reset : reset()
    Reset --> Initialized
    Stopped --> [*]

reset() brings every core, the bus, and every peripheral back to power-on state without freeing memory or unloading the ELF. Use it for deterministic test fixtures: create + load_elf once, reset + run_for many times.

Injecting services

By default the emulator wires:

  • StdoutLogger (warn-level) on stderr,
  • StdoutCharDevice for APBUART TX/RX,
  • NullFaultInjector.

To capture output programmatically:

auto cap = std::make_unique<lince::test_support::CapturingCharDevice>();
auto* ref = cap.get();
emu->set_character_device(std::move(cap));
emu->initialize();   // ← MUST come after set_character_device
emu->load_elf("hello.elf");
emu->run_for(lince::SimTimeNs{2'000'000'000ULL});

std::string output = ref->captured();

The order matters: the APBUart latches its ICharacterDevice* during initialize(). Calling set_character_device afterwards has no effect.

External memory access

The emulator publishes a curated set of bus operations:

emu->read_physical_u32(lince::PhysAddr{0x40000000}); // → Result<uint32_t>
emu->write_physical_u32(lince::PhysAddr{0x40000004}, 0xdeadbeef);

std::array<std::byte, 16> buf;
emu->read_physical(lince::PhysAddr{0x40000010}, buf);

These calls are thread-safe: they take an internal mutex so they can be invoked from a debugger thread while the round-robin loop is running. The mutex is held for the duration of the current quantum; external accesses block until the next scheduling boundary.

Inspecting CPU state

emu->core(idx) returns a reference to the CpuState of core idx. The state is mutable from outside; you can prime registers before a run_for call (useful for unit tests):

auto& cpu0 = emu->core(0);
cpu0.set_pc(0x40000000);
cpu0.set_global(7, 0x00000042);   // %g7
emu->run_for(lince::SimTimeNs{1'000'000});

The full CpuState API is documented in the C++ API reference under lince::core::CpuState.

Custom peripherals

auto demo = std::make_unique<my_company::FlightControlBoard>(
                lince::PhysAddr{0x80001000});
auto r = emu->add_peripheral(std::move(demo), lince::IrqLine{10});

See Custom peripherals for the full walkthrough, including DMA, interrupts, scheduled events, and SMP2 publishable fields.

Threading model

Emulator is single-thread-of-execution: run_for, run_until, initialize, reset and any peripheral handlers run on the calling thread. The internal mutex serialises external read/write_physical calls and add_peripheral against the run loop, so it is safe to:

  • Call read_physical_u32 from a debugger thread while another thread is in run_for.
  • Call add_peripheral between initialize() and the first run_for().

It is not safe to call run_for concurrently from two threads on the same emulator. Don't do that.