Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
238 changes: 238 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,243 @@
# 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.
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

- **`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.

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.

---

## 2026-05-08 — S08.03 slice 1 — `SolidSyslogFreeRtosDatagram` adapter

### Decision
Expand Down
Loading
Loading