Skip to content

tero_defaults

Reference implementations of the injected I* interfaces, used by the standalone build. Each can be replaced by an SMP2 wrapper, a Python binding, or a custom test harness through the matching Emulator::set_* setter.

# src/defaults/CMakeLists.txt
target_link_libraries(tero_defaults
    PUBLIC  tero::interfaces
    PRIVATE tero::warnings fmt::fmt)

Responsibility

Provide the smallest correct standalone implementation of every host service the emulator depends on, behind the interfaces, so the whole library can be dropped and re-implemented by a downstream embedder.

Depends on interfaces only

No dependency on core, bus, or peripherals. tero_runtime links it PRIVATE — these classes are constructed inside emulator.cpp (and by main.cpp), never exposed on the runtime's public header surface.

Source layout

src/defaults/
├── include/tero/defaults/
│   ├── stdout_logger.hpp
│   ├── stdout_char_device.hpp
│   ├── labeled_stdout_char_device.hpp
│   ├── null_fault_injector.hpp
│   └── debug_publisher.hpp
└── src/
    ├── stdout_logger.cpp
    ├── stdout_char_device.cpp
    ├── labeled_stdout_char_device.cpp
    ├── null_fault_injector.cpp
    └── debug_publisher.cpp
Class Implements Substituted by (SMP2)
StdoutLogger ILogger Smp::Services::ILogger adapter
StdoutCharDevice ICharacterDevice SMP2 output field / linked model
LabeledStdoutCharDevice ICharacterDevice (auxiliary-UART helper)
NullFaultInjector IFaultInjector custom SMP2 operations
DebugPublisher IPublisher Smp::IPublication

StdoutLogger

ILogger implementation that writes each record to stderr as [LEVEL] [category] message.

class StdoutLogger final : public ILogger {           // stdout_logger.hpp:10
public:
    explicit StdoutLogger(LogLevel min_level = LogLevel::Info) noexcept;
    void log(LogLevel level, std::string_view category,
             std::string_view message) override;       // 3 args — see note
    void     set_min_level(LogLevel) noexcept;
    LogLevel min_level() const noexcept;
};

log takes three arguments

The signature is log(level, category, message) — the category short tag sits between level and message. The CLI raises the threshold to Debug when --verbose is passed (main.cpp builds StdoutLogger(LogLevel::Debug)).

StdoutCharDevice

ICharacterDevice for UART output. write_char goes to stdout; input is always empty.

class StdoutCharDevice final : public ICharacterDevice { // stdout_char_device.hpp:11
public:
    void                write_char(char c) override;
    std::optional<char> read_char() override { return std::nullopt; }
    bool                has_input() const override { return false; }
};

The CLI wires one of these to the console UART (APBUART0) via set_uart_character_device(0, ...) so guest output is visible. (The ICharacterDevice contract is write_char / read_char / has_input — there is no transmit/receive API.)

LabeledStdoutCharDevice

ICharacterDevice that line-buffers TX bytes and flushes each completed line as <prefix><line>\n to stdout; RX is permanently empty. Designed for auxiliary UARTs whose output must interleave readably with the console UART in a single terminal — the tag keeps each line atomic.

class LabeledStdoutCharDevice final : public ICharacterDevice { // labeled_stdout_char_device.hpp:21
public:
    explicit LabeledStdoutCharDevice(std::string prefix) noexcept;
    ~LabeledStdoutCharDevice() override;                 // flushes any unterminated tail
    void                write_char(char c) override;
    std::optional<char> read_char() override { return std::nullopt; }
    bool                has_input() const override { return false; }
};

Bytes accumulate until a \n or \r; consecutive terminators (\r\n) collapse because flushing an empty buffer emits nothing. The hello-gr712rc / hello-gr740 examples wire one of these to every UART (including UART0), prefixed [apbuart<i>].

NullFaultInjector

IFaultInjector no-op: signals "never corrupt" on every query. This is the MVP-shipped (Level-2) implementation; Level-3 FT work adds real injectors behind the same interface.

class NullFaultInjector final : public IFaultInjector {  // null_fault_injector.hpp:11
public:
    bool should_corrupt_read(PhysAddr, std::size_t) override { return false; }
    void corrupt(std::span<std::byte>) override {}
};

Why a no-op interface at all

  1. The call sites in the core (read paths, register accesses) are part of the design. Wiring the interface in once and no-op'ing the implementation means a Level-3 injector lands with no refactor.
  2. SMP2 wrappers may surface fault injection as model operations; the no-op default lets the standalone build ignore them.

DebugPublisher

IPublisher that captures every publish_field call into an append-only list — used by tests to assert a module exposes the expected observable fields, and as the standalone "dump state" backend.

class DebugPublisher final : public IPublisher {         // debug_publisher.hpp:16
public:
    enum class FieldKind : std::uint8_t { U8, U16, U32, U64, Bool };
    struct Entry { std::string name; FieldKind kind; const void* ptr; };

    void publish_field(std::string_view, std::uint8_t*)  override;
    void publish_field(std::string_view, std::uint16_t*) override;
    void publish_field(std::string_view, std::uint32_t*) override;
    void publish_field(std::string_view, std::uint64_t*) override;
    void publish_field(std::string_view, bool*)          override;

    const std::vector<Entry>& entries() const noexcept;   // captured fields
    void                      clear() noexcept;
};

The five typed overloads mirror IPublisher exactly; an SMP2 wrapper replaces this with Smp::IPublication, which has the same shape (named fields, typed pointers).

How implementations are selected

Consumers see only the I* interfaces. The concrete class is chosen by the embedder via the Emulator::set_* injection points — which is exactly what tero_app/main.cpp does:

emu.set_logger(std::make_unique<tero::defaults::StdoutLogger>(level));
emu.set_uart_character_device(
    0, std::make_unique<tero::defaults::StdoutCharDevice>());

A library user in another context substitutes their own implementations and never includes any header from tero_defaults at all.

See also

  • Interfaces — the contracts these classes implement.
  • Runtime — the set_* injection points and where the defaults are wired.
  • App — the canonical example of injecting defaults.