Skip to content

Add Renode regression tests for the NUCLEO-F401RE target - #51

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests
Aug 31, 2026
Merged

Add Renode regression tests for the NUCLEO-F401RE target#51
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests

Conversation

@fdesbiens

@fdesbiensfdesbiens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Add Renode regression tests for the NUCLEO-F401RE target

Summary

The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console, and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which already gates CI on a headless Renode run.

Follow-up to #50, which noted this gap under "Not addressed".

Renode platform

Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic stm32f4.repl and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being accurate.

Startup self-tests

main.c gains seven self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts:

#Self-testGuards against
1_sbrk() allocation lands inside the heap reservationheap escaping its linker reservation
2_sbrk() releases back to the heap basebroken negative-increment path
3_sbrk() underflow rejected with EINVALshrinking below the heap base
4_sbrk() rejects a request that fits SRAM but not the heapbounding the heap at the end of SRAM
5Heap reservation ends at or below the ThreadX byte poollinker script layout regression
6SystemCoreClock is 84 MHza silently wrong PLL configuration
7HAL timebase (TIM2) tick advancingHAL_InitTick() re-entry leaving TIM2 stopped

The regression guard, and what it revealed

Test 4 targets the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the 512-byte heap reservation.

I verified the suite has teeth by reinstating the old end-of-SRAM bound and re-running it. It failed, and the failure was worse than expected: with that bound, newlib's firstmalloc() took roughly 4 KB and put the break at 0x20002170inside the ThreadX byte pool, which holds every thread stack and the queue buffer. That also broke self-tests 1 and 3, since the break had already escaped the reservation before they ran. The pre-#50 demo was therefore corrupting pool memory on every boot; it simply happened not to fault.

With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation. So the reservation is adequate as it stands and _Min_Heap_Size does not need raising.

Harness

scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner reached the console, that 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.

renode/nucleo_f401re_demo.robot covers the same ground for renode-test, and nucleo_f401re_demo.resc is the interactive counterpart.

The new test-nucleo-renode CI job mirrors test-polarfire-renode, consuming the ELF artifact from build-arm-nucleo.

Verification

Locally, with Renode 1.16.1 and the CI-pinned Arm GNU Toolchain 14.2.Rel1:

  • All 7 self-tests pass; harness exits 0.
  • Reintroducing the heap-bound bug: 3 self-tests fail and the harness exits 1, so the gate genuinely gates.
  • Clean build, -Wall -Wshadow -Wdouble-promotion -Werror: 22068 B ROM (up from 20120 B for the self-tests), 6000 B RAM unchanged.

Also

main.c drops a hardcoded 0x20018000 in favour of BSP_RAM_END, now that board_config.h is in scope, and names the 4 KB main-stack margin. Same value, no behaviour change.

AI assistance disclosure

This work was AI-assisted by me (@fdesbiens). The self-tests live in main.c, whose header follows the project's existing MIT form; the AGENTS.md AI Disclosure headers added in #50 remain on the four BSP files that carry Codex-derived content.

The ARM target had no runtime test. Its CI job checked only that the ELF
existed, so the clock configuration, the TIM2 HAL timebase, the console and
the heap bound were all unexercised. This brings it to parity with the
PolarFire target, which gates CI on a headless Renode run.
Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl
derives one from the generic STM32F4 CPU platform and corrects Flash to
512 KB and SRAM to 96 KB. The generic platform is sized for the larger
F407/F429 parts, and both the linker script's heap reservation and the
_sbrk() bound depend on those limits being right.
main.c gains seven startup self-tests that run before tx_kernel_enter(), so
a failure is reported even when the scheduler never starts. They cover the
_sbrk() allocate, release, underflow and over-limit paths, the invariant
that the heap reservation ends at or below the ThreadX byte pool, the
84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig()
re-enters HAL_InitTick().
Test 4 is the regression guard for the heap bound fixed in eclipse-threadx#50. Requesting
32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation.
Reinstating the old end-of-SRAM bound was verified to fail the suite:
newlib's first malloc() then took roughly 4 KB and put the break at
0x20002170, inside the ThreadX byte pool, which also broke self-tests 1
and 3. With the bound correct, _sbrk() refuses the oversized request,
newlib retries smaller, and malloc(64) succeeds using 72 bytes of the
512-byte reservation - so the reservation is adequate and no heap growth
is needed.
scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed
span of virtual time and quits on its own rather than depending on wall
clock. Beyond the self-tests it asserts the boot banner, that 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. The suite was confirmed to exit 1 on a
reintroduced bug and 0 on the fixed tree.
The Robot Framework suite covers the same ground for renode-test, and the
new test-nucleo-renode CI job mirrors test-polarfire-renode.
main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that
board_config.h is in scope, with the 4 KB main-stack margin named.
@fdesbiens
fdesbiens merged commit ef61eae into eclipse-threadx:devAug 31, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feat/nucleo-renode-tests branch August 31, 2026 15:44
fdesbiens added a commit that referenced this pull request Aug 31, 2026
Two problems in the pipeline, one of them mine.
The NUCLEO Renode job fetched renode-latest.linux-portable.tar.gz with no
version pin and no checksum, while the PolarFire job three jobs above it
already pinned Renode 1.16.1 and verified its SHA256. That pin landed in #49,
so it was present in this file when #51 added the NUCLEO job; the new job was
modelled on an older copy of the PolarFire step rather than the current one.
The result was a suite whose emulator could change under it on any Renode
release, with nothing verifying what was downloaded. The NUCLEO job now uses
the same pinned, checksum-verified step as PolarFire. The checksum was
recomputed from the published artefact rather than copied on trust.
Separately, every run re-downloaded roughly a gigabyte: the xPack RISC-V
toolchain at ~414 MB and Renode at ~52 MB in each of two jobs. All three are
now restored by actions/cache, keyed on the pinned version so a future bump
invalidates the cache instead of silently serving the old one. This completes
what #53 started for the Arm toolchain.
Caching only makes sense because these are now pinned. Caching an unpinned
"latest" artefact would have frozen CI on whichever build happened to be
fetched first, turning a reproducibility gap into an invisible one.
All four jobs now follow the same shape: cache, install only on a cache miss,
then put the tool on PATH as a separate step so it runs on hit and miss alike.
The PolarFire job also gains a version-reporting step, matching the Arm job, so
the log records which compiler produced the ELF.
Verified both constructed download URLs resolve, and that the Renode 1.16.1
checksum matches the published artefact.
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, '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" + '
Add Renode regression tests for the NUCLEO-F401RE target by fdesbiens · Pull Request #51 · eclipse-threadx/samplex · GitHub
Skip to content

