Skip to content

tero_app

The standalone CLI executable, tero-emu (OUTPUT_NAME "tero-emu"). A thin shell over tero::runtime::Emulator that parses arguments, builds an EmulatorConfig from a SoC recipe, injects the default host services, loads an ELF, and runs.

# src/app/CMakeLists.txt
add_executable(tero_app src/main.cpp)
set_target_properties(tero_app PROPERTIES OUTPUT_NAME "tero-emu")
target_link_libraries(tero_app
    PRIVATE tero::runtime tero::defaults tero::warnings fmt::fmt)

Responsibility

Wire a CLI invocation to a runnable Emulator: recipe → config → inject defaults → initialize → load → run → report. It is the only place in the tree where direct host I/O (stdout/stderr) is allowed.

Source layout

src/app/
└── src/main.cpp        ← single translation unit (~670 LOC incl. the
                          diagnostic lockstep/oracle harnesses)

Output discipline: guest UART text goes to stdout; emulator status, the post-mortem dump, and the --trace stream go to stderr (so guest output stays cleanly separable). The only logging the app itself emits is through StdoutLogger, which it injects.

CLI flags

Parsed by a hand-rolled parse_cli (main.cpp:123) — a simple --flag value scan over argv, not getopt_long.

Flag Effect Maps to
--image <path> ELF image to load (SPARC BE, ET_EXEC) load_elf
--soc <gr712rc\|gr740> SoC recipe (default gr712rc) gr712rc_config() / gr740_config()
--ram <MiB> RAM size (default 16; ignored for gr740) cfg.ram_size
--cores <1..4> core count (default 1; ignored for gr740) cfg.num_cores
--mhz <1..1000> override CPU frequency (default: recipe clock) cfg.cpu_clock_hz
--cpi <value> cycles per instruction (default 1.0; <1 raises IPC) cfg.cpi
--budget <ns> max simulated ns (default 1e9 = 1 s) run_for argument
--gdb-port <N> bind GDB stub to TCP port N (0 = off) cfg.gdb_stub_port
--gdb-wait block at startup until GDB connects cfg.gdb_stub_wait_for_client
--turbo free-run (skip wall-clock pacing) cfg.pacing = Turbo
--mt / --multithread thread-per-core (ADR-001) cfg.execution_mode = MultiThread
--verbose debug-level logging StdoutLogger(LogLevel::Debug)
--version / --help / -h print and exit

Diagnostic flags (developer use): --no-jit-opt (Baseline-only JIT), --no-translation (force the Switch interpreter / oracle), --ir-interp-only (translation on, never JIT-compile), --trace (per-instruction cpu pc trace to stderr — installs an observer, which forces the Switch path), --quantum <N>, --jit-region-blocks <N>, --lockstep (IR-vs-Switch multi-core divergence diff), --oracle-lockstep (per-block IR-vs-core::step oracle).

See the CLI reference for the full guide.

What main() does

  1. Parse arguments into CliOptions (main.cpp:65). --version / --help return early; a parse error returns exit 2. No --image prints a hint and returns 0.
  2. Branch to the diagnostic harnesses if --lockstep / --oracle-lockstep were given (run_lockstep / run_oracle).
  3. Build the config from the SoC recipe, then overlay CLI overrides (cores/ram/mhz/cpi/pacing/execution_mode/JIT knobs/GDB port). ns_per_insn is re-derived by Emulator::create from cpu_clock_hz and cpi.
  4. Create the emulator: Emulator::create(cfg). Failure → exit 3.
  5. Inject defaults:
    emu.set_logger(std::make_unique<StdoutLogger>(level));
    emu.set_uart_character_device(0, std::make_unique<StdoutCharDevice>());
    if (opts.trace) emu.set_observer(std::make_unique<TraceObserver>());
    
  6. Initialize (emu.initialize() — wires RAM, PROM, peripherals, PnP, bus routing, the JIT). Failure → exit 3.
  7. Load the ELF (emu.load_elf(opts.image)). Failure → exit 4.
  8. Run: emu.run_for(SimTimeNs{opts.budget_ns}).
  9. GDB stop/resume loop: while the run halted on a Breakpoint and a client is connected, hand control to stub->process_until_resume(); on Continue/Step re-enter run_for with the remaining budget; exit the loop on Detach/Kill.
  10. Report and exit (see below).

Exit codes and the post-mortem

The app prints a one-line halt summary (reason, instructions_executed, sim_time_ns) to stderr, then:

Halt reason Action Exit
DurationExpired / DeadlineReached normal end 0
Breakpoint (no/disconnected stub) falls through 0
HaltedMode dump core 0 post-mortem 5

The post-mortem (main.cpp:639) dumps, all to stderr:

  • pc, npc, tbr, tt (= (tbr >> 4) & 0xFF), psr, wim;
  • %g0..%g7;
  • the active window's %i0..%i7, %l0..%l7, %o0..%o7.

The locals are printed because SPARC V8 stores the faulting PC/nPC in %l1/%l2 of the trap-handler window, so they expose the offending instruction even before the handler runs.

HaltedMode, not a crash

Exit 5 with HaltedMode means a guest core took a trap with ET=0 — typically RTEMS' deliberate _CPU_Fatal_halt / _exit (ta 0). The emulator behaved correctly; see RunResult.

Why such a minimal CLI

  1. The library is the product. The CLI exists so a newcomer can run tero-emu --image hello.elf in one shot, but the canonical integration is via tero_runtime.
  2. It is the canonical example of injecting defaults. New embedders learn by reading main.cpp.
  3. An SMP2 wrapper has no use for it. Anything CLI-flag-shaped becomes an SMP2 model property.

Extending the CLI

  • Fork main.cpp. Copy it, add your flags, link tero_runtime, ship a different binary.
  • Add a field to EmulatorConfig. If the knob is general enough to belong on the public API, add it there and expose a flag.

The CLI will not grow into a configuration framework — anything sufficiently complex is a library consumer's job.

See also