User guide¶
This guide covers how to build, run, and consume the Lince emulator. If you want to modify the emulator internals, read the architecture overview first.
The dedicated sub-pages drill into individual topics:
- CLI reference — every flag of
lince-emu - Embedding as a library —
Emulator::create, lifecycle, ownership rules - Configuration — every field of
EmulatorConfig, including the GDB stub - UART and console — capturing, suppressing, redirecting stdout
The remainder of this page is a single linear walkthrough that covers the most common case end-to-end.
1. Build and Installation¶
Lince requires a modern C++ host environment.
Requirements: - CMake ≥ 3.25 - GCC ≥ 12 or Clang ≥ 16 - C++20 standard library support
Dependencies:
Lince is practically dependency-free. The only external libraries are fetched automatically by CMake during the configure step:
- Catch2 (v3.5.3) for the test suite.
- tl::expected (v1.1.0) as a C++20 polyfill for the C++23 std::expected type.
Building:
git clone https://github.com/.../lince.git
cd lince
cmake -S . -B build -G Ninja
cmake --build build
Testing:
You should see all 262 unit and integration tests passing. Some tests (liketest_rtems_boot) automatically skip if you don't have the required cross-compiled ELF payloads.
2. Running the CLI (lince-emu)¶
The standalone application is located at build/src/app/lince-emu.
Options Reference¶
| Option | Default | Description |
|---|---|---|
--image <path> |
(required) | Path to the SPARC big-endian ELF executable to load. |
--ram <MiB> |
16 |
Amount of simulated RAM in MiB. |
--cores <1\|2> |
1 |
Number of LEON cores to instantiate. |
--budget <ns> |
1000000000 |
Simulated time limit (1,000,000,000 ns = 1 second). |
--verbose |
Off | Enables Debug level logging to stderr. |
--version |
— | Prints the emulator version and cleanly exits. |
--help, -h |
— | Prints the usage menu and cleanly exits. |
Exit Codes and ErrorMode¶
0(OK): Execution naturally hit the simulated time budget.2(Invalid Args): Malformed CLI parameters.3(Init Error): Invalid core/RAM configuration.4(Load Error): Rejection of the ELF (invalid format, not SPARC, read fail).5(ErrorMode): Execution was halted due to a fatal trap.
ErrorMode Diagnostics:
If lince-emu returns exit code 5, a SPARC V8 fatal hardware fault occurred (specifically, a hardware TRAP or software Tcc generated while PSR.ET == 0). The CLI dumps a complete post-mortem dump of Core 0's registers, globals, and active window:
[lince-emu] core0 post-mortem: pc=0x400034a0 npc=0x400034a4 tbr=0x40000020 tt=0x02 psr=0x00000083 wim=0x00000002
[lince-emu] g0-g7: 0 0x00000000 0x00000000 ...
[lince-emu] i0-i7: 0x400fff40 0x00000008 ...
3. Using Lince as a C++ Library¶
Lince exposes a robust, type-safe C++ API for integration into other applications (such as automated testing rigs, debuggers, or SMP2 simulation wrappers).
The Minimal Flow¶
#include "lince/runtime/emulator.hpp"
#include "lince/runtime/emulator_config.hpp"
#include <iostream>
int main() {
// 1. Get the sensible defaults for a GR712RC-like configuration
auto cfg = lince::runtime::gr712rc_config();
cfg.num_cores = 1;
cfg.ram_size = 16 * 1024 * 1024; // 16 MiB
// 2. Create the emulator instance
auto emu_res = lince::runtime::Emulator::create(cfg);
if (!emu_res) return -1; // Handle errors properly in real code
auto emu = std::move(emu_res.value());
// 3. Initialize mapping and default peripherals (IRQMP, UART, etc.)
emu->initialize();
// 4. Load a payload
emu->load_elf("my_firmware.elf");
// 5. Run the emulator for 5 seconds simulated time
auto result = emu->run_for(lince::SimTimeNs{5'000'000'000ULL});
std::cout << "Executed " << result.instructions_executed
<< " instructions.\n";
return 0;
}
4. Advanced Configuration¶
The EmulatorConfig Object¶
The EmulatorConfig object instructs the Emulator on its internal boundaries before runtime initialization.
| Field | Type | Default | Description |
|---|---|---|---|
num_cores |
std::uint32_t |
1 |
Core count (GR712RC supports 1 or 2). Secondary cores boot halted (Power-down). |
ram_base |
std::uint32_t |
0x40000000 |
Physical address where the internal RAM block starts. |
ram_size |
std::uint32_t |
16MiB |
Bytes allocated. |
ns_per_insn |
std::uint64_t |
20 |
Equivalent simulated instruction speed (~50 MIPS). |
quantum |
std::uint32_t |
1000 |
Cooperative instructions executed per core round-robin before context switching. |
entry_point_override |
std::uint32_t |
0 |
If non-zero, overrides the e_entry entrypoint dictated by an ELF header. |
Accessing Result Data (RunResult)¶
A call to run_for() or run_until() returns a RunResult containing metadata about the completed execution block. Check the reason field (HaltReason) to know exactly why control was returned to the host.
HaltReason Enum Value |
Meaning | Host Action |
|---|---|---|
DurationExpired |
Sim time ran out (run_for). |
Proceed to another run_for(). |
DeadlineReached |
Sim time hit limit (run_until). |
Proceed to another run_until(). |
ErrorMode |
Trap fired while PSR.ET == 0 (fatal). |
Abort and inspect emu->core(0). |
Breakpoint |
A software/hardware breakpoint hit. | Proceed with debug inspection. |
5. Console UART Emulation¶
By default, the emulator hooks the ApbUart component directly to stdout / stdin. To aggressively suppress console emissions during library integration (e.g. inside unit tests) or to capture output programmatically:
#include "tests/support/capturing_char_device.hpp"
// Inject a capture buffer before calling emu->initialize()
auto cap = std::make_unique<lince::test_support::CapturingCharDevice>();
auto* ref = cap.get();
emu->set_character_device(std::move(cap));
emu->run_for(...);
std::string output = ref->captured();
6. Writing Custom Peripherals¶
Adding a user-provided peripheral is designed to be trivial. A custom class must simply implement the lince::IPeripheral interface and be registered against the emulator with add_peripheral().
You do not need to recompile the project. The emulator natively routes physical bus traffic via the central SystemBus at runtime.
To learn exactly how to format the DMA memory translations, interface stubs, and attach-phase injection required to add a memory-mapped peripheral, read the complete custom peripheral tutorial.