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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -139,3 +139,36 @@ jobs:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf
retention-days: 1

test-nucleo-renode:
name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE)
needs: build-arm-nucleo
runs-on: ubuntu-24.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Download Built NUCLEO-F401RE ELF
uses: actions/download-artifact@v4
with:
name: nucleo-f401re-demo-elf
path: targets/STMicroelectronics/NUCLEO_F401RE/build/app

- name: Install Portable Renode Emulation Environment
run: |
wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz
mkdir -p $HOME/renode
tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1
rm renode-latest.linux-portable.tar.gz
echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
run: |
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
58 changes: 56 additions & 2 deletions targets/STMicroelectronics/NUCLEO_F401RE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc

These scripts clean the build directory, run CMake configuration, and compile the target executable.

## Emulation & Regression Testing

Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl`
derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB
and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and
the `_sbrk()` bound both depend on them.

### Interactive Simulation

```bash
renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc
```

USART2 is the demo console, shown in a terminal analyzer.

### Automated Headless Test

```bash
python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py
```

Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and
quits on its own, so the result does not depend on host speed. Exits non-zero
if any assertion is unmet. This gates CI as the `test-nucleo-renode` job.

The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same
ground for use with `renode-test`.

### What is asserted

The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a
`[SELF-TEST]` summary the harness asserts on:

| # | Self-test | Guards against |
|---|-----------|----------------|
| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation |
| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path |
| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base |
| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM |
| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression |
| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration |
| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped |

Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside
the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()`
against the end of SRAM rather than `_heap_limit` let it succeed and handed
`malloc()` memory owned by the ThreadX byte pool and the main stack.

Beyond the self-tests, the harness asserts the boot banner reaches the console,
the blink thread and the 1 Hz application timer have both run (covering the LED
path and the timer service), and that the mutex, queue, event-flag and semaphore
counters are all non-zero.

## Flashing the Application

