From 8339d9a7591fa850e13ef18267641b661de710a9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 9 May 2026 13:24:24 +0000 Subject: [PATCH 1/2] feat: S08.03 slice 3b.2 SingleTask example with SolidSyslogUdpSender + interactive command channel Replaces 3b.1's link-up "ping" smoke with a real SolidSyslog wiring: NullBuffer + SolidSyslogUdpSender on the slice-1 SolidSyslogFreeRtosDatagram and the slice-3a SolidSyslogFreeRtosStaticResolver, exposed via Example/Common/ExampleInteractive over qemu -serial stdio. CmsdkUart grew a host-TDD'd RX path (CmsdkUart_GetChar; Init now writes TX_EN | RX_EN); Syscalls.c::_read calls into it with CR->LF translation and echo so fgets behaves as a cooked-mode TTY. Toolchain file gained -mcpu=cortex-m3 -mthumb in CMAKE_C_FLAGS_INIT so libSolidSyslog.a comes back as Thumb-2 and links into the cross binary; without this the first cross-library call hard-faulted on entry. Closes #296 Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/CMakeLists.txt | 2 +- DEVLOG.md | 134 +++++++++++ Example/FreeRtos/Common/CmsdkUart.c | 31 ++- Example/FreeRtos/Common/CmsdkUart.h | 1 + Example/FreeRtos/Common/Syscalls.c | 35 ++- Example/FreeRtos/SingleTask/CMakeLists.txt | 31 ++- Example/FreeRtos/SingleTask/main.c | 214 ++++++++++++------ Example/FreeRtos/cmake/arm-none-eabi.cmake | 10 + Platform/FreeRtos/Source/SolidSyslogAddress.c | 6 + Tests/FreeRtos/CmsdkUartFake.c | 39 +++- Tests/FreeRtos/CmsdkUartFake.h | 10 + Tests/FreeRtos/CmsdkUartTest.cpp | 34 +++ 12 files changed, 464 insertions(+), 83 deletions(-) create mode 100644 Platform/FreeRtos/Source/SolidSyslogAddress.c diff --git a/Core/Source/CMakeLists.txt b/Core/Source/CMakeLists.txt index e0561799..c093c982 100644 --- a/Core/Source/CMakeLists.txt +++ b/Core/Source/CMakeLists.txt @@ -31,7 +31,7 @@ if(HAVE_ATOMIC_COUNTER) list(APPEND SOURCES SolidSyslogAtomicCounter.c) endif() -if(SOLIDSYSLOG_POSIX OR SOLIDSYSLOG_WINSOCK) +if(SOLIDSYSLOG_POSIX OR SOLIDSYSLOG_WINSOCK OR DEFINED ENV{FREERTOS_KERNEL_PATH}) list(APPEND SOURCES SolidSyslogResolver.c SolidSyslogDatagram.c diff --git a/DEVLOG.md b/DEVLOG.md index 8f6dcb06..e211f115 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,139 @@ # Dev Log +## 2026-05-09 — S08.03 slice 3b.2 — SingleTask example with SolidSyslogUdpSender + interactive command channel (#296) + +Slice 3b.1's "send a hardcoded ping on link-up" smoke is replaced with a +real SolidSyslog wiring: `SolidSyslogConfig` driven by a `NullBuffer` + +`SolidSyslogUdpSender`, sat on the slice-1 `SolidSyslogFreeRtosDatagram` +and the slice-3a `SolidSyslogFreeRtosStaticResolver`, exposed via +`Example/Common/ExampleInteractive` running over `qemu -serial stdio`. +Typing `send N` over the UART emits N RFC 5424 datagrams to the slirp +gateway (10.0.2.2:5514); `quit` cleanly shuts the example down. Slice +3b.1.5's ARP priming inside the datagram adapter means the first send +after cold start is delivered — no warm-up message needed. + +### CmsdkUart evolves rather than splits + +Issue #296 originally proposed a sibling `Example/FreeRtos/SingleTask/ +UartRx.{c,h}`. Decided against it: the issue body was a hint, not a +constraint, and we don't want two CMSDK UART drivers. Instead `Example/ +FreeRtos/Common/CmsdkUart.{h,c}` grew `CmsdkUart_GetChar` (blocking poll +on `STATE.RXFULL` with the same `sleep(1)` yield idiom as `PutChar`), and +`CmsdkUart_Init` now writes `CTRL ← TX_EN | RX_EN` so the receiver is +enabled in one shot. `--gc-sections` strips `GetChar` from the HelloWorld +ELF, so HelloWorld pays nothing for the new code path. + +### TDD progression — 5 ZOMBIES cycles against CmsdkUartFake + +Strict red→green→refactor in `Tests/FreeRtos/CmsdkUartTest.cpp`. The fake +gained an RX side: `SetReceivedByte(byte)` arms the next DATA read, +`SetReadsBeforeRxReady(N)` delays `RXFULL` becoming set until N STATE +reads have happened (mirror of the existing TX `SetReadsBeforeTxReady`), +and DATA reads return the byte only when `RXFULL=1`, clearing it on +read — matching the silicon contract documented in `CMSDK_UART.md`. + +1. `InitEnablesReceiver` — drove `CTRL_OFFSET ← TX_EN | RX_EN` (existing + `InitEnablesTransmitter` stays green; cycle 1 only checks bit 1 was + set, doesn't disturb bit 0). +2. `GetCharReturnsByteFromDataRegister` — drove the public `GetChar` + API; minimum impl is just `read32(DATA)`. +3. `GetCharSpinsForRxFullToBecomeSetBeforeReadingDataRegister` — forced + the spin loop on `STATE.RXFULL` (without it, DATA returns 0 on + cache-miss because the fake gates DATA on `RXFULL`). +4. `GetCharCallsSleepWhileSpinningForRxFull` — forced the `Yield()` call + inside the spin so the IP task can run while RX is empty. +5. `GetCharReturnsImmediatelyWhenReceiverHasByte` — boundary: with + `SetReadsBeforeRxReady(0)` the fake asserts `RXFULL` immediately; + verifies no spurious sleep when data is already available. Locked in + by passing without production change. + +Refactor pass extracted `static inline ReceiverHasByte()` and +`ReadDataRegister()` helpers so `GetChar` reads one-line top-down, +matching the existing `PutChar` / `TransmitterIsBusy` / `WriteDataRegister` +shape. + +### Newlib `_read` line discipline lives in Syscalls.c, not the driver + +`Example/FreeRtos/Common/Syscalls.c::_read` was extended to call +`CmsdkUart_GetChar()` once per call, translate CR (0x0D) to LF (0x0A) so +fgets terminates regardless of which newline the host terminal sends, and +echo each byte back via `CmsdkUart_PutChar` so the user sees what they +type over `qemu -serial stdio`. Driver stays a minimal byte-in/byte-out; +TTY/cooked-mode policy lives next to the newlib seam. + +### Two surprises that ate most of the slice — captured here so the next reader doesn't repeat them + +**1. The cross toolchain wasn't setting `-mthumb`, so libSolidSyslog.a +came back in ARM mode.** First QEMU run hard-faulted at PC=0x5d8 inside +`SolidSyslogUdpSender_Create`. Disassembly showed 32-bit ARM-mode +encodings (`e92d4800 push {fp, lr}`) at the function entry — Cortex-M3 +only decodes Thumb-2, so the first instruction in the first cross-library +call faulted. Cause: `Example/FreeRtos/cmake/arm-none-eabi.cmake` set the +compiler driver but didn't set `CMAKE_C_FLAGS_INIT`. The HelloWorld and +SingleTask executables added `-mcpu=cortex-m3 -mthumb` via +`target_compile_options(... PRIVATE ...)`, but those don't propagate to +dependencies, and `Core/Source/CMakeLists.txt` doesn't add per-target +flags — it inherits whatever's global. Slice 3b.1 didn't surface this +because its smoke task body never called any Core library function. Fix: +moved `-mcpu=cortex-m3 -mthumb -ffunction-sections -fdata-sections +-fno-common` to `CMAKE_C_FLAGS_INIT` (and CXX/ASM equivalents) in the +toolchain file. The per-target additions in HelloWorld / SingleTask are +now redundant but harmless; left in place so each example reads +self-contained. + +**2. Stack budget guesswork mid-debug.** While chasing the ARM-mode bug +above, the symptom looked plausibly like stack overflow (hard-fault deep +into the InteractiveTask, all caller-saved registers pinned at the +FreeRTOS canary 0xa5a5a5a5). Bumped `INTERACTIVE_TASK_STACK_DEPTH` from +`configMINIMAL_STACK_SIZE * 8` (4 KB) to `* 16` (8 KB) — same crash, then +to `* 32` (16 KB) — same crash. None of those bumps made the symptom +move because the root cause was the Thumb-mode bug, not stack. Once the +toolchain was fixed the boot completed at `* 32` and a `send 3` + `quit` +smoke runs to completion. The true minimum with the toolchain fixed has +not been re-bisected — `* 32` was kept as a safe ceiling. The realistic +peak shape is `SolidSyslog_Log` allocating two +`char[SOLIDSYSLOG_MAX_MESSAGE_SIZE]` frames (~4 KB) + a +`SolidSyslogFormatterStorage` for ~2 KB on its formatter path, plus +`ExampleInteractive`'s 256-byte fgets line, plus newlib printf (~1 KB) — +roughly 7–8 KB peak — so `* 16` (8 KB) is probably enough but unverified. +A follow-up will introduce CMake-driven memory scaling and a measured +budget; tracked separately. + +### What this leaves for slice 4+ + +- Configuration injection over the interactive command grammar + (replacing the `TEST_*` walking-skeleton hostname / appName / processId + / clock with values driven by `set` commands). +- BDD harness pointed at the FreeRTOS target. +- Service-thread + non-NullBuffer FreeRTOS-side wiring (CircularBuffer + + FreeRTOS mutex) — slice 5+. +- Real RTC-backed clock callback to replace the hardcoded RFC 5424 + publication-date timestamp. +- CMake-driven stack-budget scaling so the `* 32` magic number can come + back down once formatter / printf footprints are pinned numerically. + +### Pre-PR checks + +- `clang-format --dry-run --Werror` on every changed file — clean. +- `Tests/FreeRtos/CmsdkUartTest` (14 / 14, 18 checks), + `SolidSyslogFreeRtosDatagramTest` (21 / 21, 42 checks), + `SolidSyslogFreeRtosStaticResolverTest` (10 / 10, 9 checks) — all + green. Full host ctest (5 exe) — green. +- `cmake --build --preset freertos-cross` — clean (HelloWorld + SingleTask + ELFs both link). +- HelloWorld QEMU smoke — banner prints, no regression from the toolchain + change. +- SingleTask QEMU smoke (`send 1`, `send 3`, `quit`) — the host Python + UDP listener at 5514 receives 1 and 3 RFC 5424 datagrams respectively, + format `<134>1 2009-03-23T00:00:00.000000Z FreeRtosExample + SolidSyslogExample 1 example - Hello from FreeRTOS`, `quit` cleanly + exits the task. +- Not run locally (CI's responsibility): `build-linux-gcc`, + `build-linux-clang`, `sanitize-linux-gcc`, `coverage-linux-gcc`, + `analyze-tidy`, `analyze-cppcheck`, `analyze-iwyu`, + `build-windows-msvc`, BDD, OpenSSL integration. The freertos-cross + devcontainer doesn't ship clang or the analyze tooling. + ## 2026-05-09 — S08.03 slice 3b.1.5 — FreeRtosDatagram ARP priming on cache miss (#298) `SolidSyslogFreeRtosDatagram::SendTo` now probes ARP transparently when the diff --git a/Example/FreeRtos/Common/CmsdkUart.c b/Example/FreeRtos/Common/CmsdkUart.c index 054dea56..64fc2986 100644 --- a/Example/FreeRtos/Common/CmsdkUart.c +++ b/Example/FreeRtos/Common/CmsdkUart.c @@ -10,7 +10,9 @@ #define BAUD_DIVISOR 16U #define TX_ENABLE 0x01U +#define RX_ENABLE 0x02U #define TX_FULL_BIT 0x01U +#define RX_FULL_BIT 0x02U #define YIELD_MILLISECONDS 1 @@ -18,17 +20,19 @@ static const CmsdkUartMemoryAccess* memoryAccess = NULL; static uintptr_t base = 0U; static inline void SetBaudDivisor(void); -static inline void EnableTransmitter(void); +static inline void EnableTxAndRx(void); static inline bool TransmitterIsBusy(void); static inline void Yield(void); static inline void WriteDataRegister(char c); +static inline bool ReceiverHasByte(void); +static inline char ReadDataRegister(void); void CmsdkUart_Init(const CmsdkUartMemoryAccess* access, uintptr_t baseAddress) { memoryAccess = access; base = baseAddress; SetBaudDivisor(); - EnableTransmitter(); + EnableTxAndRx(); } static inline void SetBaudDivisor(void) @@ -36,9 +40,9 @@ static inline void SetBaudDivisor(void) memoryAccess->write32(base + BAUDDIV_OFFSET, BAUD_DIVISOR); } -static inline void EnableTransmitter(void) +static inline void EnableTxAndRx(void) { - memoryAccess->write32(base + CTRL_OFFSET, TX_ENABLE); + memoryAccess->write32(base + CTRL_OFFSET, TX_ENABLE | RX_ENABLE); } void CmsdkUart_PutChar(char c) @@ -72,3 +76,22 @@ void CmsdkUart_Write(const char* buffer, size_t length) CmsdkUart_PutChar(buffer[i]); } } + +char CmsdkUart_GetChar(void) +{ + while (!ReceiverHasByte()) + { + Yield(); + } + return ReadDataRegister(); +} + +static inline bool ReceiverHasByte(void) +{ + return (memoryAccess->read32(base + STATE_OFFSET) & RX_FULL_BIT) != 0U; +} + +static inline char ReadDataRegister(void) +{ + return (char) (memoryAccess->read32(base + DATA_OFFSET) & 0xFFU); +} diff --git a/Example/FreeRtos/Common/CmsdkUart.h b/Example/FreeRtos/Common/CmsdkUart.h index 7ac22c05..4b4819f4 100644 --- a/Example/FreeRtos/Common/CmsdkUart.h +++ b/Example/FreeRtos/Common/CmsdkUart.h @@ -23,6 +23,7 @@ extern "C" void CmsdkUart_Init(const CmsdkUartMemoryAccess* access, uintptr_t baseAddress); void CmsdkUart_PutChar(char c); void CmsdkUart_Write(const char* buffer, size_t length); + char CmsdkUart_GetChar(void); #ifdef __cplusplus } diff --git a/Example/FreeRtos/Common/Syscalls.c b/Example/FreeRtos/Common/Syscalls.c index e00da499..1f88df51 100644 --- a/Example/FreeRtos/Common/Syscalls.c +++ b/Example/FreeRtos/Common/Syscalls.c @@ -2,13 +2,16 @@ * * 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. + * QEMU surfaces over `-serial stdio`. _read pulls one byte at a time from + * CmsdkUart_GetChar (blocking poll on STATE.RXFULL) so fgets in + * Example/Common/ExampleInteractive can drive `send N` / `quit` over the + * same serial channel. CR (0x0D) is translated to LF (0x0A) so terminals + * that send carriage-return on Enter still terminate fgets, and each + * received byte is echoed back over TX so the user sees what they type. * * 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. */ + * smoke is the integration check that proves the path end-to-end. */ #include "CmsdkUart.h" @@ -61,9 +64,27 @@ int _write(int file, char* buffer, int length) int _read(int file, char* buffer, int length) { (void) file; - (void) buffer; - (void) length; - return 0; + int bytesRead = 0; + if (length > 0) + { + char byte = CmsdkUart_GetChar(); + if (byte == '\r') + { + byte = '\n'; + } + if (byte == '\n') + { + CmsdkUart_PutChar('\r'); + CmsdkUart_PutChar('\n'); + } + else + { + CmsdkUart_PutChar(byte); + } + buffer[0] = byte; + bytesRead = 1; + } + return bytesRead; } int _close(int file) diff --git a/Example/FreeRtos/SingleTask/CMakeLists.txt b/Example/FreeRtos/SingleTask/CMakeLists.txt index 07134dbd..28458681 100644 --- a/Example/FreeRtos/SingleTask/CMakeLists.txt +++ b/Example/FreeRtos/SingleTask/CMakeLists.txt @@ -1,18 +1,22 @@ -# FreeRTOS-Plus-TCP single-task UDP smoke ELF for QEMU mps2-an385 (Cortex-M3). +# FreeRTOS-Plus-TCP single-task SolidSyslog example ELF for QEMU mps2-an385 +# (Cortex-M3). # # Compiled cross-only — gated by the top-level CMakeLists.txt on # CMAKE_CROSSCOMPILING + CMAKE_SYSTEM_PROCESSOR == "arm". # -# Slice 3b.1 of S08.03 stands the IP stack up and emits a single UDP -# datagram on link-up. Slice 3b.2 swaps main.c's body for SolidSyslog wiring; -# the directory + CMakeLists stay. +# Slice 3b.2 of S08.03 wires SolidSyslogUdpSender behind the slice-1 +# SolidSyslogFreeRtosDatagram adapter and the slice-3a static resolver, with +# Example/Common/ExampleInteractive driving `send N` / `quit` over the +# QEMU -serial stdio UART (CmsdkUart RX path landed alongside in this +# slice). # # Source layout: upstream FreeRTOS-Kernel + Plus-TCP sources are compiled # into a separate OBJECT library (`solid_syslog_freertos_upstream`) that # accepts the relaxed warning set those headers / sources require. Local -# project code (main.c, Startup.c, Common/CmsdkUart.c, Common/Syscalls.c) -# stays in the executable target under the strict project warning bar so -# regressions in our own code can't slip through. +# project code (main.c, Startup.c, Common/CmsdkUart.c, Common/Syscalls.c, +# the Platform/FreeRtos/Source adapters, and Example/Common/ +# ExampleInteractive.c) stays in the executable target under the strict +# project warning bar so regressions in our own code can't slip through. set(FREERTOS_KERNEL_PATH "$ENV{FREERTOS_KERNEL_PATH}") if(NOT FREERTOS_KERNEL_PATH) @@ -139,6 +143,10 @@ add_executable(SolidSyslogFreeRtosSingleTask Startup.c ${SOLID_SYSLOG_FREERTOS_EXAMPLE_COMMON_DIR}/CmsdkUart.c ${SOLID_SYSLOG_FREERTOS_EXAMPLE_COMMON_DIR}/Syscalls.c + ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogAddress.c + ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosDatagram.c + ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source/SolidSyslogFreeRtosStaticResolver.c + ${CMAKE_SOURCE_DIR}/Example/Common/ExampleInteractive.c $ ) @@ -155,12 +163,21 @@ target_compile_options(SolidSyslogFreeRtosSingleTask PRIVATE target_include_directories(SolidSyslogFreeRtosSingleTask PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} # FreeRTOSConfig.h, FreeRTOSIPConfig.h ${SOLID_SYSLOG_FREERTOS_EXAMPLE_COMMON_DIR} # CmsdkUart.h + ${CMAKE_SOURCE_DIR}/Core/Interface # SolidSyslog*.h + ${CMAKE_SOURCE_DIR}/Core/Source # SolidSyslogAddress.h (consumed by adapters) + ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Interface # SolidSyslogFreeRtos*.h + ${CMAKE_SOURCE_DIR}/Platform/FreeRtos/Source # SolidSyslogAddressInternal.h + ${CMAKE_SOURCE_DIR}/Example/Common # ExampleInteractive.h ${FREERTOS_KERNEL_PATH}/include ${FREERTOS_PORT_DIR} # portmacro.h ${PLUS_TCP_SRC_DIR}/include ${PLUS_TCP_PORT_GCC_DIR} # pack_struct_*.h ) +target_link_libraries(SolidSyslogFreeRtosSingleTask PRIVATE + ${PROJECT_NAME} # libSolidSyslog.a (cross-compiled) +) + target_link_options(SolidSyslogFreeRtosSingleTask PRIVATE -mcpu=cortex-m3 -mthumb diff --git a/Example/FreeRtos/SingleTask/main.c b/Example/FreeRtos/SingleTask/main.c index 3bb8aeb2..f724c3b7 100644 --- a/Example/FreeRtos/SingleTask/main.c +++ b/Example/FreeRtos/SingleTask/main.c @@ -1,32 +1,38 @@ -/* FreeRTOS-Plus-TCP bring-up smoke test on QEMU mps2-an385 (Cortex-M3). +/* FreeRTOS-Plus-TCP single-task SolidSyslog example for QEMU mps2-an385. * - * Slice 3b.1 of S08.03: prove the IP stack initialises and a UDP datagram - * escapes the guest via slirp. Static IPv4 (10.0.2.15), default gateway - * (10.0.2.2 — the slirp gateway, routed to the host), no DHCP / DNS. - * - * On link-up the network event hook spawns a one-shot smoke task that - * - prints "network up\n" over the CMSDK UART (QEMU -serial stdio) - * - opens a UDP socket - * - sends "ping" to {10.0.2.2, 5514} - * - closes the socket and self-deletes. - * - * Slice 3b.2 replaces this scaffold with a SolidSyslogConfig + UdpSender - * wiring that drives the slice-1 SolidSyslogFreeRtosDatagram adapter via - * the slice-3a SolidSyslogFreeRtosStaticResolver. */ + * Slice 3b.2 of S08.03 — replaces 3b.1's hardcoded "ping" smoke task with + * a real SolidSyslog wiring. Static IPv4 (10.0.2.15) on the QEMU slirp + * network with the host reachable at the slirp gateway 10.0.2.2; a single + * FreeRTOS task runs Example/Common/ExampleInteractive over qemu -serial + * stdio (CmsdkUart RX wired into newlib's _read in Example/FreeRtos/ + * Common/Syscalls.c). On link-up the IP-task event hook spawns the + * interactive task once; SolidSyslog is configured with a NullBuffer + + * UdpSender driving the slice-1 SolidSyslogFreeRtosDatagram via the + * slice-3a static resolver, so each `send N` line over the UART emits N + * RFC 5424 datagrams to {10.0.2.2, 5514}. */ #include "CmsdkUart.h" +#include "ExampleInteractive.h" +#include "SolidSyslog.h" +#include "SolidSyslogConfig.h" +#include "SolidSyslogEndpoint.h" +#include "SolidSyslogFormatter.h" +#include "SolidSyslogFreeRtosDatagram.h" +#include "SolidSyslogFreeRtosStaticResolver.h" +#include "SolidSyslogNullBuffer.h" +#include "SolidSyslogNullStore.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogTimestamp.h" +#include "SolidSyslogUdpSender.h" #include #include -#include #include #include -#include #include #include -#include #define CMSDK_UART0_BASE_ADDRESS UINT32_C(0x40004000) @@ -48,28 +54,50 @@ * IRQ 13 at a higher-than-syscall-safe priority. */ #define ETHERNET_IRQ_PRIORITY ((uint8_t) (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8U - configPRIO_BITS))) -/* Static IPv4 wiring matching the QEMU slirp default. 10.0.2.15 is the - * standard slirp DHCP-allocated guest address; we hardcode it here so no - * DHCP server is required. */ -static const uint8_t TEST_IP_ADDRESS[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 15U}; -static const uint8_t TEST_NETMASK[ipIP_ADDRESS_LENGTH_BYTES] = {255U, 255U, 255U, 0U}; -static const uint8_t TEST_GATEWAY[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 2U}; -static const uint8_t TEST_DNS[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 3U}; - -/* Locally-administered MAC (U/L bit set, multicast bit clear). */ -static const uint8_t TEST_MAC[ipMAC_ADDRESS_LENGTH_BYTES] = {0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U}; +/* Unprivileged mirror of SOLIDSYSLOG_UDP_DEFAULT_PORT (514) for BDD listeners. */ +#define EXAMPLE_UDP_PORT 5514U -#define TEST_TARGET_PORT 5514U -static const uint8_t TEST_PAYLOAD[] = {'p', 'i', 'n', 'g'}; +/* SolidSyslog_Log allocates two char[SOLIDSYSLOG_MAX_MESSAGE_SIZE] frames + * (~4 KB) plus formatter storage on its formatter path; ExampleInteractive + * adds a 256-byte fgets line and newlib printf (~1 KB). Empirically the + * task hard-faults at *16 (8 KB) once the SolidSyslog setup runs — newlib + * printf and the formatter path together exceed that budget. *32 (16 KB) + * keeps the smoke run stable; a follow-up will introduce CMake-driven + * memory scaling once the budget is properly characterised. The task only + * exists in this single-task example, so heap_4 (96 KB) absorbs it. */ +#define INTERACTIVE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 32U) -/* Static storage for the network interface and its single endpoint. The - * Plus-TCP API requires these to outlive the IP stack. */ +/* Static IPv4 wiring matching the QEMU slirp default. 10.0.2.15 is the + * standard slirp DHCP-allocated guest address; we hardcode it here so no + * DHCP server is required. The destination address — 10.0.2.2, the slirp + * gateway routed to the QEMU host — is the listener target driven into + * the static resolver. */ +static const uint8_t TEST_IP_ADDRESS[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 15U}; +static const uint8_t TEST_NETMASK[ipIP_ADDRESS_LENGTH_BYTES] = {255U, 255U, 255U, 0U}; +static const uint8_t TEST_GATEWAY[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 2U}; +static const uint8_t TEST_DNS[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 3U}; +static const uint8_t TEST_MAC[ipMAC_ADDRESS_LENGTH_BYTES] = {0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U}; +static const uint8_t TEST_DESTINATION_IPV4[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 2U}; + +/* Walking-skeleton TEST_* values — slice-4 will replace these with config + * injected over the interactive command grammar. */ +static const char TEST_HOSTNAME[] = "FreeRtosExample"; +static const char TEST_APP_NAME[] = "SolidSyslogExample"; +static const char TEST_PROCESS_ID[] = "1"; +static const char TEST_MESSAGE_ID[] = "example"; +static const char TEST_MESSAGE[] = "Hello from FreeRTOS"; + +/* Plus-TCP requires the network interface descriptor and its endpoint(s) + * to outlive the IP stack. */ static NetworkInterface_t networkInterface; static NetworkEndPoint_t networkEndPoint; -/* Ensures the smoke task is created exactly once even if the network goes - * down and back up. */ -static BaseType_t smokeTaskCreated = pdFALSE; +static SolidSyslogFreeRtosStaticResolverStorage resolverStorage; +static SolidSyslogFreeRtosDatagramStorage datagramStorage; + +/* Ensures the interactive task is created exactly once even if the network + * goes down and back up. */ +static BaseType_t interactiveTaskCreated = pdFALSE; extern NetworkInterface_t* pxMPS2_FillInterfaceDescriptor(BaseType_t xEMACIndex, NetworkInterface_t* pxInterface); @@ -99,33 +127,93 @@ static void SetEthernetIrqPriority(void) *ipr = ETHERNET_IRQ_PRIORITY; } -static void SmokeTask(void* argument) +static void GetHostname(struct SolidSyslogFormatter* formatter) { - (void) argument; + SolidSyslogFormatter_BoundedString(formatter, TEST_HOSTNAME, sizeof(TEST_HOSTNAME) - 1U); +} - printf("network up\n"); - fflush(stdout); +static void GetAppName(struct SolidSyslogFormatter* formatter) +{ + SolidSyslogFormatter_BoundedString(formatter, TEST_APP_NAME, sizeof(TEST_APP_NAME) - 1U); +} - Socket_t socket = FreeRTOS_socket(FREERTOS_AF_INET, FREERTOS_SOCK_DGRAM, FREERTOS_IPPROTO_UDP); - if (socket != FREERTOS_INVALID_SOCKET) - { - struct freertos_sockaddr destination; - memset(&destination, 0, sizeof(destination)); - destination.sin_len = (uint8_t) sizeof(destination); - destination.sin_family = FREERTOS_AF_INET; - destination.sin_port = FreeRTOS_htons(TEST_TARGET_PORT); - destination.sin_address.ulIP_IPv4 = FreeRTOS_inet_addr_quick(TEST_GATEWAY[0], TEST_GATEWAY[1], TEST_GATEWAY[2], TEST_GATEWAY[3]); - - /* Pre-resolve ARP for the gateway. Without this the first sendto - * triggers ARP and the datagram is dropped while resolution is in - * flight; under slirp ARP completes in well under 200 ms. */ - FreeRTOS_OutputARPRequest(destination.sin_address.ulIP_IPv4); - vTaskDelay(pdMS_TO_TICKS(200)); - - (void) FreeRTOS_sendto(socket, TEST_PAYLOAD, sizeof(TEST_PAYLOAD), 0, &destination, sizeof(destination)); - - (void) FreeRTOS_closesocket(socket); - } +static void GetProcessId(struct SolidSyslogFormatter* formatter) +{ + SolidSyslogFormatter_BoundedString(formatter, TEST_PROCESS_ID, sizeof(TEST_PROCESS_ID) - 1U); +} + +/* RFC 5424 publication date — walking-skeleton placeholder until S08.03 + * slice 4+ injects a real RTC-backed clock callback. */ +static void GetTimestamp(struct SolidSyslogTimestamp* timestamp) +{ + timestamp->year = 2009U; + timestamp->month = 3U; + timestamp->day = 23U; + timestamp->hour = 0U; + timestamp->minute = 0U; + timestamp->second = 0U; + timestamp->microsecond = 0U; + timestamp->utcOffsetMinutes = 0; +} + +static void GetEndpoint(struct SolidSyslogEndpoint* endpoint) +{ + /* SolidSyslogFreeRtosStaticResolver ignores the host string, so we + * leave it empty; the port still needs to be populated for sendto. */ + SolidSyslogFormatter_BoundedString(endpoint->host, "", 0U); + endpoint->port = (uint16_t) EXAMPLE_UDP_PORT; +} + +static uint32_t GetEndpointVersion(void) +{ + return 0U; +} + +static void InteractiveTask(void* argument) +{ + (void) argument; + + struct SolidSyslogResolver* resolver = SolidSyslogFreeRtosStaticResolver_Create(&resolverStorage, TEST_DESTINATION_IPV4); + struct SolidSyslogDatagram* datagram = SolidSyslogFreeRtosDatagram_Create(&datagramStorage); + + struct SolidSyslogUdpSenderConfig udpConfig = { + .resolver = resolver, + .datagram = datagram, + .endpoint = GetEndpoint, + .endpointVersion = GetEndpointVersion, + }; + struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&udpConfig); + struct SolidSyslogBuffer* buffer = SolidSyslogNullBuffer_Create(sender); + struct SolidSyslogStore* store = SolidSyslogNullStore_Create(); + + struct SolidSyslogConfig config = { + .buffer = buffer, + .sender = NULL, + .clock = GetTimestamp, + .getHostname = GetHostname, + .getAppName = GetAppName, + .getProcessId = GetProcessId, + .store = store, + .sd = NULL, + .sdCount = 0U, + }; + SolidSyslog_Create(&config); + + struct SolidSyslogMessage message = { + .facility = SOLIDSYSLOG_FACILITY_LOCAL0, + .severity = SOLIDSYSLOG_SEVERITY_INFO, + .messageId = TEST_MESSAGE_ID, + .msg = TEST_MESSAGE, + }; + + ExampleInteractive_Run(&message, stdin, NULL); + + SolidSyslog_Destroy(); + SolidSyslogNullStore_Destroy(); + SolidSyslogNullBuffer_Destroy(); + SolidSyslogUdpSender_Destroy(); + SolidSyslogFreeRtosDatagram_Destroy(datagram); + SolidSyslogFreeRtosStaticResolver_Destroy(resolver); vTaskDelete(NULL); } @@ -133,11 +221,11 @@ static void SmokeTask(void* argument) void vApplicationIPNetworkEventHook_Multi(eIPCallbackEvent_t eNetworkEvent, struct xNetworkEndPoint* pxEndPoint) { (void) pxEndPoint; - if ((eNetworkEvent == eNetworkUp) && (smokeTaskCreated == pdFALSE)) + if ((eNetworkEvent == eNetworkUp) && (interactiveTaskCreated == pdFALSE)) { - if (xTaskCreate(SmokeTask, "smoke", configMINIMAL_STACK_SIZE * 4, NULL, tskIDLE_PRIORITY + 1, NULL) == pdPASS) + if (xTaskCreate(InteractiveTask, "interactive", INTERACTIVE_TASK_STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, NULL) == pdPASS) { - smokeTaskCreated = pdTRUE; + interactiveTaskCreated = pdTRUE; } } } @@ -184,8 +272,8 @@ void vApplicationStackOverflowHook(TaskHandle_t task, char* taskName) /* Plus-TCP requires the application to provide a per-endpoint random seed * source for ARP / IP-ID generation. The QEMU mps2-an385 has no RNG; for a - * deterministic smoke test we return the run-time tick mixed with the - * endpoint pointer. */ + * deterministic smoke test we return the run-time tick mixed with a fixed + * constant. */ BaseType_t xApplicationGetRandomNumber(uint32_t* pulValue) { *pulValue = (uint32_t) xTaskGetTickCount() ^ 0xA5A5A5A5U; diff --git a/Example/FreeRtos/cmake/arm-none-eabi.cmake b/Example/FreeRtos/cmake/arm-none-eabi.cmake index 5fc38d42..65c559e6 100644 --- a/Example/FreeRtos/cmake/arm-none-eabi.cmake +++ b/Example/FreeRtos/cmake/arm-none-eabi.cmake @@ -22,3 +22,13 @@ set(CMAKE_RANLIB arm-none-eabi-ranlib) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +# Architecture flags applied globally so libSolidSyslog.a (compiled outside +# the executable target's PRIVATE compile_options) emits Thumb-2 instructions +# the Cortex-M3 can decode. arm-none-eabi-gcc otherwise defaults to ARM mode +# for armv7+, which links cleanly but hard-faults on the first call from +# Thumb code (slice 3b.2 hit this on SolidSyslogUdpSender_Create). Per-target +# additions in HelloWorld / SingleTask remain idempotent on top of this. +set(CMAKE_C_FLAGS_INIT "-mcpu=cortex-m3 -mthumb -ffunction-sections -fdata-sections -fno-common") +set(CMAKE_CXX_FLAGS_INIT "-mcpu=cortex-m3 -mthumb -ffunction-sections -fdata-sections -fno-common") +set(CMAKE_ASM_FLAGS_INIT "-mcpu=cortex-m3 -mthumb") diff --git a/Platform/FreeRtos/Source/SolidSyslogAddress.c b/Platform/FreeRtos/Source/SolidSyslogAddress.c new file mode 100644 index 00000000..86c56276 --- /dev/null +++ b/Platform/FreeRtos/Source/SolidSyslogAddress.c @@ -0,0 +1,6 @@ +#include "SolidSyslogAddress.h" + +struct SolidSyslogAddress* SolidSyslogAddress_FromStorage(SolidSyslogAddressStorage* storage) +{ + return (struct SolidSyslogAddress*) storage; +} diff --git a/Tests/FreeRtos/CmsdkUartFake.c b/Tests/FreeRtos/CmsdkUartFake.c index 95a2eb45..7aad4613 100644 --- a/Tests/FreeRtos/CmsdkUartFake.c +++ b/Tests/FreeRtos/CmsdkUartFake.c @@ -9,6 +9,7 @@ #define BAUDDIV_OFFSET 0x010U #define TX_FULL_BIT 0x01U +#define RX_FULL_BIT 0x02U #define TX_OVRE_BIT 0x04U static struct @@ -23,6 +24,9 @@ static struct int readsRemainingBeforeTxReady; bool txOverrunOccurred; int sleepCallCount; + uint32_t receivedByte; + int readsBeforeRxReadyDefault; + int readsRemainingBeforeRxReady; } fake; static uint32_t Fake_Read32(uintptr_t address) @@ -39,11 +43,23 @@ static uint32_t Fake_Read32(uintptr_t address) { fake.state &= ~TX_FULL_BIT; } + if (fake.readsRemainingBeforeRxReady > 0) + { + fake.readsRemainingBeforeRxReady--; + if (fake.readsRemainingBeforeRxReady == 0) + { + fake.state |= RX_FULL_BIT; + } + } result = fake.state; } else if (offset == DATA_OFFSET) { - result = fake.data; + if ((fake.state & RX_FULL_BIT) != 0U) + { + result = fake.receivedByte; + fake.state &= ~RX_FULL_BIT; + } } else if (offset == CTRL_OFFSET) { @@ -114,6 +130,9 @@ void CmsdkUartFake_Reset(uintptr_t baseAddress) fake.readsRemainingBeforeTxReady = 0; fake.txOverrunOccurred = false; fake.sleepCallCount = 0; + fake.receivedByte = 0U; + fake.readsBeforeRxReadyDefault = 0; + fake.readsRemainingBeforeRxReady = 0; } const CmsdkUartMemoryAccess* CmsdkUartFake_Access(void) @@ -150,3 +169,21 @@ int CmsdkUartFake_SleepCallCount(void) { return fake.sleepCallCount; } + +void CmsdkUartFake_SetReceivedByte(char byte) +{ + fake.receivedByte = (uint32_t) (unsigned char) byte; + if (fake.readsBeforeRxReadyDefault == 0) + { + fake.state |= RX_FULL_BIT; + } + else + { + fake.readsRemainingBeforeRxReady = fake.readsBeforeRxReadyDefault; + } +} + +void CmsdkUartFake_SetReadsBeforeRxReady(int reads) +{ + fake.readsBeforeRxReadyDefault = reads; +} diff --git a/Tests/FreeRtos/CmsdkUartFake.h b/Tests/FreeRtos/CmsdkUartFake.h index d14d234e..6b9f863a 100644 --- a/Tests/FreeRtos/CmsdkUartFake.h +++ b/Tests/FreeRtos/CmsdkUartFake.h @@ -36,6 +36,16 @@ extern "C" * Lets tests assert the spin loop yielded the CPU between status reads. */ int CmsdkUartFake_SleepCallCount(void); + /* Pre-arm a received byte: the next DATA read returns this value, with + * STATE.RXFULL=1 reported on the preceding STATE read so the driver's + * RX-ready check sees data available. */ + void CmsdkUartFake_SetReceivedByte(char byte); + + /* Number of STATE reads after SetReceivedByte before the fake asserts + * STATE.RXFULL. Default is 0 — RXFULL set immediately. Use a positive + * value to force the driver to spin on STATE before reading DATA. */ + void CmsdkUartFake_SetReadsBeforeRxReady(int reads); + #ifdef __cplusplus } #endif diff --git a/Tests/FreeRtos/CmsdkUartTest.cpp b/Tests/FreeRtos/CmsdkUartTest.cpp index 65754aa6..cfcaa7ee 100644 --- a/Tests/FreeRtos/CmsdkUartTest.cpp +++ b/Tests/FreeRtos/CmsdkUartTest.cpp @@ -26,6 +26,11 @@ TEST(CmsdkUart, InitEnablesTransmitter) LONGS_EQUAL(0x01, CmsdkUartFake_GetCtrl() & 0x01); } +TEST(CmsdkUart, InitEnablesReceiver) +{ + LONGS_EQUAL(0x02, CmsdkUartFake_GetCtrl() & 0x02); +} + TEST(CmsdkUart, PutCharWritesByteToDataRegister) { CmsdkUart_PutChar('A'); @@ -74,3 +79,32 @@ TEST(CmsdkUart, WriteOfMultipleBytesEmitsAllByteWithoutOverrun) LONGS_EQUAL('B', CmsdkUartFake_GetData()); CHECK_FALSE(CmsdkUartFake_TxOverrunOccurred()); } + +TEST(CmsdkUart, GetCharReturnsByteFromDataRegister) +{ + CmsdkUartFake_SetReceivedByte('Q'); + LONGS_EQUAL('Q', CmsdkUart_GetChar()); +} + +TEST(CmsdkUart, GetCharSpinsForRxFullToBecomeSetBeforeReadingDataRegister) +{ + CmsdkUartFake_SetReadsBeforeRxReady(2); + CmsdkUartFake_SetReceivedByte('Z'); + LONGS_EQUAL('Z', CmsdkUart_GetChar()); +} + +TEST(CmsdkUart, GetCharCallsSleepWhileSpinningForRxFull) +{ + CmsdkUartFake_SetReadsBeforeRxReady(2); + CmsdkUartFake_SetReceivedByte('Z'); + (void) CmsdkUart_GetChar(); + CHECK(CmsdkUartFake_SleepCallCount() > 0); +} + +TEST(CmsdkUart, GetCharReturnsImmediatelyWhenReceiverHasByte) +{ + CmsdkUartFake_SetReadsBeforeRxReady(0); + CmsdkUartFake_SetReceivedByte('X'); + LONGS_EQUAL('X', CmsdkUart_GetChar()); + LONGS_EQUAL(0, CmsdkUartFake_SleepCallCount()); +} From 6c12ab665f3d90900177ca6ab49367cf7f35d81b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 9 May 2026 13:39:23 +0000 Subject: [PATCH 2/2] fix: address CodeRabbit review feedback on slice 3b.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings, all valid: 1. CmsdkUartFake.c::SetReceivedByte didn't clear leftover RX_FULL_BIT or readsRemainingBeforeRxReady before applying the new mode, so back-to-back arms could carry stale state into the new arm. Added a regression test (SetReadsBeforeRxReady(0)+SetReceivedByte then SetReadsBeforeRxReady(2)+SetReceivedByte; the second GetChar must spin) which fails on the unfixed fake and passes once SetReceivedByte resets RX_FULL_BIT and the countdown up front. 2. main.c::RtosSleep called vTaskDelay(pdMS_TO_TICKS(milliseconds)) which truncates 1 ms to 0 ticks at configTICK_RATE_HZ=100. vTaskDelay(0) returns without yielding, so CmsdkUart's spin-with-sleep(1) loops would busy-spin instead of blocking the task. Latent on QEMU (TX spin never iterates; RX spin gets preempted by SysTick→IP task) but a real correctness bug on silicon. Floor non-zero requests at one tick. 3. main.c hardcoded the RFC 5424 publication-date timestamp inline, inconsistent with the other walking-skeleton TEST_* defaults (TEST_HOSTNAME, TEST_APP_NAME, etc.). Promoted to TEST_TIMESTAMP fixture; GetTimestamp now does *timestamp = TEST_TIMESTAMP. Co-Authored-By: Claude Opus 4.7 (1M context) --- Example/FreeRtos/SingleTask/main.c | 35 ++++++++++++++++++++---------- Tests/FreeRtos/CmsdkUartFake.c | 4 ++++ Tests/FreeRtos/CmsdkUartTest.cpp | 10 +++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Example/FreeRtos/SingleTask/main.c b/Example/FreeRtos/SingleTask/main.c index f724c3b7..273d8bfa 100644 --- a/Example/FreeRtos/SingleTask/main.c +++ b/Example/FreeRtos/SingleTask/main.c @@ -87,6 +87,19 @@ static const char TEST_PROCESS_ID[] = "1"; static const char TEST_MESSAGE_ID[] = "example"; static const char TEST_MESSAGE[] = "Hello from FreeRTOS"; +/* RFC 5424 publication date — placeholder until S08.03 slice 4+ injects a + * real RTC-backed clock callback. */ +static const struct SolidSyslogTimestamp TEST_TIMESTAMP = { + .year = 2009U, + .month = 3U, + .day = 23U, + .hour = 0U, + .minute = 0U, + .second = 0U, + .microsecond = 0U, + .utcOffsetMinutes = 0, +}; + /* Plus-TCP requires the network interface descriptor and its endpoint(s) * to outlive the IP stack. */ static NetworkInterface_t networkInterface; @@ -115,7 +128,16 @@ static void MmioWrite32(uintptr_t address, uint32_t value) static void RtosSleep(int milliseconds) { - vTaskDelay(pdMS_TO_TICKS((TickType_t) milliseconds)); + /* Round any non-zero millisecond request up to at least one tick so a + * sub-tick sleep (e.g. CmsdkUart's 1 ms yield against a 100 Hz tick, + * where pdMS_TO_TICKS(1) == 0) still blocks the task instead of falling + * through vTaskDelay(0) and busy-spinning the spin-and-yield loops. */ + TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds); + if ((milliseconds > 0) && (ticks == 0U)) + { + ticks = 1U; + } + vTaskDelay(ticks); } static const CmsdkUartMemoryAccess MMIO_ACCESS = {MmioRead32, MmioWrite32, RtosSleep}; @@ -142,18 +164,9 @@ static void GetProcessId(struct SolidSyslogFormatter* formatter) SolidSyslogFormatter_BoundedString(formatter, TEST_PROCESS_ID, sizeof(TEST_PROCESS_ID) - 1U); } -/* RFC 5424 publication date — walking-skeleton placeholder until S08.03 - * slice 4+ injects a real RTC-backed clock callback. */ static void GetTimestamp(struct SolidSyslogTimestamp* timestamp) { - timestamp->year = 2009U; - timestamp->month = 3U; - timestamp->day = 23U; - timestamp->hour = 0U; - timestamp->minute = 0U; - timestamp->second = 0U; - timestamp->microsecond = 0U; - timestamp->utcOffsetMinutes = 0; + *timestamp = TEST_TIMESTAMP; } static void GetEndpoint(struct SolidSyslogEndpoint* endpoint) diff --git a/Tests/FreeRtos/CmsdkUartFake.c b/Tests/FreeRtos/CmsdkUartFake.c index 7aad4613..748a9029 100644 --- a/Tests/FreeRtos/CmsdkUartFake.c +++ b/Tests/FreeRtos/CmsdkUartFake.c @@ -172,7 +172,11 @@ int CmsdkUartFake_SleepCallCount(void) void CmsdkUartFake_SetReceivedByte(char byte) { + /* Clear leftover RX-ready state from a prior arm so back-to-back calls + * don't carry RX_FULL_BIT or a stale countdown into the new mode. */ fake.receivedByte = (uint32_t) (unsigned char) byte; + fake.state &= ~RX_FULL_BIT; + fake.readsRemainingBeforeRxReady = 0; if (fake.readsBeforeRxReadyDefault == 0) { fake.state |= RX_FULL_BIT; diff --git a/Tests/FreeRtos/CmsdkUartTest.cpp b/Tests/FreeRtos/CmsdkUartTest.cpp index cfcaa7ee..2d288b42 100644 --- a/Tests/FreeRtos/CmsdkUartTest.cpp +++ b/Tests/FreeRtos/CmsdkUartTest.cpp @@ -108,3 +108,13 @@ TEST(CmsdkUart, GetCharReturnsImmediatelyWhenReceiverHasByte) LONGS_EQUAL('X', CmsdkUart_GetChar()); LONGS_EQUAL(0, CmsdkUartFake_SleepCallCount()); } + +TEST(CmsdkUart, GetCharSpinsAfterReArmFromImmediateReadyToDelayedReady) +{ + CmsdkUartFake_SetReadsBeforeRxReady(0); + CmsdkUartFake_SetReceivedByte('A'); + CmsdkUartFake_SetReadsBeforeRxReady(2); + CmsdkUartFake_SetReceivedByte('B'); + LONGS_EQUAL('B', CmsdkUart_GetChar()); + CHECK(CmsdkUartFake_SleepCallCount() > 0); +}