Add Renode regression tests for the NUCLEO-F401RE target - #51

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests
Aug 31, 2026
Merged

Add Renode regression tests for the NUCLEO-F401RE target#51
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests

Conversation

@fdesbiens

@fdesbiensfdesbiens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Add Renode regression tests for the NUCLEO-F401RE target

Summary

The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console, and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which already gates CI on a headless Renode run.

Follow-up to #50, which noted this gap under "Not addressed".

Renode platform

Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic stm32f4.repl and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being accurate.

Startup self-tests

main.c gains seven self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts:

#Self-testGuards against
1_sbrk() allocation lands inside the heap reservationheap escaping its linker reservation
2_sbrk() releases back to the heap basebroken negative-increment path
3_sbrk() underflow rejected with EINVALshrinking below the heap base
4_sbrk() rejects a request that fits SRAM but not the heapbounding the heap at the end of SRAM
5Heap reservation ends at or below the ThreadX byte poollinker script layout regression
6SystemCoreClock is 84 MHza silently wrong PLL configuration
7HAL timebase (TIM2) tick advancingHAL_InitTick() re-entry leaving TIM2 stopped

The regression guard, and what it revealed

Test 4 targets the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the 512-byte heap reservation.

I verified the suite has teeth by reinstating the old end-of-SRAM bound and re-running it. It failed, and the failure was worse than expected: with that bound, newlib's firstmalloc() took roughly 4 KB and put the break at 0x20002170inside the ThreadX byte pool, which holds every thread stack and the queue buffer. That also broke self-tests 1 and 3, since the break had already escaped the reservation before they ran. The pre-#50 demo was therefore corrupting pool memory on every boot; it simply happened not to fault.

With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation. So the reservation is adequate as it stands and _Min_Heap_Size does not need raising.

Harness

scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner reached the console, that 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.

renode/nucleo_f401re_demo.robot covers the same ground for renode-test, and nucleo_f401re_demo.resc is the interactive counterpart.

The new test-nucleo-renode CI job mirrors test-polarfire-renode, consuming the ELF artifact from build-arm-nucleo.

Verification

Locally, with Renode 1.16.1 and the CI-pinned Arm GNU Toolchain 14.2.Rel1:

  • All 7 self-tests pass; harness exits 0.
  • Reintroducing the heap-bound bug: 3 self-tests fail and the harness exits 1, so the gate genuinely gates.
  • Clean build, -Wall -Wshadow -Wdouble-promotion -Werror: 22068 B ROM (up from 20120 B for the self-tests), 6000 B RAM unchanged.

Also

main.c drops a hardcoded 0x20018000 in favour of BSP_RAM_END, now that board_config.h is in scope, and names the 4 KB main-stack margin. Same value, no behaviour change.

AI assistance disclosure

This work was AI-assisted by me (@fdesbiens). The self-tests live in main.c, whose header follows the project's existing MIT form; the AGENTS.md AI Disclosure headers added in #50 remain on the four BSP files that carry Codex-derived content.

The ARM target had no runtime test. Its CI job checked only that the ELF
existed, so the clock configuration, the TIM2 HAL timebase, the console and
the heap bound were all unexercised. This brings it to parity with the
PolarFire target, which gates CI on a headless Renode run.
Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl
derives one from the generic STM32F4 CPU platform and corrects Flash to
512 KB and SRAM to 96 KB. The generic platform is sized for the larger
F407/F429 parts, and both the linker script's heap reservation and the
_sbrk() bound depend on those limits being right.
main.c gains seven startup self-tests that run before tx_kernel_enter(), so
a failure is reported even when the scheduler never starts. They cover the
_sbrk() allocate, release, underflow and over-limit paths, the invariant
that the heap reservation ends at or below the ThreadX byte pool, the
84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig()
re-enters HAL_InitTick().
Test 4 is the regression guard for the heap bound fixed in eclipse-threadx#50. Requesting
32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation.
Reinstating the old end-of-SRAM bound was verified to fail the suite:
newlib's first malloc() then took roughly 4 KB and put the break at
0x20002170, inside the ThreadX byte pool, which also broke self-tests 1
and 3. With the bound correct, _sbrk() refuses the oversized request,
newlib retries smaller, and malloc(64) succeeds using 72 bytes of the
512-byte reservation - so the reservation is adequate and no heap growth
is needed.
scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed
span of virtual time and quits on its own rather than depending on wall
clock. Beyond the self-tests it asserts the boot banner, that 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. The suite was confirmed to exit 1 on a
reintroduced bug and 0 on the fixed tree.
The Robot Framework suite covers the same ground for renode-test, and the
new test-nucleo-renode CI job mirrors test-polarfire-renode.
main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that
board_config.h is in scope, with the 4 KB main-stack margin named.
@fdesbiens
fdesbiens merged commit ef61eae into eclipse-threadx:devAug 31, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feat/nucleo-renode-tests branch August 31, 2026 15:44
fdesbiens added a commit that referenced this pull request Aug 31, 2026
Two problems in the pipeline, one of them mine.
The NUCLEO Renode job fetched renode-latest.linux-portable.tar.gz with no
version pin and no checksum, while the PolarFire job three jobs above it
already pinned Renode 1.16.1 and verified its SHA256. That pin landed in #49,
so it was present in this file when #51 added the NUCLEO job; the new job was
modelled on an older copy of the PolarFire step rather than the current one.
The result was a suite whose emulator could change under it on any Renode
release, with nothing verifying what was downloaded. The NUCLEO job now uses
the same pinned, checksum-verified step as PolarFire. The checksum was
recomputed from the published artefact rather than copied on trust.
Separately, every run re-downloaded roughly a gigabyte: the xPack RISC-V
toolchain at ~414 MB and Renode at ~52 MB in each of two jobs. All three are
now restored by actions/cache, keyed on the pinned version so a future bump
invalidates the cache instead of silently serving the old one. This completes
what #53 started for the Arm toolchain.
Caching only makes sense because these are now pinned. Caching an unpinned
"latest" artefact would have frozen CI on whichever build happened to be
fetched first, turning a reproducibility gap into an invisible one.
All four jobs now follow the same shape: cache, install only on a cache miss,
then put the tool on PATH as a separate step so it runs on hit and miss alike.
The PolarFire job also gains a version-reporting step, matching the Arm job, so
the log records which compiler produced the ELF.
Verified both constructed download URLs resolve, and that the Renode 1.16.1
checksum matches the published artefact.
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, '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('^' + ".*" + ' Add Renode regression tests for the NUCLEO-F401RE target by fdesbiens · Pull Request #51 · eclipse-threadx/samplex · GitHub
Skip to content

