Skip to content

APBUART — Serial port

GRLIB APBUART (GR712RC §15 / GR740 §23 / GRLIB IP core). Provides polled and interrupt-driven serial I/O. TX bytes are forwarded to an injected ICharacterDevice; RX bytes come from the same device. Tero models the data path and interrupts but not real UART timing (baud rate, stop bits, parity, TX shift latency).

Property Value
Class peripherals::ApbUart
MMIO window size 0x100
RX FIFO depth 8 (FifoSize)
TX FIFO not modelled (immediate drain)
Source src/peripherals/src/apbuart.cpp

Using it

How UARTs are wired in a config

Each UART is a PeripheralSpec with a chardev_index into EmulatorConfig::character_devices. The recipes (gr712rc_config()/gr740_config()) build them with make_apbuart_spec (emulator_config.cpp:22): the spec's instance_name is apbuart{N}, the chardev_index is N, and irqs is the single silicon IRQ line. The runtime injects character_devices[N] into ctx.chardev before attach().

The instance-name convention (apbuart0..apbuart5) is load-bearing: the PnP table builder looks UART IRQs up by it (pnp_table.cpp:169), and Emulator::set_uart_character_device(N, dev) takes ownership of a chardev and overrides slot N's pointer (emulator.cpp:1443).

GR712RC — 6 UARTs across two APB bridges

UART MMIO base APB bridge IRQ Console
apbuart0 0x80000100 bridge 1 (0x80000000) 2
apbuart1 0x80100100 bridge 2 (0x80100000) 17
apbuart2 0x80100200 bridge 2 18
apbuart3 0x80100300 bridge 2 19
apbuart4 0x80100400 bridge 2 20
apbuart5 0x80100500 bridge 2 21

GR740 — 2 UARTs on a single APB bridge

UART MMIO base IRQ Console
apbuart0 0xFF900000 29
apbuart1 0xFF901000 30

The console UART (apbuart0) is the one the standalone tero-emu binds to host stdout. Auxiliary UARTs default to a nullptr chardev: TX is dropped, RX reads return 0.

Register map

Offset Name Access Description
0x00 DATA R/W Read pops one RX-FIFO byte (low 8 bits); write enqueues one TX byte.
0x04 STATUS R (writes ignored) Status flags + live RCNT field.
0x08 CONTROL R/W Control flags (FA bit forced to 1).
0x0C SCALER R/W Baud-rate scaler, low 8 bits stored; no observable effect.

All registers are 32-bit, word-only — byte and half-word accesses return ErrorCode::AlignmentError.

Word-only access

A stb/ldub to a UART register faults (AlignmentError). Use st/ld. This matches the GRLIB APB bus and is enforced by is_word_access.

Control register

Bit Const Name Meaning
0 CtrlRe RE Receiver enable. RX FIFO is filled only when set.
1 CtrlTe TE Transmitter enable. TX bytes are dropped when clear.
2 CtrlRi RI Receive-interrupt enable (raise when DR && RI).
3 CtrlTi TI Transmit-interrupt enable (raise when TE-status && TI).
4 CtrlPs PS Parity select (no effect — timing not modelled).
5 CtrlPe PE Parity enable (no effect).
7 CtrlLb LB Loopback enable (stored, no effect).
9 CtrlTf TF TX-FIFO interrupt (stored, no effect — no TX FIFO).
10 CtrlRf RF RX-FIFO interrupt (stored).
11 CtrlDb DB FIFO-debug enable (stored).
31 CtrlFa FA FIFOs-available — forced to 1 on every write (control_ = (value & ~CtrlFa) \| CtrlFa).

RTEMS sets TE during BSP init

Bit 1 (TE) gates all transmit: with TE clear, mmio_write(DATA) silently drops the byte (apbuart.cpp:78). RTEMS sets TE during BSP init; raw assembly tests must do it explicitly or no output appears. The canonical fixture is tests/guest-programs/asm/hello-uart/hello_uart.S.

Status register

STATUS (0x04) is read-only — writes are accepted and ignored (apbuart.cpp:83). On read, the live DR flag and RCNT field are recomputed from the RX FIFO, and FA is forced to 1 (apbuart.cpp:47):

