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
2 changes: 1 addition & 1 deletion Core/Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
@@ -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 - <BOM>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
Expand Down
31 changes: 27 additions & 4 deletions Example/FreeRtos/Common/CmsdkUart.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,39 @@

#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

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)
{
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)
Expand Down Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions Example/FreeRtos/Common/CmsdkUart.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
35 changes: 28 additions & 7 deletions Example/FreeRtos/Common/Syscalls.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down
31 changes: 24 additions & 7 deletions Example/FreeRtos/SingleTask/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
$<TARGET_OBJECTS:solid_syslog_freertos_upstream>
)

Expand All @@ -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
Expand Down
Loading
Loading