Add Renode regression tests for the NUCLEO-F401RE target - #51

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests
Aug 31, 2026
Merged

Add Renode regression tests for the NUCLEO-F401RE target#51
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests

Conversation

@fdesbiens

@fdesbiensfdesbiens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Add Renode regression tests for the NUCLEO-F401RE target

Summary

The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console, and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which already gates CI on a headless Renode run.

Follow-up to #50, which noted this gap under "Not addressed".

Renode platform

Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic stm32f4.repl and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being accurate.

Startup self-tests

main.c gains seven self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts:

#Self-testGuards against
1_sbrk() allocation lands inside the heap reservationheap escaping its linker reservation
2_sbrk() releases back to the heap basebroken negative-increment path
3_sbrk() underflow rejected with EINVALshrinking below the heap base
4_sbrk() rejects a request that fits SRAM but not the heapbounding the heap at the end of SRAM
5Heap reservation ends at or below the ThreadX byte poollinker script layout regression
6SystemCoreClock is 84 MHza silently wrong PLL configuration
7HAL timebase (TIM2) tick advancingHAL_InitTick() re-entry leaving TIM2 stopped

The regression guard, and what it revealed

Test 4 targets the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the 512-byte heap reservation.

I verified the suite has teeth by reinstating the old end-of-SRAM bound and re-running it. It failed, and the failure was worse than expected: with that bound, newlib's firstmalloc() took roughly 4 KB and put the break at 0x20002170inside the ThreadX byte pool, which holds every thread stack and the queue buffer. That also broke self-tests 1 and 3, since the break had already escaped the reservation before they ran. The pre-#50 demo was therefore corrupting pool memory on every boot; it simply happened not to fault.

With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation. So the reservation is adequate as it stands and _Min_Heap_Size does not need raising.

Harness

scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner reached the console, that 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.

renode/nucleo_f401re_demo.robot covers the same ground for renode-test, and nucleo_f401re_demo.resc is the interactive counterpart.

The new test-nucleo-renode CI job mirrors test-polarfire-renode, consuming the ELF artifact from build-arm-nucleo.

Verification

Locally, with Renode 1.16.1 and the CI-pinned Arm GNU Toolchain 14.2.Rel1:

  • All 7 self-tests pass; harness exits 0.
  • Reintroducing the heap-bound bug: 3 self-tests fail and the harness exits 1, so the gate genuinely gates.
  • Clean build, -Wall -Wshadow -Wdouble-promotion -Werror: 22068 B ROM (up from 20120 B for the self-tests), 6000 B RAM unchanged.

Also

main.c drops a hardcoded 0x20018000 in favour of BSP_RAM_END, now that board_config.h is in scope, and names the 4 KB main-stack margin. Same value, no behaviour change.

AI assistance disclosure

This work was AI-assisted by me (@fdesbiens). The self-tests live in main.c, whose header follows the project's existing MIT form; the AGENTS.md AI Disclosure headers added in #50 remain on the four BSP files that carry Codex-derived content.

