diff --git a/bsp/include/bsp/memory.h b/bsp/include/bsp/memory.h new file mode 100644 index 0000000..2a3d205 --- /dev/null +++ b/bsp/include/bsp/memory.h @@ -0,0 +1,41 @@ +/*************************************************************************** + * 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 BSP_MEMORY_H +#define BSP_MEMORY_H + +#include + +/** + * @brief Reports the RAM region the application may claim. + * + * ThreadX hands tx_application_define() the first address it believes to be + * unused, but only the board knows what sits above it: a C heap reservation, a + * main stack at the top of RAM, or a memory-mapped peripheral window. This + * call clamps the region against whatever the board holds back, so an + * application can size a TX_BYTE_POOL without naming a single board symbol. + * + * The returned region is the application's to divide up. A board must never + * include its own heap or stack reservations in it. + * + * @param first_unused The pointer ThreadX passed to tx_application_define(). + * @param base Receives the first address the application owns. Must not be + * NULL. + * @param size Receives the length of that region in bytes, zero when the board + * has no RAM to spare. Must not be NULL. + */ +void bsp_ram_region(void *first_unused, void **base, size_t *size); + +#endif /* BSP_MEMORY_H */ diff --git a/bsp/include/bsp/selftest.h b/bsp/include/bsp/selftest.h new file mode 100644 index 0000000..6b42059 --- /dev/null +++ b/bsp/include/bsp/selftest.h @@ -0,0 +1,51 @@ +/*************************************************************************** + * 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 BSP_SELFTEST_H +#define BSP_SELFTEST_H + +/** + * @brief Reports the outcome of one board self-test. + * + * The board owns the checks; the application owns how their results reach the + * user. Formatting the line - and therefore the choice between printf() and + * bsp_console_write() - stays entirely on the application side of this + * callback, so a board never has to know which is available. + * + * @param passed Non-zero when the check succeeded, zero when it failed. + * @param message Description of the check, including any measured values. + * Valid only for the duration of the call. + * @param context The context pointer that was handed to bsp_self_test(). + */ +typedef void (*bsp_selftest_report_fn)(int passed, const char *message, + void *context); + +/** + * @brief Runs the board's startup self-tests. + * + * Intended to be called before tx_kernel_enter(), so a hardware or runtime + * fault is reported even when the scheduler never starts. Each check reports + * through @p report in execution order; checks that mutate board state undo it + * before returning, leaving the board as the caller found it. + * + * @param report Callback invoked once per check. Must not be NULL; passing + * NULL runs no check and returns 1, since a board whose results + * cannot be reported must not be assumed healthy. + * @param context Opaque pointer passed back to @p report unmodified. + * @return Number of checks that failed; zero when every check passed. + */ +unsigned bsp_self_test(bsp_selftest_report_fn report, void *context); + +#endif /* BSP_SELFTEST_H */ diff --git a/docs/architecture.md b/docs/architecture.md index b932fcb..5bfa785 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,7 +9,7 @@ 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 and the console through the abstract interfaces in `/bsp`, not through vendor registers. Applications are target-resident: each target owns its demo under `app/`, and today's demos do additionally include their own `board_config.h` for memory sizing and vendor headers for board-specific startup self-tests. A fully portable shared application layer is a goal of the framework, not a property it has yet. +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. --- @@ -29,7 +29,7 @@ samplex/ (repository root) │ └── STMicroelectronics/ │ └── NUCLEO_F401RE/ # Board-specific BSP implementation & Renode target ├── bsp/ # [Framework] Abstract BSP interface definitions -│ └── include/bsp/ # board.h, led.h, console.h +│ └── include/bsp/ # board.h, led.h, console.h, memory.h, selftest.h ├── docs/ # [Framework] Architecture and onboarding documentation └── templates/ # [Framework] Templates for onboarding new boards ``` @@ -56,6 +56,22 @@ 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. +### Application RAM Budget (`memory.h`) + +* `void bsp_ram_region(void *first_unused, void **base, size_t *size)`: Reports the RAM region the application may claim, given the pointer ThreadX passed to `tx_application_define()`. + +ThreadX reports the first address it believes to be unused, but only the board knows what sits above it - a C heap reservation, a main stack at the top of RAM, or a peripheral window. This interface is what lets an application size a `TX_BYTE_POOL` without naming a board symbol. A board must never include its own reservations in the region it returns; an application will allocate every byte of it. + +The two shipped targets show the two shapes this takes. The NUCLEO-F401RE keeps its main stack at the top of SRAM and clamps the region below a fixed reservation; the PolarFire SoC Icicle Kit keeps its boot stack *below* ThreadX's first unused address and only has to skip its C heap reservation. + +### Startup Self-Tests (`selftest.h`) + +* `unsigned bsp_self_test(bsp_selftest_report_fn report, void *context)`: Runs the board's startup self-tests, reporting each through the callback, and returns the number of failures. + +Checks that the board came up as its own configuration promised are BSP tests, not application tests: they need vendor headers, linker symbols and register maps that no portable application can see. Keeping them behind this interface is what removed those headers from both demos' `main.c`. + +The application supplies only the reporting callback, so message formatting - and therefore the choice between `printf()` and `bsp_console_write()` - stays on the application side. Both shipped targets verify that their C heap cannot grow into the region `bsp_ram_region()` promises the application; the NUCLEO-F401RE additionally checks its clock tree and HAL timebase, and the PolarFire its CLINT tick arithmetic and PLIC routing. + --- ## 4. How to Onboard a New Board diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md b/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md index 4ea1e9f..b2f5fb4 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md @@ -55,7 +55,7 @@ four things and exits non-zero if any of them is missing: | Assertion | Covers | |---|---| -| Startup self-tests all passed | `_sbrk()` bounds, timer catch-up, PLIC configuration | +| 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 | diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/common/startup/newlib_stubs.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/common/startup/newlib_stubs.c index 8d19650..cf63f69 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/common/startup/newlib_stubs.c +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/common/startup/newlib_stubs.c @@ -18,7 +18,7 @@ #include "board_config.h" #include "bsp/console.h" -extern char __end; /* Symbol set by linker at end of BSS / top of boot stack */ +/* __end, BSP_HEAP_BASE and BSP_HEAP_LIMIT come from board_config.h. */ static char *heap_ptr = NULL; static inline uintptr_t disable_interrupts(void) { @@ -77,7 +77,10 @@ void *_sbrk(ptrdiff_t incr) { } if (incr > 0) { - if ((uintptr_t)heap_ptr + (uintptr_t)incr > (uintptr_t)BSP_RAM_END || + /* Bound against the end of the heap reservation, not the end of DRAM: + * everything above BSP_HEAP_LIMIT is what bsp_ram_region() hands the + * application for its ThreadX pool. */ + if ((uintptr_t)heap_ptr + (uintptr_t)incr > BSP_HEAP_LIMIT || (uintptr_t)heap_ptr + (uintptr_t)incr < (uintptr_t)heap_ptr) { restore_interrupts(mstatus); errno = ENOMEM; diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c index 50e3808..9ca5e60 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c @@ -14,23 +14,11 @@ #include #include #include -#include #include "tx_api.h" -#include "hwtimer.h" -#include "plic.h" -#include "csr.h" #include "bsp/board.h" #include "bsp/console.h" #include "bsp/led.h" - -extern void *_sbrk(ptrdiff_t incr); - -/* board_config.h derives TICK_CYCLES from BSP_TICK_RATE_HZ without seeing the - * ThreadX headers. This translation unit sees both, so it is where the two are - * checked against each other. C99 has no _Static_assert, hence the negative - * array size idiom. */ -typedef char bsp_tick_rate_matches_threadx[ - (BSP_TICK_RATE_HZ == (unsigned long long)TX_TIMER_TICKS_PER_SECOND) ? 1 : -1]; +#include "bsp/selftest.h" #define DEMO_STACK_SIZE 4096 #define DEMO_QUEUE_ITEMS 10 @@ -90,13 +78,16 @@ static volatile ULONG s_sampler_runs = 0; static volatile ULONG s_analyzer_runs = 0; static volatile ULONG s_reporter_runs = 0; -static unsigned s_selftest_failures = 0; +/* The startup checks themselves belong to the board and live in its BSP; this + * side only decides how their results are printed. scripts/test_renode.py + * asserts on the summary line. */ + +static void selftest_report(int passed, const char *message, void *context) { + (void)context; -static void selftest_report(int passed, const char *message) { if (passed) { console_print("[+] PASS: "); } else { - s_selftest_failures++; console_print("[-] FAIL: "); } console_print(message); @@ -104,71 +95,20 @@ static void selftest_report(int passed, const char *message) { } static void run_startup_self_tests(void) { - extern char __end; - char num_buf[160]; - console_print("[SELF-TEST] Starting Hardware & Runtime Verification...\n"); - - /* 1. _sbrk() Valid allocation test */ - void *p1 = _sbrk(64); - selftest_report(p1 != (void *)-1 && (uintptr_t)p1 >= (uintptr_t)&__end, - "_sbrk() valid allocation returned base pointer"); - - /* 2. _sbrk() Underflow test (shrink below heap base) */ - errno = 0; - void *p_under = _sbrk(-128); - selftest_report(p_under == (void *)-1 && errno == EINVAL, - "_sbrk() underflow guard rejected with EINVAL"); - - /* 3. _sbrk() Overflow test (request beyond 1 GiB DRAM) */ - errno = 0; - void *p_over = _sbrk((ptrdiff_t)0x40000000ULL); - selftest_report(p_over == (void *)-1 && errno == ENOMEM, - "_sbrk() overflow guard rejected with ENOMEM"); - - /* 4. HWTimer catch-up clamp. - * - * Exercised as pure arithmetic through hwtimer_next_cmp(), so no CLINT - * register is disturbed: mtime is the platform-wide counter shared by every - * hart, and mtimecmp drives the live kernel tick. Both branches are covered - * - a deadline still in the future advances relatively, one already missed - * is rebased onto the current time instead of firing continuously. */ - uint64_t now = 5000000ULL; - uint64_t missed_cmp = now - (TICK_CYCLES * 8ULL); /* 8 ticks behind */ - uint64_t pending_cmp = now - (TICK_CYCLES / 2ULL); /* deadline not yet due */ - int clamp_ok = (hwtimer_next_cmp(missed_cmp, now) == now + TICK_CYCLES) && - (hwtimer_next_cmp(pending_cmp, now) == pending_cmp + TICK_CYCLES); - snprintf(num_buf, sizeof(num_buf), - "HWTimer catch-up (missed deadline rebased to %llu, pending deadline advanced to %llu)", - (unsigned long long)hwtimer_next_cmp(missed_cmp, now), - (unsigned long long)hwtimer_next_cmp(pending_cmp, now)); - selftest_report(clamp_ok, num_buf); - - /* 5. PLIC Configuration & Addressing Verification. - * A wrong PLIC base would read back zeroes here, so the register values - * confirm the addressing as well as the configuration. */ - uint32_t prio = PLIC_PRIORITY_REG(MMUART1_IRQ); - uint32_t en_bitmap = PLIC_HART1_M_ENABLE_REG(MMUART1_IRQ); - uint32_t thresh = PLIC_HART1_M_THRESHOLD_REG; - uint64_t mie_val; - __asm__ volatile("csrr %0, mie" : "=r"(mie_val)); - - int plic_ok = (prio == 1) && - ((en_bitmap & (1U << (MMUART1_IRQ % 32))) != 0) && - (thresh == 0) && - ((mie_val & MIE_MEIE) != 0); - snprintf(num_buf, sizeof(num_buf), - "PLIC Hart 1 (IRQ %u prio=%u en=0x%08X thresh=%u mie=0x%llX)", - (unsigned)MMUART1_IRQ, (unsigned)prio, (unsigned)en_bitmap, - (unsigned)thresh, (unsigned long long)mie_val); - selftest_report(plic_ok, num_buf); - - if (s_selftest_failures == 0) { + char msg[96]; + unsigned failures; + + console_print("[SELF-TEST] Starting BSP & Runtime Verification...\n"); + + failures = bsp_self_test(selftest_report, NULL); + + if (failures == 0U) { console_print("[SELF-TEST] All startup verification tests PASSED!\n\n"); } else { - snprintf(num_buf, sizeof(num_buf), + snprintf(msg, sizeof(msg), "[SELF-TEST] %u startup verification test(s) FAILED!\n\n", - s_selftest_failures); - console_print(num_buf); + failures); + console_print(msg); } } diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/CMakeLists.txt b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/CMakeLists.txt index a211156..27e02a7 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/CMakeLists.txt +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/CMakeLists.txt @@ -18,10 +18,18 @@ add_library(polarfire_bsp STATIC src/plic.c src/hwtimer.c src/trap.c + src/bsp_memory.c + src/bsp_selftest.c ) +# The ThreadX headers are needed for headers only: hwtimer.c checks the board's +# configured tick rate against TX_TIMER_TICKS_PER_SECOND at compile time. The +# BSP links no ThreadX code, so no dependency on the threadx target is implied. target_include_directories(polarfire_bsp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${SAMPLEX_ROOT_DIR}/bsp/include + PRIVATE + ${THREADX_DIR}/common/inc + ${THREADX_DIR}/ports/risc-v64/gnu/inc ) diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/board_config.h b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/board_config.h index 40400d9..bb129a4 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/board_config.h +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/include/board_config.h @@ -13,6 +13,8 @@ #ifndef BOARD_CONFIG_H #define BOARD_CONFIG_H +#include + /* CPU Subsystem Frequencies */ #define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */ #define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ @@ -25,6 +27,23 @@ #define BSP_UART_BAUDRATE 115200 #define BSP_RAM_END 0xC0000000ULL /* 1 GiB LPDDR4 DRAM End Address */ +/* First byte of DRAM above the 16 KB boot stack, placed by linker.ld. + * MISRA C:2012 Rule 8.6 deviation: defined by the linker script rather than by + * any translation unit, which is the only way to import a link-time address. */ +extern char __end; + +/* Bytes of DRAM reserved for the newlib heap immediately above __end. + * + * _sbrk() bounds itself against BSP_HEAP_LIMIT and bsp_ram_region() hands the + * application only the DRAM above it, so malloc() and a ThreadX byte pool + * placed at the first unused address cannot overlap. Bounding _sbrk() against + * BSP_RAM_END instead would let one oversized malloc() take memory the + * application already owns - the failure NUCLEO-F401RE shipped with before its + * heap was bounded against the linker reservation. */ +#define BSP_HEAP_RESERVE_BYTES 0x10000ULL /* 64 KB */ +#define BSP_HEAP_BASE ((uintptr_t)&__end) +#define BSP_HEAP_LIMIT (BSP_HEAP_BASE + (uintptr_t)BSP_HEAP_RESERVE_BYTES) + #define BSP_HAS_LED 1 #define BSP_HAS_CONSOLE 1 diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_memory.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_memory.c new file mode 100644 index 0000000..539c75e --- /dev/null +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_memory.c @@ -0,0 +1,59 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* + * Application RAM budget for the PolarFire SoC Icicle Kit. + * + * linker.ld lays DRAM out as .text, .data, .bss, the 16 KB boot stack, then + * __end - which tx_initialize_low_level.S reports as ThreadX's first unused + * address. Unlike a Cortex-M board there is no stack at the top of RAM to hold + * back, because the boot stack sits below __end; the only reservation above it + * is the newlib heap, which _sbrk() grows up from __end towards + * BSP_HEAP_LIMIT. Everything above that reservation is the application's. + */ + +#include + +#include "bsp/memory.h" +#include "board_config.h" + +/* ThreadX wants an aligned pool base, and linker.ld aligns its sections to 16 + * bytes, so the region starts on the same boundary. */ +#define BSP_RAM_REGION_ALIGN 16U + +void bsp_ram_region(void *first_unused, void **base, size_t *size) +{ + /* + * MISRA C:2012 Rule 11.4 deviation: converting between a pointer and an + * integer is unavoidable here. Clamping against the heap reservation and + * the end of DRAM is arithmetic on absolute addresses, which C cannot + * express on pointers to distinct objects. + */ + uintptr_t start = (uintptr_t)first_unused; + + /* Never hand out the heap reservation, wherever ThreadX believes free + * memory begins. */ + if (start < BSP_HEAP_LIMIT) { + start = BSP_HEAP_LIMIT; + } + + start = (start + (BSP_RAM_REGION_ALIGN - 1U)) & + ~(uintptr_t)(BSP_RAM_REGION_ALIGN - 1U); + + *base = (void *)start; + *size = ((uintptr_t)BSP_RAM_END > start) + ? (size_t)((uintptr_t)BSP_RAM_END - start) + : (size_t)0U; +} 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 new file mode 100644 index 0000000..e329fcc --- /dev/null +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/bsp_selftest.c @@ -0,0 +1,167 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* + * Startup self-tests for the PolarFire SoC Icicle Kit. + * + * These check the board support package - the bounds its heap enforces, the + * CLINT arithmetic its timebase relies on, the PLIC routing its console + * interrupt needs - so they live with the board rather than with any + * application. The application supplies only the reporting callback, which is + * why this translation unit can include plic.h, hwtimer.h and csr.h without + * dragging any of them into an application. + * + * Every check runs before tx_kernel_enter(), so a failure is reported even + * when the scheduler never starts. + */ + +#include +#include +#include +#include + +#include "bsp/memory.h" +#include "bsp/selftest.h" +#include "board_config.h" +#include "csr.h" +#include "hwtimer.h" +#include "plic.h" + +/* Defined in app/common/startup/newlib_stubs.c, which newlib declares only in + * its private headers. */ +extern void *_sbrk(ptrdiff_t incr); + +/* Reporting state, kept on the caller's stack so bsp_self_test() holds no + * mutable state of its own. */ +typedef struct { + bsp_selftest_report_fn report; + void *context; + unsigned failures; +} selftest_state; + +static void check(selftest_state *state, int passed, const char *message) +{ + if (passed == 0) { + state->failures++; + } + state->report(passed, message, state->context); +} + +unsigned bsp_self_test(bsp_selftest_report_fn report, void *context) +{ + selftest_state state; + char msg[160]; + + void *region_base = NULL; + size_t region_size = 0U; + void *p1; + void *p_under; + void *p_over; + void *p_past_heap; + uint64_t now; + uint64_t missed_cmp; + uint64_t pending_cmp; + uint32_t prio; + uint32_t en_bitmap; + uint32_t thresh; + uint64_t mie_val; + int clamp_ok; + int plic_ok; + + if (report == NULL) { + return 1U; + } + + state.report = report; + state.context = context; + state.failures = 0U; + + /* 1. _sbrk() valid allocation test. */ + p1 = _sbrk(64); + 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). */ + errno = 0; + p_under = _sbrk(-128); + check(&state, (p_under == (void *)-1) && (errno == EINVAL), + "_sbrk() underflow guard rejected with EINVAL"); + + /* 3. _sbrk() overflow test (request beyond the 1 GiB DRAM). */ + errno = 0; + p_over = _sbrk((ptrdiff_t)0x40000000ULL); + check(&state, (p_over == (void *)-1) && (errno == ENOMEM), + "_sbrk() overflow guard rejected with ENOMEM"); + + /* 4. Regression guard for the heap bound. This request fits DRAM with a + * gigabyte to spare but exceeds the heap reservation, so bounding _sbrk() + * against BSP_RAM_END rather than BSP_HEAP_LIMIT lets it succeed and hands + * malloc() memory bsp_ram_region() has already promised the application. */ + errno = 0; + p_past_heap = _sbrk((ptrdiff_t)(BSP_HEAP_RESERVE_BYTES + 0x1000ULL)); + check(&state, (p_past_heap == (void *)-1) && (errno == ENOMEM), + "_sbrk() rejects a request that fits DRAM but not the heap"); + + /* 5. The region bsp_ram_region() hands the application must start above + * the heap reservation and end inside DRAM. This is the invariant that + * keeps malloc() and a ThreadX byte pool from ever overlapping. */ + bsp_ram_region(&__end, ®ion_base, ®ion_size); + (void)snprintf(msg, sizeof(msg), + "bsp_ram_region() yields [0x%llX,0x%llX), clear of the heap and inside DRAM", + (unsigned long long)(uintptr_t)region_base, + (unsigned long long)((uintptr_t)region_base + region_size)); + check(&state, + (region_size > 0U) && ((uintptr_t)region_base >= BSP_HEAP_LIMIT) && + (((uintptr_t)region_base + region_size) <= (uintptr_t)BSP_RAM_END), + msg); + + /* 6. HWTimer catch-up clamp. + * + * Exercised as pure arithmetic through hwtimer_next_cmp(), so no CLINT + * register is disturbed: mtime is the platform-wide counter shared by every + * hart, and mtimecmp drives the live kernel tick. Both branches are covered + * - a deadline still in the future advances relatively, one already missed + * is rebased onto the current time instead of firing continuously. */ + now = 5000000ULL; + missed_cmp = now - (TICK_CYCLES * 8ULL); /* 8 ticks behind */ + pending_cmp = now - (TICK_CYCLES / 2ULL); /* deadline not yet due */ + clamp_ok = (hwtimer_next_cmp(missed_cmp, now) == now + TICK_CYCLES) && + (hwtimer_next_cmp(pending_cmp, now) == pending_cmp + TICK_CYCLES); + (void)snprintf(msg, sizeof(msg), + "HWTimer catch-up (missed deadline rebased to %llu, pending deadline advanced to %llu)", + (unsigned long long)hwtimer_next_cmp(missed_cmp, now), + (unsigned long long)hwtimer_next_cmp(pending_cmp, now)); + check(&state, clamp_ok, msg); + + /* 7. PLIC configuration and addressing verification. + * A wrong PLIC base would read back zeroes here, so the register values + * confirm the addressing as well as the configuration. */ + prio = PLIC_PRIORITY_REG(MMUART1_IRQ); + en_bitmap = PLIC_HART1_M_ENABLE_REG(MMUART1_IRQ); + thresh = PLIC_HART1_M_THRESHOLD_REG; + __asm__ volatile("csrr %0, mie" : "=r"(mie_val)); + + plic_ok = (prio == 1) && + ((en_bitmap & (1U << (MMUART1_IRQ % 32))) != 0) && + (thresh == 0) && + ((mie_val & MIE_MEIE) != 0); + (void)snprintf(msg, sizeof(msg), + "PLIC Hart 1 (IRQ %u prio=%u en=0x%08X thresh=%u mie=0x%llX)", + (unsigned)MMUART1_IRQ, (unsigned)prio, (unsigned)en_bitmap, + (unsigned)thresh, (unsigned long long)mie_val); + check(&state, plic_ok, msg); + + return state.failures; +} diff --git a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/hwtimer.c b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/hwtimer.c index 2f7635a..374722c 100644 --- a/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/hwtimer.c +++ b/targets/Microchip/POLARFIRE_ICICLE_RENODE/lib/bsp/src/hwtimer.c @@ -11,6 +11,15 @@ // Some portions generated by Claude Code (Opus 5) #include "hwtimer.h" +#include "tx_api.h" + +/* hwtimer.h derives TICK_CYCLES from BSP_TICK_RATE_HZ without seeing the + * ThreadX headers. This translation unit sees both, so it is where the two are + * checked against each other. C99 has no _Static_assert, hence the negative + * array size idiom. */ +typedef char bsp_tick_rate_matches_threadx[ + (BSP_TICK_RATE_HZ == (unsigned long long)TX_TIMER_TICKS_PER_SECOND) ? 1 : -1]; + void hwtimer_init(void) { /* Program initial mtimecmp target to current mtime + 10,000 cycles (10ms) */ diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/README.md b/targets/STMicroelectronics/NUCLEO_F401RE/README.md index e1d3025..dbee3ce 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/README.md +++ b/targets/STMicroelectronics/NUCLEO_F401RE/README.md @@ -80,8 +80,11 @@ ground for use with `renode-test`. ### What is asserted -The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a -`[SELF-TEST]` summary the harness asserts on: +The board runs eight startup self-tests before `tx_kernel_enter()` and the demo +prints a `[SELF-TEST]` summary the harness asserts on. The checks live in +`lib/bsp/src/bsp_selftest.c` behind ``, because they test the +BSP - its linker reservations, its clock tree, its HAL timebase - rather than +the application. `main.c` supplies only the reporting callback: | # | Self-test | Guards against | |---|-----------|----------------| @@ -92,6 +95,7 @@ The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a | 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression | | 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration | | 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped | +| 8 | `bsp_ram_region()` stays clear of the heap and inside SRAM | a bad stack reservation handing the byte pool memory it does not own | Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()` @@ -197,7 +201,7 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro ### Verification Environment - **Toolchain**: Arm GNU Toolchain 14.3.Rel1 (GCC 14.3.1), the version pinned by CI and by the ThreadX Cortex-M ports -- **Static ROM usage**: 22064 Bytes (4.21% of 512 KB Flash), including the startup self-tests +- **Static ROM usage**: 22272 Bytes (4.25% of 512 KB Flash), including the startup self-tests - **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM) - **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers). - **Board Hardware**: NUCLEO-F401RE diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c b/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c index 5493933..401e04d 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c +++ b/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c @@ -8,25 +8,19 @@ * SPDX-License-Identifier: MIT */ -#include +// Some portions generated by Claude Code (Opus 5) + #include -#include #include #include "tx_api.h" #include "bsp/board.h" #include "bsp/led.h" -#include "bsp/console.h" -#include "board_config.h" -#include "cloud_config.h" -#include "stm32f4xx_hal.h" +#include "bsp/memory.h" +#include "bsp/selftest.h" #define THREAD_STACK_SIZE 1024 -/* Bytes of SRAM held back above the ThreadX byte pool for the main stack, - * which serves every interrupt handler once the scheduler is running. */ -#define MAIN_STACK_MARGIN 4096 - typedef struct { CHAR *name; UINT state; @@ -355,15 +349,17 @@ static void queue_receiver_entry(ULONG parameter) { void tx_application_define(void *first_unused_memory) { UINT status; CHAR *stack_ptr; - ULONG pool_size; + void *pool_base; + size_t pool_size; - /* Calculate available RAM for the byte pool, leaving a 4 KB margin for the - * main stack at the top of SRAM. */ - pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory; + /* The board decides how much of the RAM above ThreadX's first unused address + * the application may claim; whatever it reserves for its own heap and stack + * is already excluded. */ + bsp_ram_region(first_unused_memory, &pool_base, &pool_size); /* Initialize the byte pool */ - status = tx_byte_pool_create(&byte_pool, "system byte pool", - first_unused_memory, pool_size); + status = tx_byte_pool_create(&byte_pool, "system byte pool", pool_base, + (ULONG)pool_size); if (status != TX_SUCCESS) { printf("Byte pool create failed: 0x%08x\r\n", status); return; @@ -552,91 +548,33 @@ void tx_application_define(void *first_unused_memory) { /* ------------------------------------------------------------------------- * * Startup self-tests * - * These run before tx_kernel_enter() so a failure is reported even when the - * scheduler never starts. scripts/test_renode.py asserts on the summary line. + * The checks themselves belong to the board and live in its BSP; this side + * only decides how their results are printed. scripts/test_renode.py asserts + * on the summary line. * ------------------------------------------------------------------------- */ -/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */ -extern char _end; -extern char _heap_limit; -extern char __RAM_segment_used_end__; - -extern void *_sbrk(ptrdiff_t incr); +static void selftest_report(int passed, const char *message, void *context) { + (void)context; -static unsigned selftest_failures = 0; - -static void selftest_report(int passed, const char *message) { if (passed) { printf("[+] PASS: %s\r\n", message); } else { - selftest_failures++; printf("[-] FAIL: %s\r\n", message); } } static void run_startup_self_tests(void) { - const uintptr_t heap_base = (uintptr_t)&_end; - const uintptr_t heap_limit = (uintptr_t)&_heap_limit; - const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__; - char msg[128]; + unsigned failures; printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n"); - /* 1. A modest request lands inside the heap reservation. */ - void *p_ok = _sbrk(64); - selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) && - ((uintptr_t)p_ok < heap_limit), - "_sbrk() valid allocation inside the heap reservation"); - - /* 2. Releasing it returns the break to the heap base, leaving the heap - * exactly as the remaining tests found it. */ - selftest_report(_sbrk(-64) != (void *)-1, - "_sbrk() released 64 bytes back to the heap base"); - - /* 3. Shrinking below the heap base is rejected. */ - errno = 0; - void *p_under = _sbrk(-128); - selftest_report((p_under == (void *)-1) && (errno == EINVAL), - "_sbrk() underflow guard rejected with EINVAL"); - - /* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM - * but far exceeds the heap reservation, so bounding _sbrk() against the end - * of SRAM rather than _heap_limit let this succeed and handed malloc() - * memory owned by the ThreadX byte pool and the main stack. */ - errno = 0; - void *p_over = _sbrk((ptrdiff_t)0x8000); - selftest_report((p_over == (void *)-1) && (errno == ENOMEM), - "_sbrk() rejects a request that fits SRAM but not the heap"); - - /* 5. The heap reservation must end at or below the first byte ThreadX owns. */ - snprintf(msg, sizeof(msg), - "heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX", - (unsigned long)heap_base, (unsigned long)heap_limit, - (unsigned long)pool_base); - selftest_report(heap_limit <= pool_base, msg); - - /* 6. SystemClock_Config() reached the documented 84 MHz. */ - snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)", - (unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ); - selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg); - - /* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick() - * is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the - * timer is still ticking afterwards. The spin cap is a liveness bound, not a - * timing expectation: one TIM2 tick is ~84000 core cycles. */ - uint32_t tick_start = HAL_GetTick(); - uint32_t spins = 0; - while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) { - spins++; - } - selftest_report(HAL_GetTick() != tick_start, - "HAL timebase (TIM2) tick advancing"); + failures = bsp_self_test(selftest_report, NULL); - if (selftest_failures == 0) { + if (failures == 0U) { printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n"); } else { printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n", - selftest_failures); + failures); } } diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/CMakeLists.txt b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/CMakeLists.txt index bd70c9b..3b1a9ba 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/CMakeLists.txt +++ b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/CMakeLists.txt @@ -14,6 +14,8 @@ add_library(nucleo_bsp STATIC src/bsp_board.c src/bsp_led.c src/bsp_console.c + src/bsp_memory.c + src/bsp_selftest.c ) target_include_directories(nucleo_bsp diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/board_config.h b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/board_config.h index 3f7af47..929e081 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/board_config.h +++ b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/include/board_config.h @@ -8,6 +8,8 @@ * SPDX-License-Identifier: MIT */ +// Some portions generated by Claude Code (Opus 5) + #ifndef BOARD_CONFIG_H #define BOARD_CONFIG_H @@ -29,6 +31,11 @@ #define BSP_RAM_SIZE 0x00018000UL /* 96 KB */ #define BSP_RAM_END (BSP_RAM_START + BSP_RAM_SIZE) +/* Bytes of SRAM held back below _estack for the main stack, which serves every + * interrupt handler once the scheduler is running. bsp_ram_region() clamps the + * application's RAM budget below this reservation. */ +#define BSP_MAIN_STACK_RESERVE 4096UL + #define BSP_HAS_LED 1 #define BSP_HAS_CONSOLE 1 diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_memory.c b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_memory.c new file mode 100644 index 0000000..a087e4b --- /dev/null +++ b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_memory.c @@ -0,0 +1,45 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* + * Application RAM budget for the NUCLEO-F401RE. + * + * NUCLEO_F401RE.ld lays SRAM out as .data, .bss, the newlib heap reservation + * and _Min_Stack_Size, then __RAM_segment_used_end__ - which is what + * tx_initialize_low_level.S reports as ThreadX's first unused address. The + * main stack grows down from _estack at the top of SRAM, so the region between + * the two is what the application may claim. + */ + +#include + +#include "bsp/memory.h" +#include "board_config.h" + +void bsp_ram_region(void *first_unused, void **base, size_t *size) +{ + /* + * MISRA C:2012 Rule 11.4 deviation: converting between a pointer and an + * integer is unavoidable here. The comparison against BSP_RAM_END is + * arithmetic on absolute addresses, which C cannot express on pointers to + * distinct objects. + */ + const uintptr_t start = (uintptr_t)first_unused; + const uintptr_t end = (uintptr_t)BSP_RAM_END - + (uintptr_t)BSP_MAIN_STACK_RESERVE; + + *base = first_unused; + *size = (end > start) ? (size_t)(end - start) : (size_t)0U; +} diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c new file mode 100644 index 0000000..4c89396 --- /dev/null +++ b/targets/STMicroelectronics/NUCLEO_F401RE/lib/bsp/src/bsp_selftest.c @@ -0,0 +1,161 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* + * Startup self-tests for the NUCLEO-F401RE. + * + * These check the board support package - the heap bounds its linker script + * reserves, the clock its init code programs, the timebase its HAL drives - so + * they live with the board rather than with any application. The application + * supplies only the reporting callback, which is why this translation unit can + * include board_config.h and the vendor HAL without dragging either into an + * application. + * + * Every check runs before tx_kernel_enter(), so a failure is reported even + * when the scheduler never starts. + */ + +#include +#include +#include +#include + +#include "bsp/memory.h" +#include "bsp/selftest.h" +#include "board_config.h" +#include "stm32f4xx_hal.h" + +/* + * Bounds of the heap reservation and of ThreadX's first free byte. + * MISRA C:2012 Rule 8.6 deviation: these symbols are defined by + * NUCLEO_F401RE.ld rather than by any translation unit, which is the only way + * to import an address decided at link time. + */ +extern char _end; +extern char _heap_limit; +extern char __RAM_segment_used_end__; + +/* Defined in newlib_stubs.c, which newlib declares only in private headers. */ +extern void *_sbrk(ptrdiff_t incr); + +/* Reporting state, kept on the caller's stack so bsp_self_test() holds no + * mutable state of its own. */ +typedef struct { + bsp_selftest_report_fn report; + void *context; + unsigned failures; +} selftest_state; + +static void check(selftest_state *state, int passed, const char *message) +{ + if (passed == 0) { + state->failures++; + } + state->report(passed, message, state->context); +} + +unsigned bsp_self_test(bsp_selftest_report_fn report, void *context) +{ + selftest_state state; + char msg[128]; + + const uintptr_t heap_base = (uintptr_t)&_end; + const uintptr_t heap_limit = (uintptr_t)&_heap_limit; + const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__; + + void *region_base = NULL; + size_t region_size = 0U; + void *p_ok; + void *p_under; + void *p_over; + uint32_t tick_start; + uint32_t spins; + + if (report == NULL) { + return 1U; + } + + state.report = report; + state.context = context; + state.failures = 0U; + + /* 1. A modest request lands inside the heap reservation. */ + p_ok = _sbrk(64); + check(&state, (p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) && + ((uintptr_t)p_ok < heap_limit), + "_sbrk() valid allocation inside the heap reservation"); + + /* 2. Releasing it returns the break to the heap base, leaving the heap + * exactly as the remaining checks found it. */ + check(&state, _sbrk(-64) != (void *)-1, + "_sbrk() released 64 bytes back to the heap base"); + + /* 3. Shrinking below the heap base is rejected. */ + errno = 0; + p_under = _sbrk(-128); + check(&state, (p_under == (void *)-1) && (errno == EINVAL), + "_sbrk() underflow guard rejected with EINVAL"); + + /* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM + * but far exceeds the heap reservation, so bounding _sbrk() against the end + * of SRAM rather than _heap_limit let this succeed and handed malloc() + * memory owned by the ThreadX byte pool and the main stack. */ + errno = 0; + p_over = _sbrk((ptrdiff_t)0x8000); + check(&state, (p_over == (void *)-1) && (errno == ENOMEM), + "_sbrk() rejects a request that fits SRAM but not the heap"); + + /* 5. The heap reservation must end at or below the first byte ThreadX + * owns. */ + (void)snprintf(msg, sizeof(msg), + "heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX", + (unsigned long)heap_base, (unsigned long)heap_limit, + (unsigned long)pool_base); + check(&state, heap_limit <= pool_base, msg); + + /* 6. SystemClock_Config() reached the documented 84 MHz. */ + (void)snprintf(msg, sizeof(msg), + "SystemCoreClock is %lu Hz (expected %lu Hz)", + (unsigned long)SystemCoreClock, + (unsigned long)BSP_CPU_CLOCK_HZ); + check(&state, SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg); + + /* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick() + * is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm + * the timer is still ticking afterwards. The spin cap is a liveness bound, + * not a timing expectation: one TIM2 tick is ~84000 core cycles. */ + tick_start = HAL_GetTick(); + spins = 0; + while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) { + spins++; + } + check(&state, HAL_GetTick() != tick_start, + "HAL timebase (TIM2) tick advancing"); + + /* 8. The region bsp_ram_region() hands the application must start above + * the heap reservation and end inside SRAM. This is the invariant that + * keeps malloc() and the ThreadX byte pool from ever overlapping. */ + bsp_ram_region(&__RAM_segment_used_end__, ®ion_base, ®ion_size); + (void)snprintf(msg, sizeof(msg), + "bsp_ram_region() yields [0x%08lX,0x%08lX), clear of the heap and inside SRAM", + (unsigned long)(uintptr_t)region_base, + (unsigned long)((uintptr_t)region_base + region_size)); + check(&state, + (region_size > 0U) && ((uintptr_t)region_base >= heap_limit) && + (((uintptr_t)region_base + region_size) <= (uintptr_t)BSP_RAM_END), + msg); + + return state.failures; +} diff --git a/templates/target/README.md b/templates/target/README.md index a5be06d..beddee2 100644 --- a/templates/target/README.md +++ b/templates/target/README.md @@ -46,11 +46,11 @@ Repository To maintain long-term framework maintainability and portability, the following root directories should generally remain **unchanged** when onboarding a new board: -* `/bsp`: Defines the target-agnostic C interface contracts (`board.h`, `led.h`, `console.h`). New board targets must implement these existing interfaces rather than modifying core interface definitions. +* `/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. > [!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: today's demos also include their target's `board_config.h` for memory sizing and vendor headers for board-specific startup self-tests. Onboard a new board by starting from the app in this template, not by linking one from a shared directory. +> **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 ``. > [!NOTE] > **Core Architectural Principle**: @@ -72,7 +72,7 @@ The table below maps common embedded software components to their designated loc | **Startup Assembly & System Code** | `targets///app/common/startup/` | Vendor SDK (`startup_.s`, `system_.c`) | | **Linker Script** | `targets///app/common/linker/` | Vendor SDK (`.ld` or compiler script) | | **ThreadX Low-Level Setup** | `targets///app/common/startup/` | `libs/threadx/ports///src/` | -| **BSP Driver Implementation** | `targets///lib/bsp/src/` | Target developer (`bsp_board.c`, `bsp_led.c`, `bsp_console.c`) | +| **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`) | @@ -95,12 +95,15 @@ To add support for a new board (e.g. `MY_VENDOR / MY_BOARD`): - Set `BSP_SYSTEM_CLOCK_HZ` to your core CPU frequency. - Set `BSP_UART_BAUDRATE` to your debug serial speed. - Set `BSP_RAM_END` to the physical top address of your MCU SRAM. + - Set `BSP_MAIN_STACK_RESERVE` to the bytes your board holds back at the top of RAM for the main stack, so `bsp_ram_region()` keeps an application's byte pool clear of it. 3. **Implement Abstract C BSP Drivers**: 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_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. 4. **Add Startup Files & Linker Script**: Obtain the standard startup assembly (`startup_.s`), system initialization (`system_.c`), and linker script (`.ld`) **directly from your MCU vendor's official SDK or reference package** (do not write these from scratch). Developers should avoid modifying vendor startup code unless strictly necessary, as these files are maintained by the silicon vendor. Place them under `targets/MyVendor/MY_BOARD/app/common/startup/` and `targets/MyVendor/MY_BOARD/app/common/linker/`. @@ -133,12 +136,14 @@ 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_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. | ### Target Configuration Component (`board_config.h`) | Configuration File | Expected Constants | Description | | :--- | :--- | :--- | -| `board_config.h` | `BSP_RAM_END`, `BSP_SYSTEM_CLOCK_HZ`, `BSP_UART_BAUDRATE` | Compile-time hardware specification constants consumed by the BSP driver implementation layer. | +| `board_config.h` | `BSP_RAM_END`, `BSP_MAIN_STACK_RESERVE`, `BSP_SYSTEM_CLOCK_HZ`, `BSP_UART_BAUDRATE` | Compile-time hardware specification constants consumed by the BSP driver implementation layer. | > [!TIP] > **Optional Interfaces & Hardware Variants**: diff --git a/templates/target/app/main.c b/templates/target/app/main.c index 668cf68..1169b3f 100644 --- a/templates/target/app/main.c +++ b/templates/target/app/main.c @@ -18,12 +18,12 @@ * * This file deliberately depends on nothing but and the generic BSP * contracts in , so it compiles for any target that implements them. - * Thread stacks are static, which avoids needing the board's memory extents. + * That includes the two things a demo used to need board headers for: sizing a + * byte pool, which answers, and running the board's startup + * self-tests, which answers. * - * Once the board boots this, grow the demo in place. Real targets do include - * their own board_config.h and vendor headers - for byte-pool sizing and for - * board-specific startup self-tests - and that is expected; applications live - * with their target rather than in a shared directory. + * Once the board boots this, grow the demo in place. Applications live with + * their target rather than in a shared directory. */ #include "tx_api.h" @@ -31,7 +31,10 @@ #include "bsp/board.h" #include "bsp/console.h" #include "bsp/led.h" +#include "bsp/memory.h" +#include "bsp/selftest.h" +#include #include #define DEMO_STACK_SIZE 1024 @@ -49,6 +52,18 @@ static void console_print(const char *text) bsp_console_write(text, strlen(text)); } +/* The startup checks belong to the board and live in its BSP; this side only + * decides how their results are printed. A board with no console of its own + * could just as well count failures and blink the LED. */ +static void selftest_report(int passed, const char *message, void *context) +{ + (void)context; + + console_print(passed ? "[+] PASS: " : "[-] FAIL: "); + console_print(message); + console_print("\r\n"); +} + static void blink_thread_entry(ULONG parameter) { (void)parameter; @@ -73,11 +88,16 @@ static void report_thread_entry(ULONG parameter) void tx_application_define(void *first_unused_memory) { - (void)first_unused_memory; + void *pool_base; + size_t pool_size; - /* TODO: Create a TX_BYTE_POOL from first_unused_memory if this target's - * demo outgrows static stacks. Doing so needs the board's RAM extent, so - * it belongs with the target rather than in shared code. */ + /* Stacks below are static, so this pool is unused as shipped. It is here + * because it is how a demo grows past static stacks without naming a + * single board symbol: the board reports what it has to spare, having + * already excluded its own heap and stack reservations. */ + bsp_ram_region(first_unused_memory, &pool_base, &pool_size); + (void)pool_base; + (void)pool_size; (void)tx_thread_create(&blink_thread, "blink thread", blink_thread_entry, 0, blink_stack, DEMO_STACK_SIZE, @@ -94,8 +114,16 @@ int main(void) console_print("\r\n=== Eclipse ThreadX target template ===\r\n"); - /* TODO: Run any board-specific startup self-tests here, before the - * scheduler starts, so failures are reported even if it never runs. */ + /* Run the board's startup self-tests before the scheduler starts, so a + * failure is reported even if it never runs. */ + if (bsp_self_test(selftest_report, NULL) == 0U) + { + console_print("[SELF-TEST] All startup verification tests PASSED!\r\n"); + } + else + { + console_print("[SELF-TEST] Startup verification FAILED!\r\n"); + } tx_kernel_enter(); diff --git a/templates/target/lib/bsp/CMakeLists.txt b/templates/target/lib/bsp/CMakeLists.txt index 8b7ddeb..ca07397 100644 --- a/templates/target/lib/bsp/CMakeLists.txt +++ b/templates/target/lib/bsp/CMakeLists.txt @@ -11,6 +11,8 @@ add_library(target_bsp STATIC src/bsp_board.c src/bsp_led.c src/bsp_console.c + src/bsp_memory.c + src/bsp_selftest.c ) # Export BSP headers and shared abstract C BSP headers to PUBLIC include paths diff --git a/templates/target/lib/bsp/include/board_config.h b/templates/target/lib/bsp/include/board_config.h index 6fc2cc8..5898dc0 100644 --- a/templates/target/lib/bsp/include/board_config.h +++ b/templates/target/lib/bsp/include/board_config.h @@ -29,6 +29,12 @@ */ #define BSP_RAM_END 0x20018000 +/* TODO: Set the number of bytes this board reserves at the top of RAM, if any. + * bsp_ram_region() subtracts it so an application's byte pool never overlaps + * the main stack. Boards that keep their stack below ThreadX's first unused + * address can leave this at 0 and reserve for the C heap instead. */ +#define BSP_MAIN_STACK_RESERVE 4096UL + /* Optional hardware peripheral availability flags */ #define BSP_HAS_LED 1 #define BSP_HAS_CONSOLE 1 diff --git a/templates/target/lib/bsp/src/bsp_memory.c b/templates/target/lib/bsp/src/bsp_memory.c new file mode 100644 index 0000000..217b0e4 --- /dev/null +++ b/templates/target/lib/bsp/src/bsp_memory.c @@ -0,0 +1,49 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +#include + +#include "bsp/memory.h" +#include "board_config.h" + +/* + * Reports how much of the RAM above ThreadX's first unused address the + * application may claim. Only this board knows what sits above it, which is + * why the application never has to. + * + * The two shipped targets show the two shapes this takes: + * + * - NUCLEO-F401RE keeps its main stack at the top of SRAM, so it clamps the + * region below a fixed reservation. + * - PolarFire SoC keeps its boot stack below the first unused address and + * only has to skip the C heap reservation. + * + * TODO: Subtract whatever this board reserves. Anything the C heap, the main + * stack, or a memory-mapped window owns must NOT appear in the returned + * region - an application will allocate every byte of it. + */ +void bsp_ram_region(void *first_unused, void **base, size_t *size) +{ + /* + * MISRA C:2012 Rule 11.4 deviation: converting between a pointer and an + * integer is unavoidable when comparing against an absolute address, which + * C cannot express on pointers to distinct objects. + */ + const uintptr_t start = (uintptr_t)first_unused; + const uintptr_t end = (uintptr_t)BSP_RAM_END - (uintptr_t)BSP_MAIN_STACK_RESERVE; + + *base = first_unused; + *size = (end > start) ? (size_t)(end - start) : (size_t)0U; +} diff --git a/templates/target/lib/bsp/src/bsp_selftest.c b/templates/target/lib/bsp/src/bsp_selftest.c new file mode 100644 index 0000000..a9664f5 --- /dev/null +++ b/templates/target/lib/bsp/src/bsp_selftest.c @@ -0,0 +1,77 @@ +/*************************************************************************** + * 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 + **************************************************************************/ + +/* + * Startup self-tests for this board. + * + * Checks that the board came up as its own configuration promised belong here + * rather than in an application: they need vendor headers, linker symbols and + * register maps that no portable application can see. The application supplies + * only the reporting callback, so this file is free to include anything the + * board needs. + * + * Worthwhile checks, drawn from the two shipped targets: + * + * - The C heap cannot grow into memory bsp_ram_region() hands the + * application. Both shipped targets carry a regression guard for exactly + * this, because getting it wrong corrupts thread stacks silently. + * - The clock the board programmed matches BSP_SYSTEM_CLOCK_HZ. + * - The timebase driving the kernel tick is actually advancing. + * - The interrupt controller routes the console IRQ where the driver expects. + */ + +#include + +#include "bsp/selftest.h" +#include "board_config.h" + +/* TODO: Include the vendor HAL, linker-symbol externs and register headers the + * checks below need. */ + +/* Reporting state, kept on the caller's stack so bsp_self_test() holds no + * mutable state of its own. */ +typedef struct { + bsp_selftest_report_fn report; + void *context; + unsigned failures; +} selftest_state; + +static void check(selftest_state *state, int passed, const char *message) +{ + if (passed == 0) { + state->failures++; + } + state->report(passed, message, state->context); +} + +unsigned bsp_self_test(bsp_selftest_report_fn report, void *context) +{ + selftest_state state; + + if (report == NULL) { + return 1U; + } + + state.report = report; + state.context = context; + state.failures = 0U; + + /* TODO: Replace this placeholder with the board's real checks. Report each + * through check(), including any measured values in the message so a + * failure is diagnosable from the console log alone. */ + check(&state, 1, "board self-tests not yet implemented for this target"); + + return state.failures; +}