Connect the NUCLEO-F401RE board via the ST-LINK USB connector.
Expand DownExpand Up@@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro
- **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers
- **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`)
- **`cmake/`**: Cross-compilation module definitions
- **`scripts/`**: Utility build automation scripts
- **`scripts/`**: Utility build automation scripts and the headless Renode test runner
- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite


## Validation Record

### Verification Environment
- **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI
- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash)
- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests
- **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM)
- **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers).
- **Board Hardware**: NUCLEO-F401RE
Expand Down
108 changes: 105 additions & 3 deletions targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,16 +8,25 @@
* SPDX-License-Identifier: MIT
*/

#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
#include "bsp/board.h"
#include "bsp/led.h"
#include "bsp/console.h"
#include "board_config.h"
#include "cloud_config.h"
#include "stm32f4xx_hal.h"

#define THREAD_STACK_SIZE 1024

/* Bytes of SRAM held back above the ThreadX byte pool for the main stack,
* which serves every interrupt handler once the scheduler is running. */
#define MAIN_STACK_MARGIN 4096

typedef struct {
CHAR *name;
UINT state;
Expand DownExpand Up@@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) {
CHAR *stack_ptr;
ULONG pool_size;

/* Calculate available RAM for the byte pool, leaving 4KB margin for system
* stack at the top (0x20018000) */
pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory;
/* Calculate available RAM for the byte pool, leaving a 4 KB margin for the
* main stack at the top of SRAM. */
pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory;

/* Initialize the byte pool */
status = tx_byte_pool_create(&byte_pool, "system byte pool",
Expand DownExpand Up@@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) {
thread_registry[8] = &semaphore_thread;
}

/* ------------------------------------------------------------------------- *
* Startup self-tests
*
* These run before tx_kernel_enter() so a failure is reported even when the
* scheduler never starts. scripts/test_renode.py asserts on the summary line.
* ------------------------------------------------------------------------- */

/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */
extern char _end;
extern char _heap_limit;
extern char __RAM_segment_used_end__;

extern void *_sbrk(ptrdiff_t incr);

static unsigned selftest_failures = 0;

static void selftest_report(int passed, const char *message) {
if (passed) {
printf("[+] PASS: %s\r\n", message);
} else {
selftest_failures++;
printf("[-] FAIL: %s\r\n", message);
}
}

static void run_startup_self_tests(void) {
const uintptr_t heap_base = (uintptr_t)&_end;
const uintptr_t heap_limit = (uintptr_t)&_heap_limit;
const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__;
char msg[128];

printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n");

/* 1. A modest request lands inside the heap reservation. */
void *p_ok = _sbrk(64);
selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) &&
((uintptr_t)p_ok < heap_limit),
"_sbrk() valid allocation inside the heap reservation");

/* 2. Releasing it returns the break to the heap base, leaving the heap
* exactly as the remaining tests found it. */
selftest_report(_sbrk(-64) != (void *)-1,
"_sbrk() released 64 bytes back to the heap base");

/* 3. Shrinking below the heap base is rejected. */
errno = 0;
void *p_under = _sbrk(-128);
selftest_report((p_under == (void *)-1) && (errno == EINVAL),
"_sbrk() underflow guard rejected with EINVAL");

/* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM
* but far exceeds the heap reservation, so bounding _sbrk() against the end
* of SRAM rather than _heap_limit let this succeed and handed malloc()
* memory owned by the ThreadX byte pool and the main stack. */
errno = 0;
void *p_over = _sbrk((ptrdiff_t)0x8000);
selftest_report((p_over == (void *)-1) && (errno == ENOMEM),
"_sbrk() rejects a request that fits SRAM but not the heap");

/* 5. The heap reservation must end at or below the first byte ThreadX owns. */
snprintf(msg, sizeof(msg),
"heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX",
(unsigned long)heap_base, (unsigned long)heap_limit,
(unsigned long)pool_base);
selftest_report(heap_limit <= pool_base, msg);

/* 6. SystemClock_Config() reached the documented 84 MHz. */
snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)",
(unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ);
selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg);

/* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick()
* is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the
* timer is still ticking afterwards. The spin cap is a liveness bound, not a
* timing expectation: one TIM2 tick is ~84000 core cycles. */
uint32_t tick_start = HAL_GetTick();
uint32_t spins = 0;
while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) {
spins++;
}
selftest_report(HAL_GetTick() != tick_start,
"HAL timebase (TIM2) tick advancing");

if (selftest_failures == 0) {
printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n");
} else {
printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n",
selftest_failures);
}
}

int main(void) {
bsp_board_init();

run_startup_self_tests();

/* Start the ThreadX kernel */
tx_kernel_enter();

Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2026 Eclipse ThreadX contributors
//
// This program and the accompanying materials are made available
// under the terms of the MIT license which is available at
// https://opensource.org/license/mit.
//
// SPDX-License-Identifier: MIT
//
// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F).
//
// Renode ships no NUCLEO-F401RE board description, so this derives one from the
// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429
// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker
// script and _sbrk() bound both depend on those limits being accurate.

using "platforms/cpus/stm32f4.repl"

sram:
size: 0x18000

flash:
size: 0x80000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
:name: NUCLEO-F401RE - ThreadX Demo (headless CI run)
:description: Deterministic virtual-time run driven by scripts/test_renode.py.

# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of
# it. Renode resolves $ORIGIN only in variable assignment, so an included script
# cannot be located relative to the file including it, and the two run
# differently anyway: the demo script free-runs for interactive use, while this
# one advances a fixed span of virtual time so the run is reproducible and
# terminates on its own instead of depending on wall clock.

Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what
# the assertions in test_renode.py read.
showAnalyzer usart2

$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

# Long enough for the startup self-tests, the 1 Hz application timer, and at
# least two reporter-thread status blocks with non-zero RTOS counters.
emulation RunFor "4"

quit
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
:name: NUCLEO-F401RE - ThreadX Device Monitor Demo
:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4)

# Clear previous emulation state
Clear

using sysbus
mach create "NUCLEO_F401RE"

$platform?=$ORIGIN/nucleo_f401re.repl
machine LoadPlatformDescription $platform

# USART2 is the ST-LINK virtual COM port on this board and the demo console.
showAnalyzer usart2

# Portable relative path using Renode's built-in $ORIGIN variable
$bin?=$ORIGIN/../build/app/nucleo_f401re.elf

macro reset
"""
sysbus LoadELF $bin
"""
runMacro $reset

start
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
*** Settings ***
Suite Setup Setup
Suite Teardown Teardown
Test Setup Reset Emulation
Test Teardown Test Teardown
Resource ${RENODEKEYWORDS}

*** Test Cases ***
Should Pass Startup Self-Tests
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15
Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15
Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15

Should Run ThreadX Scheduler And Exercise RTOS Primitives
Execute Command include @${CURDIR}/nucleo_f401re_demo.resc
Create Terminal Tester sysbus.usart2

Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15
Wait For Line On Uart System Status: timeout=15
# The blink thread drives bsp_led_toggle() on PA5 and the application timer
# drives the wake counter, so a non-zero pair covers both BSP paths.
Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true
Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true
Loading
Loading