Bit Const Name Behaviour
0 StatDr DR Data ready — recomputed: set iff RX FIFO non-empty.
1 StatTs TS Transmitter shift-register empty — reset value, always set.
2 StatTe TE Transmitter FIFO empty — always set (no TX FIFO).
3 StatBr BR Break received (not generated).
4 StatOv OV Overrun (not generated; RX over-full bytes are dropped, see below).
5 StatPe PE Parity error (not generated).
6 StatFe FE Framing error (not generated).
7 StatTh TH TX-FIFO half-full / hold — reset value, always set.
8 StatRh RH RX-FIFO half-full (not driven).
9 StatTf TF TX-FIFO full — always 0 (no TX FIFO).
10 StatRf RF RX-FIFO full (not driven separately).
26:20 TCNT TX-FIFO count (5-bit, always 0).
31:26 RCNT RX-FIFO count (5-bit) — recomputed live as rx_fifo_.size().
31 StatFa FA FIFOs available — forced to 1.

ResetStatus = TE | TS | TH | FA (0x...86 plus FA) — the transmit side is reported idle/ready at reset.

Chardev binding (the console)

ICharacterDevice (src/interfaces/include/tero/icharacter_device.hpp) is the byte-stream the UART talks to:

virtual void               write_char(char c) = 0;     // TX sink
virtual std::optional<char> read_char() = 0;           // RX source (nullopt = none)
virtual bool               has_input() const = 0;

The default standalone implementation (defaults::StdoutCharDevice) writes to host stdout and reports no input. Bind a custom one before initialize():

emu.set_uart_character_device(0, std::make_unique<MyConsole>());   // takes ownership
// or, non-owning, via the config:
cfg.character_devices[0] = my_chardev_ptr;                         // must outlive Emulator

Internals / how it's modelled

TX path

CPU writes to DATA (apbuart.cpp:77):

  • If CTRL.TE == 0 → drop the byte.
  • Else ctx_.chardev->write_char(value & 0xFF) immediately (no TX FIFO, no per-byte timing — the byte hits the host the same simulated cycle).

There is no separate "raise on TX" in the write path; the TX interrupt is driven by update_irq whenever the control register is rewritten (TE-status is always set, so CTRL.TI alone gates the TX IRQ).

RX path

tick(now) (apbuart.cpp:94) polls the chardev each scheduling round:

if CTRL.RE == 0 or no chardev: return
if chardev.has_input() and rx_fifo.size() < 8:
    push chardev.read_char()
    STATUS.DR = 1
    update_irq()

The FIFO is capped at 8; when full, tick simply skips the poll that cycle (no OV is synthesised in the current model). CPU reads of DATA (apbuart.cpp:33): if CTRL.RE == 0 or the FIFO is empty, return 0 (GRLIB convention); otherwise pop the head and clear STATUS.DR when the FIFO empties.

Interrupt logic

update_irq() (apbuart.cpp:109) is level-driven:

rx_irq = (CTRL.RI && STATUS.DR);
tx_irq = (CTRL.TI && STATUS.TE);   // STATUS.TE is always set
ctx.irq ? (rx_irq || tx_irq ? raise() : lower());

It is called on a CONTROL write and on a successful RX push.

Extended-IRQ delivery

UART IRQs above 15 (GR712RC apbuart1..5 = 17..21, GR740 both UARTs = 29/30) are in the extended range. The peripheral just calls ctx.irq->raise() with its bridge mask 1 << line; the IRQMP/IRQAMP redirects the trap to MPSTAT.EIRQ (default level 12) and records the real index in the per-CPU EID register on acknowledge. The peripheral is unaware of the redirection — it never knows its global line number.

Reset state

reset() (apbuart.cpp:13): STATUS = ResetStatus (TE|TS|TH|FA), CONTROL = CtrlFa, SCALER = 0, RX FIFO drained.

Tests

  • tests/unit/test_apbuart.cpp — register coverage, FIFO push/pop, IRQ raise on RI, TX dropped without TE, word-only access rejection.
  • tests/integration/test_hello_uart_elf.cpp — drives the assembly fixture end-to-end. The RTEMS hello-world boot test depends on this peripheral.

See also