The ARM target had no runtime test. Its CI job checked only that the ELF
existed, so the clock configuration, the TIM2 HAL timebase, the console and
the heap bound were all unexercised. This brings it to parity with the
PolarFire target, which gates CI on a headless Renode run.
Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl
derives one from the generic STM32F4 CPU platform and corrects Flash to
512 KB and SRAM to 96 KB. The generic platform is sized for the larger
F407/F429 parts, and both the linker script's heap reservation and the
_sbrk() bound depend on those limits being right.
main.c gains seven startup self-tests that run before tx_kernel_enter(), so
a failure is reported even when the scheduler never starts. They cover the
_sbrk() allocate, release, underflow and over-limit paths, the invariant
that the heap reservation ends at or below the ThreadX byte pool, the
84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig()
re-enters HAL_InitTick().
Test 4 is the regression guard for the heap bound fixed in eclipse-threadx#50. Requesting
32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation.
Reinstating the old end-of-SRAM bound was verified to fail the suite:
newlib's first malloc() then took roughly 4 KB and put the break at
0x20002170, inside the ThreadX byte pool, which also broke self-tests 1
and 3. With the bound correct, _sbrk() refuses the oversized request,
newlib retries smaller, and malloc(64) succeeds using 72 bytes of the
512-byte reservation - so the reservation is adequate and no heap growth
is needed.
scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed
span of virtual time and quits on its own rather than depending on wall
clock. Beyond the self-tests it asserts the boot banner, that 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. The suite was confirmed to exit 1 on a
reintroduced bug and 0 on the fixed tree.
The Robot Framework suite covers the same ground for renode-test, and the
new test-nucleo-renode CI job mirrors test-polarfire-renode.
main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that
board_config.h is in scope, with the 4 KB main-stack margin named.
@fdesbiens
fdesbiens merged commit ef61eae into eclipse-threadx:devAug 31, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feat/nucleo-renode-tests branch August 31, 2026 15:44
fdesbiens added a commit that referenced this pull request Aug 31, 2026
Two problems in the pipeline, one of them mine.
The NUCLEO Renode job fetched renode-latest.linux-portable.tar.gz with no
version pin and no checksum, while the PolarFire job three jobs above it
already pinned Renode 1.16.1 and verified its SHA256. That pin landed in #49,
so it was present in this file when #51 added the NUCLEO job; the new job was
modelled on an older copy of the PolarFire step rather than the current one.
The result was a suite whose emulator could change under it on any Renode
release, with nothing verifying what was downloaded. The NUCLEO job now uses
the same pinned, checksum-verified step as PolarFire. The checksum was
recomputed from the published artefact rather than copied on trust.
Separately, every run re-downloaded roughly a gigabyte: the xPack RISC-V
toolchain at ~414 MB and Renode at ~52 MB in each of two jobs. All three are
now restored by actions/cache, keyed on the pinned version so a future bump
invalidates the cache instead of silently serving the old one. This completes
what #53 started for the Arm toolchain.
Caching only makes sense because these are now pinned. Caching an unpinned
"latest" artefact would have frozen CI on whichever build happened to be
fetched first, turning a reproducibility gap into an invisible one.
All four jobs now follow the same shape: cache, install only on a cache miss,
then put the tool on PATH as a separate step so it runs on hit and miss alike.
The PolarFire job also gains a version-reporting step, matching the Arm job, so
the log records which compiler produced the ELF.
Verified both constructed download URLs resolve, and that the Renode 1.16.1
checksum matches the published artefact.
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, '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('^' + ".*" + ' Add Renode regression tests for the NUCLEO-F401RE target by fdesbiens · Pull Request #51 · eclipse-threadx/samplex · GitHub
Skip to content

Add Renode regression tests for the NUCLEO-F401RE target - #51

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests
Aug 31, 2026
Merged

Add Renode regression tests for the NUCLEO-F401RE target#51
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests

Conversation

@fdesbiens

@fdesbiensfdesbiens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Add Renode regression tests for the NUCLEO-F401RE target

Summary

The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console, and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which already gates CI on a headless Renode run.

Follow-up to #50, which noted this gap under "Not addressed".

Renode platform

Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic stm32f4.repl and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being accurate.

Startup self-tests

main.c gains seven self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts:

#Self-testGuards against
1_sbrk() allocation lands inside the heap reservationheap escaping its linker reservation
2_sbrk() releases back to the heap basebroken negative-increment path
3_sbrk() underflow rejected with EINVALshrinking below the heap base
4_sbrk() rejects a request that fits SRAM but not the heapbounding the heap at the end of SRAM
5Heap reservation ends at or below the ThreadX byte poollinker script layout regression
6SystemCoreClock is 84 MHza silently wrong PLL configuration
7HAL timebase (TIM2) tick advancingHAL_InitTick() re-entry leaving TIM2 stopped

The regression guard, and what it revealed

Test 4 targets the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the 512-byte heap reservation.

I verified the suite has teeth by reinstating the old end-of-SRAM bound and re-running it. It failed, and the failure was worse than expected: with that bound, newlib's firstmalloc() took roughly 4 KB and put the break at 0x20002170inside the ThreadX byte pool, which holds every thread stack and the queue buffer. That also broke self-tests 1 and 3, since the break had already escaped the reservation before they ran. The pre-#50 demo was therefore corrupting pool memory on every boot; it simply happened not to fault.

With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation. So the reservation is adequate as it stands and _Min_Heap_Size does not need raising.

Harness

scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner reached the console, that 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.

renode/nucleo_f401re_demo.robot covers the same ground for renode-test, and nucleo_f401re_demo.resc is the interactive counterpart.

The new test-nucleo-renode CI job mirrors test-polarfire-renode, consuming the ELF artifact from build-arm-nucleo.

Verification

Locally, with Renode 1.16.1 and the CI-pinned Arm GNU Toolchain 14.2.Rel1:

  • All 7 self-tests pass; harness exits 0.
  • Reintroducing the heap-bound bug: 3 self-tests fail and the harness exits 1, so the gate genuinely gates.
  • Clean build, -Wall -Wshadow -Wdouble-promotion -Werror: 22068 B ROM (up from 20120 B for the self-tests), 6000 B RAM unchanged.

Also

main.c drops a hardcoded 0x20018000 in favour of BSP_RAM_END, now that board_config.h is in scope, and names the 4 KB main-stack margin. Same value, no behaviour change.

AI assistance disclosure

This work was AI-assisted by me (@fdesbiens). The self-tests live in main.c, whose header follows the project's existing MIT form; the AGENTS.md AI Disclosure headers added in #50 remain on the four BSP files that carry Codex-derived content.

The ARM target had no runtime test. Its CI job checked only that the ELF
existed, so the clock configuration, the TIM2 HAL timebase, the console and
the heap bound were all unexercised. This brings it to parity with the
PolarFire target, which gates CI on a headless Renode run.
Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl
derives one from the generic STM32F4 CPU platform and corrects Flash to
512 KB and SRAM to 96 KB. The generic platform is sized for the larger
F407/F429 parts, and both the linker script's heap reservation and the
_sbrk() bound depend on those limits being right.
main.c gains seven startup self-tests that run before tx_kernel_enter(), so
a failure is reported even when the scheduler never starts. They cover the
_sbrk() allocate, release, underflow and over-limit paths, the invariant
that the heap reservation ends at or below the ThreadX byte pool, the
84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig()
re-enters HAL_InitTick().
Test 4 is the regression guard for the heap bound fixed in eclipse-threadx#50. Requesting
32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation.
Reinstating the old end-of-SRAM bound was verified to fail the suite:
newlib's first malloc() then took roughly 4 KB and put the break at
0x20002170, inside the ThreadX byte pool, which also broke self-tests 1
and 3. With the bound correct, _sbrk() refuses the oversized request,
newlib retries smaller, and malloc(64) succeeds using 72 bytes of the
512-byte reservation - so the reservation is adequate and no heap growth
is needed.
scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed
span of virtual time and quits on its own rather than depending on wall
clock. Beyond the self-tests it asserts the boot banner, that 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. The suite was confirmed to exit 1 on a
reintroduced bug and 0 on the fixed tree.
The Robot Framework suite covers the same ground for renode-test, and the
new test-nucleo-renode CI job mirrors test-polarfire-renode.
main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that
board_config.h is in scope, with the 4 KB main-stack margin named.
@fdesbiens
fdesbiens merged commit ef61eae into eclipse-threadx:devAug 31, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feat/nucleo-renode-tests branch August 31, 2026 15:44
fdesbiens added a commit that referenced this pull request Aug 31, 2026
Two problems in the pipeline, one of them mine.
The NUCLEO Renode job fetched renode-latest.linux-portable.tar.gz with no
version pin and no checksum, while the PolarFire job three jobs above it
already pinned Renode 1.16.1 and verified its SHA256. That pin landed in #49,
so it was present in this file when #51 added the NUCLEO job; the new job was
modelled on an older copy of the PolarFire step rather than the current one.
The result was a suite whose emulator could change under it on any Renode
release, with nothing verifying what was downloaded. The NUCLEO job now uses
the same pinned, checksum-verified step as PolarFire. The checksum was
recomputed from the published artefact rather than copied on trust.
Separately, every run re-downloaded roughly a gigabyte: the xPack RISC-V
toolchain at ~414 MB and Renode at ~52 MB in each of two jobs. All three are
now restored by actions/cache, keyed on the pinned version so a future bump
invalidates the cache instead of silently serving the old one. This completes
what #53 started for the Arm toolchain.
Caching only makes sense because these are now pinned. Caching an unpinned
"latest" artefact would have frozen CI on whichever build happened to be
fetched first, turning a reproducibility gap into an invisible one.
All four jobs now follow the same shape: cache, install only on a cache miss,
then put the tool on PATH as a separate step so it runs on hit and miss alike.
The PolarFire job also gains a version-reporting step, matching the Arm job, so
the log records which compiler produced the ELF.
Verified both constructed download URLs resolve, and that the Renode 1.16.1
checksum matches the published artefact.
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, '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" + ' Add Renode regression tests for the NUCLEO-F401RE target by fdesbiens · Pull Request #51 · eclipse-threadx/samplex · GitHub
Skip to content

Add Renode regression tests for the NUCLEO-F401RE target - #51

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests
Aug 31, 2026
Merged

Add Renode regression tests for the NUCLEO-F401RE target#51
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests

Conversation

@fdesbiens

@fdesbiensfdesbiens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Add Renode regression tests for the NUCLEO-F401RE target

Summary

The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console, and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which already gates CI on a headless Renode run.

Follow-up to #50, which noted this gap under "Not addressed".

Renode platform

Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic stm32f4.repl and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being accurate.

Startup self-tests

main.c gains seven self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts:

#Self-testGuards against
1_sbrk() allocation lands inside the heap reservationheap escaping its linker reservation
2_sbrk() releases back to the heap basebroken negative-increment path
3_sbrk() underflow rejected with EINVALshrinking below the heap base
4_sbrk() rejects a request that fits SRAM but not the heapbounding the heap at the end of SRAM
5Heap reservation ends at or below the ThreadX byte poollinker script layout regression
6SystemCoreClock is 84 MHza silently wrong PLL configuration
7HAL timebase (TIM2) tick advancingHAL_InitTick() re-entry leaving TIM2 stopped

The regression guard, and what it revealed

Test 4 targets the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the 512-byte heap reservation.

I verified the suite has teeth by reinstating the old end-of-SRAM bound and re-running it. It failed, and the failure was worse than expected: with that bound, newlib's firstmalloc() took roughly 4 KB and put the break at 0x20002170inside the ThreadX byte pool, which holds every thread stack and the queue buffer. That also broke self-tests 1 and 3, since the break had already escaped the reservation before they ran. The pre-#50 demo was therefore corrupting pool memory on every boot; it simply happened not to fault.

With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation. So the reservation is adequate as it stands and _Min_Heap_Size does not need raising.

Harness

scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner reached the console, that 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.

renode/nucleo_f401re_demo.robot covers the same ground for renode-test, and nucleo_f401re_demo.resc is the interactive counterpart.

The new test-nucleo-renode CI job mirrors test-polarfire-renode, consuming the ELF artifact from build-arm-nucleo.

Verification

Locally, with Renode 1.16.1 and the CI-pinned Arm GNU Toolchain 14.2.Rel1:

  • All 7 self-tests pass; harness exits 0.
  • Reintroducing the heap-bound bug: 3 self-tests fail and the harness exits 1, so the gate genuinely gates.
  • Clean build, -Wall -Wshadow -Wdouble-promotion -Werror: 22068 B ROM (up from 20120 B for the self-tests), 6000 B RAM unchanged.

