From f283019f7b129f6d74ae4600e4b85a8f40fa7983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 31 Aug 2026 16:52:18 -0400 Subject: [PATCH] Moved the demo into apps/ and made both targets build it 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///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) --- .github/workflows/ci.yml | 32 ++- .../app/starter => apps/threadx_demo}/main.c | 95 ++++++-- bsp/include/bsp/console.h | 33 +++ docs/architecture.md | 16 +- .../POLARFIRE_ICICLE_RENODE/README.md | 84 ++++++- .../app/CMakeLists.txt | 64 ++++-- .../POLARFIRE_ICICLE_RENODE/app/main.c | 12 +- .../lib/bsp/include/polarfire_console.h | 36 +++ .../lib/bsp/src/bsp_console.c | 27 +++ .../lib/bsp/src/bsp_selftest.c | 13 +- .../lib/bsp/src/trap.c | 6 +- .../renode/polarfire_threadx_demo.resc | 32 +++ .../renode/polarfire_threadx_demo_ci.resc | 38 +++ .../scripts/test_renode.py | 216 ++++++++++++++---- .../NUCLEO_F401RE/README.md | 20 +- .../NUCLEO_F401RE/app/CMakeLists.txt | 10 +- .../NUCLEO_F401RE/app/starter/cloud_config.h | 16 -- .../lib/bsp/include/nucleo_console.h | 14 ++ .../NUCLEO_F401RE/lib/bsp/src/bsp_console.c | 34 +++ .../NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c | 18 +- .../renode/nucleo_f401re_demo.robot | 2 +- .../NUCLEO_F401RE/scripts/test_renode.py | 11 +- templates/target/README.md | 17 +- templates/target/app/CMakeLists.txt | 5 +- templates/target/app/main.c | 7 +- templates/target/lib/bsp/src/bsp_console.c | 37 +++ 26 files changed, 734 insertions(+), 161 deletions(-) rename {targets/STMicroelectronics/NUCLEO_F401RE/app/starter => apps/threadx_demo}/main.c (81%) create mode 100644 targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/polarfire_console.h create mode 100644 targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo.resc create mode 100644 targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo_ci.resc mode change 100644 => 100755 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py delete mode 100644 targets/STMicroelectronics/NUCLEO_F401RE/app/starter/cloud_config.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 721cdc2e..f883765e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,20 +61,26 @@ jobs: - name: Report the toolchain version run: riscv-none-elf-gcc --version - - name: Build SampleX PolarFire Condition-Monitoring Demo + - name: Build SampleX PolarFire Executables run: | bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild - - name: Verify SampleX Demo ELF + - name: Verify Both PolarFire ELFs run: | + # Two executables: the board's own LM75 demo, and the shared portable + # demo from apps/threadx_demo that the NUCLEO-F401RE builds from the + # very same source. test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf - echo "[OK] PolarFire SampleX demo ELF verified." + test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf + echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified." - - name: Archive Built PolarFire ELF + - name: Archive Built PolarFire ELFs uses: actions/upload-artifact@v4 with: name: polarfire-demo-elf - path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf + path: | + targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf + targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf retention-days: 1 test-polarfire-renode: @@ -98,7 +104,7 @@ jobs: with: python-version: "3.11" - - name: Download Built PolarFire ELF + - name: Download Built PolarFire ELFs uses: actions/download-artifact@v4 with: name: polarfire-demo-elf @@ -125,9 +131,17 @@ jobs: - name: Put Renode on PATH run: echo "$HOME/renode" >> $GITHUB_PATH - - name: Run Deterministic Headless Renode Test + - name: Run Deterministic Headless Renode Test (LM75 demo) + run: | + python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75 + + # The other half of the portability claim. apps/threadx_demo/main.c is + # asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit + # Cortex-M4, against the same console output from the same source file. + # A claim only one architecture verifies is not a claim. + - name: Run Deterministic Headless Renode Test (shared ThreadX demo) run: | - python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py + python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo build-arm-nucleo: name: Build ST Nucleo-F401RE (ARM Cortex-M4) @@ -181,6 +195,8 @@ jobs: - name: Report the toolchain version run: ${{ env.GCC_TARGET }}-gcc --version + # Builds apps/threadx_demo/main.c, the same application source the + # PolarFire SoC Icicle Kit job builds for 64-bit RISC-V. - name: Build NUCLEO-F401RE Demo run: | bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c b/apps/threadx_demo/main.c similarity index 81% rename from targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c rename to apps/threadx_demo/main.c index 401e04de..c1541f7e 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c +++ b/apps/threadx_demo/main.c @@ -1,16 +1,42 @@ -/* +/*************************************************************************** * 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/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/* + * Eclipse ThreadX device monitor demo - shared across every target. * - * 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. + * This application depends on nothing but the C standard library, + * and the board contracts in , so the same source builds for any + * target that implements them. It is built today for the ST NUCLEO-F401RE + * (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit + * RISC-V), and CI runs it under Renode on both: a portability claim only one + * architecture verifies is not a claim. * - * SPDX-License-Identifier: MIT + * Nothing here may name a board symbol, a vendor header or a linker-defined + * address. The two things a demo used to need those for are answered by + * contracts instead - sizes the byte pool, and + * runs the board's startup checks - and anything else a + * future demo needs belongs behind a new contract in bsp/include/bsp/, not an + * #ifdef here. + * + * A board-specific demo is still legitimate; it just lives with its board, + * under targets///app/. A target's app/CMakeLists.txt picks + * either. */ -// Some portions generated by Claude Code (Opus 5) - #include +#include #include #include "tx_api.h" @@ -19,7 +45,13 @@ #include "bsp/memory.h" #include "bsp/selftest.h" -#define THREAD_STACK_SIZE 1024 +/* Thread stacks are sized in machine words rather than in bytes. Every saved + * register, return address and spilled pointer doubles in width between the + * 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does + * the newlib printf() call chain each of these threads reaches. The reporter + * thread's measured peak is printed in the status table below, which is where + * to look before changing this. */ +#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *))) typedef struct { CHAR *name; @@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) { } stack_lowest++; } + /* MISRA C:2012 Rule 11.4 deviation: the distance between two + * stack addresses is only expressible by converting them to an + * integer type. uintptr_t is the width-correct one on both a + * 32-bit and a 64-bit target. */ ULONG unused = - (ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start; + (ULONG)((uintptr_t)stack_lowest - + (uintptr_t)thread->tx_thread_stack_start); system_stats.threads[i].stack_used = thread->tx_thread_stack_size - unused; system_stats.threads[i].stack_size = thread->tx_thread_stack_size; @@ -185,13 +222,21 @@ static void monitor_thread_entry(ULONG parameter) { } } -/* Reporter Thread: Consumer/Printer of statistics */ +/* Reporter Thread: Consumer/Printer of statistics + * + * Every ULONG reaching printf() is cast to unsigned long, because ULONG is not + * the same type on every port: the Cortex-M4 port defines it as unsigned long + * and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one + * of the two targets this file builds for, and the cast is what makes one + * format string correct on both. */ static void reporter_thread_entry(ULONG parameter) { (void)parameter; + /* Deliberately names no board: this same source runs on every target, and + * both Renode suites assert on this line. Any third-party licensing notice + * belongs with the target that carries the third-party code, not here. */ printf("\r\n==========================================\r\n"); - printf("NUCLEO-F401RE Device Monitor Demo\r\n"); - printf("Third-party licensing info in NOTICE.md\r\n"); + printf("Eclipse ThreadX Device Monitor Demo\r\n"); printf("==========================================\r\n"); while (1) { @@ -199,10 +244,13 @@ static void reporter_thread_entry(ULONG parameter) { printf("\r\nSystem Status:\r\n"); printf("------------------------------------------\r\n"); - printf("Uptime: %lu s\r\n", system_stats.uptime); - printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total); - printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used); - printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free); + printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime); + printf("Byte Pool Size: %lu bytes\r\n", + (unsigned long)system_stats.byte_pool_total); + printf("Allocated Memory: %lu bytes\r\n", + (unsigned long)system_stats.byte_pool_used); + printf("Free Memory: %lu bytes\r\n", + (unsigned long)system_stats.byte_pool_free); printf("------------------------------------------\r\n"); printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority", @@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) { printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n", system_stats.threads[i].name, system_stats.threads[i].priority, get_state_string(system_stats.threads[i].state), - system_stats.threads[i].run_count, - system_stats.threads[i].stack_used, - system_stats.threads[i].stack_size, pct); + (unsigned long)system_stats.threads[i].run_count, + (unsigned long)system_stats.threads[i].stack_used, + (unsigned long)system_stats.threads[i].stack_size, + (unsigned long)pct); } } printf("-------------------------------------------------------------------" "---------\r\n"); printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n", - monitor_counter, reporter_counter, blink_counter, timer_counter); + (unsigned long)monitor_counter, (unsigned long)reporter_counter, + (unsigned long)blink_counter, (unsigned long)timer_counter); printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n", - mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes); + (unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2, + (unsigned long)queue_msgs_received, + (unsigned long)event_flags_processed, + (unsigned long)semaphore_wakes); tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */ } diff --git a/bsp/include/bsp/console.h b/bsp/include/bsp/console.h index 46d52324..442022bb 100644 --- a/bsp/include/bsp/console.h +++ b/bsp/include/bsp/console.h @@ -8,6 +8,8 @@ * SPDX-License-Identifier: MIT */ +// Some portions generated by Claude Code (Opus 5) + #ifndef BSP_CONSOLE_H #define BSP_CONSOLE_H @@ -26,4 +28,35 @@ void bsp_console_init(void); */ void bsp_console_write(const char *data, size_t length); +/** + * @brief Receives one byte that arrived on the console. + * + * Invoked from interrupt context on boards that drive their console receiver + * from an interrupt, so it must not block, allocate, or call any ThreadX + * service that is illegal from an ISR. + * + * @param c The byte that arrived. + * @param context The context pointer that was handed to + * bsp_console_set_rx_handler(). + */ +typedef void (*bsp_console_rx_fn)(char c, void *context); + +/** + * @brief Registers the handler invoked for each byte the console receives. + * + * A board that raises an interrupt per received byte must route it here rather + * than to a symbol the application is required to define: an application that + * does not care about console input should not have to define anything to + * link. Bytes that arrive with no handler attached are dropped. + * + * Boards whose console has no receive-interrupt path still implement this + * call; the handler they store is simply never invoked. An application can + * therefore register unconditionally. + * + * @param handler Handler to invoke per received byte. Passing NULL detaches + * the current handler. + * @param context Opaque pointer passed back to @p handler unmodified. + */ +void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context); + #endif /* BSP_CONSOLE_H */ diff --git a/docs/architecture.md b/docs/architecture.md index 5bfa785b..83d5f6af 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations. 1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems. -2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `` and `` headers. Applications are still target-resident, though: each target owns its demo under `app/`, and there is no shared application directory to link one from. A fully portable shared application layer is a goal of the framework, not a property it has yet. -3. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another. +2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `` and ``. +3. **A Shared Application Layer**: `apps/` holds portable demos; `targets///app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim. +4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another. --- @@ -23,6 +24,8 @@ samplex/ (repository root) ├── MXChip/ # [Pre-framework] Standalone board sample ├── OpenHW/ # [Pre-framework] Standalone board sample ├── STMicroelectronics/ # [Pre-framework] Standalone board samples +├── apps/ # [Framework] Portable applications, built by any target +│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V ├── targets/ # [Framework] Supported BSP target boards │ ├── Microchip/ │ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target @@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract * `void bsp_console_init(void)`: Initializes the default UART console. * `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface. +* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it. + +A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header. + +The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt. ### Application RAM Budget (`memory.h`) @@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an 3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access. 4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory. -Start from `templates/target/app/main.c`, which depends only on `` and the `` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from. +A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor. + +The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets///app/`. diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md b/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md index b2f5fb49..fefb6ff5 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md @@ -1,6 +1,13 @@ # Microchip PolarFire SoC Icicle Kit Target Integration (Renode 64-Bit RISC-V) -This directory contains the target Board Support Package (BSP) and condition-monitoring demonstration application for the **Microchip PolarFire SoC Icicle Kit** running in the **Renode** emulation environment. +This directory contains the target Board Support Package (BSP) for the **Microchip PolarFire SoC Icicle Kit** running in the **Renode** emulation environment, and it builds **two executables** against that one BSP: + +| Executable | Application source | Why it lives there | +|---|---|---| +| `polarfire_icicle_demo.elf` | `app/main.c` | The board's own LM75 condition-monitoring demo. It models a sensor and drives the MMUART1 receive interrupt, so it is board specific and stays with the board. | +| `polarfire_threadx_demo.elf` | `apps/threadx_demo/main.c` | The shared portable demo, byte for byte the same source `targets/STMicroelectronics/NUCLEO_F401RE` builds for 32-bit Cortex-M4. Building it here is what makes the framework's portability claim something CI verifies on a second architecture rather than something the documentation asserts. | + +Both are exercised by their own headless Renode suite, and CI runs both. --- @@ -13,7 +20,7 @@ This directory contains the target Board Support Package (BSP) and condition-mon * **System DRAM**: 1 GiB LPDDR4 Memory (`0x80000000` – `0xC0000000`) * **Machine Timer**: SiFive CLINT `mtime` running at 1 MHz (`0x02000000`, 10ms tick = 10,000 cycles) * **Serial Debug Console**: Microchip MMUART1 (`0x20100000`) at 115200 baud (8-N-1) -* **Telemetry**: Simulated LM75 temperature data processed via ThreadX queues and event flags +* **Telemetry**: Simulated LM75 temperature data processed via ThreadX queues and event flags (LM75 demo only) --- @@ -38,34 +45,57 @@ bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild ## 3. Renode Execution & Verification ### Interactive Simulation (GUI / Terminal Analyzers): -Inside the Renode monitor: +Inside the Renode monitor, pick the executable to watch free-run: ```renode include @targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_demo.resc +include @targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo.resc ``` -This free-runs the machine so the telemetry stream can be watched live. ### Automated Headless Test Runner: +One runner drives both suites; `--app` selects which executable to verify. ```bash -python targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py +python targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75 # default +python targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo ``` -The runner drives `renode/polarfire_ci.resc`, which steps through fixed -virtual-time intervals rather than free-running, injects a byte into MMUART1 to -exercise the PLIC external-interrupt path, and quits on its own. It asserts on -four things and exits non-zero if any of them is missing: +Both step through fixed virtual-time intervals rather than free-running and +quit on their own, and both exit non-zero if any assertion is missing. They +share every line of Renode process handling, which is the reason they are one +script: the plumbing is what would drift if it were copied. + +**`--app lm75`** drives `renode/polarfire_ci.resc`, which also injects a byte +into MMUART1 to exercise the PLIC external-interrupt path: | Assertion | Covers | |---|---| | Startup self-tests all passed | `_sbrk()` bounds against the heap reservation, the `bsp_ram_region()` invariant, timer catch-up, PLIC configuration | | ThreadX system tick advancing | CLINT machine timer and `_tx_timer_interrupt` | | LM75 overtemperature alarm | Queue, event flags, and the analyzer thread | -| PLIC IRQ 91 RX interrupt delivered | MMUART1 -> PLIC -> Hart 1 machine-mode trap path | +| PLIC IRQ 91 RX interrupt delivered | MMUART1 -> PLIC -> Hart 1 machine-mode trap path, and the handler registered through `bsp_console_set_rx_handler()` | The last of these is the only check that proves the PLIC is programmed for the right context. Reading the controller's registers back cannot: the machine-mode context (1) and the supervisor-mode context (2) for Hart 1 both accept the writes and read back identically, but only the former ever raises `MEIP`. -### Expected Output Stream (`mmuart1` @ 115200 baud): +**`--app threadx_demo`** drives `renode/polarfire_threadx_demo_ci.resc`. Its +four assertions are deliberately the same ones +`targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py` makes, so the +two runs compare one source file built for two architectures: + +| Assertion | Covers | +|---|---| +| Boot banner reached the console | `bsp_console_write()` through `printf()`, and a banner that names no board | +| Startup self-tests all passed | The same BSP checks as above, reached through the portable `` callback | +| LED blink thread and application timer ran | `bsp_led_toggle()` and the ThreadX application timer | +| Mutex, queue, event flag and semaphore all exercised | The RTOS primitives, sized out of the pool `bsp_ram_region()` reports | + +The shared demo prints through `printf()` where the LM75 demo calls +`bsp_console_write()` directly, so it is the first executable on this board to +pull in newlib stdio. Measured heap use is **2944 bytes** of the 64 KB +`BSP_HEAP_RESERVE_BYTES`, and the whole run completes with the reservation +temporarily cut to 4 KB, so the reservation holds with room to spare. + +### Expected Output Stream, `--app lm75` (`mmuart1` @ 115200 baud): ```text ==================================================== Microchip PolarFire SoC Icicle Kit (Renode Target) @@ -86,3 +116,35 @@ Microchip PolarFire SoC Icicle Kit (Renode Target) [Monitor] Ticks: 200 | Active Runs: Sampler=5, Analyzer=5, Reporter=3 [LM75 Sensor] Temperature: OVERTEMP ALARM TRIGGERED (>45.0C) ``` + +### Expected Output Stream, `--app threadx_demo`: +```text +========================================== +Eclipse ThreadX Device Monitor Demo +========================================== + +System Status: +------------------------------------------ +Uptime: 4 s +Byte Pool Size: 1073585120 bytes +Allocated Memory: 18896 bytes +Free Memory: 1073566224 bytes +------------------------------------------ + +Thread Name Priority State Run Count Stack Peak (Max / Size) +---------------------------------------------------------------------------- +monitor thread 9 READY 41 536 / 2048 bytes (26%) +reporter thread 10 READY 2 1048 / 2048 bytes (51%) +blink thread 11 READY 8 536 / 2048 bytes (26%) +... +---------------------------------------------------------------------------- +Runs: Monitor: 41 | Reporter: 3 | Blink: 8 | Timer Wakes: 4 +RTOS Showcase: Mutex Locks: 40/40 | Queue Msgs: 20 | Event Wakes: 8 | Sema Wakes: 8 +``` + +Thread stacks are 2048 bytes here and 1024 on the NUCLEO-F401RE from the one +`THREAD_STACK_SIZE` expression, which is written in machine words rather than +bytes: every saved register and spilled pointer doubles in width on a 64-bit +hart, as does the newlib `printf()` call chain. The byte pool spans the whole +region `bsp_ram_region()` reports, which on this board is the DRAM above the +heap reservation - roughly 1 GiB. diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/CMakeLists.txt b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/CMakeLists.txt index 64605b99..6f36200a 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/CMakeLists.txt +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/CMakeLists.txt @@ -32,31 +32,55 @@ target_include_directories(threadx ) # ------------------------------------------------------------- -# PolarFire SoC Icicle Kit Telemetry Executable +# Executables +# +# This target builds two. Both link the same BSP, the same startup assembly +# and the same newlib retargeting; they differ only in which application +# source they compile. +# +# polarfire_icicle_demo - the board's own LM75 condition-monitoring demo, +# in app/main.c. It models a sensor and drives the +# MMUART1 receive interrupt, so it stays here with +# the board. +# polarfire_threadx_demo - the shared portable demo from apps/threadx_demo/, +# the same source the NUCLEO-F401RE builds. Nothing +# about it is RISC-V specific; building it here is +# what turns the framework's portability claim into +# something CI verifies on a second architecture. # ------------------------------------------------------------- -add_executable(polarfire_icicle_demo - main.c - common/startup/entry.S - common/startup/newlib_stubs.c -) +function(polarfire_add_app TARGET_NAME APP_SOURCE) + add_executable(${TARGET_NAME} + ${APP_SOURCE} + ${CMAKE_CURRENT_SOURCE_DIR}/common/startup/entry.S + ${CMAKE_CURRENT_SOURCE_DIR}/common/startup/newlib_stubs.c + ) -set_target_properties(polarfire_icicle_demo PROPERTIES SUFFIX ".elf") + set_target_properties(${TARGET_NAME} PROPERTIES SUFFIX ".elf") -target_include_directories(polarfire_icicle_demo - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../lib/bsp/include - ${SAMPLEX_ROOT_DIR}/bsp/include -) + # board_config.h is on this path for newlib_stubs.c, which bounds _sbrk() + # against the board's heap reservation. The shared application never + # includes it - and cannot, since it also has to compile for Cortex-M4. + target_include_directories(${TARGET_NAME} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../lib/bsp/include + ${SAMPLEX_ROOT_DIR}/bsp/include + ) + target_link_libraries(${TARGET_NAME} + PRIVATE + -Wl,--start-group + polarfire_bsp + threadx + -Wl,--end-group + ) +endfunction() + +polarfire_add_app(polarfire_icicle_demo ${CMAKE_CURRENT_SOURCE_DIR}/main.c) +polarfire_add_app(polarfire_threadx_demo ${SAMPLEX_ROOT_DIR}/apps/threadx_demo/main.c) + +# Applies to the LM75 demo only: it is the executable with the deliberate +# illegal instruction that exercises trap_handler(). option(ENABLE_FAULT_INJECTION "Enable deliberate synchronous fault injection for trap handler testing" OFF) if(ENABLE_FAULT_INJECTION) target_compile_definitions(polarfire_icicle_demo PRIVATE TEST_FAULT_INJECTION=1) endif() - -target_link_libraries(polarfire_icicle_demo - PRIVATE - -Wl,--start-group - polarfire_bsp - threadx - -Wl,--end-group -) diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c index 9ca5e605..e4a4c150 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c @@ -55,7 +55,12 @@ volatile char g_last_rx_char = 0; volatile uint32_t g_rx_irq_count = 0; volatile uint32_t g_rx_flag_errors = 0; -void console_rx_isr_callback(char c) { +/* Registered with bsp_console_set_rx_handler() in main(), and invoked from the + * MMUART1 trap path. The BSP no longer requires this application to define a + * fixed symbol, so an application that ignores console input links unchanged. */ +static void console_rx_handler(char c, void *context) { + (void)context; + g_last_rx_char = c; g_rx_irq_count++; if (tx_event_flags_set(&alarm_flags, EVENT_FLAG_UART_RX, TX_OR) != TX_SUCCESS) { @@ -116,6 +121,11 @@ int main(void) { /* Initialize Board Peripherals & MMUART1 */ bsp_board_init(); + /* Attach the console receiver before any byte can arrive. Bytes that + * arrive with no handler attached are dropped rather than lost to a link + * error, which is what lets the shared demo link the same BSP. */ + bsp_console_set_rx_handler(console_rx_handler, NULL); + console_print("\n====================================================\n"); console_print("Microchip PolarFire SoC Icicle Kit (Renode Target)\n"); console_print("64-Bit RISC-V Industrial LM75 Condition-Monitoring App\n"); diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/polarfire_console.h b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/polarfire_console.h new file mode 100644 index 00000000..62064b30 --- /dev/null +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/polarfire_console.h @@ -0,0 +1,36 @@ +/*************************************************************************** + * 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/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +#ifndef POLARFIRE_CONSOLE_H +#define POLARFIRE_CONSOLE_H + +/* + * Board-private console plumbing. This header is internal to the PolarFire + * BSP: it lives beside the driver rather than in bsp/include/bsp/ because no + * application should reach for it. + */ + +/** + * @brief Hands one received byte to the registered bsp/console.h RX handler. + * + * Called by trap_handler() from machine-mode interrupt context once MMUART1 + * has raised its PLIC source. Drops the byte when no handler is attached, so + * the trap path never depends on a symbol the application must define. + * + * @param c The byte read out of the MMUART1 receive holding register. + */ +void polarfire_console_rx_dispatch(char c); + +#endif /* POLARFIRE_CONSOLE_H */ diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_console.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_console.c index 4695e21b..f915a7af 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_console.c +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_console.c @@ -8,13 +8,24 @@ * SPDX-License-Identifier: MIT */ +// Some portions generated by Claude Code (Opus 5) + #include "bsp/console.h" #include "board_config.h" +#include "polarfire_console.h" #include extern void uart_init(void); extern void uart_write(const char *data, size_t len); +/* Receive handler registered through bsp_console_set_rx_handler(). Written + * from thread context and read from the MMUART1 trap path, so both are + * volatile: the compiler must not cache either across the store. A single + * aligned pointer store is atomic on RV64, so no further guard is needed for + * attaching or detaching. */ +static bsp_console_rx_fn volatile s_rx_handler = NULL; +static void *volatile s_rx_context = NULL; + void bsp_console_init(void) { #if BSP_HAS_CONSOLE uart_init(); @@ -27,3 +38,19 @@ void bsp_console_write(const char *data, size_t length) { uart_write(data, length); #endif } + +void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context) { + /* Publish the context first: the trap path reads the handler to decide + * whether to dispatch at all, so a handler that is visible must already + * have its context beside it. */ + s_rx_context = context; + s_rx_handler = handler; +} + +void polarfire_console_rx_dispatch(char c) { + bsp_console_rx_fn handler = s_rx_handler; + + if (handler != NULL) { + handler(c, s_rx_context); + } +} diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_selftest.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_selftest.c index e329fcc2..817d1421 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_selftest.c +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_selftest.c @@ -70,6 +70,7 @@ unsigned bsp_self_test(bsp_selftest_report_fn report, void *context) void *p_under; void *p_over; void *p_past_heap; + ptrdiff_t heap_taken; uint64_t now; uint64_t missed_cmp; uint64_t pending_cmp; @@ -93,9 +94,17 @@ unsigned bsp_self_test(bsp_selftest_report_fn report, void *context) check(&state, (p1 != (void *)-1) && ((uintptr_t)p1 >= BSP_HEAP_BASE), "_sbrk() valid allocation returned base pointer"); - /* 2. _sbrk() underflow test (shrink below the heap base). */ + /* 2. _sbrk() underflow test (shrink below the heap base). + * + * The decrement is derived from the live break rather than fixed. An + * application that reached printf() before this point already carries a + * newlib stdio buffer on the heap, so a fixed -128 would be an ordinary + * shrink rather than an underflow, and this check would pass or fail on + * which application happened to link the BSP. Handing back one byte more + * than has ever been taken underflows no matter what ran first. */ errno = 0; - p_under = _sbrk(-128); + heap_taken = (ptrdiff_t)((uintptr_t)_sbrk(0) - BSP_HEAP_BASE); + p_under = _sbrk(-(heap_taken + 1)); check(&state, (p_under == (void *)-1) && (errno == EINVAL), "_sbrk() underflow guard rejected with EINVAL"); diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/trap.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/trap.c index 00795f5a..71c283e3 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/trap.c +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/trap.c @@ -8,13 +8,15 @@ * SPDX-License-Identifier: MIT */ +// Some portions generated by Claude Code (Opus 5) + #include #include "hwtimer.h" #include "plic.h" +#include "polarfire_console.h" #include "uart.h" extern void _tx_timer_interrupt(void); -extern void console_rx_isr_callback(char c); static void print_hex64(uint64_t val) { const char hex_chars[] = "0123456789ABCDEF"; @@ -43,7 +45,7 @@ void trap_handler(uint64_t mcause, uint64_t mepc, uint64_t mtval) { if (source == MMUART1_IRQ) { while (uart_has_rx()) { char c = uart_getc(); - console_rx_isr_callback(c); + polarfire_console_rx_dispatch(c); } } plic_complete(source); diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo.resc b/targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo.resc new file mode 100644 index 00000000..fe55262f --- /dev/null +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo.resc @@ -0,0 +1,32 @@ +:name: PolarFire SoC - shared Eclipse ThreadX Device Monitor Demo +:description: apps/threadx_demo on Microchip PolarFire SoC (Renode 64-Bit RISC-V) + +# Interactive counterpart to polarfire_threadx_demo_ci.resc: this one free-runs +# so the status table can be watched live. It loads the shared portable demo, +# the same source targets/STMicroelectronics/NUCLEO_F401RE builds. + +Clear + +using sysbus +mach create "PolarFire_SoC_Icicle" +machine LoadPlatformDescription @platforms/cpus/polarfire-soc.repl + +# Show terminal analyzer for Hart 1 (u54_1) MMUART1 at 0x20100000 +showAnalyzer mmuart1 + +# Portable relative path using Renode's built-in $ORIGIN variable +$bin?=$ORIGIN/../build/app/polarfire_threadx_demo.elf + +macro reset +""" + sysbus LoadELF $bin + # Route execution directly to Hart 1 (U54 Core 1) + u54_1 PC `e51 PC` + e51 IsHalted true + u54_2 IsHalted true + u54_3 IsHalted true + u54_4 IsHalted true +""" +runMacro $reset + +start diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo_ci.resc b/targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo_ci.resc new file mode 100644 index 00000000..70cb964e --- /dev/null +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/renode/polarfire_threadx_demo_ci.resc @@ -0,0 +1,38 @@ +:name: PolarFire SoC - shared Eclipse ThreadX demo (headless CI run) +:description: Deterministic virtual-time run of apps/threadx_demo, driven by scripts/test_renode.py --app threadx_demo. + +# Counterpart to polarfire_ci.resc for the second executable this target +# builds. The two differ only in which ELF they load and how long they run: +# the shared demo has no MMUART1 receive path to exercise, so there is no +# WriteChar step here, and its reporter thread prints every two seconds rather +# than every one. + +Clear + +using sysbus +mach create "PolarFire_SoC_Icicle" +machine LoadPlatformDescription @platforms/cpus/polarfire-soc.repl + +# Under --plain --disable-gui this logs MMUART1 traffic to stdout, which is +# what the assertions in test_renode.py read. +showAnalyzer mmuart1 + +$bin?=$ORIGIN/../build/app/polarfire_threadx_demo.elf + +macro reset +""" + sysbus LoadELF $bin + # Route execution directly to Hart 1 (U54 Core 1) + u54_1 PC `e51 PC` + e51 IsHalted true + u54_2 IsHalted true + u54_3 IsHalted true + u54_4 IsHalted true +""" +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 "5" + +quit diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py b/targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py old mode 100644 new mode 100755 index c9731e9b..ba3566bc --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py @@ -10,28 +10,153 @@ # """ -Headless Renode Verification Test for PolarFire SoC Icicle Kit ThreadX Demo +Headless Renode verification tests for the PolarFire SoC Icicle Kit. + +This target builds two executables, so this runner drives two suites: + + --app lm75 the board's own LM75 condition-monitoring demo + (app/main.c), which is the default + --app threadx_demo the shared portable demo (apps/threadx_demo/main.c), + the same source the NUCLEO-F401RE builds + +They share every line of Renode process handling below and differ only in +which .resc they load and what they assert on, which is why they are one +script rather than two: the plumbing is what would drift if it were copied. """ +import argparse import os -import sys -import time -import subprocess +import re import shutil +import subprocess +import sys import threading import queue +import time + + +class Assertions(object): + """Consumes console lines and tracks which named checks have been met.""" + + def __init__(self, names): + self._met = dict((name, False) for name in names) + + def mark(self, name): + self._met[name] = True + + def is_met(self, name): + return self._met[name] + + def satisfied(self): + return all(self._met.values()) + + def unmet(self): + return [name for name, ok in self._met.items() if not ok] + + +class Lm75Suite(object): + """The board's own demo: sensor pipeline plus the MMUART1 receive path.""" + + resc = "polarfire_ci.resc" + # The .resc injects this byte into MMUART1 itself; Renode's monitor is not + # on stdin, so the injection has to happen there rather than here. + description = "LM75 condition-monitoring demo (app/main.c)" + timeout_seconds = 120.0 + + NAMES = ( + "startup self-tests passed", + "ThreadX system tick advancing", + "LM75 overtemperature alarm", + "PLIC IRQ 91 RX interrupt delivered", + ) + + def __init__(self): + self.checks = Assertions(self.NAMES) + self.failed_selftest = False + + def feed(self, line): + if "[-] FAIL:" in line or "startup verification test(s) FAILED" in line: + self.failed_selftest = True + if "[SELF-TEST] All startup verification tests PASSED!" in line: + self.checks.mark("startup self-tests passed") + if "Ticks:" in line: + self.checks.mark("ThreadX system tick advancing") + if "OVERTEMP ALARM TRIGGERED" in line: + self.checks.mark("LM75 overtemperature alarm") + # The only check that proves the PLIC is programmed for the machine-mode + # context: reading its registers back cannot distinguish that from the + # supervisor one. It now also proves bsp_console_set_rx_handler() took + # effect, since the trap path dispatches through the registered handler. + if "PLIC IRQ 91 handled" in line: + self.checks.mark("PLIC IRQ 91 RX interrupt delivered") + + def success_message(self): + return ("startup self-tests, ThreadX ticks, LM75 alarm, and PLIC RX " + "interrupt all verified!") + + +class ThreadxDemoSuite(object): + """The shared portable demo, asserted here exactly as it is on Cortex-M4.""" + + resc = "polarfire_threadx_demo_ci.resc" + description = "shared portable demo (apps/threadx_demo/main.c)" + timeout_seconds = 600.0 + + # The reporter thread prints these once the RTOS primitives have run. + RUNS_RE = re.compile( + r"Runs: Monitor: (\d+) \| Reporter: (\d+) \| Blink: (\d+) \| Timer Wakes: (\d+)") + RTOS_RE = re.compile( + r"Mutex Locks: (\d+)/(\d+) \| Queue Msgs: (\d+) \| Event Wakes: (\d+) \| Sema Wakes: (\d+)") + + NAMES = ( + "boot banner reached the console", + "startup self-tests passed", + "LED blink thread and application timer ran", + "mutex, queue, event flag and semaphore all exercised", + ) + + def __init__(self): + self.checks = Assertions(self.NAMES) + self.failed_selftest = False + + def feed(self, line): + # Deliberately board-neutral: the identical assertion runs against the + # Cortex-M4 build of this same source. + if "Eclipse ThreadX Device Monitor Demo" in line: + self.checks.mark("boot banner reached the console") + if "[-] FAIL:" in line or "startup verification test(s) FAILED" in line: + self.failed_selftest = True + if "[SELF-TEST] All startup verification tests PASSED!" in line: + self.checks.mark("startup self-tests passed") + + # The blink thread is the only caller of bsp_led_toggle(), and timer + # wakes come from the 1 Hz ThreadX application timer, so a non-zero + # pair covers the LED path and the timer service. + m = self.RUNS_RE.search(line) + if m and int(m.group(3)) > 0 and int(m.group(4)) > 0: + self.checks.mark("LED blink thread and application timer ran") + + m = self.RTOS_RE.search(line) + if m and all(int(g) > 0 for g in m.groups()): + self.checks.mark("mutex, queue, event flag and semaphore all exercised") + + def success_message(self): + return ("boot banner, startup self-tests, LED and timer activity, and " + "all RTOS primitives verified!") + + +SUITES = { + "lm75": Lm75Suite, + "threadx_demo": ThreadxDemoSuite, +} -# Deterministic virtual-time run script. It injects RX_TEST_CHAR into MMUART1 -# itself; Renode's monitor is not on stdin, so injection has to happen there. -RESC_NAME = "polarfire_ci.resc" -RX_TEST_CHAR = "X" def find_renode(): # Check PATH first renode_bin = shutil.which("renode") if renode_bin: return renode_bin - + # Common Windows locations win_paths = [ r"C:\Program Files\Renode\renode.exe", @@ -40,9 +165,10 @@ def find_renode(): for path in win_paths: if os.path.isfile(path): return path - + return "renode" + def reader_thread_fn(pipe, q): try: for line in iter(pipe.readline, ''): @@ -52,20 +178,23 @@ def reader_thread_fn(pipe, q): finally: pipe.close() -def run_test(test_timeout_mode=False): + +def run_test(app, test_timeout_mode=False): + suite = SUITES[app]() renode = find_renode() script_dir = os.path.dirname(os.path.abspath(__file__)) target_dir = os.path.dirname(script_dir) - resc_path = os.path.join(target_dir, "renode", RESC_NAME).replace("\\", "/") - + resc_path = os.path.join(target_dir, "renode", suite.resc).replace("\\", "/") + if test_timeout_mode: print("[*] Running intentional timeout test mode (2.0s deadline against unresponsive wait)...") timeout_seconds = 2.0 else: print(f"[*] Starting headless Renode test using: {renode}") + print(f"[*] Suite: {suite.description}") print(f"[*] Loading script: {resc_path}") - timeout_seconds = 120.0 - + timeout_seconds = suite.timeout_seconds + cmd = [ renode, "--plain", @@ -73,7 +202,7 @@ def run_test(test_timeout_mode=False): "--port", "-1", "-e", f"include @{resc_path}" ] - + proc = subprocess.Popen( cmd, stdin=subprocess.PIPE, @@ -82,45 +211,27 @@ def run_test(test_timeout_mode=False): text=True, bufsize=1 ) - + output_q = queue.Queue() reader_t = threading.Thread(target=reader_thread_fn, args=(proc.stdout, output_q), daemon=True) reader_t.start() - - output_lines = [] - found_selftests = False - found_selftest_failure = False - found_ticks = False - found_alarm = False - found_plic_rx = False + start_time = time.time() try: while time.time() - start_time < timeout_seconds: try: line = output_q.get(timeout=0.1) - output_lines.append(line) print(line, end="") if test_timeout_mode: continue - if "[-] FAIL:" in line or "startup verification test(s) FAILED" in line: - found_selftest_failure = True - if "[SELF-TEST] All startup verification tests PASSED!" in line: - found_selftests = True - if "Ticks:" in line: - found_ticks = True - if "OVERTEMP ALARM TRIGGERED" in line: - found_alarm = True - if "PLIC IRQ 91 handled" in line: - found_plic_rx = True + suite.feed(line) # A failed self-test is recorded but does not stop the run, so # the remaining assertions are still reported rather than hidden. - if (found_selftests and not found_selftest_failure - and found_ticks and found_alarm and found_plic_rx): - print("\n[+] SUCCESS: startup self-tests, ThreadX ticks, " - "LM75 alarm, and PLIC RX interrupt all verified!") + if suite.checks.satisfied() and not suite.failed_selftest: + print("\n[+] SUCCESS: " + suite.success_message()) break except queue.Empty: if proc.poll() is not None: @@ -128,7 +239,7 @@ def run_test(test_timeout_mode=False): finally: try: proc.terminate() - proc.wait(timeout=2) + proc.wait(timeout=5) except Exception: try: proc.kill() @@ -141,16 +252,12 @@ def run_test(test_timeout_mode=False): f"and terminated child process cleanly.") sys.exit(0) - checks = { - "startup self-tests passed": found_selftests and not found_selftest_failure, - "ThreadX system tick advancing": found_ticks, - "LM75 overtemperature alarm": found_alarm, - "PLIC IRQ 91 RX interrupt delivered": found_plic_rx, - } - failed = [name for name, ok in checks.items() if not ok] + failed = suite.checks.unmet() + if suite.failed_selftest and "startup self-tests passed" not in failed: + failed.append("startup self-tests passed") if not failed: - print("[+] Renode headless test PASSED.") + print(f"[+] Renode headless test PASSED ({suite.description}).") sys.exit(0) print("\n[-] FAILED. Unmet assertions:") @@ -158,6 +265,15 @@ def run_test(test_timeout_mode=False): print(" - %s" % name) sys.exit(1) + if __name__ == "__main__": - timeout_test = "--test-timeout" in sys.argv - run_test(test_timeout_mode=timeout_test) + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--app", choices=sorted(SUITES), default="lm75", + help="which of this target's two executables to verify " + "(default: lm75)") + parser.add_argument("--test-timeout", action="store_true", + help="exercise the runner's own deadline and cleanup path") + args = parser.parse_args() + + run_test(args.app, test_timeout_mode=args.test_timeout) diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/README.md b/targets/STMicroelectronics/NUCLEO_F401RE/README.md index dbee3ce3..b872f93f 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/README.md +++ b/targets/STMicroelectronics/NUCLEO_F401RE/README.md @@ -11,13 +11,13 @@ Eclipse ThreadX contributors - Initial version and validation. --> -# Eclipse ThreadX NUCLEO-F401RE Starter Application +# Eclipse ThreadX NUCLEO-F401RE Target -This demonstration application validates the NUCLEO-F401RE Board Support Package for Eclipse ThreadX. +This target validates the NUCLEO-F401RE Board Support Package for Eclipse ThreadX. -The demo creates multiple threads, blinks the onboard LED, and reports runtime statistics through USART2 using the ST-LINK virtual COM port. +The application it builds is **`apps/threadx_demo/main.c`**, the shared portable demo, not a source file of its own. That same file is built by `targets/Microchip/POLARFIRE_ICICLE_RENODE` for 64-bit RISC-V, and CI asserts on the same console output from both, so this target's role is to prove the BSP contracts hold on 32-bit Cortex-M4. The demo creates multiple threads, blinks the onboard LED, and reports runtime statistics through USART2 using the ST-LINK virtual COM port; it reaches all three through `` and names no STM32 symbol. -It integrates the Eclipse ThreadX RTOS with the STM32CubeF4 HAL library in a clean, self-contained CMake build structure. +Everything board-specific lives under `lib/bsp/` and `app/common/`: this target integrates the Eclipse ThreadX RTOS with the STM32CubeF4 HAL library in a clean, self-contained CMake build structure. Third-party licensing information is provided in [NOTICE.md](NOTICE.md). @@ -126,7 +126,7 @@ To view console output: ## Hardware Configuration -The starter application is preconfigured to work with the physical STMicroelectronics NUCLEO-F401RE board: +The BSP is preconfigured to work with the physical STMicroelectronics NUCLEO-F401RE board: - **MCU**: Single-core ARM Cortex-M4 running at 84 MHz - **Onboard User LED**: LD2 connected to pin `PA5` for heartbeat feedback - **UART Console**: USART2 connected to the ST-Link virtual COM port on pins `PA2` (TX) and `PA3` (RX) configured for 115200 baud, 8N1 @@ -172,6 +172,7 @@ The BSP separates the ThreadX scheduler tick from the STM32 HAL timebase to avoi - **USART2** - Operates in polling mode and does not use interrupts in this demonstration. + - `bsp_console_set_rx_handler()` is therefore implemented as a store only: the handler an application registers is kept, but nothing on this board delivers received bytes one at a time to invoke it. `nucleo_console_rx_dispatch()` in `lib/bsp/src/bsp_console.c` is the hook a `USART2_IRQHandler` would call. The contract is honoured anyway so a portable application can register unconditionally. ### HAL Timebase Integration @@ -182,14 +183,17 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro ## File Organization -- **`app/`**: Main application logic and boot setups +- **`../../../apps/threadx_demo/`**: The application this target builds. Shared + with the PolarFire SoC Icicle Kit; nothing in it is board specific. +- **`app/`**: Boot setup and the CMake glue that pulls in the shared application - **`app/common/`**: Vendor HAL MSP hooks and boot sources - **`app/common/startup/`**: Linker scripts and assembly boot startup code - - **`app/starter/`**: Demo main thread logic - **`lib/`**: Libraries and dependencies - **`lib/bsp/`**: Board support implementing the generic `` interfaces (`bsp_board.c` clocks and HAL timebase, `bsp_led.c` LD2, `bsp_console.c` - USART2, `newlib_stubs.c` newlib syscall overrides) + USART2 and the receive-handler registration, `bsp_memory.c` the application + RAM budget, `bsp_selftest.c` the startup checks, `newlib_stubs.c` newlib + syscall overrides) - **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers - **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`) - **`cmake/`**: Cross-compilation module definitions diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/app/CMakeLists.txt b/targets/STMicroelectronics/NUCLEO_F401RE/app/CMakeLists.txt index 19172319..71b18bf4 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/app/CMakeLists.txt +++ b/targets/STMicroelectronics/NUCLEO_F401RE/app/CMakeLists.txt @@ -7,7 +7,12 @@ # SPDX-License-Identifier: MIT set(COMMON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/common) -set(STARTER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/starter) + +# The shared portable demo, built from apps/ rather than from this target. It +# names no board symbol, so the PolarFire SoC Icicle Kit builds the same file. +# A board-specific application would live here under app/ instead; a target +# picks one or the other. +set(SHARED_APP_DIR ${SAMPLEX_ROOT_DIR}/apps/threadx_demo) set(SOURCES ${COMMON_DIR}/startup/system_stm32f4xx.c @@ -20,7 +25,7 @@ set(SOURCES # libnucleo_bsp.a so the linker always takes these definitions instead of # falling back to the libnosys stubs. ${CMAKE_CURRENT_SOURCE_DIR}/../lib/bsp/src/newlib_stubs.c - ${STARTER_DIR}/main.c + ${SHARED_APP_DIR}/main.c ) add_executable(${PROJECT_NAME} ${SOURCES}) @@ -35,7 +40,6 @@ target_link_libraries(${PROJECT_NAME} target_include_directories(${PROJECT_NAME} PUBLIC ${COMMON_DIR} - ${STARTER_DIR} ) # Linker script diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/cloud_config.h b/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/cloud_config.h deleted file mode 100644 index 03b5e1f9..00000000 --- a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/cloud_config.h +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 - */ - -#ifndef CLOUD_CONFIG_H -#define CLOUD_CONFIG_H - -/* Placeholder for future cloud connectivity settings. */ - -#endif /* CLOUD_CONFIG_H */ diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/nucleo_console.h b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/nucleo_console.h index a9f66dff..d899a2b3 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/nucleo_console.h +++ b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/nucleo_console.h @@ -8,6 +8,8 @@ * SPDX-License-Identifier: MIT */ +// Some portions generated by Claude Code (Opus 5) + #ifndef NUCLEO_CONSOLE_H #define NUCLEO_CONSOLE_H @@ -25,4 +27,16 @@ */ void nucleo_console_read(char *data, size_t length); +/** + * @brief Hands one received character to the registered bsp/console.h handler. + * + * Board-private, and currently without a caller: USART2 is read by polling on + * this board, so no interrupt delivers bytes one at a time. It is the hook a + * USART2_IRQHandler would call, and it is what reads the pointer that + * bsp_console_set_rx_handler() stores. + * + * @param c The character read out of the USART2 data register. + */ +void nucleo_console_rx_dispatch(char c); + #endif /* NUCLEO_CONSOLE_H */ diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_console.c b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_console.c index df71a8ca..f78b7d3d 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_console.c +++ b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_console.c @@ -13,6 +13,8 @@ * SPDX-License-Identifier: MIT and CC0-1.0 **************************************************************************/ +// Some portions generated by Claude Code (Opus 5) + #include "bsp/console.h" #include "board_config.h" #include "nucleo_console.h" @@ -38,6 +40,38 @@ static void console_error_handler(void) } #endif +/* + * Receive handler registered through bsp_console_set_rx_handler(). + * + * USART2 is read by polling on this board - nucleo_console_read() blocks in + * HAL_UART_Receive() - so nothing raises a per-byte receive interrupt and this + * handler is never invoked. It is stored anyway because the contract in + * is what lets a portable application register unconditionally + * without asking which boards have wired an RX interrupt. Wiring USART2_IRQHandler + * is what would give it a caller. + */ +static bsp_console_rx_fn volatile console_rx_handler = NULL; +static void *volatile console_rx_context = NULL; + +void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context) +{ + /* Publish the context first: a reader tests the handler to decide whether + * to dispatch, so a visible handler must already have its context beside + * it. A single aligned pointer store is atomic on Cortex-M4. */ + console_rx_context = context; + console_rx_handler = handler; +} + +void nucleo_console_rx_dispatch(char c) +{ + bsp_console_rx_fn handler = console_rx_handler; + + if (handler != NULL) + { + handler(c, console_rx_context); + } +} + void bsp_console_init(void) { #if BSP_HAS_CONSOLE diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c index 4c893966..8e7d6e2c 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c +++ b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c @@ -80,6 +80,7 @@ unsigned bsp_self_test(bsp_selftest_report_fn report, void *context) void *p_ok; void *p_under; void *p_over; + ptrdiff_t heap_taken; uint32_t tick_start; uint32_t spins; @@ -97,14 +98,23 @@ unsigned bsp_self_test(bsp_selftest_report_fn report, void *context) ((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 checks found it. */ + /* 2. Releasing it puts the break back where step 1 found it, leaving the + * heap exactly as the remaining checks found it. That is the heap base + * only for an application that has not yet allocated. */ check(&state, _sbrk(-64) != (void *)-1, "_sbrk() released 64 bytes back to the heap base"); - /* 3. Shrinking below the heap base is rejected. */ + /* 3. Shrinking below the heap base is rejected. + * + * The decrement is derived from the live break rather than fixed. Step 2 + * only returns the break to where step 1 found it, which is the heap base + * only for an application that has not yet allocated; one that reached + * printf() first carries a newlib stdio buffer below it, and a fixed -128 + * would then be an ordinary shrink rather than an underflow. Handing back + * one byte more than has ever been taken underflows either way. */ errno = 0; - p_under = _sbrk(-128); + heap_taken = (ptrdiff_t)((uintptr_t)_sbrk(0) - heap_base); + p_under = _sbrk(-(heap_taken + 1)); check(&state, (p_under == (void *)-1) && (errno == EINVAL), "_sbrk() underflow guard rejected with EINVAL"); diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot index 20f94ee6..4e788db5 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot +++ b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot @@ -10,7 +10,7 @@ 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 Eclipse ThreadX 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 diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py b/targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py index e37a8a74..03e4afc2 100755 --- a/targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py +++ b/targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py @@ -15,6 +15,12 @@ Runs targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_ci.resc and asserts on the USART2 console output. The run advances a fixed span of virtual time and quits on its own, so the result does not depend on host speed. + +The application under test is apps/threadx_demo/main.c, shared with the +Microchip PolarFire SoC Icicle Kit. The assertions below are deliberately the +same ones targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py +makes with --app threadx_demo, so the two runs compare the same source built +for a 32-bit Cortex-M4 and a 64-bit RISC-V hart. """ import os @@ -106,7 +112,10 @@ def run_test(test_timeout_mode=False): if test_timeout_mode: continue - if "NUCLEO-F401RE Device Monitor Demo" in line: + # apps/threadx_demo/main.c prints this, and it names no + # board on purpose: the PolarFire suite asserts on the very + # same line against the RISC-V build of that same source. + if "Eclipse ThreadX Device Monitor Demo" in line: found_banner = True if "[-] FAIL:" in line or "startup verification test(s) FAILED" in line: found_selftest_failure = True diff --git a/templates/target/README.md b/templates/target/README.md index beddee2d..3e8f1d07 100644 --- a/templates/target/README.md +++ b/templates/target/README.md @@ -49,8 +49,12 @@ To maintain long-term framework maintainability and portability, the following r * `/bsp`: Defines the target-agnostic C interface contracts (`board.h`, `led.h`, `console.h`, `memory.h`, `selftest.h`). New board targets must implement these existing interfaces rather than modifying core interface definitions. * `/libs`: Shared RTOS components consumed by every target as submodules. Targets reference these rather than vendoring their own copy. +* `/apps`: Portable applications that any target can build. `apps/threadx_demo/main.c` is built today by both shipped targets from the one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V. Point a new board's `app/CMakeLists.txt` at it to get a demo for free; extend it only in ways that stay board-neutral. + > [!NOTE] -> **Applications are currently target-resident.** Each target owns its demo under `targets///app/`, along with its own `cmake/` toolchain files and build helpers. A shared `/apps` layer is a goal of this framework, not something it provides yet, so onboard a new board by starting from the app in this template rather than by linking one from a shared directory. What a demo no longer needs is board headers: `` covers byte-pool sizing and `` covers startup self-tests, so both shipped demos include only `` and ``. +> **A demo belongs in `/apps` or with its board, and what it names decides which.** An application that names no board symbol, vendor header or linker-defined address is portable and belongs in `apps/`; one that does belongs under `targets///app/`. Both are legitimate - the PolarFire SoC Icicle Kit keeps its LM75 monitor as a target app because it genuinely models a sensor, and builds the shared demo as a second executable alongside it. A target's `app/CMakeLists.txt` picks either. +> +> What no demo needs any more is board headers. `` covers byte-pool sizing and `` covers startup self-tests, so every shipped demo includes only C standard headers, `` and ``. > [!NOTE] > **Core Architectural Principle**: @@ -75,7 +79,8 @@ The table below maps common embedded software components to their designated loc | **BSP Driver Implementation** | `targets///lib/bsp/src/` | Target developer (`bsp_board.c`, `bsp_led.c`, `bsp_console.c`, `bsp_memory.c`, `bsp_selftest.c`) | | **Target Specification Constants** | `targets///lib/bsp/include/board_config.h` | Target developer (declarative defines only) | | **Target Build Automation** | `targets///scripts/build.ps1` | Target developer (PowerShell automation template) | -| **Application Code** | `targets///app/` | Target developer (start from this template's `app/main.c`) | +| **Portable Application Code** | `apps//` | Shared across targets (`apps/threadx_demo/` builds on every target) | +| **Board-Specific Application Code** | `targets///app/` | Target developer (start from this template's `app/main.c`) | | **Toolchain & Build Helpers** | `targets///cmake/` | Target developer (cross-compilation settings per target) | --- @@ -101,7 +106,7 @@ To add support for a new board (e.g. `MY_VENDOR / MY_BOARD`): Populate the driver stubs in `targets/MyVendor/MY_BOARD/lib/bsp/src/`: - `bsp_board.c`: Configure System Clocks, Flash Wait States, and low-level timers in `bsp_board_init()`. - `bsp_led.c`: Configure GPIO pin muxing and implement `bsp_led_on()`, `bsp_led_off()`, `bsp_led_toggle()`. - - `bsp_console.c`: Configure UART peripheral and implement `bsp_console_write()`. + - `bsp_console.c`: Configure the UART peripheral and implement `bsp_console_write()`. Store what `bsp_console_set_rx_handler()` is given even if this board only polls its console; if the board does raise a receive interrupt, dispatch to the stored handler from its IRQ handler rather than calling a symbol the application must define. A BSP that requires an application-defined symbol cannot be linked by an application that does not want it. - `bsp_memory.c`: Report the RAM the application may claim in `bsp_ram_region()`, subtracting whatever this board reserves for its C heap and stacks. - `bsp_selftest.c`: Verify in `bsp_self_test()` that the board came up as configured - clocks, timebase, interrupt routing, and that the C heap cannot grow into the region `bsp_ram_region()` hands out. @@ -115,7 +120,7 @@ To add support for a new board (e.g. `MY_VENDOR / MY_BOARD`): The target's CMake build scripts are responsible for exposing include paths and linking the BSP drivers, vendor SDK, ThreadX kernel, and shared application executable together into the final firmware image: - Update `targets/MyVendor/MY_BOARD/CMakeLists.txt` with your target project name and any required vendor SDK configuration. - Update `targets/MyVendor/MY_BOARD/lib/CMakeLists.txt` to register vendor SDK libraries and subdirectories. - - Update `targets/MyVendor/MY_BOARD/app/CMakeLists.txt` to register startup assembly files, system initialization code, linker scripts, target BSP sources, and link against the required ThreadX libraries. + - Update `targets/MyVendor/MY_BOARD/app/CMakeLists.txt` to register startup assembly files, system initialization code, linker scripts, target BSP sources, and link against the required ThreadX libraries. Point it either at `${SAMPLEX_ROOT_DIR}/apps/threadx_demo/main.c` for the shared portable demo or at this template's own `app/main.c` to grow a board-specific one. 7. **Build and Test**: Run the PowerShell build script: @@ -135,7 +140,7 @@ The current framework defines the following core baseline C interfaces in `/bsp/ | :--- | :--- | :--- | | `` | `bsp_board_init()` | Core MCU clock tree, power scaling, and flash wait state setup. | | `` | `bsp_led_init()`, `bsp_led_toggle()`, etc. | GPIO user LED initialization and state toggling. | -| `` | `bsp_console_init()`, `bsp_console_write()` | Serial UART initialization and output transmission. | +| `` | `bsp_console_init()`, `bsp_console_write()`, `bsp_console_set_rx_handler()` | Serial UART initialization, output transmission, and registration of a receive handler. | | `` | `bsp_ram_region()` | RAM the application may claim, clamped against the board's own heap and stack reservations. | | `` | `bsp_self_test()` | Board startup verification, reported through an application-supplied callback. | @@ -148,4 +153,4 @@ The current framework defines the following core baseline C interfaces in `/bsp/ > [!TIP] > **Optional Interfaces & Hardware Variants**: > * The baseline interfaces currently cover core board setup, user LED control, and debug console output. Future versions of the BSP framework may introduce additional optional interfaces (e.g., non-volatile storage, networking, I2C/SPI bus drivers). -> * If a specific target board lacks dedicated LED or UART hardware, implement the interface functions as **no-op implementations** so that generic application binaries continue to link and execute cleanly. +> * If a specific target board lacks dedicated LED or UART hardware, implement the interface functions as **no-op implementations** so that generic application binaries continue to link and execute cleanly. The same applies to `bsp_console_set_rx_handler()` on a board with no receive path: accept and store the handler, and simply never invoke it. diff --git a/templates/target/app/CMakeLists.txt b/templates/target/app/CMakeLists.txt index 6e9a585f..3ec30b76 100644 --- a/templates/target/app/CMakeLists.txt +++ b/templates/target/app/CMakeLists.txt @@ -15,8 +15,9 @@ set(SOURCES # ${COMMON_DIR}/startup/startup_mcu.s # ${COMMON_DIR}/startup/tx_initialize_low_level.S - # This target's ThreadX application. Applications are target-resident: - # there is no shared apps/ directory to link one from. + # This target's ThreadX application. Swap this for + # ${SAMPLEX_ROOT_DIR}/apps/threadx_demo/main.c to build the shared portable + # demo instead; a target picks one or the other. ${CMAKE_CURRENT_SOURCE_DIR}/main.c # Link GCC Newlib syscall stubs diff --git a/templates/target/app/main.c b/templates/target/app/main.c index 1169b3f6..1ce95f70 100644 --- a/templates/target/app/main.c +++ b/templates/target/app/main.c @@ -22,8 +22,11 @@ * byte pool, which answers, and running the board's startup * self-tests, which answers. * - * Once the board boots this, grow the demo in place. Applications live with - * their target rather than in a shared directory. + * Once the board boots this, either grow the demo in place - which is what a + * board-specific application such as the PolarFire LM75 monitor does - or + * point app/CMakeLists.txt at apps/threadx_demo/main.c and get the shared + * portable demo instead. What a demo names decides where it belongs: name a + * board symbol and it stays here, name none and it belongs in apps/. */ #include "tx_api.h" diff --git a/templates/target/lib/bsp/src/bsp_console.c b/templates/target/lib/bsp/src/bsp_console.c index 23dae914..d84d1aa3 100644 --- a/templates/target/lib/bsp/src/bsp_console.c +++ b/templates/target/lib/bsp/src/bsp_console.c @@ -8,9 +8,13 @@ * SPDX-License-Identifier: MIT */ +// Some portions generated by Claude Code (Opus 5) + #include "bsp/console.h" #include "board_config.h" +#include + /* TODO: Include vendor UART / Serial hardware headers here */ void bsp_console_init(void) @@ -33,3 +37,36 @@ void bsp_console_write(const char *data, size_t length) (void)length; #endif } + +/* Handler registered by the application to receive console input. + * + * Store it even if this board has no receive-interrupt path: the contract lets + * an application register unconditionally, and a board that silently drops the + * registration would make a portable demo fail only on that board. */ +static bsp_console_rx_fn volatile console_rx_handler = NULL; +static void *volatile console_rx_context = NULL; + +void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context) +{ + /* Publish the context first, so a handler visible to an interrupt already + * has its context beside it. */ + console_rx_context = context; + console_rx_handler = handler; +} + +/* + * Call this from the board's UART receive interrupt handler, once per byte. + * + * TODO: Enable the peripheral's RX interrupt in bsp_console_init() and invoke + * this from its IRQ handler. A board that only polls its console can leave + * this without a caller; the registration above still has to work. + */ +void bsp_console_rx_dispatch(char c) +{ + bsp_console_rx_fn handler = console_rx_handler; + + if (handler != NULL) + { + handler(c, console_rx_context); + } +}