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); + } +}