Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
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 demo into apps/ and made both targets build it by fdesbiens · Pull Request #57 · eclipse-threadx/samplex · GitHub
Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
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 demo into apps/ and made both targets build it by fdesbiens · Pull Request #57 · eclipse-threadx/samplex · GitHub
Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
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 demo into apps/ and made both targets build it by fdesbiens · Pull Request #57 · eclipse-threadx/samplex · GitHub
Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
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 demo into apps/ and made both targets build it by fdesbiens · Pull Request #57 · eclipse-threadx/samplex · GitHub
Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
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 demo into apps/ and made both targets build it by fdesbiens · Pull Request #57 · eclipse-threadx/samplex · GitHub
Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
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 demo into apps/ and made both targets build it by fdesbiens · Pull Request #57 · eclipse-threadx/samplex · GitHub
Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
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 demo into apps/ and made both targets build it by fdesbiens · Pull Request #57 · eclipse-threadx/samplex · GitHub
Skip to content
Open
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
32 changes: 24 additions & 8 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,20 +61,26 @@ jobs:
- name: Report the toolchain version
run: riscv-none-elf-gcc --version

- name: Build SampleX PolarFire Condition-Monitoring Demo
- name: Build SampleX PolarFire Executables
run: |
bash targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/build.sh --rebuild

- name: Verify SampleX Demo ELF
- name: Verify Both PolarFire ELFs
run: |
# Two executables: the board's own LM75 demo, and the shared portable
# demo from apps/threadx_demo that the NUCLEO-F401RE builds from the
# very same source.
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
echo "[OK] PolarFire SampleX demo ELF verified."
test -f targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
echo "[OK] PolarFire LM75 and shared ThreadX demo ELFs verified."

- name: Archive Built PolarFire ELF
- name: Archive Built PolarFire ELFs
uses: actions/upload-artifact@v4
with:
name: polarfire-demo-elf
path: targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
path: |
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_icicle_demo.elf
targets/Microchip/POLARFIRE_ICICLE_RENODE/build/app/polarfire_threadx_demo.elf
retention-days: 1

test-polarfire-renode:
Expand All@@ -98,7 +104,7 @@ jobs:
with:
python-version: "3.11"

- name: Download Built PolarFire ELF
- name: Download Built PolarFire ELFs
uses: actions/download-artifact@v4
with:
name: polarfire-demo-elf
Expand All@@ -125,9 +131,17 @@ jobs:
- name: Put Renode on PATH
run: echo "$HOME/renode" >> $GITHUB_PATH