Also

main.c drops a hardcoded 0x20018000 in favour of BSP_RAM_END, now that board_config.h is in scope, and names the 4 KB main-stack margin. Same value, no behaviour change.

AI assistance disclosure

This work was AI-assisted by me (@fdesbiens). The self-tests live in main.c, whose header follows the project's existing MIT form; the AGENTS.md AI Disclosure headers added in #50 remain on the four BSP files that carry Codex-derived content.

The ARM target had no runtime test. Its CI job checked only that the ELF
existed, so the clock configuration, the TIM2 HAL timebase, the console and
the heap bound were all unexercised. This brings it to parity with the
PolarFire target, which gates CI on a headless Renode run.
Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl
derives one from the generic STM32F4 CPU platform and corrects Flash to
512 KB and SRAM to 96 KB. The generic platform is sized for the larger
F407/F429 parts, and both the linker script's heap reservation and the
_sbrk() bound depend on those limits being right.
main.c gains seven startup self-tests that run before tx_kernel_enter(), so
a failure is reported even when the scheduler never starts. They cover the
_sbrk() allocate, release, underflow and over-limit paths, the invariant
that the heap reservation ends at or below the ThreadX byte pool, the
84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig()
re-enters HAL_InitTick().
Test 4 is the regression guard for the heap bound fixed in eclipse-threadx#50. Requesting
32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation.
Reinstating the old end-of-SRAM bound was verified to fail the suite:
newlib's first malloc() then took roughly 4 KB and put the break at
0x20002170, inside the ThreadX byte pool, which also broke self-tests 1
and 3. With the bound correct, _sbrk() refuses the oversized request,
newlib retries smaller, and malloc(64) succeeds using 72 bytes of the
512-byte reservation - so the reservation is adequate and no heap growth
is needed.
scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed
span of virtual time and quits on its own rather than depending on wall
clock. Beyond the self-tests it asserts the boot banner, that 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. The suite was confirmed to exit 1 on a
reintroduced bug and 0 on the fixed tree.
The Robot Framework suite covers the same ground for renode-test, and the
new test-nucleo-renode CI job mirrors test-polarfire-renode.
main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that
board_config.h is in scope, with the 4 KB main-stack margin named.
@fdesbiens
fdesbiens merged commit ef61eae into eclipse-threadx:devAug 31, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feat/nucleo-renode-tests branch August 31, 2026 15:44
fdesbiens added a commit that referenced this pull request Aug 31, 2026
Two problems in the pipeline, one of them mine.
The NUCLEO Renode job fetched renode-latest.linux-portable.tar.gz with no
version pin and no checksum, while the PolarFire job three jobs above it
already pinned Renode 1.16.1 and verified its SHA256. That pin landed in #49,
so it was present in this file when #51 added the NUCLEO job; the new job was
modelled on an older copy of the PolarFire step rather than the current one.
The result was a suite whose emulator could change under it on any Renode
release, with nothing verifying what was downloaded. The NUCLEO job now uses
the same pinned, checksum-verified step as PolarFire. The checksum was
recomputed from the published artefact rather than copied on trust.
Separately, every run re-downloaded roughly a gigabyte: the xPack RISC-V
toolchain at ~414 MB and Renode at ~52 MB in each of two jobs. All three are
now restored by actions/cache, keyed on the pinned version so a future bump
invalidates the cache instead of silently serving the old one. This completes
what #53 started for the Arm toolchain.
Caching only makes sense because these are now pinned. Caching an unpinned
"latest" artefact would have frozen CI on whichever build happened to be
fetched first, turning a reproducibility gap into an invisible one.
All four jobs now follow the same shape: cache, install only on a cache miss,
then put the tool on PATH as a separate step so it runs on hit and miss alike.
The PolarFire job also gains a version-reporting step, matching the Arm job, so
the log records which compiler produced the ELF.
Verified both constructed download URLs resolve, and that the Renode 1.16.1
checksum matches the published artefact.
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, '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('^' + ".*" + ' Add Renode regression tests for the NUCLEO-F401RE target by fdesbiens · Pull Request #51 · eclipse-threadx/samplex · GitHub
Skip to content

Add Renode regression tests for the NUCLEO-F401RE target - #51

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests
Aug 31, 2026
Merged

Add Renode regression tests for the NUCLEO-F401RE target#51
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests

Conversation

@fdesbiens

@fdesbiensfdesbiens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Add Renode regression tests for the NUCLEO-F401RE target

Summary

The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console, and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which already gates CI on a headless Renode run.

Follow-up to #50, which noted this gap under "Not addressed".

Renode platform

Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic stm32f4.repl and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being accurate.

Startup self-tests

main.c gains seven self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts:

#Self-testGuards against
1_sbrk() allocation lands inside the heap reservationheap escaping its linker reservation
2_sbrk() releases back to the heap basebroken negative-increment path
3_sbrk() underflow rejected with EINVALshrinking below the heap base
4_sbrk() rejects a request that fits SRAM but not the heapbounding the heap at the end of SRAM
5Heap reservation ends at or below the ThreadX byte poollinker script layout regression
6SystemCoreClock is 84 MHza silently wrong PLL configuration
7HAL timebase (TIM2) tick advancingHAL_InitTick() re-entry leaving TIM2 stopped

The regression guard, and what it revealed

Test 4 targets the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the 512-byte heap reservation.

I verified the suite has teeth by reinstating the old end-of-SRAM bound and re-running it. It failed, and the failure was worse than expected: with that bound, newlib's firstmalloc() took roughly 4 KB and put the break at 0x20002170inside the ThreadX byte pool, which holds every thread stack and the queue buffer. That also broke self-tests 1 and 3, since the break had already escaped the reservation before they ran. The pre-#50 demo was therefore corrupting pool memory on every boot; it simply happened not to fault.

