From e83110101d12a64fe544ac0f226511d6670f5061 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 8 May 2026 17:05:36 +0100 Subject: [PATCH 1/5] feat: S08.03 slice 2 CMSDK UART + newlib retargeting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace QEMU mps2-an385 HelloWorld's semihosting transport with a silicon-correct polled CMSDK UART0 driver and newlib retargeting, so printf/getchar route through `qemu -serial stdio` instead of BKPT traps. Foundation for slice 3+ where FreeRTOS-Plus-TCP runs concurrently with the example task — semihosting traps would stall the IP stack on every host-serviced read/write. Driver — Example/FreeRtos/HelloWorld/CmsdkUart.{h,c}: - Polls STATE.TX_FULL before writing DATA, per CMSDK APB UART TRM (DDI 0479C/D §4.3) and the QEMU model semantics (which set TX_FULL inside the DATA-write path even when the chardev backend drains synchronously). - Memory access injected via CmsdkUartMemoryAccess struct (read32 / write32 function pointers) — production wires MmioRead32/MmioWrite32 in main.c, tests wire CmsdkUartFake. - BAUDDIV (=16), TX_EN, RX off, IRQs off all hardcoded — no caller- facing surface to misconfigure. Newlib retargeting — Example/FreeRtos/HelloWorld/Syscalls.c: - _write routes to CmsdkUart_Write. _read returns EOF (slice 3 wires to UART RX). _sbrk is a 4 KiB bump allocator for newlib re-entrancy buffers (FreeRTOS heap_4 still owns the kernel/task heap). - Linker swap: --specs=rdimon.specs replaced with --specs=nano.specs --specs=nosys.specs. Tests — Tests/FreeRtos/CmsdkUart{Test,Fake}.{cpp,c,h}: - 8 host tests, each driven from a failing assertion. - CmsdkUartFake is a stateful in-memory model of the UART register block. STATE.TX_FULL clears after a configurable number of STATE reads (default 2; knob via SetReadsBeforeTxReady). TxOverrunOccurred flag flips when DATA is written while TX_FULL is set — the spin-loop test asserts it stays false across two consecutive PutChar calls. CI — .github/workflows/ci.yml: - build-freertos-target QEMU args: -nographic + -semihosting-config replaced with -display none -serial stdio. Banner-grep unchanged. References (CMSDK contract documented during review): - Arm DDI 0479C/D §4.3 — register layout, bit semantics - Arm DAI 0385D §3.7 — UART base addresses on mps2-an385 - QEMU hw/char/cmsdk-apb-uart.c — modelled semantics - Zephyr drivers/serial/uart_cmsdk_apb.c — canonical poll-out idiom - mbed-OS targets/TARGET_ARM_FM/TARGET_FVP_MPS2/serial_api.c — Arm HAL, BAUDDIV ≥ 16 minimum Closes #290. --- .github/workflows/ci.yml | 10 +- DEVLOG.md | 175 +++++++++++++++++++++ Example/FreeRtos/HelloWorld/CMakeLists.txt | 9 +- Example/FreeRtos/HelloWorld/CmsdkUart.c | 44 ++++++ Example/FreeRtos/HelloWorld/CmsdkUart.h | 29 ++++ Example/FreeRtos/HelloWorld/Syscalls.c | 107 +++++++++++++ Example/FreeRtos/HelloWorld/main.c | 30 +++- Tests/FreeRtos/CMakeLists.txt | 22 +++ Tests/FreeRtos/CmsdkUartFake.c | 139 ++++++++++++++++ Tests/FreeRtos/CmsdkUartFake.h | 39 +++++ Tests/FreeRtos/CmsdkUartTest.cpp | 69 ++++++++ 11 files changed, 666 insertions(+), 7 deletions(-) create mode 100644 Example/FreeRtos/HelloWorld/CmsdkUart.c create mode 100644 Example/FreeRtos/HelloWorld/CmsdkUart.h create mode 100644 Example/FreeRtos/HelloWorld/Syscalls.c create mode 100644 Tests/FreeRtos/CmsdkUartFake.c create mode 100644 Tests/FreeRtos/CmsdkUartFake.h create mode 100644 Tests/FreeRtos/CmsdkUartTest.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 002c151f..7d4415b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -704,6 +704,12 @@ jobs: # the success path here — we want the scheduler to keep idling so GDB # can attach in interactive use. # + # -display none + -serial stdio routes UART0 (CMSDK_UART0 @ 0x40004000) + # to the host's stdin/stdout. Slice 2 (#290) replaced semihosting with + # a polled CMSDK UART driver + newlib retargeting so the IP stack and + # console can run independently in slice 3+ — semihosting BKPT traps + # would otherwise stall the entire VM during every read/write. + # # -icount shift=auto,sleep=off,align=off advances the virtual clock by # retired guest instructions rather than wall-clock, decoupling guest # timing from host load. Forward-compatibility groundwork for the BDD @@ -714,8 +720,8 @@ jobs: set -o pipefail set +e OUT=$(timeout 5 qemu-system-arm \ - -M mps2-an385 -m 16M -nographic \ - -semihosting-config enable=on,target=native \ + -M mps2-an385 -m 16M \ + -display none -serial stdio \ -icount shift=auto,sleep=off,align=off \ -kernel build/freertos-cross/Example/FreeRtos/HelloWorld/SolidSyslogFreeRtosHelloWorld.elf \ 2>&1) diff --git a/DEVLOG.md b/DEVLOG.md index 6ff80bbc..35011539 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,180 @@ # Dev Log +## 2026-05-08 — S08.03 slice 2 — CMSDK UART + newlib retargeting (#290) + +### Decision + +Replaced the QEMU mps2-an385 HelloWorld's semihosting transport with a +silicon-correct polled CMSDK UART0 driver and newlib syscalls. `printf` +now routes through `_write` → `CmsdkUart_Write` → `CmsdkUart_PutChar` → +MMIO at `0x40004000`, which QEMU surfaces over `-serial stdio`. + +The motivation is forward compatibility with slice 3+, where +FreeRTOS-Plus-TCP runs concurrently with the example task. Semihosting +`BKPT` traps pause the entire VM during every host-serviced read/write — +on a blocking `_read` with no Behave input ready, the IP stack and the +`Service` task would also stall. CMSDK UART is plain MMIO: the IP stack and +console run independently, and Behave drives the QEMU image over +stdin/stdout exactly as it drives the Linux/Windows examples today. + +### Driver shape — injected memory access for testability + +Initial v1 of the driver had `CmsdkUart_PutChar` writing the `DATA` +register unconditionally, with no `STATE.TXFULL` poll. Code review caught +this: it works in QEMU because the chardev backend always drains +synchronously inside the DATA-write path, but on real silicon (or any +backpressuring backend) it would drop bytes. The Zephyr / mbed-OS +reference drivers and the CMSDK TRM (DDI 0479C/D §4.3) all specify the +poll-then-write protocol; we'd skipped it on the (then-rationalised) basis +that the spin loop wasn't host-TDD-able. + +The v2 driver lands silicon-correct via a memory-access seam: + +```c +typedef uint32_t (*CmsdkUartRead32Function)(uintptr_t address); +typedef void (*CmsdkUartWrite32Function)(uintptr_t address, uint32_t); +typedef struct { + CmsdkUartRead32Function read32; + CmsdkUartWrite32Function write32; +} CmsdkUartMemoryAccess; + +void CmsdkUart_Init(const CmsdkUartMemoryAccess* access, uintptr_t base); +void CmsdkUart_PutChar(char c); +void CmsdkUart_Write(const char* buffer, size_t length); +``` + +Production wires `MmioRead32` / `MmioWrite32` (in `main.c`) which cast +`address` to `volatile uint32_t*` and dereference. Tests wire a stateful +fake (`Tests/FreeRtos/CmsdkUartFake.{h,c}`) whose `Read32`/`Write32` +intercept every register access against an in-memory model, and where +the fake's behaviour can be tuned per-test via +`CmsdkUartFake_SetReadsBeforeTxReady(N)`. The fake clears `TX_FULL` on +the Nth STATE read after a DATA write (default `N=2`, modelling +silicon's per-character drain delay). + +This pins the spin contract: `CmsdkUartFake_TxOverrunOccurred()` flips +`true` if the driver writes DATA while `TX_FULL=1`. A test that does two +consecutive `PutChar` calls and asserts no overrun forces the spin loop +into existence — a naive driver fails the assertion immediately. + +Configuration (baud, parity, flow control) is hardcoded inside the +driver: BAUD_DIVISOR=16, TX_EN=1, no RX, no interrupts. There is no +caller-facing way to misconfigure those — kills an entire class of +silicon-traps the contract document called out (BAUDDIV<16 silently +broken on silicon, DATA-before-TX_EN implementation-defined, etc.). + +### What landed + +- **`Example/FreeRtos/HelloWorld/CmsdkUart.{h,c}`** — driver: `Init` + writes `BAUDDIV=16` then `CTRL=TX_EN`, `PutChar` polls `STATE.TX_FULL` + before writing `DATA`, `Write` loops `PutChar` over a buffer. Module- + static `memoryAccess` + `base` (the syscall layer needs a singleton — + no context to thread). +- **`Example/FreeRtos/HelloWorld/Syscalls.c`** — `_write`/`_read`/`_sbrk` + + minimal nosys-overriding stubs. `_sbrk` is a 4 KiB bump allocator + for newlib re-entrancy buffers (FreeRTOS heap_4 still owns the + kernel/task heap). `_read` returns 0 (EOF) for now; slice 3 wires it + to a UART RX helper so `Example/Common/ExampleInteractive.c` can + drive `send N` / `quit`. Cross-only file, untested at the host layer + — covered by the QEMU banner smoke. +- **`Example/FreeRtos/HelloWorld/main.c`** — drops + `initialise_monitor_handles`, defines `MmioRead32` / `MmioWrite32` + for the production memory-access seam, and initialises `CmsdkUart` + with `&MMIO_ACCESS` + `0x40004000`. Banner string unchanged. +- **`Example/FreeRtos/HelloWorld/CMakeLists.txt`** — + `--specs=rdimon.specs` replaced with `--specs=nano.specs + --specs=nosys.specs`. CmsdkUart.c + Syscalls.c added to the ELF + source list. +- **`Tests/FreeRtos/CmsdkUartFake.{h,c}`** — stateful in-memory fake + for the CMSDK UART register block. Models `STATE.TX_FULL` set on + DATA writes and cleared after N STATE reads (default 2, knob via + `SetReadsBeforeTxReady`); records `TX_OVRE` and a sticky + `txOverrunOccurred` flag when DATA is written while `TX_FULL=1`. + W1C semantics on STATE/INTSTATUS modelled per QEMU + `hw/char/cmsdk-apb-uart.c`. +- **`Tests/FreeRtos/CmsdkUartTest.cpp`** — 8 host tests against the + fake, driven from failing assertions one at a time. +- **`.github/workflows/ci.yml`** — `build-freertos-target` swaps + `-nographic -semihosting-config enable=on,target=native` for + `-display none -serial stdio`. Banner-grep unchanged. + +### Slice 2 ZOMBIES progression + +Each test landed against a failing assertion before the matching +production line: + +1. `InitWritesBaudDivisor` → forces `write32(base+0x10, 16)`. +2. `InitEnablesTransmitter` → forces `write32(base+0x08, TX_EN)`. +3. `PutCharWritesByteToDataRegister` → forces a DATA write. +4. `PutCharWritesTheGivenByte` → forces parameter use (also satisfies + `-Werror=unused-parameter`). +5. `PutCharSpinsForTxFullToClearBeforeWritingNextByte` → two + consecutive `PutChar` calls, asserts no overrun. Forces the + `while (state & TX_FULL_BIT)` poll. +6. `PutCharWritesImmediatelyWhenTransmitterIsAlwaysReady` — + `SetReadsBeforeTxReady(0)`, two `PutChar` calls, no overrun. Proves + the spin path also works under always-ready (the QEMU model's + actual behaviour) without spurious overrun. +7. `WriteOfSingleByteEmitsThatByte` → forces `Write` to call `PutChar`. +8. `WriteOfMultipleBytesEmitsAllByteWithoutOverrun` → forces the loop. + +### Deferred to later slices + +- **`quit` / round-trip stdin handling** — slice 3 brings in + `Example/Common/ExampleInteractive.c`, where `_read` will route to a + UART RX helper. Slice 2's banner-only smoke is sufficient evidence + that the TX path works. +- **RX path** (`CmsdkUart_GetChar`, `STATE.RX_FULL` polling, `RX_OVRE` + semantics) — slice 3 alongside the IP stack bring-up. +- **Mutex injection** — single-task HelloWorld, single-producer. + Slice 3's `Example/Common/` brings a Service thread + interactive + task, both of which can `printf`; mutex slot will be added then. +- **Tidy / cppcheck on `Tests/FreeRtos/`** — pre-existing gap (slice 1 + inherits the same — those files only enter the build when + `FREERTOS_KERNEL_PATH` is set, which the analyze-* CI jobs don't + set). Defer to a later infrastructure pass. + +### Verified locally + +- `cpputest-freertos:sha-44efeae` host TDD — `SolidSyslogTests`, + `SolidSyslogFreeRtosDatagramTest`, `CmsdkUartTest`: 3/3 green. +- `cpputest-freertos-cross:sha-44efeae` cross build + QEMU mps2-an385 + with `-display none -serial stdio` — banner emitted, `rc=124` + (timeout success path, scheduler still idling for GDB attach). +- `clang-format --dry-run --Werror` over `Core/Interface Core/Source + Tests Example` — clean. (`TEST_GROUP` with no data members trips + clang-format's class-detection heuristic — added a localised + `// clang-format off`/`on` pair around the fixture.) + +### Pre-flight for slice 3 — verified now to avoid late surprises + +- `/opt/freertos/plus-tcp/source/portable/NetworkInterface/MPS2_AN385/` + exists in the cross image (`NetworkInterface.c` + `ether_lan9118/`). + The LAN9118 NIC driver is in place — slice 3's IP-stack bring-up + isn't blocked on missing portable code. + +### References + +- Arm DDI 0479C/D, *Cortex-M System Design Kit TRM*, §4.3 APB UART — + register layout and bit semantics. +- Arm DAI 0385D, *AN385: Cortex-M3 SMM on V2M-MPS2*, §3.7 — UART base + addresses on `mps2-an385`. +- QEMU `hw/char/cmsdk-apb-uart.c` — modelled semantics (W1C handling, + TXFULL set inside DATA write, `uart_can_receive` chardev + backpressure that hides `RX_OVRE` under stdio). +- Zephyr `drivers/serial/uart_cmsdk_apb.c` — canonical poll-out idiom. +- mbed-OS `targets/TARGET_ARM_FM/TARGET_FVP_MPS2/serial_api.c` — Arm's + own HAL; corroborates `BAUDDIV ≥ 16` minimum. + +A full contract document covering the polled-mode TX/RX register +sequences, QEMU-vs-silicon traps, and concurrency considerations was +written up by claude.ai during slice-2 review and informed the v2 +rewrite. The document lives in conversation history (PR #291 review +trail); worth promoting into `docs/` if we hit similar peripheral +work in future epics. + +--- + ## 2026-05-08 — S08.03 slice 1 — `SolidSyslogFreeRtosDatagram` adapter ### Decision diff --git a/Example/FreeRtos/HelloWorld/CMakeLists.txt b/Example/FreeRtos/HelloWorld/CMakeLists.txt index b1aaa574..1ba29838 100644 --- a/Example/FreeRtos/HelloWorld/CMakeLists.txt +++ b/Example/FreeRtos/HelloWorld/CMakeLists.txt @@ -15,6 +15,8 @@ set(FREERTOS_PORT_DIR "${FREERTOS_KERNEL_PATH}/portable/GCC/ARM_CM3") add_executable(SolidSyslogFreeRtosHelloWorld main.c startup.c + CmsdkUart.c + Syscalls.c ${FREERTOS_KERNEL_PATH}/tasks.c ${FREERTOS_KERNEL_PATH}/queue.c ${FREERTOS_KERNEL_PATH}/list.c @@ -46,7 +48,12 @@ target_link_options(SolidSyslogFreeRtosHelloWorld PRIVATE -mcpu=cortex-m3 -mthumb -T${CMAKE_CURRENT_SOURCE_DIR}/mps2-an385.ld - --specs=rdimon.specs # newlib semihosting stubs + # nano: size-optimised newlib variant. + # nosys: empty syscall stubs for symbols Syscalls.c does not override + # (open, unlink, etc.); our _write/_read/_sbrk wins as a strong + # symbol, so printf routes to CmsdkUart over QEMU `-serial stdio`. + --specs=nano.specs + --specs=nosys.specs -Wl,--gc-sections -Wl,-Map=$.map ) diff --git a/Example/FreeRtos/HelloWorld/CmsdkUart.c b/Example/FreeRtos/HelloWorld/CmsdkUart.c new file mode 100644 index 00000000..68ecc0b2 --- /dev/null +++ b/Example/FreeRtos/HelloWorld/CmsdkUart.c @@ -0,0 +1,44 @@ +#include "CmsdkUart.h" + +#include + +#define DATA_OFFSET 0x000U +#define STATE_OFFSET 0x004U +#define CTRL_OFFSET 0x008U +#define BAUDDIV_OFFSET 0x010U + +#define BAUD_DIVISOR 16U +#define TX_ENABLE 0x01U +#define TX_FULL_BIT 0x01U + +static const CmsdkUartMemoryAccess* memoryAccess = NULL; +static uintptr_t base = 0U; + +void CmsdkUart_Init(const CmsdkUartMemoryAccess* access, uintptr_t baseAddress) +{ + memoryAccess = access; + base = baseAddress; + access->write32(baseAddress + BAUDDIV_OFFSET, BAUD_DIVISOR); + access->write32(baseAddress + CTRL_OFFSET, TX_ENABLE); +} + +void CmsdkUart_PutChar(char c) +{ + while ((memoryAccess->read32(base + STATE_OFFSET) & TX_FULL_BIT) != 0U) + { + /* Spin while the transmit holding register is occupied — writing to + * DATA in this state sets STATE.TX_OVRE on silicon and the byte is + * lost. QEMU's CMSDK model drains synchronously inside the DATA + * write, so this spin never iterates there; it is silicon-correct + * and the host-side fake exercises both the busy and idle paths. */ + } + memoryAccess->write32(base + DATA_OFFSET, (uint32_t) (unsigned char) c); +} + +void CmsdkUart_Write(const char* buffer, size_t length) +{ + for (size_t i = 0; i < length; ++i) + { + CmsdkUart_PutChar(buffer[i]); + } +} diff --git a/Example/FreeRtos/HelloWorld/CmsdkUart.h b/Example/FreeRtos/HelloWorld/CmsdkUart.h new file mode 100644 index 00000000..b1954aa5 --- /dev/null +++ b/Example/FreeRtos/HelloWorld/CmsdkUart.h @@ -0,0 +1,29 @@ +#ifndef CMSDK_UART_H +#define CMSDK_UART_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef uint32_t (*CmsdkUartRead32Function)(uintptr_t address); + typedef void (*CmsdkUartWrite32Function)(uintptr_t address, uint32_t value); + + typedef struct + { + CmsdkUartRead32Function read32; + CmsdkUartWrite32Function write32; + } CmsdkUartMemoryAccess; + + void CmsdkUart_Init(const CmsdkUartMemoryAccess* access, uintptr_t baseAddress); + void CmsdkUart_PutChar(char c); + void CmsdkUart_Write(const char* buffer, size_t length); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Example/FreeRtos/HelloWorld/Syscalls.c b/Example/FreeRtos/HelloWorld/Syscalls.c new file mode 100644 index 00000000..d4598c50 --- /dev/null +++ b/Example/FreeRtos/HelloWorld/Syscalls.c @@ -0,0 +1,107 @@ +/* Newlib system-call retargeting for the QEMU mps2-an385 FreeRTOS image. + * + * Replaces the rdimon (semihosting) syscalls. printf and friends route + * through _write -> CmsdkUart_Write -> the CMSDK UART0 data register, which + * QEMU surfaces over `-serial stdio`. Reads return EOF for now; slice 3 + * wires _read to the UART receive path so Example/Common/ExampleInteractive + * can drive `send N` / `quit` over the same serial channel. + * + * Not host-TDD'd — this file exists only in the cross build (gated by + * CMAKE_CROSSCOMPILING + arm in the top-level CMakeLists.txt). The QEMU + * banner smoke is the integration check that proves the path end-to-end. */ + +#include "CmsdkUart.h" + +#include +#include +#include + +/* Small heap reserved for newlib re-entrancy buffers (printf's _impure_ptr). + * FreeRTOS heap_4 manages the kernel/task heap separately. */ +#define SYSCALL_HEAP_SIZE 4096U + +static char syscallHeap[SYSCALL_HEAP_SIZE]; +static char* syscallHeapBreak = syscallHeap; + +void* _sbrk(int increment) +{ + char* previousBreak = syscallHeapBreak; + char* nextBreak = syscallHeapBreak + increment; + void* result = (void*) -1; + if (nextBreak >= syscallHeap && (size_t) (nextBreak - syscallHeap) <= sizeof(syscallHeap)) + { + syscallHeapBreak = nextBreak; + result = previousBreak; + } + else + { + errno = ENOMEM; + } + return result; +} + +int _write(int file, char* buffer, int length) +{ + (void) file; + if (length > 0) + { + CmsdkUart_Write(buffer, (size_t) length); + } + return length; +} + +int _read(int file, char* buffer, int length) +{ + (void) file; + (void) buffer; + (void) length; + return 0; +} + +int _close(int file) +{ + (void) file; + return -1; +} + +int _lseek(int file, int offset, int whence) +{ + (void) file; + (void) offset; + (void) whence; + return 0; +} + +int _fstat(int file, struct stat* status) +{ + (void) file; + status->st_mode = S_IFCHR; + return 0; +} + +int _isatty(int file) +{ + (void) file; + return 1; +} + +int _kill(int pid, int signal) +{ + (void) pid; + (void) signal; + errno = EINVAL; + return -1; +} + +int _getpid(void) +{ + return 1; +} + +void _exit(int status) +{ + (void) status; + for (;;) + { + } +} diff --git a/Example/FreeRtos/HelloWorld/main.c b/Example/FreeRtos/HelloWorld/main.c index 1dd5c78e..e1c111cd 100644 --- a/Example/FreeRtos/HelloWorld/main.c +++ b/Example/FreeRtos/HelloWorld/main.c @@ -1,11 +1,33 @@ +#include "CmsdkUart.h" + #include #include +#include #include -/* newlib rdimon stub — opens stdin/stdout/stderr via semihosting so that - * printf is routed to QEMU's host stdout. */ -extern void initialise_monitor_handles(void); +/* CMSDK UART0 base on QEMU mps2-an385 (Cortex-M3). QEMU exposes UART0 over + * `-serial stdio`, decoupling stdout from the IP stack so the network and + * console can run independently — see #290 / DEVLOG. */ +#define CMSDK_UART0_BASE_ADDRESS UINT32_C(0x40004000) + +/* Real MMIO accessors injected into CmsdkUart via the CmsdkUartMemoryAccess + * seam. The driver itself never touches volatile memory directly — that + * keeps the polled-spin and register-write logic host-TDD-able against an + * in-memory fake (see Tests/FreeRtos/CmsdkUartFake). */ +static uint32_t MmioRead32(uintptr_t address) +{ + // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. + return *(volatile uint32_t*) address; +} + +static void MmioWrite32(uintptr_t address, uint32_t value) +{ + // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. + *(volatile uint32_t*) address = value; +} + +static const CmsdkUartMemoryAccess MMIO_ACCESS = {MmioRead32, MmioWrite32}; static void HelloTask(void* argument) { @@ -23,7 +45,7 @@ static void HelloTask(void* argument) int main(void) { - initialise_monitor_handles(); + CmsdkUart_Init(&MMIO_ACCESS, CMSDK_UART0_BASE_ADDRESS); if (xTaskCreate(HelloTask, "hello", configMINIMAL_STACK_SIZE * 4, NULL, tskIDLE_PRIORITY + 1, NULL) != pdPASS) { diff --git a/Tests/FreeRtos/CMakeLists.txt b/Tests/FreeRtos/CMakeLists.txt index 78c68afd..f544342c 100644 --- a/Tests/FreeRtos/CMakeLists.txt +++ b/Tests/FreeRtos/CMakeLists.txt @@ -34,3 +34,25 @@ target_include_directories(SolidSyslogFreeRtosDatagramTest PRIVATE ) add_test(NAME SolidSyslogFreeRtosDatagramTest COMMAND SolidSyslogFreeRtosDatagramTest) + +# CMSDK UART driver host tests — recompiles the driver source from the +# Example/FreeRtos/HelloWorld board glue against an in-memory fake register +# block. No FreeRTOS-Plus-TCP involvement, so the include path is intentionally +# narrower than the datagram test. +add_executable(CmsdkUartTest + CmsdkUartTest.cpp + CmsdkUartFake.c + main.cpp + ${CMAKE_SOURCE_DIR}/Example/FreeRtos/HelloWorld/CmsdkUart.c +) + +target_link_libraries(CmsdkUartTest PRIVATE + CppUTest + CppUTestExt +) + +target_include_directories(CmsdkUartTest PRIVATE + ${CMAKE_SOURCE_DIR}/Example/FreeRtos/HelloWorld +) + +add_test(NAME CmsdkUartTest COMMAND CmsdkUartTest) diff --git a/Tests/FreeRtos/CmsdkUartFake.c b/Tests/FreeRtos/CmsdkUartFake.c new file mode 100644 index 00000000..68ac46e0 --- /dev/null +++ b/Tests/FreeRtos/CmsdkUartFake.c @@ -0,0 +1,139 @@ +#include "CmsdkUartFake.h" + +#include + +#define DATA_OFFSET 0x000U +#define STATE_OFFSET 0x004U +#define CTRL_OFFSET 0x008U +#define INTSTAT_OFFSET 0x00CU +#define BAUDDIV_OFFSET 0x010U + +#define TX_FULL_BIT 0x01U +#define TX_OVRE_BIT 0x04U + +static struct +{ + uintptr_t base; + uint32_t data; + uint32_t state; + uint32_t ctrl; + uint32_t intStatusOrClear; + uint32_t bauddiv; + int readsBeforeTxReadyDefault; + int readsRemainingBeforeTxReady; + bool txOverrunOccurred; +} fake; + +static uint32_t Fake_Read32(uintptr_t address) +{ + uint32_t result = 0; + uintptr_t offset = address - fake.base; + if (offset == STATE_OFFSET) + { + if (fake.readsRemainingBeforeTxReady > 0) + { + fake.readsRemainingBeforeTxReady--; + } + if (fake.readsRemainingBeforeTxReady == 0) + { + fake.state &= ~TX_FULL_BIT; + } + result = fake.state; + } + else if (offset == DATA_OFFSET) + { + result = fake.data; + } + else if (offset == CTRL_OFFSET) + { + result = fake.ctrl; + } + else if (offset == BAUDDIV_OFFSET) + { + result = fake.bauddiv; + } + else if (offset == INTSTAT_OFFSET) + { + result = fake.intStatusOrClear; + } + return result; +} + +static void Fake_Write32(uintptr_t address, uint32_t value) +{ + uintptr_t offset = address - fake.base; + if (offset == DATA_OFFSET) + { + if ((fake.state & TX_FULL_BIT) != 0U) + { + fake.state |= TX_OVRE_BIT; + fake.txOverrunOccurred = true; + } + fake.data = value; + fake.state |= TX_FULL_BIT; + fake.readsRemainingBeforeTxReady = fake.readsBeforeTxReadyDefault; + } + else if (offset == STATE_OFFSET) + { + /* W1C — only the overrun bits are software-clearable. */ + fake.state &= ~(value & (TX_OVRE_BIT | 0x08U)); + } + else if (offset == CTRL_OFFSET) + { + fake.ctrl = value; + } + else if (offset == BAUDDIV_OFFSET) + { + fake.bauddiv = value; + } + else if (offset == INTSTAT_OFFSET) + { + fake.state &= ~(value & (TX_OVRE_BIT | 0x08U)); + fake.intStatusOrClear &= ~value; + } +} + +static const CmsdkUartMemoryAccess FAKE_ACCESS = {Fake_Read32, Fake_Write32}; + +void CmsdkUartFake_Reset(uintptr_t baseAddress) +{ + fake.base = baseAddress; + fake.data = 0U; + fake.state = 0U; + fake.ctrl = 0U; + fake.intStatusOrClear = 0U; + fake.bauddiv = 0U; + fake.readsBeforeTxReadyDefault = 2; + fake.readsRemainingBeforeTxReady = 0; + fake.txOverrunOccurred = false; +} + +const CmsdkUartMemoryAccess* CmsdkUartFake_Access(void) +{ + return &FAKE_ACCESS; +} + +uint32_t CmsdkUartFake_GetData(void) +{ + return fake.data; +} + +uint32_t CmsdkUartFake_GetCtrl(void) +{ + return fake.ctrl; +} + +uint32_t CmsdkUartFake_GetBaudDiv(void) +{ + return fake.bauddiv; +} + +void CmsdkUartFake_SetReadsBeforeTxReady(int reads) +{ + fake.readsBeforeTxReadyDefault = reads; +} + +bool CmsdkUartFake_TxOverrunOccurred(void) +{ + return fake.txOverrunOccurred; +} diff --git a/Tests/FreeRtos/CmsdkUartFake.h b/Tests/FreeRtos/CmsdkUartFake.h new file mode 100644 index 00000000..585f8418 --- /dev/null +++ b/Tests/FreeRtos/CmsdkUartFake.h @@ -0,0 +1,39 @@ +#ifndef CMSDK_UART_FAKE_H +#define CMSDK_UART_FAKE_H + +#include "CmsdkUart.h" + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* Reset the fake's internal state. baseAddress must match the value passed + * to CmsdkUart_Init so the fake can decode register offsets. */ + void CmsdkUartFake_Reset(uintptr_t baseAddress); + + /* CmsdkUartMemoryAccess wired to the fake's intercept functions. Pass to + * CmsdkUart_Init in tests instead of the real MMIO access. */ + const CmsdkUartMemoryAccess* CmsdkUartFake_Access(void); + + uint32_t CmsdkUartFake_GetData(void); + uint32_t CmsdkUartFake_GetCtrl(void); + uint32_t CmsdkUartFake_GetBaudDiv(void); + + /* Number of STATE reads after a DATA write before the fake clears TX_FULL. + * Default is 2. Set to 0 for "always ready" (TX_FULL never observed set). + * Models the per-character drain delay the QEMU stdio backend hides. */ + void CmsdkUartFake_SetReadsBeforeTxReady(int reads); + + /* True when the driver wrote to DATA while STATE.TX_FULL was set — i.e. + * the spin loop was missing or broken. Mirrors STATE.TX_OVRE on silicon. */ + bool CmsdkUartFake_TxOverrunOccurred(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Tests/FreeRtos/CmsdkUartTest.cpp b/Tests/FreeRtos/CmsdkUartTest.cpp new file mode 100644 index 00000000..34902b16 --- /dev/null +++ b/Tests/FreeRtos/CmsdkUartTest.cpp @@ -0,0 +1,69 @@ +#include "CppUTest/TestHarness.h" + +#include "CmsdkUart.h" +#include "CmsdkUartFake.h" + +static const uintptr_t TEST_BASE_ADDRESS = 0x40004000U; + +// clang-format off +TEST_GROUP(CmsdkUart) +{ + void setup() override + { + CmsdkUartFake_Reset(TEST_BASE_ADDRESS); + CmsdkUart_Init(CmsdkUartFake_Access(), TEST_BASE_ADDRESS); + } +}; +// clang-format on + +TEST(CmsdkUart, InitWritesBaudDivisor) +{ + LONGS_EQUAL(16, CmsdkUartFake_GetBaudDiv()); +} + +TEST(CmsdkUart, InitEnablesTransmitter) +{ + LONGS_EQUAL(0x01, CmsdkUartFake_GetCtrl() & 0x01); +} + +TEST(CmsdkUart, PutCharWritesByteToDataRegister) +{ + CmsdkUart_PutChar('A'); + LONGS_EQUAL('A', CmsdkUartFake_GetData()); +} + +TEST(CmsdkUart, PutCharWritesTheGivenByte) +{ + CmsdkUart_PutChar('B'); + LONGS_EQUAL('B', CmsdkUartFake_GetData()); +} + +TEST(CmsdkUart, PutCharSpinsForTxFullToClearBeforeWritingNextByte) +{ + CmsdkUart_PutChar('A'); + CmsdkUart_PutChar('B'); + LONGS_EQUAL('B', CmsdkUartFake_GetData()); + CHECK_FALSE(CmsdkUartFake_TxOverrunOccurred()); +} + +TEST(CmsdkUart, PutCharWritesImmediatelyWhenTransmitterIsAlwaysReady) +{ + CmsdkUartFake_SetReadsBeforeTxReady(0); + CmsdkUart_PutChar('X'); + CmsdkUart_PutChar('Y'); + LONGS_EQUAL('Y', CmsdkUartFake_GetData()); + CHECK_FALSE(CmsdkUartFake_TxOverrunOccurred()); +} + +TEST(CmsdkUart, WriteOfSingleByteEmitsThatByte) +{ + CmsdkUart_Write("X", 1); + LONGS_EQUAL('X', CmsdkUartFake_GetData()); +} + +TEST(CmsdkUart, WriteOfMultipleBytesEmitsAllByteWithoutOverrun) +{ + CmsdkUart_Write("AB", 2); + LONGS_EQUAL('B', CmsdkUartFake_GetData()); + CHECK_FALSE(CmsdkUartFake_TxOverrunOccurred()); +} From 7a650ce4852386a704709ae79a75df8b68a7bc48 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 8 May 2026 18:10:50 +0100 Subject: [PATCH 2/5] wip: extract IsWithinSyscallHeap predicate in _sbrk Composite bounds check (pointer arithmetic + cast + two comparisons) moved into a static inline named predicate per CodeRabbit review and the project's intent-naming-predicates rule. Helper forward-declared above _sbrk and defined immediately beneath it. Refs #290. --- Example/FreeRtos/HelloWorld/Syscalls.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Example/FreeRtos/HelloWorld/Syscalls.c b/Example/FreeRtos/HelloWorld/Syscalls.c index d4598c50..e00da499 100644 --- a/Example/FreeRtos/HelloWorld/Syscalls.c +++ b/Example/FreeRtos/HelloWorld/Syscalls.c @@ -13,6 +13,7 @@ #include "CmsdkUart.h" #include +#include #include #include @@ -23,12 +24,14 @@ static char syscallHeap[SYSCALL_HEAP_SIZE]; static char* syscallHeapBreak = syscallHeap; +static inline bool IsWithinSyscallHeap(const char* candidateBreak); + void* _sbrk(int increment) { char* previousBreak = syscallHeapBreak; char* nextBreak = syscallHeapBreak + increment; void* result = (void*) -1; - if (nextBreak >= syscallHeap && (size_t) (nextBreak - syscallHeap) <= sizeof(syscallHeap)) + if (IsWithinSyscallHeap(nextBreak)) { syscallHeapBreak = nextBreak; result = previousBreak; @@ -40,6 +43,11 @@ void* _sbrk(int increment) return result; } +static inline bool IsWithinSyscallHeap(const char* candidateBreak) +{ + return candidateBreak >= syscallHeap && (size_t) (candidateBreak - syscallHeap) <= sizeof(syscallHeap); +} + int _write(int file, char* buffer, int length) { (void) file; From 3f4ab24419a3f389055f668c9e6e60683aafa733 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 8 May 2026 18:10:58 +0100 Subject: [PATCH 3/5] wip: scope FREERTOS_PLUS_TCP_PATH guard to datagram test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CmsdkUartTest tests UART board glue and never includes Plus-TCP headers, so the global FATAL_ERROR was wrongly blocking it in FREERTOS_KERNEL_PATH-only environments. Wrap the existence check around SolidSyslogFreeRtosDatagramTest only — print a STATUS message and skip that target when the path is unset, leave CmsdkUartTest unaffected. Per CodeRabbit nitpick. Refs #290. --- Tests/FreeRtos/CMakeLists.txt | 59 ++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/Tests/FreeRtos/CMakeLists.txt b/Tests/FreeRtos/CMakeLists.txt index f544342c..27be70aa 100644 --- a/Tests/FreeRtos/CMakeLists.txt +++ b/Tests/FreeRtos/CMakeLists.txt @@ -3,38 +3,39 @@ # source under test with the FreeRtosFakes config and links against the # fakes — see project_header_configured_platforms memory. -# Top-level CMakeLists.txt only checks FREERTOS_KERNEL_PATH before adding this -# subdirectory. Adapter tests transitively include FreeRTOS-Plus-TCP headers, -# so guard FREERTOS_PLUS_TCP_PATH here too — fail loud rather than expand to -# a stray "/source/include" suffix on an empty path. +# SolidSyslogFreeRtosDatagramTest transitively includes FreeRTOS-Plus-TCP +# headers, so it requires FREERTOS_PLUS_TCP_PATH. CmsdkUartTest does not — +# it tests UART board glue and never touches the IP stack. Scope the guard +# to the datagram test so a minimal FREERTOS_KERNEL_PATH-only environment +# can still build the UART test. if("$ENV{FREERTOS_PLUS_TCP_PATH}" STREQUAL "") - message(FATAL_ERROR "FREERTOS_PLUS_TCP_PATH must be set to build Tests/FreeRtos.") + message(STATUS "Skipping SolidSyslogFreeRtosDatagramTest: FREERTOS_PLUS_TCP_PATH not set.") +else() + add_executable(SolidSyslogFreeRtosDatagramTest + SolidSyslogFreeRtosDatagramTest.cpp + main.cpp + ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosDatagram.c + ${CMAKE_SOURCE_DIR}/Tests/Support/FreeRtosFakes/Source/FreeRtosSocketsFake.c + ) + + target_link_libraries(SolidSyslogFreeRtosDatagramTest PRIVATE + ${PROJECT_NAME} + CppUTest + CppUTestExt + ) + + target_include_directories(SolidSyslogFreeRtosDatagramTest PRIVATE + ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Interface + ${CMAKE_SOURCE_DIR}/Core/Interface + ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_SOURCE_DIR}/Tests/Support/FreeRtosFakes/Interface + $ENV{FREERTOS_KERNEL_PATH}/include + $ENV{FREERTOS_PLUS_TCP_PATH}/source/include + ) + + add_test(NAME SolidSyslogFreeRtosDatagramTest COMMAND SolidSyslogFreeRtosDatagramTest) endif() -add_executable(SolidSyslogFreeRtosDatagramTest - SolidSyslogFreeRtosDatagramTest.cpp - main.cpp - ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosDatagram.c - ${CMAKE_SOURCE_DIR}/Tests/Support/FreeRtosFakes/Source/FreeRtosSocketsFake.c -) - -target_link_libraries(SolidSyslogFreeRtosDatagramTest PRIVATE - ${PROJECT_NAME} - CppUTest - CppUTestExt -) - -target_include_directories(SolidSyslogFreeRtosDatagramTest PRIVATE - ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Interface - ${CMAKE_SOURCE_DIR}/Core/Interface - ${CMAKE_SOURCE_DIR}/Core/Source - ${CMAKE_SOURCE_DIR}/Tests/Support/FreeRtosFakes/Interface - $ENV{FREERTOS_KERNEL_PATH}/include - $ENV{FREERTOS_PLUS_TCP_PATH}/source/include -) - -add_test(NAME SolidSyslogFreeRtosDatagramTest COMMAND SolidSyslogFreeRtosDatagramTest) - # CMSDK UART driver host tests — recompiles the driver source from the # Example/FreeRtos/HelloWorld board glue against an in-memory fake register # block. No FreeRTOS-Plus-TCP involvement, so the include path is intentionally From 1109eb3647790d8550da26a3e6d453a03979bf4b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 8 May 2026 18:11:08 +0100 Subject: [PATCH 4/5] wip: yield via injected sleep in PutChar spin + intent-named helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CmsdkUartMemoryAccess gains a sleep(int milliseconds) hook called inside PutChar's STATE.TX_FULL spin so the polled wait doesn't burn CPU on real silicon. Production wires vTaskDelay(pdMS_TO_TICKS(1)); the host fake counts the calls and a new test asserts the spin yields between status reads. Driver body extracted into intent-named static inline helpers — Init becomes SetBaudDivisor() then EnableTransmitter(); PutChar becomes while (TransmitterIsBusy()) Yield(); WriteDataRegister(c). Helpers forward-declared at the top of the file, defined immediately beneath their first caller. No internal comments — names and CMSDK_UART.md carry the documentation. Refs #290. --- Example/FreeRtos/HelloWorld/CmsdkUart.c | 46 ++++++++++++++++++++----- Example/FreeRtos/HelloWorld/CmsdkUart.h | 2 ++ Example/FreeRtos/HelloWorld/main.c | 14 ++++---- Tests/FreeRtos/CmsdkUartFake.c | 15 +++++++- Tests/FreeRtos/CmsdkUartFake.h | 4 +++ Tests/FreeRtos/CmsdkUartTest.cpp | 7 ++++ 6 files changed, 71 insertions(+), 17 deletions(-) diff --git a/Example/FreeRtos/HelloWorld/CmsdkUart.c b/Example/FreeRtos/HelloWorld/CmsdkUart.c index 68ecc0b2..054dea56 100644 --- a/Example/FreeRtos/HelloWorld/CmsdkUart.c +++ b/Example/FreeRtos/HelloWorld/CmsdkUart.c @@ -1,5 +1,6 @@ #include "CmsdkUart.h" +#include #include #define DATA_OFFSET 0x000U @@ -11,27 +12,56 @@ #define TX_ENABLE 0x01U #define TX_FULL_BIT 0x01U +#define YIELD_MILLISECONDS 1 + static const CmsdkUartMemoryAccess* memoryAccess = NULL; static uintptr_t base = 0U; +static inline void SetBaudDivisor(void); +static inline void EnableTransmitter(void); +static inline bool TransmitterIsBusy(void); +static inline void Yield(void); +static inline void WriteDataRegister(char c); + void CmsdkUart_Init(const CmsdkUartMemoryAccess* access, uintptr_t baseAddress) { memoryAccess = access; base = baseAddress; - access->write32(baseAddress + BAUDDIV_OFFSET, BAUD_DIVISOR); - access->write32(baseAddress + CTRL_OFFSET, TX_ENABLE); + SetBaudDivisor(); + EnableTransmitter(); +} + +static inline void SetBaudDivisor(void) +{ + memoryAccess->write32(base + BAUDDIV_OFFSET, BAUD_DIVISOR); +} + +static inline void EnableTransmitter(void) +{ + memoryAccess->write32(base + CTRL_OFFSET, TX_ENABLE); } void CmsdkUart_PutChar(char c) { - while ((memoryAccess->read32(base + STATE_OFFSET) & TX_FULL_BIT) != 0U) + while (TransmitterIsBusy()) { - /* Spin while the transmit holding register is occupied — writing to - * DATA in this state sets STATE.TX_OVRE on silicon and the byte is - * lost. QEMU's CMSDK model drains synchronously inside the DATA - * write, so this spin never iterates there; it is silicon-correct - * and the host-side fake exercises both the busy and idle paths. */ + Yield(); } + WriteDataRegister(c); +} + +static inline bool TransmitterIsBusy(void) +{ + return (memoryAccess->read32(base + STATE_OFFSET) & TX_FULL_BIT) != 0U; +} + +static inline void Yield(void) +{ + memoryAccess->sleep(YIELD_MILLISECONDS); +} + +static inline void WriteDataRegister(char c) +{ memoryAccess->write32(base + DATA_OFFSET, (uint32_t) (unsigned char) c); } diff --git a/Example/FreeRtos/HelloWorld/CmsdkUart.h b/Example/FreeRtos/HelloWorld/CmsdkUart.h index b1954aa5..7ac22c05 100644 --- a/Example/FreeRtos/HelloWorld/CmsdkUart.h +++ b/Example/FreeRtos/HelloWorld/CmsdkUart.h @@ -11,11 +11,13 @@ extern "C" typedef uint32_t (*CmsdkUartRead32Function)(uintptr_t address); typedef void (*CmsdkUartWrite32Function)(uintptr_t address, uint32_t value); + typedef void (*CmsdkUartSleepFunction)(int milliseconds); typedef struct { CmsdkUartRead32Function read32; CmsdkUartWrite32Function write32; + CmsdkUartSleepFunction sleep; } CmsdkUartMemoryAccess; void CmsdkUart_Init(const CmsdkUartMemoryAccess* access, uintptr_t baseAddress); diff --git a/Example/FreeRtos/HelloWorld/main.c b/Example/FreeRtos/HelloWorld/main.c index e1c111cd..86e15980 100644 --- a/Example/FreeRtos/HelloWorld/main.c +++ b/Example/FreeRtos/HelloWorld/main.c @@ -6,15 +6,8 @@ #include #include -/* CMSDK UART0 base on QEMU mps2-an385 (Cortex-M3). QEMU exposes UART0 over - * `-serial stdio`, decoupling stdout from the IP stack so the network and - * console can run independently — see #290 / DEVLOG. */ #define CMSDK_UART0_BASE_ADDRESS UINT32_C(0x40004000) -/* Real MMIO accessors injected into CmsdkUart via the CmsdkUartMemoryAccess - * seam. The driver itself never touches volatile memory directly — that - * keeps the polled-spin and register-write logic host-TDD-able against an - * in-memory fake (see Tests/FreeRtos/CmsdkUartFake). */ static uint32_t MmioRead32(uintptr_t address) { // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. @@ -27,7 +20,12 @@ static void MmioWrite32(uintptr_t address, uint32_t value) *(volatile uint32_t*) address = value; } -static const CmsdkUartMemoryAccess MMIO_ACCESS = {MmioRead32, MmioWrite32}; +static void RtosSleep(int milliseconds) +{ + vTaskDelay(pdMS_TO_TICKS((TickType_t) milliseconds)); +} + +static const CmsdkUartMemoryAccess MMIO_ACCESS = {MmioRead32, MmioWrite32, RtosSleep}; static void HelloTask(void* argument) { diff --git a/Tests/FreeRtos/CmsdkUartFake.c b/Tests/FreeRtos/CmsdkUartFake.c index 68ac46e0..95a2eb45 100644 --- a/Tests/FreeRtos/CmsdkUartFake.c +++ b/Tests/FreeRtos/CmsdkUartFake.c @@ -22,6 +22,7 @@ static struct int readsBeforeTxReadyDefault; int readsRemainingBeforeTxReady; bool txOverrunOccurred; + int sleepCallCount; } fake; static uint32_t Fake_Read32(uintptr_t address) @@ -93,7 +94,13 @@ static void Fake_Write32(uintptr_t address, uint32_t value) } } -static const CmsdkUartMemoryAccess FAKE_ACCESS = {Fake_Read32, Fake_Write32}; +static void Fake_Sleep(int milliseconds) +{ + (void) milliseconds; + fake.sleepCallCount++; +} + +static const CmsdkUartMemoryAccess FAKE_ACCESS = {Fake_Read32, Fake_Write32, Fake_Sleep}; void CmsdkUartFake_Reset(uintptr_t baseAddress) { @@ -106,6 +113,7 @@ void CmsdkUartFake_Reset(uintptr_t baseAddress) fake.readsBeforeTxReadyDefault = 2; fake.readsRemainingBeforeTxReady = 0; fake.txOverrunOccurred = false; + fake.sleepCallCount = 0; } const CmsdkUartMemoryAccess* CmsdkUartFake_Access(void) @@ -137,3 +145,8 @@ bool CmsdkUartFake_TxOverrunOccurred(void) { return fake.txOverrunOccurred; } + +int CmsdkUartFake_SleepCallCount(void) +{ + return fake.sleepCallCount; +} diff --git a/Tests/FreeRtos/CmsdkUartFake.h b/Tests/FreeRtos/CmsdkUartFake.h index 585f8418..d14d234e 100644 --- a/Tests/FreeRtos/CmsdkUartFake.h +++ b/Tests/FreeRtos/CmsdkUartFake.h @@ -32,6 +32,10 @@ extern "C" * the spin loop was missing or broken. Mirrors STATE.TX_OVRE on silicon. */ bool CmsdkUartFake_TxOverrunOccurred(void); + /* Number of times the driver called the sleep hook on the access struct. + * Lets tests assert the spin loop yielded the CPU between status reads. */ + int CmsdkUartFake_SleepCallCount(void); + #ifdef __cplusplus } #endif diff --git a/Tests/FreeRtos/CmsdkUartTest.cpp b/Tests/FreeRtos/CmsdkUartTest.cpp index 34902b16..65754aa6 100644 --- a/Tests/FreeRtos/CmsdkUartTest.cpp +++ b/Tests/FreeRtos/CmsdkUartTest.cpp @@ -46,6 +46,13 @@ TEST(CmsdkUart, PutCharSpinsForTxFullToClearBeforeWritingNextByte) CHECK_FALSE(CmsdkUartFake_TxOverrunOccurred()); } +TEST(CmsdkUart, PutCharCallsSleepWhileSpinningForTxFull) +{ + CmsdkUart_PutChar('A'); + CmsdkUart_PutChar('B'); + CHECK(CmsdkUartFake_SleepCallCount() > 0); +} + TEST(CmsdkUart, PutCharWritesImmediatelyWhenTransmitterIsAlwaysReady) { CmsdkUartFake_SetReadsBeforeTxReady(0); From 17a23324781acd0b55a20dfae767480904c998d6 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 8 May 2026 18:11:17 +0100 Subject: [PATCH 5/5] docs: add CMSDK_UART.md driver contract + DEVLOG update Captures the polled-mode TX/RX register sequences, bit semantics, and QEMU-vs-silicon traps that informed the v2 driver. Co-located with CmsdkUart.{h,c} so a future reader finds the contract one directory entry away. RX and concurrency sections are slice-3+ scope, included so the slice-3 author doesn't re-derive them. DEVLOG entry rewritten to cover the sleep injection, the static-inline helper refactor, the docs landing place, and the CodeRabbit findings (two folded in, one declined with rationale). Refs #290. --- DEVLOG.md | 75 ++++- Example/FreeRtos/HelloWorld/CMSDK_UART.md | 386 ++++++++++++++++++++++ 2 files changed, 455 insertions(+), 6 deletions(-) create mode 100644 Example/FreeRtos/HelloWorld/CMSDK_UART.md diff --git a/DEVLOG.md b/DEVLOG.md index 35011539..d2b3e9ac 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -117,6 +117,45 @@ production line: actual behaviour) without spurious overrun. 7. `WriteOfSingleByteEmitsThatByte` → forces `Write` to call `PutChar`. 8. `WriteOfMultipleBytesEmitsAllByteWithoutOverrun` → forces the loop. +9. `PutCharCallsSleepWhileSpinningForTxFull` → forces the spin to + yield via the injected `sleep` hook. `CmsdkUartFake` counts the + calls; production wires a `vTaskDelay(1)`-based sleep in `main.c` + so the spin doesn't hog CPU on real silicon. (See "Co-operative + sleep in the spin" below.) + +### Driver layout — intent-named static-inline helpers + +`CmsdkUart_PutChar` reads as +`while (TransmitterIsBusy()) { Yield(); } WriteDataRegister(c);` after +extracting `static inline` helpers — same pattern for +`CmsdkUart_Init` (`SetBaudDivisor()` then `EnableTransmitter()`). +Helpers are forward-declared at the top of the file and defined +immediately beneath their first caller, per the project's "one thing +at one level of abstraction" rule. No comments inside the helpers — +the names are the documentation; the per-register quirks live in +`CMSDK_UART.md`. + +### Co-operative sleep in the spin + +`CmsdkUartMemoryAccess` carries a third hook, +`void (*sleep)(int milliseconds)`, called inside `PutChar`'s +`while (TransmitterIsBusy())` body. Production wires it to +`vTaskDelay(pdMS_TO_TICKS(1))`; the host fake counts the calls. + +Rationale: at 115200 8N1, one character is ~87 µs; without a yield, +the spin would burn CPU at 100% on real silicon for the duration of +each character. With `configTICK_RATE_HZ = 100` (10 ms tick), +`vTaskDelay(1)` rounds up to one tick — which is more than a single +character's worth, but the spin is bounded by the actual hardware +drain rate and yields enough that other tasks run. QEMU's CMSDK model +drains synchronously, so the spin (and the sleep) never iterate +there; the yield is silicon-only behaviour. + +The signature mirrors `SolidSyslogSleepFunction` +(`Core/Interface/SolidSyslogSleep.h`) — a local typedef for now to +keep `Example/FreeRtos/HelloWorld/` free of `Core/Interface/` +coupling, but easy to lift into the library if the driver moves to +`Platform/FreeRtos/` in a later epic. ### Deferred to later slices @@ -166,12 +205,36 @@ production line: - mbed-OS `targets/TARGET_ARM_FM/TARGET_FVP_MPS2/serial_api.c` — Arm's own HAL; corroborates `BAUDDIV ≥ 16` minimum. -A full contract document covering the polled-mode TX/RX register -sequences, QEMU-vs-silicon traps, and concurrency considerations was -written up by claude.ai during slice-2 review and informed the v2 -rewrite. The document lives in conversation history (PR #291 review -trail); worth promoting into `docs/` if we hit similar peripheral -work in future epics. +The full polled-mode TX/RX contract — register layout, bit semantics, +QEMU-vs-silicon traps, concurrency considerations — was written up +during slice-2 review and lives at +`Example/FreeRtos/HelloWorld/CMSDK_UART.md`. Co-located with the +driver so a future reader of `CmsdkUart.{h,c}` finds it one directory +entry away. RX and concurrency sections in there are slice-3+ scope, +included so the slice-3 author doesn't have to re-derive them. + +### Code review feedback addressed + +Two CodeRabbit findings folded in during review: + +- **`Syscalls.c::_sbrk` bounds check** — extracted into + `static inline bool IsWithinSyscallHeap(const char* candidateBreak)` + per the project's intent-naming-predicates rule. The mixed + pointer-arithmetic / cast / two-comparisons composite is now a + single named call site. +- **`Tests/FreeRtos/CMakeLists.txt` `FREERTOS_PLUS_TCP_PATH` guard** — + scoped to `SolidSyslogFreeRtosDatagramTest` only. + `CmsdkUartTest` doesn't need Plus-TCP and now builds in a minimal + `FREERTOS_KERNEL_PATH`-only environment. + +A third finding — copy `CmsdkUartMemoryAccess` by value internally +plus add NULL-checks and an `isInitialized` flag in the driver — was +**declined**. The project rule is "no validation for scenarios that +can't happen"; `main.c` passes `&MMIO_ACCESS` where `MMIO_ACCESS` is +`static const` (program-lifetime), and the store-by-pointer pattern +matches the rest of the codebase (e.g. `SolidSyslogStreamSenderConfig` +holds resolver/stream/endpoint by pointer). If the API ever grows a +caller with stack-allocated access, we revisit. --- diff --git a/Example/FreeRtos/HelloWorld/CMSDK_UART.md b/Example/FreeRtos/HelloWorld/CMSDK_UART.md new file mode 100644 index 00000000..722c6666 --- /dev/null +++ b/Example/FreeRtos/HelloWorld/CMSDK_UART.md @@ -0,0 +1,386 @@ +# CMSDK APB UART — driver contract + +Reference document for `CmsdkUart.{h,c}` — the polled UART0 driver used by +the FreeRTOS HelloWorld example to route `printf` over QEMU `-serial stdio`. +Written during S08.03 slice 2 (#290) review after a code-review pass found +the v1 driver was missing `STATE.TX_FULL` polling. + +The aim of this document is to capture *what the hardware demands* (vs what +QEMU happens to tolerate), so that future readers — or readers porting to +real mps2-an385 silicon, or to a FreeRTOS-Plus-TCP environment that drives +UART concurrently with the IP stack — know which parts of the driver are +load-bearing and which parts are slice-2-scope simplifications. + +Each section flags places where QEMU's behaviour and silicon's specified +behaviour may diverge — those are the spots where a QEMU-only test passes +and silicon fails. + +--- + +## 1. Authoritative register reference + +### 1.1 What "CMSDK APB UART" means + +The peripheral is the `cmsdk_apb_uart` block from Arm's Cortex-M System +Design Kit (CMSDK), specified in the *Cortex-M System Design Kit Technical +Reference Manual* (ARM DDI 0479C/D), §4.3 *APB UART*. The MPS2 AN385 FPGA +image instantiates five copies in the CMSDK APB subsystem (UART0–UART4); +see *Application Note AN385: Cortex-M3 SMM on V2M-MPS2* (ARM DAI 0385D), +§3.7 "CMSDK APB subsystem". + +Both the silicon RTL and QEMU implement the same programmer's model. The +mismatches are in **timing, side-effects, and what happens between the bus +access and the wire** — covered per-register below. + +### 1.2 Base addresses on `mps2-an385` + +From AN385 §3.7 Table 3-4 ("APB Memory Map"), corroborated by QEMU's +`hw/arm/mps2.c` machine model: + +| UART | Base | RX IRQ | TX IRQ | Combined-overrun IRQ | +|-------|------------|--------|--------|----------------------| +| UART0 | 0x40004000 | 0 | 1 | 12 (shared) | +| UART1 | 0x40005000 | 2 | 3 | 12 (shared) | +| UART2 | 0x40006000 | 4 | 5 | 12 (shared) | +| UART3 | 0x40007000 | 18 | 19 | none | +| UART4 | 0x40009000 | 20 | 21 | none | + +The driver currently uses UART0 and is hardcoded to `0x40004000` in +`main.c`. Other UARTs are reference only. + +### 1.3 Register map (offsets from base) + +| Offset | Name | Access | Reset | +|--------|-----------------------|----------|----------| +| 0x000 | DATA | R/W | 0 | +| 0x004 | STATE | mixed | 0 | +| 0x008 | CTRL | R/W | 0 | +| 0x00C | INTSTATUS / INTCLEAR | R/W1C | 0 | +| 0x010 | BAUDDIV | R/W | 0 | +| 0xFD0–0xFFC | PID4..CID3 | RO | fixed | + +### 1.4 Bit semantics + +**DATA (0x000) — R/W, 8 valid bits** + +- **Write:** loads transmit holding byte. Effective only if `CTRL.TX_EN=1`. + In QEMU, a write while `STATE.TXFULL` is already set sets + `STATE.TXOVERRUN` and *replaces* `txbuf` with the new value. +- **Read:** returns the last received byte. Reading clears `STATE.RXFULL` + as a side-effect. + +**STATE (0x004)** + +| Bit | Name | Direction | +|-----|-----------|------------| +| 0 | TXFULL | RO from software (HW sets/clears) | +| 1 | RXFULL | RO from software (HW sets/clears) | +| 2 | TXOVERRUN | W1C | +| 3 | RXOVERRUN | W1C | + +Software cannot directly clear `TXFULL`/`RXFULL`. They clear as side-effects +of HW transmit completion (TXFULL) and DATA reads (RXFULL). Overrun bits +are write-1-to-clear: write `STATE = 0x4` to clear TXOVERRUN, `STATE = 0x8` +for RXOVERRUN, `0xC` for both. + +**CTRL (0x008)** + +| Bit | Name | +|-----|-----------| +| 0 | TX_EN | +| 1 | RX_EN | +| 2 | TX_INTEN | +| 3 | RX_INTEN | +| 4 | TXO_INTEN | +| 5 | RXO_INTEN | +| 6 | HSTEST (high-speed test mode — leave 0) | + +In QEMU the writable mask is `0x7F`; bits 7+ are dropped silently. + +**INTSTATUS / INTCLEAR (0x00C)** + +Same address; reads return INTSTATUS, writes act as INTCLEAR (W1C). + +| Bit | Name | Set by HW when | +|-----|------|--------------------------------------------------------------| +| 0 | TX | TXFULL transitions 1→0 *and* `CTRL.TX_INTEN=1` | +| 1 | RX | RXFULL transitions 0→1 *and* `CTRL.RX_INTEN=1` | +| 2 | TXO | `STATE.TXOVERRUN=1` AND `CTRL.TXO_INTEN=1` (combinational) | +| 3 | RXO | `STATE.RXOVERRUN=1` AND `CTRL.RXO_INTEN=1` (combinational) | + +**Critical: TX is edge-triggered.** From the Zephyr driver: *"In CMSDK +UART [TX] is an edge interrupt, firing on a state change of TX buffer +from full to empty."* RX is also edge-triggered, on the empty→full +transition. Pertinent only if/when this driver moves to interrupt-driven +TX (out of scope for slice 2; polled-only). + +The overrun bits in INTSTATUS are *not* independently latched — they are +recomputed by the QEMU update function as +`intstatus_overrun = state_overrun & ctrl_overrun_inten`. So clearing the +overrun-status bit in STATE (via W1C to STATE) immediately clears the +corresponding overrun bit in INTSTATUS too. + +**BAUDDIV (0x010) — R/W, 20 valid bits in QEMU (`& 0xFFFFF`)** + +Baud rate = PCLK / BAUDDIV. Minimum legal value is **16**. + +- **QEMU:** below 16 the value is ignored — `uart_baudrate_ok()` returns + false for `bauddiv < 16`, so `uart_update_parameters()` becomes a + no-op. The UART will still transmit/receive on the host pipe regardless, + because QEMU's transmit path doesn't gate on baud being valid — it + gates on `CTRL.TX_EN` only. +- **Silicon:** undefined / will not produce valid serial framing. AN385 + doesn't specify the failure mode; ARM's mbed-OS `serial_baud()` calls + `error("serial_baud")` if the requested rate is below 16. + +**This is a major QEMU-vs-silicon divergence.** The driver hardcodes +`BAUD_DIVISOR = 16` to remove this trap. + +--- + +## 2. Polled-mode TX contract — what `CmsdkUart_PutChar` implements + +### 2.1 Required register sequence + +**Initialization (once, before any `PutChar`):** + +1. Write `BAUDDIV ← (PCLK / desired_baud)`, ensuring value ≥ 16. +2. Write `CTRL ← TX_EN` (or `TX_EN | RX_EN` if RX is needed; slice 2 is + TX-only, so the driver writes `TX_EN` alone). + +The Zephyr driver does these in this order. From a clean reset (which +QEMU and silicon both honour: CTRL=0, STATE=0, BAUDDIV=0) the order +BAUDDIV-then-CTRL is sufficient. + +**Per-character TX:** + +1. Spin while `STATE & TXFULL`. +2. Write `DATA ← byte`. + +That is the entire contract. No INTCLEAR needed. No overrun handling +needed in the happy path — a polled producer that always waits for +`!TXFULL` cannot produce TXOVERRUN. + +The driver is `CmsdkUart_PutChar`: + +```c +while (TransmitterIsBusy()) /* (STATE & TX_FULL_BIT) != 0 */ +{ + Yield(); /* memoryAccess->sleep(1) */ +} +WriteDataRegister(c); /* memoryAccess->write32(base+DATA, c) */ +``` + +Both Zephyr `uart_cmsdk_apb_poll_out()` and mbed-OS implement the same +spin-then-write idiom. + +### 2.2 What to poll before writing DATA + +`STATE.TXFULL` (bit 0). It is set by the hardware when DATA is written +and cleared when the holding-register byte is shifted out (or, in QEMU, +when the chardev backend has accepted the byte). Reading STATE has no +side-effect, so a tight `while (STATE & TXFULL)` loop is side-effect-clean. + +### 2.3 TX_OVRE — how it arises and how to clear it + +`STATE.TXOVERRUN` is set when software writes DATA while `TXFULL=1`. The +**new byte overwrites `txbuf`** in QEMU — the previously-pending byte is +*kept in flight* (because `uart_transmit()` is already scheduled on it), +and the new one is what gets transmitted next when the backend drains. +So an overrun means "exactly one byte was lost"; it does not mean +"buffer corrupted." Silicon behaviour for the lost-byte semantics is not +explicitly documented in DDI 0479D — treat as implementation-specific and +assume "byte loss occurred, contents unspecified." + +Clearing: write `STATE ← 0x4` (W1C bit 2). This clears `STATE.TXOVERRUN` +and causes the next update to clear `INTSTATUS.TXO`. Writing to INTSTATUS +with bit 2 set *also* clears STATE.TXOVERRUN. + +**For the polled, single-producer driver in slice 2** — which always +checks `!TXFULL` before writing DATA — TXOVERRUN cannot arise. The +overrun handling matters only if multiple tasks share the UART without +a mutex (see §5) or if something writes DATA from an ISR concurrently +with task-level putchar. + +### 2.4 May BAUDDIV be 0 / below 16? + +**On QEMU:** Yes, in the sense that the guest will not crash and TX +will still appear on `-serial stdio`. + +**On silicon:** No. BAUDDIV<16 produces no valid serial framing. + +The driver hardcodes `BAUD_DIVISOR = 16` and has no API to set a +different value, removing this trap entirely. + +### 2.5 Must CTRL.TX_EN be set before DATA writes? + +**On QEMU:** No, but the byte will not reach the host. With TX_EN=0, +TXFULL stays set, the byte never drains, and the *next* DATA write +will see TXFULL=1 and set TXOVERRUN. + +**On silicon:** Per DDI 0479D, writes to a disabled transmitter are +implementation-specific. The driver `Init`s `CTRL ← TX_EN` before +any subsequent `PutChar`, ensuring this is never a problem. + +--- + +## 3. Polled-mode RX contract (slice 3 scope, documented for context) + +Slice 2 is TX-only; `_read` returns EOF. Slice 3 will wire `_read` to a +UART RX helper so `Example/Common/ExampleInteractive.c` can drive +`send N` / `quit` over `qemu -serial stdio`. The contract for that +slice, written down here so the reader doesn't have to chase it: + +### 3.1 Required sequence + +**Initialization (once):** +1. BAUDDIV ≥ 16. +2. CTRL.RX_EN = 1. + +**Per-character RX (non-blocking poll):** +1. If `STATE & RXFULL` is 0, return "no data". +2. Read `DATA` → byte. + +Reading DATA atomically clears `STATE.RXFULL`. There is no software +clear operation needed for RXFULL. + +### 3.2 RXFULL set/cleared + +- **Set** by hardware when a complete byte is shifted in; requires + `CTRL.RX_EN=1` (in QEMU, `uart_can_receive()` returns 0 unless RX_EN + is set; characters are dropped if they arrive with RX_EN clear). +- **Cleared** by a read of DATA. The read-then-clear is atomic from + software's point of view — a single LDR completes both. + +### 3.3 RX_OVRE behaviour + +Set when a new byte arrives while `RXFULL=1`; the **new byte overwrites +`rxbuf`** in QEMU. Clear with `STATE ← 0x8` or `INTSTATUS ← 0x8`. + +**Important QEMU divergence:** In QEMU, `uart_can_receive()` returns 0 +when RXFULL is set, so the chardev backend stops feeding bytes until +the guest reads DATA. RXOVERRUN is therefore hard to provoke under +stdio backpressure. On silicon there is no such backpressure; if the +consumer is too slow, RXOVERRUN will fire and bytes will be lost. + +### 3.4 Read-then-clear pseudocode + +``` +poll_in(): + if (STATE & RXFULL) == 0: + return EMPTY + byte = DATA # this read also clears RXFULL atomically + return byte + +read_byte_blocking(): + while (STATE & RXFULL) == 0: + Yield() # vTaskDelay(1) — same pattern as TX spin + return DATA +``` + +--- + +## 4. Reference implementations (cross-checked during slice-2 review) + +In rough order of usefulness for cross-checking the contract: + +### 4.1 Zephyr `drivers/serial/uart_cmsdk_apb.c` + +**Most directly comparable.** Linaro 2016 (modernised 2021), Apache 2.0. +Contains the canonical `while (uart->state & UART_TX_BF) {} uart->data = c` +poll-out idiom and an explicit comment about edge-triggered TX interrupts. +URL: `github.com/zephyrproject-rtos/zephyr/blob/main/drivers/serial/uart_cmsdk_apb.c`. + +### 4.2 mbed-OS `targets/TARGET_ARM_FM/TARGET_FVP_MPS2/serial_api.c` + +Arm's own (pre-acquisition) HAL driver. Apache 2.0. Confirms the +`BAUDDIV ≥ 16` minimum, the disable-before-reconfigure idiom, and +INTCLEAR W1C usage. URL: `github.com/ARMmbed/mbed-os/blob/master/targets/TARGET_ARM_FM/TARGET_FVP_MPS2/serial_api.c`. + +### 4.3 QEMU device model — `hw/char/cmsdk-apb-uart.c` + +Peter Maydell at Linaro, 2017, GPLv2. **The authoritative source for +"what QEMU actually does" — read this any time a test passes on QEMU +and you suspect silicon will behave differently.** Header comment +points to DDI 0479C as the spec. + +URL: `github.com/qemu/qemu/blob/master/hw/char/cmsdk-apb-uart.c`. + +### 4.4 FreeRTOS demo `CORTEX_MPU_M3_MPS2_QEMU_GCC` — *not useful* + +The upstream FreeRTOS demo for this board uses **semihosting** +(`BKPT 0xAB`) rather than the CMSDK UART (per FreeRTOS forum thread +22969 — `getchar()` produces a hardfault). Won't help cross-check the +UART contract. + +--- + +## 5. Concurrency under FreeRTOS preemption + FreeRTOS-Plus-TCP + +### 5.1 Setup (slice 3+ context) + +- Cortex-M3 single-core, FreeRTOS with preemption. +- One application task drives the SolidSyslog example. +- FreeRTOS-Plus-TCP runs its IP task at a high priority. +- A second application task (the BDD harness driver) reads from UART RX + (`send N` / `quit`). + +### 5.2 Mutex requirement + +The CMSDK UART has a single holding register — no hardware FIFO. The +TX critical section is `spin while STATE.TXFULL; write DATA`. If two +tasks share the UART without a mutex, lines from the two tasks become +garbled at byte granularity, and TXOVERRUN can occur. + +**For slice 2** (HelloWorld, single task) no mutex is needed — there's +only one producer. + +**For slice 3+** (`Example/Common/` brings a Service thread + interactive +task) a FreeRTOS mutex (not a binary semaphore — we want priority +inheritance) should serialise the spin-and-write critical section. +The driver API will need to grow a mutex slot at that point. + +### 5.3 Spin loop and tick rate + +At 115200 8N1, one character is ~87 µs. With `configTICK_RATE_HZ = 100` +(10 ms tick), a single character's spin will not delay the next tick. +But for sustained log bursts at high priority, the cumulative spin +time can cause missed deadlines. + +The slice-2 driver injects a **`sleep(milliseconds)`** hook called +inside the spin. Production wires this to `vTaskDelay(1)` (rounds up +to one tick = 10 ms with the current config) so the task blocks rather +than busy-spins. On QEMU the spin doesn't iterate at all (chardev +backend always drains synchronously), so this is silicon-only behaviour +and cost-free in the development environment. + +The host fake counts `sleep()` calls (`CmsdkUartFake_SleepCallCount()`), +which lets the spin behaviour be unit-tested without threading. + +### 5.4 What QEMU will not catch + +| Trap | QEMU | Silicon | +|--------------------------------------------|-------------------|-------------------| +| BAUDDIV=0 or <16 | Output works | No output | +| Write DATA before TX_EN set | Byte lost silently| Implementation-defined | +| Spin on TXFULL | Never iterates | Iterates ~87 µs at 115200 | +| RXOVERRUN under fast-producer/slow-consumer| Hidden by chardev backpressure | Bytes lost | +| RX poll loop without yield | Doesn't burn CPU | Burns CPU at 100% | + +The first three are addressed by the slice-2 driver: +- `BAUDDIV ≥ 16` is a hardcoded constant. +- `CTRL.TX_EN` is set in `Init`, before any `PutChar`. +- `STATE.TXFULL` is polled before each DATA write. + +The last two are slice-3+ concerns (RX path). + +--- + +## References + +- **Arm DDI 0479C/D** — *Cortex-M System Design Kit Technical Reference Manual*, §4.3 *APB UART*. Programmer's model, register layout, reset values. +- **Arm DAI 0385D** — *Application Note AN385: Cortex-M3 SMM on V2M-MPS2*, §3.7 *CMSDK APB subsystem*. UART base addresses on `mps2-an385`. +- **QEMU `hw/char/cmsdk-apb-uart.c`** — modelled semantics (W1C handling, edge-triggered TX, chardev backpressure that hides RX_OVRE under stdio, BAUDDIV<16 ignored). +- **QEMU `hw/arm/mps2.c`** — UART base address and IRQ assignment table for the AN385 board model. +- **Zephyr `drivers/serial/uart_cmsdk_apb.c`** — canonical poll-out idiom; the edge-triggered-TX comment. +- **mbed-OS `targets/TARGET_ARM_FM/TARGET_FVP_MPS2/serial_api.c`** — Arm HAL; corroborates BAUDDIV ≥ 16 minimum.