Skip to content

Moved the demo into apps/ and made both targets build it - #57

Merged
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feature/shared-portable-app
Sep 1, 2026
Merged

Moved the demo into apps/ and made both targets build it#57
fdesbiens merged 1 commit into
eclipse-threadx:devfrom
fdesbiens:feature/shared-portable-app

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

What this changes

The framework documented a portable application layer it did not have. Each target owned its own demo under app/, so nothing checked that a demo could actually move between boards — which is how apps/ stayed documented for weeks while not existing (#52 walked the claim back).

This adds apps/, moves the NUCLEO-F401RE demo into apps/threadx_demo/main.c, and has the PolarFire SoC Icicle Kit build that same source as a second executable beside its LM75 monitor. CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V and asserts on the same console output from both.

The rule, now written into docs/architecture.md and templates/target/README.md:

apps/ holds portable demos. targets/<Vendor>/<BOARD>/app/ holds board-specific ones. Both are legitimate; a target's app/CMakeLists.txt picks either.

PolarFire's LM75 monitor stays a target app because it genuinely models a sensor.

A third BSP interface was unavoidable

polarfire_bsp's trap.c carried a hard undefined reference to console_rx_isr_callback(), a symbol only its own demo defined. Any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback purely to say it wanted nothing. That is the same violation this effort exists to remove — the BSP reaching up into the application — expressed through the linker rather than a header.

bsp/console.h gains a registration call in the shape bsp_self_test() already established:

typedef void (*bsp_console_rx_fn)(char c, void *context);
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

The board stores a nullable pointer and checks it before dispatching, so bytes arriving with no handler attached are dropped rather than faulting. The LM75 demo registers its existing handler in main(); the NUCLEO-F401RE polls USART2 and so stores a handler it never invokes, keeping the contract uniform enough to register against unconditionally. A __attribute__((weak)) default was rejected: it compiles, but keeps the up-call and is a GCC extension in a C99/MISRA codebase.

This brought the usual obligations with it — an architecture entry, a templates/target/ stub, and README rows, as PR #56 did for the first two interfaces.

Building for a second architecture found two real defects

Both were latent and neither was reachable with one target:

  • Every %lu in the demo was wrong on one of the two boards. ThreadX defines ULONG as unsigned long on the Cortex-M4 port and unsigned int on the RISC-V 64 port. Each value now casts to unsigned long, matching the idiom already used in the LM75 demo.
  • Both boards' _sbrk() underflow self-test asked for a fixed -128, which is an underflow only when nothing has allocated yet. The NUCLEO's passed by accident — its printf buffer malloc fails against a small reservation, leaving the break at base. The PolarFire's failed outright once an application reached printf first. Both now hand back one byte more than has ever been taken, which underflows regardless of what ran before.

Measurements

NUCLEO-F401RE PolarFire Icicle
Self-tests 8 7
Thread stack size 1024 B (unchanged) 2048 B, measured peak 1048 B
Static RAM 6000 B (unchanged)
Heap use under printf 2944 B of 64 KB reserved

THREAD_STACK_SIZE is written in machine words rather than bytes, since every saved register and the newlib printf() call chain doubles in width on a 64-bit hart. BSP_HEAP_RESERVE_BYTES needed no change: the whole run also completes with the reservation temporarily cut to 4 KB, which bounds peak heap use across the run rather than just at startup.

Verification

Run locally on both toolchains (Arm GNU 14.3.Rel1, xPack RISC-V GCC 14.3.0-1, Renode 1.16.1) — the versions CI pins.

  • Three Renode suites green: NUCLEO shared demo, PolarFire --app lm75, PolarFire --app threadx_demo.
  • NUCLEO builds clean under -Werror; both PolarFire ELFs build warning-free.
  • Self-tests still have teeth. With _sbrk()'s bound deliberately widened to the end of RAM, the checks still fail: two on the NUCLEO-F401RE, one on each PolarFire executable. The NUCLEO count is two rather than the three seen before this PR, because the underflow check now correctly stays green — its old failure was incidental to the fixed -128, not that assertion doing its job.

The PolarFire runner is one script with an --app argument rather than two scripts, since the ~150 lines of Renode process handling are what would drift if copied.

Notes for review

  • app/starter/ is gone, including the empty cloud_config.h placeholder that Moved the startup self-tests and RAM sizing behind new BSP interfaces #56 stopped including.
  • The banner is now board-neutral (Eclipse ThreadX Device Monitor Demo); both Renode suites and the Robot suite assert on that one line.
  • This PR also removes the "a goal, not something it provides yet" wording from docs/architecture.md and templates/target/README.md. That was scheduled for a follow-up, but its precondition — the shared demo green on both architectures — is met by this PR, and leaving those statements in place would have shipped documentation that this change makes false.

The framework documented a portable application layer it did not have: each
target owned its own demo under app/, so nothing checked that a demo could
actually move between boards. This adds apps/, moves the NUCLEO-F401RE demo
into apps/threadx_demo/main.c unchanged in behaviour, and has the PolarFire
SoC Icicle Kit build that same source as a second executable beside its LM75
monitor. CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V and
asserts on the same console output from both, so the claim is now enforced
rather than stated.

apps/ holds portable demos; targets/<Vendor>/<BOARD>/app/ holds board-specific
ones, and a target's app/CMakeLists.txt picks either. The PolarFire LM75
monitor stays a target app because it genuinely models a sensor.

Linking a second executable first required removing a hard undefined reference:
polarfire_bsp's trap.c called console_rx_isr_callback(), a symbol only its own
demo defined, so any other application had to define a PolarFire-specific ISR
callback just to link. bsp/console.h gains a registration call in the shape
bsp_self_test() already established:

    typedef void (*bsp_console_rx_fn)(char c, void *context);
    void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

The board stores a nullable pointer and checks it before dispatching, so bytes
arriving with no handler attached are dropped instead of faulting, and an
application that ignores console input defines nothing. The LM75 demo registers
its existing handler in main(); the NUCLEO-F401RE polls USART2 and so stores a
handler it never invokes, which keeps the contract uniform enough to register
against unconditionally. A weak symbol was rejected: it keeps the up-call and
is a GCC extension in a C99 codebase.

Building the demo for a second architecture found two real defects:

  - Every %lu in the demo was wrong on one target. ThreadX defines ULONG as
    unsigned long on the Cortex-M4 port and unsigned int on the RISC-V 64 one,
    so each value now casts to unsigned long, matching the existing idiom in
    the LM75 demo.
  - Both boards' _sbrk() underflow self-test asked for a fixed -128, which is
    an underflow only when nothing has allocated yet. The NUCLEO's passed by
    accident of a printf buffer malloc that failed against a small reservation;
    the PolarFire's failed outright once a demo reached printf first. Both now
    hand back one byte more than has ever been taken, which underflows whatever
    ran before them.

Thread stacks are sized in machine words rather than bytes, since every saved
register and the newlib printf() call chain double in width on a 64-bit hart:
1024 bytes on Cortex-M4 as before, 2048 on RISC-V, measured peak 1048.

Measured PolarFire heap use under printf, the first stdio on that board, is
2944 bytes; the run also completes with BSP_HEAP_RESERVE_BYTES cut to 4 KB, so
the 64 KB reservation holds unchanged.

Verified locally on both toolchains: three Renode suites green, and with the
_sbrk() bound deliberately widened the self-tests still fail - two checks on
the NUCLEO-F401RE, one on each PolarFire executable.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fdesbiens
fdesbiens merged commit 4ddc8b4 into eclipse-threadx:dev Sep 1, 2026
5 checks passed
@fdesbiens
fdesbiens deleted the feature/shared-portable-app branch September 1, 2026 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant