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::createreturns aResult<std::unique_ptr<Emulator>>. Each step at the public API boundary returnsResult<T>; check it.- The lifecycle is fixed:
create → set_*(optional) →initialize→load_elf(orload_binary) →run_for/run_until. - The
Emulatoris not copyable or movable. Always own it through astd::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,StdoutCharDevicefor 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_u32from a debugger thread while another thread is inrun_for. - Call
add_peripheralbetweeninitialize()and the firstrun_for().
It is not safe to call run_for concurrently from two threads on
the same emulator. Don't do that.