Skip to content

UART and console

The APBUART peripheral is the only path between guest software and the host. This page covers the four common patterns.

Default: stdout / stdin

By default, Emulator::initialize() wires a StdoutCharDevice into the APBUart peripheral context. That means:

  • Every byte the guest writes to the APBUart TDR appears on the host's stdout.
  • Every byte the host types is enqueued in the RX FIFO and made available via the RDR.

This is what lince-emu uses out of the box, with no configuration.

Capturing into a string

For test rigs and CI:

#include "tests/support/capturing_char_device.hpp"

auto cap = std::make_unique<lince::test_support::CapturingCharDevice>();
auto* ref = cap.get();
emu->set_character_device(std::move(cap));
emu->initialize();
emu->run_for(lince::SimTimeNs{2'000'000'000ULL});

std::string output = ref->captured();
REQUIRE(output.find("*** END OF TEST") != std::string::npos);

The CapturingCharDevice is part of the test support tree (tests/support/); it is intentionally not part of lince_defaults so production binaries do not link an unused collector.

Suppressing output

If you don't care what the guest prints (e.g. you're benchmarking the core only), inject a sink:

class NullCharDevice : public lince::ICharacterDevice {
    void transmit(uint8_t) override {}
    bool receive(uint8_t&) override { return false; }
};

emu->set_character_device(std::make_unique<NullCharDevice>());

Output is discarded; RX is permanently empty. Cheaper than /dev/null because the byte never leaves the APBUart.

Bridging to a real serial port

For interactive RTEMS sessions, redirect the UART through a host PTY:

class PtyCharDevice : public lince::ICharacterDevice {
public:
    explicit PtyCharDevice(int fd) : fd_{fd} {}
    void transmit(uint8_t b) override { ::write(fd_, &b, 1); }
    bool receive(uint8_t& out) override {
        return ::read(fd_, &out, 1) == 1;
    }
private:
    int fd_;
};

Pair this with posix_openpt / grantpt / unlockpt to get a PTY your terminal can attach to (screen /dev/pts/N, etc.).

TX enable bit

A subtle hardware detail: the APBUart drops bytes silently unless CTRL.TE = 1. RTEMS sets it during early boot and you typically don't notice. But raw assembly tests must set it explicitly:

sethi  %hi(0x80000108), %g1     ! CTRL register address
or     %g1, %lo(0x80000108), %g1
mov    1, %g2
st     %g2, [%g1]               ! CTRL.TE = 1

This bit Decision 22 in Design decisions; the test fixture tests/asm/hello_uart.S is the canonical example.

What the FIFO does (and doesn't) model

  • RX FIFO: 8 entries, modelled with std::queue<uint8_t>. Status bit 0 (DR, data ready) is set when non-empty; the queue is shared by the APBUart peripheral and the injected ICharacterDevice::receive hook.
  • TX FIFO: not modelled. transmit() drains immediately to the injected ICharacterDevice. Status bit 31 (FA, FIFO available) always reads as 1 (Decision 18).
  • Baud rate, parity, stop bits: the registers exist but produce no observable timing change. RTEMS programs them and Lince stores the bytes; nothing else happens.

If you need a more accurate UART (cycle-accurate baud, hardware flow-control, framing errors), wrap the APBUart with a custom peripheral and intercept the data path. The architecture supports it without changing the core.