- name: Run Deterministic Headless Renode Test
- name: Run Deterministic Headless Renode Test (LM75 demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app lm75

# The other half of the portability claim. apps/threadx_demo/main.c is
# asserted here on 64-bit RISC-V and in test-nucleo-renode on 32-bit
# Cortex-M4, against the same console output from the same source file.
# A claim only one architecture verifies is not a claim.
- name: Run Deterministic Headless Renode Test (shared ThreadX demo)
run: |
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py
python3 targets/Microchip/POLARFIRE_ICICLE_RENODE/scripts/test_renode.py --app threadx_demo

build-arm-nucleo:
name: Build ST Nucleo-F401RE (ARM Cortex-M4)
Expand DownExpand Up@@ -181,6 +195,8 @@ jobs:
- name: Report the toolchain version
run: ${{ env.GCC_TARGET }}-gcc --version

# Builds apps/threadx_demo/main.c, the same application source the
# PolarFire SoC Icicle Kit job builds for 64-bit RISC-V.
- name: Build NUCLEO-F401RE Demo
run: |
bash targets/STMicroelectronics/NUCLEO_F401RE/scripts/build.sh --rebuild
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
/*
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/

/*
* Eclipse ThreadX device monitor demo - shared across every target.
*
* This program and the accompanying materials are made available
* under the terms of the MIT license which is available at
* https://opensource.org/license/mit.
* This application depends on nothing but the C standard library, <tx_api.h>
* and the board contracts in <bsp/...>, so the same source builds for any
* target that implements them. It is built today for the ST NUCLEO-F401RE
* (32-bit Cortex-M4) and the Microchip PolarFire SoC Icicle Kit (64-bit
* RISC-V), and CI runs it under Renode on both: a portability claim only one
* architecture verifies is not a claim.
*
* SPDX-License-Identifier: MIT
* Nothing here may name a board symbol, a vendor header or a linker-defined
* address. The two things a demo used to need those for are answered by
* contracts instead - <bsp/memory.h> sizes the byte pool, and
* <bsp/selftest.h> runs the board's startup checks - and anything else a
* future demo needs belongs behind a new contract in bsp/include/bsp/, not an
* #ifdef here.
*
* A board-specific demo is still legitimate; it just lives with its board,
* under targets/<Vendor>/<BOARD>/app/. A target's app/CMakeLists.txt picks
* either.
*/

// Some portions generated by Claude Code (Opus 5)

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "tx_api.h"
Expand All@@ -19,7 +45,13 @@
#include "bsp/memory.h"
#include "bsp/selftest.h"

#define THREAD_STACK_SIZE 1024
/* Thread stacks are sized in machine words rather than in bytes. Every saved
* register, return address and spilled pointer doubles in width between the
* 32-bit Cortex-M4 and the 64-bit RISC-V hart this demo runs on, and so does
* the newlib printf() call chain each of these threads reaches. The reporter
* thread's measured peak is printed in the status table below, which is where
* to look before changing this. */
#define THREAD_STACK_SIZE ((ULONG)(256U * sizeof(void *)))

typedef struct {
CHAR *name;
Expand DownExpand Up@@ -171,8 +203,13 @@ static void monitor_thread_entry(ULONG parameter) {
}
stack_lowest++;
}
/* MISRA C:2012 Rule 11.4 deviation: the distance between two
* stack addresses is only expressible by converting them to an
* integer type. uintptr_t is the width-correct one on both a
* 32-bit and a 64-bit target. */
ULONG unused =
(ULONG)stack_lowest - (ULONG)thread->tx_thread_stack_start;
(ULONG)((uintptr_t)stack_lowest -
(uintptr_t)thread->tx_thread_stack_start);
system_stats.threads[i].stack_used =
thread->tx_thread_stack_size - unused;
system_stats.threads[i].stack_size = thread->tx_thread_stack_size;
Expand All@@ -185,24 +222,35 @@ static void monitor_thread_entry(ULONG parameter) {
}
}