With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation. So the reservation is adequate as it stands and _Min_Heap_Size does not need raising.

Harness

scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner reached the console, that 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.

renode/nucleo_f401re_demo.robot covers the same ground for renode-test, and nucleo_f401re_demo.resc is the interactive counterpart.

The new test-nucleo-renode CI job mirrors test-polarfire-renode, consuming the ELF artifact from build-arm-nucleo.

Verification

Locally, with Renode 1.16.1 and the CI-pinned Arm GNU Toolchain 14.2.Rel1:

  • All 7 self-tests pass; harness exits 0.
  • Reintroducing the heap-bound bug: 3 self-tests fail and the harness exits 1, so the gate genuinely gates.
  • Clean build, -Wall -Wshadow -Wdouble-promotion -Werror: 22068 B ROM (up from 20120 B for the self-tests), 6000 B RAM unchanged.

Also

main.c drops a hardcoded 0x20018000 in favour of BSP_RAM_END, now that board_config.h is in scope, and names the 4 KB main-stack margin. Same value, no behaviour change.

AI assistance disclosure

This work was AI-assisted by me (@fdesbiens). The self-tests live in main.c, whose header follows the project's existing MIT form; the AGENTS.md AI Disclosure headers added in #50 remain on the four BSP files that carry Codex-derived content.

The ARM target had no runtime test. Its CI job checked only that the ELF
existed, so the clock configuration, the TIM2 HAL timebase, the console and
the heap bound were all unexercised. This brings it to parity with the
PolarFire target, which gates CI on a headless Renode run.
Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl
derives one from the generic STM32F4 CPU platform and corrects Flash to
512 KB and SRAM to 96 KB. The generic platform is sized for the larger
F407/F429 parts, and both the linker script's heap reservation and the
_sbrk() bound depend on those limits being right.
main.c gains seven startup self-tests that run before tx_kernel_enter(), so
a failure is reported even when the scheduler never starts. They cover the
_sbrk() allocate, release, underflow and over-limit paths, the invariant
that the heap reservation ends at or below the ThreadX byte pool, the
84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig()
re-enters HAL_InitTick().
Test 4 is the regression guard for the heap bound fixed in eclipse-threadx#50. Requesting
32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation.
Reinstating the old end-of-SRAM bound was verified to fail the suite:
newlib's first malloc() then took roughly 4 KB and put the break at
0x20002170, inside the ThreadX byte pool, which also broke self-tests 1
and 3. With the bound correct, _sbrk() refuses the oversized request,
newlib retries smaller, and malloc(64) succeeds using 72 bytes of the
512-byte reservation - so the reservation is adequate and no heap growth
is needed.
scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed
span of virtual time and quits on its own rather than depending on wall
clock. Beyond the self-tests it asserts the boot banner, that 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. The suite was confirmed to exit 1 on a
reintroduced bug and 0 on the fixed tree.
The Robot Framework suite covers the same ground for renode-test, and the
new test-nucleo-renode CI job mirrors test-polarfire-renode.
main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that
board_config.h is in scope, with the 4 KB main-stack margin named.
@fdesbiens
fdesbiens merged commit ef61eae into eclipse-threadx:devAug 31, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feat/nucleo-renode-tests branch August 31, 2026 15:44
fdesbiens added a commit that referenced this pull request Aug 31, 2026
Two problems in the pipeline, one of them mine.
The NUCLEO Renode job fetched renode-latest.linux-portable.tar.gz with no
version pin and no checksum, while the PolarFire job three jobs above it
already pinned Renode 1.16.1 and verified its SHA256. That pin landed in #49,
so it was present in this file when #51 added the NUCLEO job; the new job was
modelled on an older copy of the PolarFire step rather than the current one.
The result was a suite whose emulator could change under it on any Renode
release, with nothing verifying what was downloaded. The NUCLEO job now uses
the same pinned, checksum-verified step as PolarFire. The checksum was
recomputed from the published artefact rather than copied on trust.
Separately, every run re-downloaded roughly a gigabyte: the xPack RISC-V
toolchain at ~414 MB and Renode at ~52 MB in each of two jobs. All three are
now restored by actions/cache, keyed on the pinned version so a future bump
invalidates the cache instead of silently serving the old one. This completes
what #53 started for the Arm toolchain.
Caching only makes sense because these are now pinned. Caching an unpinned
"latest" artefact would have frozen CI on whichever build happened to be
fetched first, turning a reproducibility gap into an invisible one.
All four jobs now follow the same shape: cache, install only on a cache miss,
then put the tool on PATH as a separate step so it runs on hit and miss alike.
The PolarFire job also gains a version-reporting step, matching the Arm job, so
the log records which compiler produced the ELF.
Verified both constructed download URLs resolve, and that the Renode 1.16.1
checksum matches the published artefact.
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens
, '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); } })(); })(); Add Renode regression tests for the NUCLEO-F401RE target by fdesbiens · Pull Request #51 · eclipse-threadx/samplex · GitHub
Skip to content

Add Renode regression tests for the NUCLEO-F401RE target - #51

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests
Aug 31, 2026
Merged

Add Renode regression tests for the NUCLEO-F401RE target#51
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feat/nucleo-renode-tests

Conversation

@fdesbiens

@fdesbiensfdesbiens commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Add Renode regression tests for the NUCLEO-F401RE target

Summary

The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console, and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which already gates CI on a headless Renode run.

Follow-up to #50, which noted this gap under "Not addressed".

Renode platform

Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic stm32f4.repl and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being accurate.

Startup self-tests

main.c gains seven self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts:

