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¶
- IRQMP / IRQAMP — extended-IRQ redirection for UART1..5 / GR740 UARTs.
- Peripheral system § PeripheralContext —
the
chardevinjection. - Configuration guide — binding a console device.