/* Reporter Thread: Consumer/Printer of statistics */
/* Reporter Thread: Consumer/Printer of statistics
*
* Every ULONG reaching printf() is cast to unsigned long, because ULONG is not
* the same type on every port: the Cortex-M4 port defines it as unsigned long
* and the RISC-V 64 port as unsigned int. A bare %lu is therefore wrong on one
* of the two targets this file builds for, and the cast is what makes one
* format string correct on both. */
static void reporter_thread_entry(ULONG parameter) {
(void)parameter;

/* Deliberately names no board: this same source runs on every target, and
* both Renode suites assert on this line. Any third-party licensing notice
* belongs with the target that carries the third-party code, not here. */
printf("\r\n==========================================\r\n");
printf("NUCLEO-F401RE Device Monitor Demo\r\n");
printf("Third-party licensing info in NOTICE.md\r\n");
printf("Eclipse ThreadX Device Monitor Demo\r\n");
printf("==========================================\r\n");

while (1) {
reporter_counter++;

printf("\r\nSystem Status:\r\n");
printf("------------------------------------------\r\n");
printf("Uptime: %lu s\r\n", system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n", system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n", system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n", system_stats.byte_pool_free);
printf("Uptime: %lu s\r\n", (unsigned long)system_stats.uptime);
printf("Byte Pool Size: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_total);
printf("Allocated Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_used);
printf("Free Memory: %lu bytes\r\n",
(unsigned long)system_stats.byte_pool_free);
printf("------------------------------------------\r\n");

printf("\r\n%-16s %-8s %-10s %-12s %-22s\r\n", "Thread Name", "Priority",
Expand All@@ -219,18 +267,23 @@ static void reporter_thread_entry(ULONG parameter) {
printf("%-16s %-8u %-10s %-12lu %4lu / %4lu bytes (%lu%%)\r\n",
system_stats.threads[i].name, system_stats.threads[i].priority,
get_state_string(system_stats.threads[i].state),
system_stats.threads[i].run_count,
system_stats.threads[i].stack_used,
system_stats.threads[i].stack_size, pct);
(unsigned long)system_stats.threads[i].run_count,
(unsigned long)system_stats.threads[i].stack_used,
(unsigned long)system_stats.threads[i].stack_size,
(unsigned long)pct);
}
}
printf("-------------------------------------------------------------------"
"---------\r\n");

printf("Runs: Monitor: %lu | Reporter: %lu | Blink: %lu | Timer Wakes: %lu\r\n",
monitor_counter, reporter_counter, blink_counter, timer_counter);
(unsigned long)monitor_counter, (unsigned long)reporter_counter,
(unsigned long)blink_counter, (unsigned long)timer_counter);
printf("RTOS Showcase: Mutex Locks: %lu/%lu | Queue Msgs: %lu | Event Wakes: %lu | Sema Wakes: %lu\r\n",
mutex_acquires_1, mutex_acquires_2, queue_msgs_received, event_flags_processed, semaphore_wakes);
(unsigned long)mutex_acquires_1, (unsigned long)mutex_acquires_2,
(unsigned long)queue_msgs_received,
(unsigned long)event_flags_processed,
(unsigned long)semaphore_wakes);

tx_thread_sleep(TX_TIMER_TICKS_PER_SECOND * 2); /* Report every 2 seconds */
}
Expand Down
33 changes: 33 additions & 0 deletions bsp/include/bsp/console.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@
* SPDX-License-Identifier: MIT
*/

// Some portions generated by Claude Code (Opus 5)

#ifndef BSP_CONSOLE_H
#define BSP_CONSOLE_H

Expand All@@ -26,4 +28,35 @@ void bsp_console_init(void);
*/
void bsp_console_write(const char *data, size_t length);

/**
* @brief Receives one byte that arrived on the console.
*
* Invoked from interrupt context on boards that drive their console receiver
* from an interrupt, so it must not block, allocate, or call any ThreadX
* service that is illegal from an ISR.
*
* @param c The byte that arrived.
* @param context The context pointer that was handed to
* bsp_console_set_rx_handler().
*/
typedef void (*bsp_console_rx_fn)(char c, void *context);

/**
* @brief Registers the handler invoked for each byte the console receives.
*
* A board that raises an interrupt per received byte must route it here rather
* than to a symbol the application is required to define: an application that
* does not care about console input should not have to define anything to
* link. Bytes that arrive with no handler attached are dropped.
*
* Boards whose console has no receive-interrupt path still implement this
* call; the handler they store is simply never invoked. An application can
* therefore register unconditionally.
*
* @param handler Handler to invoke per received byte. Passing NULL detaches
* the current handler.
* @param context Opaque pointer passed back to @p handler unmodified.
*/
void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context);

#endif /* BSP_CONSOLE_H */
16 changes: 13 additions & 3 deletions docs/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,8 +9,9 @@ This document describes the architecture, design philosophy, directory structure
The BSP framework is designed to be **additive and non-invasive**, allowing new boards to be integrated without modifying existing board implementations.