#Self-testGuards against
1_sbrk() allocation lands inside the heap reservationheap escaping its linker reservation
2_sbrk() releases back to the heap basebroken negative-increment path
3_sbrk() underflow rejected with EINVALshrinking below the heap base
4_sbrk() rejects a request that fits SRAM but not the heapbounding the heap at the end of SRAM
5Heap reservation ends at or below the ThreadX byte poollinker script layout regression
6SystemCoreClock is 84 MHza silently wrong PLL configuration
7HAL timebase (TIM2) tick advancingHAL_InitTick() re-entry leaving TIM2 stopped

The regression guard, and what it revealed

Test 4 targets the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the 512-byte heap reservation.

I verified the suite has teeth by reinstating the old end-of-SRAM bound and re-running it. It failed, and the failure was worse than expected: with that bound, newlib's firstmalloc() took roughly 4 KB and put the break at 0x20002170inside the ThreadX byte pool, which holds every thread stack and the queue buffer. That also broke self-tests 1 and 3, since the break had already escaped the reservation before they ran. The pre-#50 demo was therefore corrupting pool memory on every boot; it simply happened not to fault.

With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation. So the reservation is adequate as it stands and _Min_Heap_Size does not need raising.

Harness

scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner reached the console, that 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.

renode/nucleo_f401re_demo.robot covers the same ground for renode-test, and nucleo_f401re_demo.resc is the interactive counterpart.

The new test-nucleo-renode CI job mirrors test-polarfire-renode, consuming the ELF artifact from build-arm-nucleo.

Verification

Locally, with Renode 1.16.1 and the CI-pinned Arm GNU Toolchain 14.2.Rel1:

  • All 7 self-tests pass; harness exits 0.
  • Reintroducing the heap-bound bug: 3 self-tests fail and the harness exits 1, so the gate genuinely gates.
  • Clean build, -Wall -Wshadow -Wdouble-promotion -Werror: 22068 B ROM (up from 20120 B for the self-tests), 6000 B RAM unchanged.

Also

main.c drops a hardcoded 0x20018000 in favour of BSP_RAM_END, now that board_config.h is in scope, and names the 4 KB main-stack margin. Same value, no behaviour change.

AI assistance disclosure

This work was AI-assisted by me (@fdesbiens). The self-tests live in main.c, whose header follows the project's existing MIT form; the AGENTS.md AI Disclosure headers added in #50 remain on the four BSP files that carry Codex-derived content.

The ARM target had no runtime test. Its CI job checked only that the ELF
existed, so the clock configuration, the TIM2 HAL timebase, the console and
the heap bound were all unexercised. This brings it to parity with the
PolarFire target, which gates CI on a headless Renode run.
Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl
derives one from the generic STM32F4 CPU platform and corrects Flash to
512 KB and SRAM to 96 KB. The generic platform is sized for the larger
F407/F429 parts, and both the linker script's heap reservation and the
_sbrk() bound depend on those limits being right.
main.c gains seven startup self-tests that run before tx_kernel_enter(), so
a failure is reported even when the scheduler never starts. They cover the
_sbrk() allocate, release, underflow and over-limit paths, the invariant
that the heap reservation ends at or below the ThreadX byte pool, the
84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig()
re-enters HAL_InitTick().
Test 4 is the regression guard for the heap bound fixed in eclipse-threadx#50. Requesting
32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation.
Reinstating the old end-of-SRAM bound was verified to fail the suite:
newlib's first malloc() then took roughly 4 KB and put the break at
0x20002170, inside the ThreadX byte pool, which also broke self-tests 1
and 3. With the bound correct, _sbrk() refuses the oversized request,
newlib retries smaller, and malloc(64) succeeds using 72 bytes of the
512-byte reservation - so the reservation is adequate and no heap growth
is needed.
scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed
span of virtual time and quits on its own rather than depending on wall
clock. Beyond the self-tests it asserts the boot banner, that 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. The suite was confirmed to exit 1 on a
reintroduced bug and 0 on the fixed tree.
The Robot Framework suite covers the same ground for renode-test, and the
new test-nucleo-renode CI job mirrors test-polarfire-renode.
main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that
board_config.h is in scope, with the 4 KB main-stack margin named.
@fdesbiens
fdesbiens merged commit ef61eae into eclipse-threadx:devAug 31, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feat/nucleo-renode-tests branch August 31, 2026 15:44
fdesbiens added a commit that referenced this pull request Aug 31, 2026
Two problems in the pipeline, one of them mine.
The NUCLEO Renode job fetched renode-latest.linux-portable.tar.gz with no
version pin and no checksum, while the PolarFire job three jobs above it
already pinned Renode 1.16.1 and verified its SHA256. That pin landed in #49,
so it was present in this file when #51 added the NUCLEO job; the new job was
modelled on an older copy of the PolarFire step rather than the current one.
The result was a suite whose emulator could change under it on any Renode
release, with nothing verifying what was downloaded. The NUCLEO job now uses
the same pinned, checksum-verified step as PolarFire. The checksum was
recomputed from the published artefact rather than copied on trust.
Separately, every run re-downloaded roughly a gigabyte: the xPack RISC-V
toolchain at ~414 MB and Renode at ~52 MB in each of two jobs. All three are
now restored by actions/cache, keyed on the pinned version so a future bump
invalidates the cache instead of silently serving the old one. This completes
what #53 started for the Arm toolchain.
Caching only makes sense because these are now pinned. Caching an unpinned
"latest" artefact would have frozen CI on whichever build happened to be
fetched first, turning a reproducibility gap into an invisible one.
All four jobs now follow the same shape: cache, install only on a cache miss,
then put the tool on PATH as a separate step so it runs on hit and miss alike.
The PolarFire job also gains a version-reporting step, matching the Arm job, so
the log records which compiler produced the ELF.
Verified both constructed download URLs resolve, and that the Renode 1.16.1
checksum matches the published artefact.
Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fdesbiens