Skip to content

feat: S08.03 slice 2 CMSDK UART + newlib retargeting#291

Merged
DavidCozens merged 5 commits into
mainfrom
feat/s08-03-slice2-cmsdk-uart
May 8, 2026
Merged

feat: S08.03 slice 2 CMSDK UART + newlib retargeting#291
DavidCozens merged 5 commits into
mainfrom
feat/s08-03-slice2-cmsdk-uart

Conversation

@DavidCozens

@DavidCozens DavidCozens commented May 8, 2026

Copy link
Copy Markdown
Owner

Purpose

Slice 2 of S08.03 (parent: #268; story: #290). Replaces the 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.

This is the foundation for slice 3+, where FreeRTOS-Plus-TCP and the example task run concurrently. 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 UART0 is plain MMIO at 0x40004000: the IP stack and console run independently, and Behave will drive the QEMU image over stdin/stdout exactly as it drives the Linux/Windows examples today.

Closes #290.

Change Description

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 confirmed against the QEMU model (hw/char/cmsdk-apb-uart.c) and the Zephyr / mbed-OS reference drivers. Writing DATA while TX_FULL=1 sets STATE.TX_OVRE on silicon and the byte is lost — QEMU's chardev backend drains synchronously inside the DATA-write path so the bug never manifests there, but it would drop bytes on real hardware.
  • Memory access injected via a CmsdkUartMemoryAccess struct of read32 / write32 function pointers. Production wires MmioRead32 / MmioWrite32 in main.c (cast uintptr_tvolatile uint32_t*, deref). Tests wire CmsdkUartFake whose Read32 / Write32 intercept register accesses against an in-memory model with tunable behaviour.
  • Configuration is hardcoded: BAUDDIV=16, CTRL.TX_EN=1, RX off, no IRQs. No caller-facing way to misconfigure — kills the silicon traps the contract document called out (BAUDDIV<16 silently broken on silicon, DATA-before-TX_EN implementation-defined).

Newlib retargeting — Example/FreeRtos/HelloWorld/Syscalls.c

  • _writeCmsdkUart_Write. _read returns EOF (slice 3 wires it 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--specs=nano.specs --specs=nosys.specs.

Tests — Tests/FreeRtos/CmsdkUart{Test,Fake}.{cpp,c,h}

8 host tests against the fake, each driven from a failing assertion before the matching production line:

# Test Drives
1 InitWritesBaudDivisor BAUDDIV write = 16
2 InitEnablesTransmitter CTRL.TX_EN bit
3 PutCharWritesByteToDataRegister DATA write
4 PutCharWritesTheGivenByte parameter use
5 PutCharSpinsForTxFullToClearBeforeWritingNextByte the while (state & TX_FULL) poll — two consecutive PutChars must not trip the fake's overrun flag
6 PutCharWritesImmediatelyWhenTransmitterIsAlwaysReady SetReadsBeforeTxReady(0) — proves the spin path also works under always-ready (the QEMU chardev model's actual behaviour) without spurious overrun
7 WriteOfSingleByteEmitsThatByte WritePutChar
8 WriteOfMultipleBytesEmitsAllByteWithoutOverrun the loop

The CmsdkUartFake 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 the QEMU model source.

CI — .github/workflows/ci.yml

build-freertos-target QEMU args: -nographic -semihosting-config enable=on,target=native-display none -serial stdio. Banner-grep unchanged. End-to-end: printf → newlib _writeCmsdkUart_WriteCmsdkUart_PutChar → MMIO at 0x40004000 → QEMU CMSDK UART model → -serial stdio → host stdout.

Test Evidence

  • cpputest-freertos:sha-44efeae host TDD: SolidSyslogTests (1088 tests), SolidSyslogFreeRtosDatagramTest (slice 1), CmsdkUartTest (8 tests) — all green.
  • cpputest-freertos-cross:sha-44efeae cross build + QEMU mps2-an385 with -display none -serial stdio — banner emitted, rc=124 (timeout success path; scheduler keeps idling so GDB can attach interactively).
  • 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.)

The other Linux jobs (build-linux-gcc, build-linux-clang, sanitize-linux-gcc, coverage-linux-gcc, analyze-tidy, analyze-cppcheck, analyze-iwyu) don't compile any of the changed code: every new file lives under Example/FreeRtos/ or Tests/FreeRtos/, both gated on FREERTOS_KERNEL_PATH (host TDD via cpputest-freertos) or CMAKE_CROSSCOMPILING + arm (cross build). They're skipped intentionally.

Areas Affected

  • Example/FreeRtos/HelloWorld/ — board glue (driver, syscalls, main, linker spec).
  • Tests/FreeRtos/CMakeLists.txt, Tests/FreeRtos/CmsdkUart{Test,Fake}.{cpp,c,h} — new host-TDD target with no FreeRTOS-Plus-TCP coupling.
  • .github/workflows/ci.ymlbuild-freertos-target QEMU args only.

No changes to Core/, Platform/Posix/, Platform/Windows/, Platform/FreeRtos/, or any of the existing examples.

References (CMSDK contract — informed the v2 driver)

  • Arm DDI 0479C/D, Cortex-M System Design Kit TRM, §4.3 APB UART
  • Arm DAI 0385D, AN385: Cortex-M3 SMM on V2M-MPS2, §3.7 UART base addresses
  • QEMU hw/char/cmsdk-apb-uart.c — modelled semantics (W1C, TXFULL set inside DATA write, chardev backpressure that hides overrun 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 HAL, BAUDDIV ≥ 16 minimum

Slice 3 pre-flight

Confirmed /opt/freertos/plus-tcp/source/portable/NetworkInterface/MPS2_AN385/ is present 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.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Hardware-backed CMSDK UART stdio for the FreeRTOS HelloWorld example and newlib retargeting so printf goes to serial.
  • Tests

    • Added host-side unit tests and a UART fake to validate transmit, spin/yield behavior, and overrun handling.
  • Documentation

    • Added a CMSDK UART reference describing register semantics and expected driver contract.
  • Chores

    • CI smoke runs switched to serial-stdio; DEVLOG updated with rationale and verification notes.

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1096 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1042 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1042 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 96% successful (✔️ 44 passed, 🙈 2 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 954 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces semihosting I/O with a CMSDK UART0 MMIO driver and Newlib syscall retargeting for FreeRTOS HelloWorld, adds build/linker and main.c integration, provides host-side fakes and CppUTest cases, and updates CI and DEVLOG to run the image with QEMU -serial stdio.

Changes

CMSDK UART + Newlib Retargeting

Layer / File(s) Summary
UART API & Types
Example/FreeRtos/HelloWorld/CmsdkUart.h
Adds memory-access typedefs and CmsdkUart API declarations (Init, PutChar, Write).
UART Driver Implementation
Example/FreeRtos/HelloWorld/CmsdkUart.c
Implements UART MMIO offsets, module-scoped access/base storage, Init (bauddiv + TX enable), PutChar (spin until TX ready), and Write (byte loop).
Newlib Syscall Retargeting
Example/FreeRtos/HelloWorld/Syscalls.c
Adds reserved syscall heap and implements _sbrk, _write (routes to CmsdkUart_Write), _read, _close, _lseek, _fstat, _isatty, _kill, _getpid, and _exit stubs.
Build & Linker Specs
Example/FreeRtos/HelloWorld/CMakeLists.txt
Adds CmsdkUart.c and Syscalls.c to the FreeRTOS ELF target and replaces rdimon.specs with nano.specs + nosys.specs.
Application Integration
Example/FreeRtos/HelloWorld/main.c
Adds MMIO access helpers and MMIO_ACCESS, defines CMSDK_UART0_BASE_ADDRESS, and replaces initialise_monitor_handles() with CmsdkUart_Init(...) before task startup.
Host Tests & Fakes
Tests/FreeRtos/...
Adds CmsdkUartTest target, CmsdkUartFake MMIO read/write/reset/accessors, and CppUTest suite verifying Init, PutChar, and Write behaviors including TX-full and overrun semantics.
CI & Documentation
.github/workflows/ci.yml, DEVLOG.md, Example/FreeRtos/HelloWorld/CMSDK_UART.md
CI QEMU invocation changed from -nographic/-semihosting-config to -display none -serial stdio; DEVLOG and CMSDK_UART.md document the UART contract, verification, and slice planning.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I swapped BKPT for bits that gently hum,
UART0 wakes, the console's drum,
No VM pauses, messages flow true,
Tests tick green, the CI reads anew,
A small hop forward — onward to slice three!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: S08.03 slice 2 CMSDK UART + newlib retargeting' directly summarizes the primary objective: implementing CMSDK UART and newlib retargeting for the FreeRTOS example.
Description check ✅ Passed The PR description comprehensively covers Purpose, Change Description (with subsections for Driver, Newlib retargeting, Tests, and CI), Test Evidence, and Areas Affected, matching the required template structure with detailed explanations.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #290: CMSDK UART0 driver with host-TDD coverage via CmsdkUartFake pattern, newlib syscall retargeting with _write routed to UART, example still emits banner via -serial stdio, CI updated to drop semihosting, and CmsdkUartTest runs in build-freertos-host-tdd.
Out of Scope Changes check ✅ Passed All changes are scoped to the PR objectives: new UART driver and syscalls in Example/FreeRtos/HelloWorld, host-TDD tests in Tests/FreeRtos, DEVLOG documentation, and CI QEMU args. No modifications to Core/, Platform/, or other unrelated subsystems.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/s08-03-slice2-cmsdk-uart

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Tests/FreeRtos/CMakeLists.txt (1)

10-12: ⚡ Quick win

Decouple CmsdkUartTest from the global FREERTOS_PLUS_TCP_PATH hard-fail.

CmsdkUartTest is intentionally independent of Plus-TCP, but this global fatal guard still blocks it in minimal host environments. Consider scoping the guard to SolidSyslogFreeRtosDatagramTest only.

♻️ Suggested gating split
-if("$ENV{FREERTOS_PLUS_TCP_PATH}" STREQUAL "")
-    message(FATAL_ERROR "FREERTOS_PLUS_TCP_PATH must be set to build Tests/FreeRtos.")
-endif()
+set(freeRtosPlusTcpPath "$ENV{FREERTOS_PLUS_TCP_PATH}")
...
-add_executable(SolidSyslogFreeRtosDatagramTest
+if(freeRtosPlusTcpPath STREQUAL "")
+    message(STATUS "Skipping SolidSyslogFreeRtosDatagramTest: FREERTOS_PLUS_TCP_PATH not set.")
+else()
+add_executable(SolidSyslogFreeRtosDatagramTest
     ...
 )
 ...
 target_include_directories(SolidSyslogFreeRtosDatagramTest PRIVATE
     ...
-    $ENV{FREERTOS_PLUS_TCP_PATH}/source/include
+    ${freeRtosPlusTcpPath}/source/include
 )
 add_test(NAME SolidSyslogFreeRtosDatagramTest COMMAND SolidSyslogFreeRtosDatagramTest)
+endif()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/FreeRtos/CMakeLists.txt` around lines 10 - 12, The global FATAL_ERROR
check for FREERTOS_PLUS_TCP_PATH is preventing CmsdkUartTest from building;
change the CMake logic in Tests/FreeRtos/CMakeLists.txt so the existence check
is only required when the SolidSyslogFreeRtosDatagramTest target (or the code
that needs Plus-TCP) is being configured. Concretely, remove or relax the
unconditional if("$ENV{FREERTOS_PLUS_TCP_PATH}" STREQUAL "") ... endif() guard
and instead wrap that check around the code that adds/configures
SolidSyslogFreeRtosDatagramTest (or gate it behind a CMake option/if that
indicates Plus-TCP tests), leaving CmsdkUartTest unaffected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Example/FreeRtos/HelloWorld/Syscalls.c`:
- Around line 26-41: Extract the composite bounds check inside _sbrk into a
named static inline predicate (e.g., static inline bool
IsWithinSyscallHeap(char* nextBreak)): forward-declare this helper above _sbrk,
call it from the if statement in _sbrk (replacing the original composite
condition that uses syscallHeap, syscallHeapBreak, sizeof(syscallHeap) and the
cast), and then define the static inline helper immediately below _sbrk so it
encapsulates the pointer arithmetic and size checks (using syscallHeap and
syscallHeapBreak) and returns the boolean result; leave errno and result logic
in _sbrk unchanged.

---

Nitpick comments:
In `@Tests/FreeRtos/CMakeLists.txt`:
- Around line 10-12: The global FATAL_ERROR check for FREERTOS_PLUS_TCP_PATH is
preventing CmsdkUartTest from building; change the CMake logic in
Tests/FreeRtos/CMakeLists.txt so the existence check is only required when the
SolidSyslogFreeRtosDatagramTest target (or the code that needs Plus-TCP) is
being configured. Concretely, remove or relax the unconditional
if("$ENV{FREERTOS_PLUS_TCP_PATH}" STREQUAL "") ... endif() guard and instead
wrap that check around the code that adds/configures
SolidSyslogFreeRtosDatagramTest (or gate it behind a CMake option/if that
indicates Plus-TCP tests), leaving CmsdkUartTest unaffected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93aa6004-cc43-4348-8076-393f6a44bc79

📥 Commits

Reviewing files that changed from the base of the PR and between 04559e9 and 81fc292.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • DEVLOG.md
  • Example/FreeRtos/HelloWorld/CMakeLists.txt
  • Example/FreeRtos/HelloWorld/CmsdkUart.c
  • Example/FreeRtos/HelloWorld/CmsdkUart.h
  • Example/FreeRtos/HelloWorld/Syscalls.c
  • Example/FreeRtos/HelloWorld/main.c
  • Tests/FreeRtos/CMakeLists.txt
  • Tests/FreeRtos/CmsdkUartTest.cpp

Comment thread Example/FreeRtos/HelloWorld/Syscalls.c
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.
@DavidCozens DavidCozens force-pushed the feat/s08-03-slice2-cmsdk-uart branch from 81fc292 to e831101 Compare May 8, 2026 16:05
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1096 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1042 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1042 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 96% successful (✔️ 44 passed, 🙈 2 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 954 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Example/FreeRtos/HelloWorld/CmsdkUart.c`:
- Around line 14-23: The driver currently stores the caller-owned
CmsdkUartMemoryAccess pointer in the global memoryAccess and later dereferences
it (in CmsdkUart_Init and other functions), which can dangle; instead, validate
access is non-NULL in CmsdkUart_Init, copy the contents of the
CmsdkUartMemoryAccess struct into an internal static CmsdkUartMemoryAccess
instance (not a pointer), and use that internal copy for all subsequent calls
(e.g., when invoking write32/read32 and when referencing BAUDDIV_OFFSET,
CTRL_OFFSET, TX_ENABLE); this removes the raw pointer to caller memory and
prevents dangling/NULL dereference while preserving the same API behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f723554d-63d9-473e-b365-97aabd6ee26f

📥 Commits

Reviewing files that changed from the base of the PR and between 81fc292 and e831101.

📒 Files selected for processing (11)
  • .github/workflows/ci.yml
  • DEVLOG.md
  • Example/FreeRtos/HelloWorld/CMakeLists.txt
  • Example/FreeRtos/HelloWorld/CmsdkUart.c
  • Example/FreeRtos/HelloWorld/CmsdkUart.h
  • Example/FreeRtos/HelloWorld/Syscalls.c
  • Example/FreeRtos/HelloWorld/main.c
  • Tests/FreeRtos/CMakeLists.txt
  • Tests/FreeRtos/CmsdkUartFake.c
  • Tests/FreeRtos/CmsdkUartFake.h
  • Tests/FreeRtos/CmsdkUartTest.cpp
✅ Files skipped from review due to trivial changes (2)
  • Tests/FreeRtos/CmsdkUartFake.c
  • DEVLOG.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • Example/FreeRtos/HelloWorld/CMakeLists.txt
  • Tests/FreeRtos/CMakeLists.txt
  • .github/workflows/ci.yml
  • Example/FreeRtos/HelloWorld/Syscalls.c

Comment on lines +14 to +23
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);
}

@coderabbitai coderabbitai Bot May 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid persisting caller-owned callback table by raw pointer.

At Line 14/Line 19, the driver stores access by pointer and later dereferences it at Line 27 and Line 35. If CmsdkUart_Init receives non-static storage (or invalid callbacks), this can become a dangling/NULL dereference and hard-fault during output.

Suggested fix
 `#include` "CmsdkUart.h"
 
+#include <stdbool.h>
 `#include` <stddef.h>
 
@@
-static const CmsdkUartMemoryAccess* memoryAccess = NULL;
-static uintptr_t                    base         = 0U;
+static CmsdkUartMemoryAccess memoryAccess = {0};
+static uintptr_t             baseAddress  = 0U;
+static bool                  isInitialized = false;
 
 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);
+    if ((access != NULL) && (access->read32 != NULL) && (access->write32 != NULL))
+    {
+        memoryAccess = *access; /* copy callbacks by value */
+        baseAddress  = baseAddress;
+        memoryAccess.write32(baseAddress + BAUDDIV_OFFSET, BAUD_DIVISOR);
+        memoryAccess.write32(baseAddress + CTRL_OFFSET, TX_ENABLE);
+        isInitialized = true;
+    }
 }
 
 void CmsdkUart_PutChar(char c)
 {
-    while ((memoryAccess->read32(base + STATE_OFFSET) & TX_FULL_BIT) != 0U)
+    if (isInitialized)
     {
-        /* 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. */
+        while ((memoryAccess.read32(baseAddress + STATE_OFFSET) & TX_FULL_BIT) != 0U)
+        {
+        }
+        memoryAccess.write32(baseAddress + DATA_OFFSET, (uint32_t) (unsigned char) c);
     }
-    memoryAccess->write32(base + DATA_OFFSET, (uint32_t) (unsigned char) c);
 }

Also applies to: 25-36

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Example/FreeRtos/HelloWorld/CmsdkUart.c` around lines 14 - 23, The driver
currently stores the caller-owned CmsdkUartMemoryAccess pointer in the global
memoryAccess and later dereferences it (in CmsdkUart_Init and other functions),
which can dangle; instead, validate access is non-NULL in CmsdkUart_Init, copy
the contents of the CmsdkUartMemoryAccess struct into an internal static
CmsdkUartMemoryAccess instance (not a pointer), and use that internal copy for
all subsequent calls (e.g., when invoking write32/read32 and when referencing
BAUDDIV_OFFSET, CTRL_OFFSET, TX_ENABLE); this removes the raw pointer to caller
memory and prevents dangling/NULL dereference while preserving the same API
behavior.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining this one — store-by-pointer is intentional and matches the project pattern.

Why: the project rule (CLAUDE.md) is "Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs)." The proposed isInitialized flag, the NULL guards, and the silent-no-op fallback in _PutChar all guard against scenarios that can't happen with the one production caller — main.c passes &MMIO_ACCESS where MMIO_ACCESS is static const, lifetime is the whole program.

Pattern consistency: other library configs hold function/object pointers by reference too — e.g. SolidSyslogStreamSenderConfig carries resolver/stream/endpoint as pointers, no isInitialized flag, no NULL checks. Adopting the suggested defensive pattern here would diverge from the rest of the codebase.

Cost vs benefit: struct-copy + extra state for a theoretical dangling-pointer scenario that can't materialise in our usage. If the API ever grows a stack-allocated caller, we'll revisit.

Documented in the slice-2 DEVLOG entry under "Code review feedback addressed" so this decision isn't lost.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DavidCozens, understood — the reasoning is clear and consistent. The static const lifetime of MMIO_ACCESS in main.c makes the dangling-pointer scenario genuinely impossible here, and adding isInitialized guards + struct copying would diverge from the established project pattern without real benefit. I'll withdraw the suggestion.


✏️ Learnings added
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 291
File: Example/FreeRtos/HelloWorld/CmsdkUart.c:17-42
Timestamp: 2026-05-08T17:19:38.382Z
Learning: In the DavidCozens/solid-syslog project, driver/config structs (e.g., `CmsdkUartMemoryAccess` in `Example/FreeRtos/HelloWorld/CmsdkUart.c`) are intentionally stored as raw pointers without NULL guards, `isInitialized` flags, or defensive struct copies. The project policy (CLAUDE.md) prohibits error handling or validation for scenarios that can't happen; production callers always pass pointers to `static const` objects with program lifetime. Do not flag store-by-pointer patterns or missing NULL/initialization guards for internal config/access structs in this codebase.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 153
File: Example/Windows/SolidSyslogWindowsExample.c:45-55
Timestamp: 2026-04-18T07:18:35.201Z
Learning: In this repository’s example entry-point C files under `Example/` (e.g., `SolidSyslogWindowsExample.c`, `SolidSyslogExample.c`), it is intentional to omit `argc/argv` NULL/length guards before using `argv[0]` (e.g., calling `ExampleAppName_Set(argv[0])`). C guarantees that when invoked via `main`, `argc > 0` and `argv[0]` is a valid string (§5.1.2.2.1), and the project policy (CLAUDE.md) forbids defensive checks for impossible scenarios. Therefore, do not raise review findings for missing `argc/argv` guards in these example entry points.

Learnt from: DavidCozens
Repo: DavidCozens/solid-syslog PR: 153
File: Example/Windows/SolidSyslogWindowsExample.c:57-66
Timestamp: 2026-04-18T07:18:55.197Z
Learning: In this project’s example entry-point C files, do not flag/require NULL checks after calls to the solid-syslog `Create` functions (e.g., `SolidSyslogWinsockResolver_Create`, `SolidSyslogWinsockDatagram_Create`, `SolidSyslogUdpSender_Create`, `SolidSyslogNullBuffer_Create`, `SolidSyslogNullStore_Create`, `SolidSyslogAtomicCounter_Create`, `SolidSyslogMetaSd_Create`, `SolidSyslogTimeQualitySd_Create`, `SolidSyslogOriginSd_Create`). These `Create` APIs return pointers to static instances and are designed not to fail at construction time; any runtime failures that can occur are surfaced later via the vtable at use-time (e.g., host resolution). Therefore, NULL checks after `Create` calls in example files (such as `Example/Windows/SolidSyslogWindowsExample.c` and similar examples) are intentionally omitted and should not be treated as a bug.

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.
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.
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.
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.
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1096 passed, 🙈 3 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1042 passed, 🙈 3 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1042 passed, 🙈 3 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 100% successful (✔️ 46 passed)
   🚦   bdd-windows-otel: 96% successful (✔️ 44 passed, 🙈 2 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 954 passed, 🙈 1 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: No warnings


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@DavidCozens

Copy link
Copy Markdown
Owner Author

@coderabbitai responses to the two review passes:

Tests/FreeRtos/CMakeLists.txt — FREERTOS_PLUS_TCP_PATH global guard (nitpick) — Addressed in 3f4ab24. Wrapped the existence check around SolidSyslogFreeRtosDatagramTest only, and emit a STATUS skip message when the path is unset. CmsdkUartTest now builds in a minimal FREERTOS_KERNEL_PATH-only environment.

Example/FreeRtos/HelloWorld/Syscalls.c — extract _sbrk predicate (major / quick win) — Addressed in 7a650ce (also replied inline). IsWithinSyscallHeap(const char* candidateBreak) extracted as static inline bool, forward-declared above _sbrk and defined immediately beneath it.

Example/FreeRtos/HelloWorld/CmsdkUart.c — copy-by-value + NULL guards (major)Declined (replied inline with full rationale). The store-by-pointer pattern is intentional, matches the rest of the codebase (SolidSyslogStreamSenderConfig etc.), and the proposed isInitialized flag guards against scenarios that can't happen with the one production caller — which is precisely what CLAUDE.md tells us not to add. Recorded in the slice-2 DEVLOG entry.

Thanks for the review.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@DavidCozens, thank you for the detailed responses — here's my acknowledgement on each:

  • FREERTOS_PLUS_TCP_PATH guard (nitpick) — Glad to hear it's scoped correctly now; CmsdkUartTest building in a FREERTOS_KERNEL_PATH-only environment is exactly the right outcome.

  • _sbrk predicate extraction (major)IsWithinSyscallHeap as a static inline bool is a clean solution; great that it's in.

  • Copy-by-value + NULL guards (major) — Understood and accepted. The store-by-pointer pattern being consistent with SolidSyslogStreamSenderConfig and the rest of the codebase is a sound reason, and adding an isInitialized guard that only protects against a call sequence that cannot occur with the one production caller would be dead defensive code — which is a fair reading of the CLAUDE.md guidance. No objection.

(╯°□°)╯ (one more rabbit sulking that its NULL-guard idea was rightly rejected)


✏️ Learnings added
Learnt from: DavidCozens
URL: https://github.com/DavidCozens/solid-syslog/pull/291

Timestamp: 2026-05-08T17:20:01.219Z
Learning: In the solid-syslog repository (C/C++ embedded codebase), the intentional pattern is to store a pointer to a config/access struct (e.g., `CmsdkUartMemoryAccess*`, `SolidSyslogStreamSenderConfig*`) rather than copying by value. Defensive guards such as `isInitialized` flags should NOT be added for call sequences that cannot occur with the single production caller; CLAUDE.md explicitly advises against this kind of speculative defensive programming.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Example/FreeRtos/HelloWorld/CMSDK_UART.md`:
- Line 268: The fenced code block containing the poll_in() and
read_byte_blocking() examples is missing a language tag which triggers
markdownlint MD040; update the opening fence from ``` to ```text so the block
begins with ```text (e.g., change the start of the block that contains poll_in()
and read_byte_blocking() to ```text) to satisfy the linter while leaving the
block contents unchanged.

In `@Example/FreeRtos/HelloWorld/main.c`:
- Around line 23-25: RtosSleep currently calls
vTaskDelay(pdMS_TO_TICKS((TickType_t) milliseconds)) which can yield 0 ticks for
small millisecond values; change the implementation in RtosSleep to compute
TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds) and if milliseconds
> 0 and ticks == 0 set ticks = 1 before calling vTaskDelay(ticks) so any
positive millisecond request sleeps at least one tick and preserves the TX_FULL
throttling in CmsdkUart_PutChar.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dca1f5f9-ece4-4e89-b83a-b53fffbc5227

📥 Commits

Reviewing files that changed from the base of the PR and between e831101 and 17a2332.

📒 Files selected for processing (10)
  • DEVLOG.md
  • Example/FreeRtos/HelloWorld/CMSDK_UART.md
  • Example/FreeRtos/HelloWorld/CmsdkUart.c
  • Example/FreeRtos/HelloWorld/CmsdkUart.h
  • Example/FreeRtos/HelloWorld/Syscalls.c
  • Example/FreeRtos/HelloWorld/main.c
  • Tests/FreeRtos/CMakeLists.txt
  • Tests/FreeRtos/CmsdkUartFake.c
  • Tests/FreeRtos/CmsdkUartFake.h
  • Tests/FreeRtos/CmsdkUartTest.cpp
✅ Files skipped from review due to trivial changes (1)
  • DEVLOG.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • Tests/FreeRtos/CmsdkUartFake.h
  • Example/FreeRtos/HelloWorld/CmsdkUart.h
  • Tests/FreeRtos/CmsdkUartTest.cpp
  • Example/FreeRtos/HelloWorld/CmsdkUart.c
  • Tests/FreeRtos/CmsdkUartFake.c
  • Example/FreeRtos/HelloWorld/Syscalls.c


### 3.4 Read-then-clear pseudocode

```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced block at Line 268.

This currently triggers markdownlint MD040 (“fenced-code-language”) and can break docs lint in CI.

Suggested patch
-```
+```text
 poll_in():
     if (STATE & RXFULL) == 0:
         return EMPTY
@@
 read_byte_blocking():
     while (STATE & RXFULL) == 0:
         Yield()          # vTaskDelay(1) — same pattern as TX spin
     return DATA
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 268-268: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Example/FreeRtos/HelloWorld/CMSDK_UART.md` at line 268, The fenced code block
containing the poll_in() and read_byte_blocking() examples is missing a language
tag which triggers markdownlint MD040; update the opening fence from ``` to
```text so the block begins with ```text (e.g., change the start of the block
that contains poll_in() and read_byte_blocking() to ```text) to satisfy the
linter while leaving the block contents unchanged.

Comment on lines +23 to +25
static void RtosSleep(int milliseconds)
{
vTaskDelay(pdMS_TO_TICKS((TickType_t) milliseconds));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "configTICK_RATE_HZ:"
rg -nP '^\s*#\s*define\s+configTICK_RATE_HZ\b' --type c --type h -C1

echo
echo "pdMS_TO_TICKS macro definition:"
rg -nP '^\s*#\s*define\s+pdMS_TO_TICKS\b' --type c --type h -C2

Repository: DavidCozens/solid-syslog

Length of output: 706


🏁 Script executed:

cat -n Example/FreeRtos/HelloWorld/main.c | head -40

Repository: DavidCozens/solid-syslog

Length of output: 1450


🏁 Script executed:

rg -n "RtosSleep|TX_FULL|CmsdkUart" Example/FreeRtos/HelloWorld/main.c -A2 -B1

Repository: DavidCozens/solid-syslog

Length of output: 571


🏁 Script executed:

cat -n Example/FreeRtos/HelloWorld/CmsdkUart.h | head -100

Repository: DavidCozens/solid-syslog

Length of output: 1018


🏁 Script executed:

rg -n "sleep|Sleep|delay|Delay" Example/FreeRtos/HelloWorld/ -A2 -B1 --type c

Repository: DavidCozens/solid-syslog

Length of output: 2093


🏁 Script executed:

rg -n "YIELD_MILLISECONDS" Example/FreeRtos/HelloWorld/ -B2 -A2 --type c

Repository: DavidCozens/solid-syslog

Length of output: 696


🏁 Script executed:

cat -n Example/FreeRtos/HelloWorld/CmsdkUart.c | head -100

Repository: DavidCozens/solid-syslog

Length of output: 2203


🏁 Script executed:

# Check naming compliance for the changed code
cat -n Example/FreeRtos/HelloWorld/main.c | sed -n '1,50p'

Repository: DavidCozens/solid-syslog

Length of output: 1794


🏁 Script executed:

# Verify clang-format compliance by checking indentation
git diff --no-index /dev/null Example/FreeRtos/HelloWorld/main.c 2>/dev/null | head -60 || cat Example/FreeRtos/HelloWorld/main.c | head -50

Repository: DavidCozens/solid-syslog

Length of output: 1742


Fix RtosSleep to ensure a minimum 1-tick delay when milliseconds > 0.

With configTICK_RATE_HZ = 100 (configured in FreeRTOSConfig.h), pdMS_TO_TICKS(1) evaluates to 0 via integer division. This causes vTaskDelay(0) (yield-only) to be called instead of sleeping a tick, undermining the TX_FULL spin-loop throttling in CmsdkUart_PutChar().

Suggested patch
 static void RtosSleep(int milliseconds)
 {
-    vTaskDelay(pdMS_TO_TICKS((TickType_t) milliseconds));
+    TickType_t ticks = 0;
+    if (milliseconds > 0)
+    {
+        ticks = pdMS_TO_TICKS((uint32_t) milliseconds);
+        if (ticks == 0U)
+        {
+            ticks = 1U;
+        }
+    }
+    vTaskDelay(ticks);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Example/FreeRtos/HelloWorld/main.c` around lines 23 - 25, RtosSleep currently
calls vTaskDelay(pdMS_TO_TICKS((TickType_t) milliseconds)) which can yield 0
ticks for small millisecond values; change the implementation in RtosSleep to
compute TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds) and if
milliseconds > 0 and ticks == 0 set ticks = 1 before calling vTaskDelay(ticks)
so any positive millisecond request sleeps at least one tick and preserves the
TX_FULL throttling in CmsdkUart_PutChar.

@DavidCozens DavidCozens merged commit eb494c4 into main May 8, 2026
18 checks passed
@DavidCozens DavidCozens deleted the feat/s08-03-slice2-cmsdk-uart branch May 8, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S08.03 slice 2: CMSDK UART + newlib retargeting

1 participant