1. **Legacy Isolation**: The board directories that predate this framework (`/MXChip`, `/OpenHW`, `/STMicroelectronics`) remain completely untouched, preserving their drivers, submodules, and build systems.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Both shipped demos now include only `<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.
2. **Hardware Access Through the BSP**: Application logic reaches LEDs, the console, the board's RAM budget and its startup self-tests through the abstract interfaces in `/bsp`, not through vendor registers. Every shipped demo includes only C standard headers, `<tx_api.h>` and `<bsp/...>`.
3. **A Shared Application Layer**: `apps/` holds portable demos; `targets/<Vendor>/<BOARD>/app/` holds board-specific ones. Both are legitimate, and a target's `app/CMakeLists.txt` picks either. `apps/threadx_demo/main.c` is built by both shipped targets from one source file, and CI runs it under Renode on 32-bit Cortex-M4 and 64-bit RISC-V, asserting on the same console output in both. Portability here is a property CI enforces, not a stated goal: a claim only one architecture verifies is not a claim.
4. **Independent Build Configuration**: Each target carries its own `cmake/` toolchain files and build helpers. Nothing in the build is shared between targets, so changing one board cannot break another.

---

Expand All@@ -23,6 +24,8 @@ samplex/ (repository root)
├── MXChip/ # [Pre-framework] Standalone board sample
├── OpenHW/ # [Pre-framework] Standalone board sample
├── STMicroelectronics/ # [Pre-framework] Standalone board samples
├── apps/ # [Framework] Portable applications, built by any target
│ └── threadx_demo/ # Device monitor demo; built for Cortex-M4 and RISC-V
├── targets/ # [Framework] Supported BSP target boards
│ ├── Microchip/
│ │ └── POLARFIRE_ICICLE_RENODE/ # Board-specific BSP implementation & Renode target
Expand DownExpand Up@@ -55,6 +58,11 @@ Every board added to the framework under `/targets` must implement the abstract

* `void bsp_console_init(void)`: Initializes the default UART console.
* `void bsp_console_write(const char *data, size_t length)`: Transmits a block of data over the console interface.
* `void bsp_console_set_rx_handler(bsp_console_rx_fn handler, void *context)`: Registers the handler invoked, from interrupt context, for each byte the console receives. Passing `NULL` detaches it.

A board that raises an interrupt per received byte must route it through this registration rather than to a symbol the application is required to define. The PolarFire SoC Icicle Kit is why: its trap handler called a fixed `console_rx_isr_callback()` that only its own demo defined, so any second executable linking that BSP failed to link, and a portable application would have had to define a PolarFire-specific ISR callback to say it wanted nothing. That is the same violation this framework exists to remove - the BSP reaching up into the application - simply expressed through the linker instead of through a header.

The handler is nullable and the board checks it before dispatching, so bytes that arrive with no handler attached are dropped rather than faulting. A board whose console has no receive-interrupt path still implements the call and stores what it is given; the NUCLEO-F401RE polls USART2, so the handler it stores is never invoked. That uniformity is what lets an application register unconditionally without asking which boards have wired an interrupt.

### Application RAM Budget (`memory.h`)

Expand All@@ -81,4 +89,6 @@ The application supplies only the reporting callback, so message formatting - an
3. **Implement the BSP APIs**: Implement the interfaces defined in `/bsp/include/bsp/` using the vendor SDK or direct register access.
4. **Configure CMake**: Add the board target to `CMakeLists.txt`, supply the target's toolchain file under its own `cmake/`, build the BSP as a static library, and link it with the application in the target's `app/` directory.

Start from `templates/target/app/main.c`, which depends only on `<tx_api.h>` and the `<bsp/...>` contracts and therefore builds on any target that implements them. Grow it in place as the board needs; there is no shared `/apps` directory to link an application from.
A new board has two ways to get an application. Point its `app/CMakeLists.txt` at `apps/threadx_demo/main.c` to build the shared portable demo, which is the fastest way to prove a fresh BSP implementation is complete and correct - it exercises `board.h`, `led.h`, `memory.h` and `selftest.h`, and it is already known to run on two architectures, so a failure is a finding about the new board rather than about the demo. Or start from `templates/target/app/main.c` and grow a board-specific application in place, which is what the PolarFire SoC Icicle Kit does with its LM75 monitor because that demo genuinely models a sensor.

The rule for which directory a demo belongs in is what it names: an application that names no board symbol, vendor header or linker-defined address belongs in `apps/`, and one that does belongs with its board under `targets/<Vendor>/<BOARD>/app/`.
Loading
Loading