Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Moved the startup self-tests and RAM sizing behind new BSP interfaces by fdesbiens · Pull Request #56 · eclipse-threadx/samplex · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Moved the startup self-tests and RAM sizing behind new BSP interfaces by fdesbiens · Pull Request #56 · eclipse-threadx/samplex · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Moved the startup self-tests and RAM sizing behind new BSP interfaces by fdesbiens · Pull Request #56 · eclipse-threadx/samplex · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Moved the startup self-tests and RAM sizing behind new BSP interfaces by fdesbiens · Pull Request #56 · eclipse-threadx/samplex · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Moved the startup self-tests and RAM sizing behind new BSP interfaces by fdesbiens · Pull Request #56 · eclipse-threadx/samplex · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Moved the startup self-tests and RAM sizing behind new BSP interfaces by fdesbiens · Pull Request #56 · eclipse-threadx/samplex · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Moved the startup self-tests and RAM sizing behind new BSP interfaces by fdesbiens · Pull Request #56 · eclipse-threadx/samplex · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bsp/include/bsp/memory.h
Original file line numberDiff line numberDiff line change
@@ -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 <stddef.h>

/**
* @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 */
51 changes: 51 additions & 0 deletions bsp/include/bsp/selftest.h
Original file line numberDiff line numberDiff line change
@@ -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 */
20 changes: 18 additions & 2 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 consolethrough 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 `<tx_api.h>` and `<bsp/...>` 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.

---
Expand All@@ -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
```
Expand All@@ -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
Expand Down
2 changes: 1 addition & 1 deletion targets/Microchip/POLARFIRE_ICICLE_RENODE/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) {
Expand DownExpand Up@@ -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;
Expand Down
96 changes: 18 additions & 78 deletions targets/Microchip/POLARFIRE_ICICLE_RENODE/app/main.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,23 +14,11 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#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
Expand DownExpand Up@@ -90,85 +78,37 @@ 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);
console_print("\n");
}

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

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
)
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,8 @@
#ifndef BOARD_CONFIG_H
#define BOARD_CONFIG_H

#include <stdint.h>

/* CPU Subsystem Frequencies */
#define BSP_CPU_CLOCK_HZ 600000000ULL /* 600 MHz U54 Application Core Clock */
#define BSP_SYSTEM_CLOCK_HZ BSP_CPU_CLOCK_HZ
Expand All@@ -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

Expand Down
Loading
Loading