summaryrefslogtreecommitdiff
path: root/ports/risc-v64/gnu/example_build/erbium
diff options
context:
space:
mode:
Diffstat (limited to 'ports/risc-v64/gnu/example_build/erbium')
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/CMakeLists.txt68
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/README.md140
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/board.c49
-rwxr-xr-xports/risc-v64/gnu/example_build/erbium/build.sh28
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/csr.h66
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/demo_threadx.c382
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/entry.S50
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/erbium_gnu.cmake39
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/hwtimer.c42
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/hwtimer.h41
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/link.lds92
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/plic.c118
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/plic.h54
-rwxr-xr-xports/risc-v64/gnu/example_build/erbium/test/run_simulator_tests.sh55
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/test/test_plic_race.c90
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/test/test_plic_trap.c55
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/test/test_uart_ticks.c81
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/trap.c78
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/tx_initialize_low_level.S142
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/uart.c109
-rw-r--r--ports/risc-v64/gnu/example_build/erbium/uart.h40
21 files changed, 1819 insertions, 0 deletions
diff --git a/ports/risc-v64/gnu/example_build/erbium/CMakeLists.txt b/ports/risc-v64/gnu/example_build/erbium/CMakeLists.txt
new file mode 100644
index 00000000..51b07714
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/CMakeLists.txt
@@ -0,0 +1,68 @@
+cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
+
+project(demo_threadx_erbium LANGUAGES C ASM)
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+
+set(ERBIUM_DIR ${CMAKE_CURRENT_LIST_DIR})
+set(THREADX_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../..)
+
+add_subdirectory(${THREADX_ROOT} threadx)
+
+add_executable(demo_threadx
+ ${ERBIUM_DIR}/entry.S
+ ${ERBIUM_DIR}/tx_initialize_low_level.S
+ ${ERBIUM_DIR}/board.c
+ ${ERBIUM_DIR}/hwtimer.c
+ ${ERBIUM_DIR}/plic.c
+ ${ERBIUM_DIR}/trap.c
+ ${ERBIUM_DIR}/uart.c
+ ${ERBIUM_DIR}/demo_threadx.c
+)
+set_target_properties(demo_threadx PROPERTIES
+ OUTPUT_NAME "demo_threadx.elf"
+ LINK_DEPENDS ${ERBIUM_DIR}/link.lds
+)
+
+target_include_directories(demo_threadx PRIVATE ${ERBIUM_DIR})
+
+target_link_libraries(demo_threadx PRIVATE threadx)
+
+target_compile_options(demo_threadx PRIVATE
+ -Wall
+ -ffreestanding
+ -ffunction-sections
+ -fdata-sections
+)
+
+target_link_options(demo_threadx PRIVATE
+ -T${ERBIUM_DIR}/link.lds
+ -nostdlib
+ -Wl,--gc-sections
+ -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/demo_threadx.map
+)
+
+function(add_erbium_test name source handler)
+ add_executable(${name}
+ ${ERBIUM_DIR}/entry.S
+ ${ERBIUM_DIR}/tx_initialize_low_level.S
+ ${ERBIUM_DIR}/board.c
+ ${ERBIUM_DIR}/hwtimer.c
+ ${ERBIUM_DIR}/plic.c
+ ${ERBIUM_DIR}/uart.c
+ ${source}
+ ${handler}
+ )
+ target_include_directories(${name} PRIVATE ${ERBIUM_DIR})
+ target_link_libraries(${name} PRIVATE threadx)
+ target_compile_options(${name} PRIVATE -ffreestanding)
+ target_link_options(${name} PRIVATE
+ -T${ERBIUM_DIR}/link.lds
+ -nostdlib
+ -Wl,--gc-sections
+ )
+ set_target_properties(${name} PROPERTIES LINK_DEPENDS ${ERBIUM_DIR}/link.lds)
+endfunction()
+
+add_erbium_test(test_uart_ticks ${ERBIUM_DIR}/test/test_uart_ticks.c ${ERBIUM_DIR}/trap.c)
+add_erbium_test(test_plic_race ${ERBIUM_DIR}/test/test_plic_race.c ${ERBIUM_DIR}/test/test_plic_trap.c)
diff --git a/ports/risc-v64/gnu/example_build/erbium/README.md b/ports/risc-v64/gnu/example_build/erbium/README.md
new file mode 100644
index 00000000..8cfa2828
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/README.md
@@ -0,0 +1,140 @@
+# ThreadX example for Erbium
+
+This directory runs the standard ThreadX demo on one hart of Erbium, the
+16-hart RISC-V platform of the OpenHW Foundation CORE-ET project, using the
+RISC-V64 GNU port in machine mode. The public `et-platform` functional
+simulator, `erbium_emu`, is the reference target.
+
+## Hardware used
+
+| Item | Address | Notes |
+|------|---------|-------|
+| Hart 0 | - | RV64IMC, machine mode; other harts are parked |
+| MRAM | `0x4000_0200` | 16 MiB window; the first 512 bytes belong to the boot flow |
+| System registers | `0x0200_0000` | `SystemConfig` bit 6 enables the UART |
+| UART0 | `0x0200_4000` | Original Shakti integration, polling, 115200 8N1 |
+| Machine timer | `0x80F4_0200` | `mtime`, with `mtimecmp` at `+0x08` |
+| PLIC | `0xA000_0000` | Machine-mode context of hart 0; sources 1 to 6 |
+
+## Port notes
+
+* **Soft float.** Erbium implements only part of the F extension: divide,
+ square root and conversions between single precision and 64-bit integers
+ raise an emulation exception. The example and the ThreadX library are
+ built for `rv64imc_zicsr_zifencei` with the `lp64` ABI, so the port saves
+ no FP state. `csr.h` rejects a build with F enabled.
+* **No C library.** The `riscv64-unknown-elf` toolchain installed by
+ `scripts/install_riscv.sh` ships `lp64d` libraries only, so the example
+ links with `-nostdlib` and `board.c` provides `memset`. A missing runtime
+ helper shows up as a link error, not as a mix of ABIs.
+* **No WFI.** The example never executes `wfi`, like the Zephyr Erbium
+ port. `TX_USE_WFI_IDLE` stays undefined, so the scheduler spins while
+ idle, and parked harts and fatal-trap halts spin as well.
+* **Trap vector.** Erbium ignores `mtvec` base bits 11:1, so the trap
+ vector must be 4 KiB aligned. `trap_entry` is placed in its own 4 KiB
+ aligned section and used in direct mode.
+* **PLIC.** The silicon hardwires source priorities to 1 and thresholds to
+ 0. The simulator models them as writable registers that reset to 0, so
+ `plic_init()` writes the hardwired values. Drivers register a callback and
+ enable their source with `plic_irq_enable()`.
+* **Clocks.** The UART divisor assumes a 400 MHz input clock and the tick
+ assumes a 2 MHz `mtime` rate, the values used by the Zephyr and NuttX
+ Erbium ports. If the boot firmware programs other rates, define
+ `ERBIUM_UART_CLOCK_HZ` and `ERBIUM_MTIME_FREQ_HZ` when you build.
+* **UART.** The console only polls. `uart_init()` sets the UART enable bit
+ in `SystemConfig` with a read-modify-write, which leaves the watchdog
+ setting as the boot flow left it.
+
+## Building
+
+Install the toolchain with `scripts/install_riscv.sh`, or put another
+`riscv64-unknown-elf` GCC on `PATH`, then run:
+
+```bash
+./build.sh
+```
+
+This configures `CMakeLists.txt` with `erbium_gnu.cmake`, builds
+`libthreadx.a` for the soft-float ABI and links `build/demo_threadx.elf`.
+Set `BUILD_DIR` to build elsewhere.
+
+## Running on the simulator
+
+Build `erbium_emu` from
+[et-platform](https://github.com/aifoundry-org/et-platform). The example was
+tested at revision `836a4ab600e93c3059bb58c898edbc37744cd8d0`:
+
+```bash
+git clone https://github.com/aifoundry-org/et-platform.git
+cd et-platform
+git checkout 836a4ab600e93c3059bb58c898edbc37744cd8d0
+cmake -S erbium-hal -B build-hal -DCMAKE_INSTALL_PREFIX="$PWD/install"
+cmake --build build-hal --target install
+cmake -S sw-sysemu -B build-emu -GNinja -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_CXX_FLAGS=-Wno-error=unused-result \
+ -DCMAKE_PREFIX_PATH="$PWD/install"
+cmake --build build-emu --target erbium_emu
+```
+
+At that revision a release build with a distribution GCC that enables
+`_FORTIFY_SOURCE` stops on an unused `write()` result in the UART model;
+the `CMAKE_CXX_FLAGS` setting above keeps that warning from being an error.
+
+Load the ELF and start hart 0 at the MRAM load address, bypassing the boot
+ROM:
+
+```bash
+erbium_emu -elf_load build/demo_threadx.elf -reset_pc 0x40000200 \
+ -minions 1 -single_thread -max_cycles 100000000 \
+ -uart_rx_file /dev/null -uart_tx_file uart.log
+cat uart.log
+```
+
+The log starts with `[UART0] : Erbium UART initialized`, followed by lines
+from all eight demo threads. The simulator ends with a `max cycles reached`
+error once the cycle budget is spent; judge the run by the UART log, not by
+the exit status. With the default clocks one ThreadX tick is 2,000,000
+simulated cycles, so the 100,000,000-cycle run shows `thread_0` five times.
+
+The simulator verifies register accesses, the trap flow and the console.
+It does not verify baud timing or the timer rate of real silicon.
+
+## Simulator regression tests
+
+With `erbium_emu` on `PATH`, run:
+
+```bash
+ERBIUM_EMU=erbium_emu ./test/run_simulator_tests.sh
+```
+
+The UART test checks that a long polling write does not lose ThreadX ticks.
+The PLIC test changes one source in timer context while a thread changes
+another, and checks that neither update is lost. The runner builds both
+images with CMake and Ninja and reports a failure if either check fails.
+These tests use the simulator's UART drain rate, not physical baud timing.
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `entry.S` | Reset entry: parks other harts, sets `gp`, stack and `.bss` |
+| `tx_initialize_low_level.S` | Trap vector and `_tx_initialize_low_level` |
+| `trap.c` | Dispatches the timer, the PLIC and fatal exceptions |
+| `board.c` | `board_init()` and `memset` |
+| `plic.c`, `plic.h` | PLIC driver for the hart's machine-mode context |
+| `hwtimer.c`, `hwtimer.h` | Periodic tick from `mtime` and `mtimecmp` |
+| `uart.c`, `uart.h` | Polling console |
+| `demo_threadx.c` | The standard ThreadX demo |
+| `link.lds` | MRAM layout |
+| `erbium_gnu.cmake` | Toolchain file: `rv64imc_zicsr_zifencei`, `lp64` |
+| `CMakeLists.txt`, `build.sh` | Build of the library and the demo |
+| `test/` | Simulator tests for UART timekeeping and PLIC updates |
+
+## References
+
+* [Erbium processor](https://github.com/openhwfoundation/core-et-erbium)
+* [UART registers](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/uart.md)
+* [CPU memory map](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/cpu_mm.md)
+* [Platform interrupts](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/interrupts.md)
+* [CPU subsystem](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/cpu_subsystem.md)
+* [et-platform simulator](https://github.com/aifoundry-org/et-platform)
diff --git a/ports/risc-v64/gnu/example_build/erbium/board.c b/ports/risc-v64/gnu/example_build/erbium/board.c
new file mode 100644
index 00000000..b801b55d
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/board.c
@@ -0,0 +1,49 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+#include "plic.h"
+#include "hwtimer.h"
+#include "uart.h"
+#include <stdint.h>
+#include <stddef.h>
+
+/* The example links without a C library; ThreadX needs memset. */
+void *memset(void *des, int c, size_t n)
+{
+ unsigned char *target = des;
+
+ for (size_t i = 0; i < n; i++)
+ target[i] = (unsigned char)c;
+ return des;
+}
+
+int board_init(void)
+{
+ int ret;
+
+ ret = plic_init();
+ if (ret)
+ return ret;
+
+ ret = uart_init();
+ if (ret)
+ return ret;
+
+ ret = hwtimer_init();
+ if (ret)
+ return ret;
+
+ return 0;
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/build.sh b/ports/risc-v64/gnu/example_build/erbium/build.sh
new file mode 100755
index 00000000..effaed0a
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/build.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+##############################################################################
+# 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.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
+##############################################################################
+
+# Build the ThreadX library and the Erbium demo, build/demo_threadx.elf.
+
+set -eu
+
+SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+BUILD_DIR=${BUILD_DIR:-"${SCRIPT_DIR}/build"}
+
+cmake -S "${SCRIPT_DIR}" -B "${BUILD_DIR}" -GNinja \
+ -DCMAKE_TOOLCHAIN_FILE="${SCRIPT_DIR}/erbium_gnu.cmake"
+cmake --build "${BUILD_DIR}" --target demo_threadx
+
+printf 'Built %s\n' "${BUILD_DIR}/demo_threadx.elf"
diff --git a/ports/risc-v64/gnu/example_build/erbium/csr.h b/ports/risc-v64/gnu/example_build/erbium/csr.h
new file mode 100644
index 00000000..e2aa4e18
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/csr.h
@@ -0,0 +1,66 @@
+/***************************************************************************
+ * 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.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
+ **************************************************************************/
+
+/* Machine-mode CSR definitions and helpers for the Erbium example. */
+
+#ifndef RISCV_CSR_H
+#define RISCV_CSR_H
+
+/* The Erbium FPU implements only part of the F extension: single-precision
+ divide, square root and conversions to and from 64-bit integers raise an
+ emulation exception that this example does not handle. Build for the
+ integer ISA with the soft-float lp64 ABI. */
+#ifdef __riscv_flen
+#error "The Erbium example requires a soft-float build: use -march=rv64imc_zicsr_zifencei -mabi=lp64."
+#endif
+
+/* Machine Status Register, mstatus */
+#define MSTATUS_MIE (1L << 3) /* machine-mode interrupt enable */
+#define MSTATUS_MPIE (1L << 7) /* previous MIE */
+#define MSTATUS_MPP_M (3L << 11) /* previous mode: machine */
+
+/* Machine Interrupt Enable, mie */
+#define MIE_MTIE (1L << 7) /* machine timer */
+#define MIE_MEIE (1L << 11) /* machine external */
+
+#ifndef __ASSEMBLER__
+
+#include <stdint.h>
+
+static inline uintptr_t riscv_get_core(void)
+{
+ uintptr_t x;
+ __asm__ volatile("csrr %0, mhartid" : "=r" (x));
+ return x;
+}
+
+/* Clear mstatus.MIE and return whether it was set. */
+static inline int riscv_mintr_save(void)
+{
+ uintptr_t x;
+ __asm__ volatile("csrrci %0, mstatus, %1" : "=r" (x) : "i" (MSTATUS_MIE) : "memory");
+ return (x & MSTATUS_MIE) != 0;
+}
+
+/* Set mstatus.MIE again if riscv_mintr_save found it set. */
+static inline void riscv_mintr_restore(int enabled)
+{
+ if (enabled)
+ __asm__ volatile("csrsi mstatus, %0" : : "i" (MSTATUS_MIE) : "memory");
+}
+
+#endif /* __ASSEMBLER__ */
+
+#endif /* RISCV_CSR_H */
diff --git a/ports/risc-v64/gnu/example_build/erbium/demo_threadx.c b/ports/risc-v64/gnu/example_build/erbium/demo_threadx.c
new file mode 100644
index 00000000..2023df12
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/demo_threadx.c
@@ -0,0 +1,382 @@
+/***************************************************************************/
+/* Copyright (c) 2024 Microsoft Corporation */
+/* 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. */
+/* */
+/* SPDX-License-Identifier: MIT */
+/***************************************************************************/
+
+/* This is a small demo of the high-performance ThreadX kernel. It includes examples of eight
+ threads of different priorities, using a message queue, semaphore, mutex, event flags group,
+ byte pool, and block pool. */
+
+#include "tx_api.h"
+#include "uart.h"
+#define DEMO_STACK_SIZE 1024
+#define DEMO_BYTE_POOL_SIZE 9180
+#define DEMO_BLOCK_POOL_SIZE 100
+#define DEMO_QUEUE_SIZE 100
+
+
+/* Define the ThreadX object control blocks... */
+
+TX_THREAD thread_0;
+TX_THREAD thread_1;
+TX_THREAD thread_2;
+TX_THREAD thread_3;
+TX_THREAD thread_4;
+TX_THREAD thread_5;
+TX_THREAD thread_6;
+TX_THREAD thread_7;
+TX_QUEUE queue_0;
+TX_SEMAPHORE semaphore_0;
+TX_MUTEX mutex_0;
+TX_EVENT_FLAGS_GROUP event_flags_0;
+TX_BYTE_POOL byte_pool_0;
+TX_BLOCK_POOL block_pool_0;
+UCHAR memory_area[DEMO_BYTE_POOL_SIZE];
+
+
+/* Define the counters used in the demo application... */
+
+ULONG thread_0_counter;
+ULONG thread_1_counter;
+ULONG thread_1_messages_sent;
+ULONG thread_2_counter;
+ULONG thread_2_messages_received;
+ULONG thread_3_counter;
+ULONG thread_4_counter;
+ULONG thread_5_counter;
+ULONG thread_6_counter;
+ULONG thread_7_counter;
+
+
+/* Define thread prototypes. */
+
+void thread_0_entry(ULONG thread_input);
+void thread_1_entry(ULONG thread_input);
+void thread_2_entry(ULONG thread_input);
+void thread_3_and_4_entry(ULONG thread_input);
+void thread_5_entry(ULONG thread_input);
+void thread_6_and_7_entry(ULONG thread_input);
+
+
+/* Define main entry point. */
+
+int main()
+{
+
+ /* Enter the ThreadX kernel. */
+ tx_kernel_enter();
+}
+
+
+/* Define what the initial system looks like. */
+
+void tx_application_define(void *first_unused_memory)
+{
+
+CHAR *pointer = TX_NULL;
+
+
+ /* Create a byte memory pool from which to allocate the thread stacks. */
+ tx_byte_pool_create(&byte_pool_0, "byte pool 0", memory_area, DEMO_BYTE_POOL_SIZE);
+
+ /* Put system definition stuff in here, e.g. thread creates and other assorted
+ create information. */
+
+ /* Allocate the stack for thread 0. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create the main thread. */
+ tx_thread_create(&thread_0, "thread 0", thread_0_entry, 0,
+ pointer, DEMO_STACK_SIZE,
+ 1, 1, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+
+ /* Allocate the stack for thread 1. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create threads 1 and 2. These threads pass information through a ThreadX
+ message queue. It is also interesting to note that these threads have a time
+ slice. */
+ tx_thread_create(&thread_1, "thread 1", thread_1_entry, 1,
+ pointer, DEMO_STACK_SIZE,
+ 16, 16, 4, TX_AUTO_START);
+
+ /* Allocate the stack for thread 2. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ tx_thread_create(&thread_2, "thread 2", thread_2_entry, 2,
+ pointer, DEMO_STACK_SIZE,
+ 16, 16, 4, TX_AUTO_START);
+
+ /* Allocate the stack for thread 3. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create threads 3 and 4. These threads compete for a ThreadX counting semaphore.
+ An interesting thing here is that both threads share the same instruction area. */
+ tx_thread_create(&thread_3, "thread 3", thread_3_and_4_entry, 3,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 4. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ tx_thread_create(&thread_4, "thread 4", thread_3_and_4_entry, 4,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 5. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create thread 5. This thread simply pends on an event flag which will be set
+ by thread_0. */
+ tx_thread_create(&thread_5, "thread 5", thread_5_entry, 5,
+ pointer, DEMO_STACK_SIZE,
+ 4, 4, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 6. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ /* Create threads 6 and 7. These threads compete for a ThreadX mutex. */
+ tx_thread_create(&thread_6, "thread 6", thread_6_and_7_entry, 6,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the stack for thread 7. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_STACK_SIZE, TX_NO_WAIT);
+
+ tx_thread_create(&thread_7, "thread 7", thread_6_and_7_entry, 7,
+ pointer, DEMO_STACK_SIZE,
+ 8, 8, TX_NO_TIME_SLICE, TX_AUTO_START);
+
+ /* Allocate the message queue. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_QUEUE_SIZE*sizeof(ULONG), TX_NO_WAIT);
+
+ /* Create the message queue shared by threads 1 and 2. */
+ tx_queue_create(&queue_0, "queue 0", TX_1_ULONG, pointer, DEMO_QUEUE_SIZE*sizeof(ULONG));
+
+ /* Create the semaphore used by threads 3 and 4. */
+ tx_semaphore_create(&semaphore_0, "semaphore 0", 1);
+
+ /* Create the event flags group used by threads 1 and 5. */
+ tx_event_flags_create(&event_flags_0, "event flags 0");
+
+ /* Create the mutex used by thread 6 and 7 without priority inheritance. */
+ tx_mutex_create(&mutex_0, "mutex 0", TX_NO_INHERIT);
+
+ /* Allocate the memory for a small block pool. */
+ tx_byte_allocate(&byte_pool_0, (VOID **) &pointer, DEMO_BLOCK_POOL_SIZE, TX_NO_WAIT);
+
+ /* Create a block memory pool to allocate a message buffer from. */
+ tx_block_pool_create(&block_pool_0, "block pool 0", sizeof(ULONG), pointer, DEMO_BLOCK_POOL_SIZE);
+
+ /* Allocate a block and release the block memory. */
+ tx_block_allocate(&block_pool_0, (VOID **) &pointer, TX_NO_WAIT);
+
+ /* Release the block back to the pool. */
+ tx_block_release(pointer);
+}
+
+
+
+/* Define the test threads. */
+
+void thread_0_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This thread simply sits in while-forever-sleep loop. */
+ while(1)
+ {
+ puts("[Thread] : thread_0_entry is here!");
+ /* Increment the thread counter. */
+ thread_0_counter++;
+
+ /* Sleep for 10 ticks. */
+ tx_thread_sleep(10);
+
+ /* Set event flag 0 to wakeup thread 5. */
+ status = tx_event_flags_set(&event_flags_0, 0x1, TX_OR);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+ }
+}
+
+
+void thread_1_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This thread simply sends messages to a queue shared by thread 2. */
+ while(1)
+ {
+ puts("[Thread] : thread_1_entry is here!");
+ /* Increment the thread counter. */
+ thread_1_counter++;
+
+ /* Send message to queue 0. */
+ status = tx_queue_send(&queue_0, &thread_1_messages_sent, TX_WAIT_FOREVER);
+
+ /* Check completion status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Increment the message sent. */
+ thread_1_messages_sent++;
+ }
+}
+
+
+void thread_2_entry(ULONG thread_input)
+{
+
+ULONG received_message;
+UINT status;
+
+ /* This thread retrieves messages placed on the queue by thread 1. */
+ while(1)
+ {
+ puts("[Thread] : thread_2_entry is here!");
+ /* Increment the thread counter. */
+ thread_2_counter++;
+
+ /* Retrieve a message from the queue. */
+ status = tx_queue_receive(&queue_0, &received_message, TX_WAIT_FOREVER);
+
+ /* Check completion status and make sure the message is what we
+ expected. */
+ if ((status != TX_SUCCESS) || (received_message != thread_2_messages_received))
+ break;
+
+ /* Otherwise, all is okay. Increment the received message count. */
+ thread_2_messages_received++;
+ }
+}
+
+
+void thread_3_and_4_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This function is executed from thread 3 and thread 4. As the loop
+ below shows, these function compete for ownership of semaphore_0. */
+ while(1)
+ {
+ puts("[Thread] : thread_3_and_4_entry is here!");
+
+ /* Increment the thread counter. */
+ if (thread_input == 3)
+ thread_3_counter++;
+ else
+ thread_4_counter++;
+
+ /* Get the semaphore with suspension. */
+ status = tx_semaphore_get(&semaphore_0, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Sleep for 2 ticks to hold the semaphore. */
+ tx_thread_sleep(2);
+
+ /* Release the semaphore. */
+ status = tx_semaphore_put(&semaphore_0);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+ }
+}
+
+
+void thread_5_entry(ULONG thread_input)
+{
+
+UINT status;
+ULONG actual_flags;
+
+
+ /* This thread simply waits for an event in a forever loop. */
+ while(1)
+ {
+ puts("[Thread] : thread_5_entry is here!");
+ /* Increment the thread counter. */
+ thread_5_counter++;
+
+ /* Wait for event flag 0. */
+ status = tx_event_flags_get(&event_flags_0, 0x1, TX_OR_CLEAR,
+ &actual_flags, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if ((status != TX_SUCCESS) || (actual_flags != 0x1))
+ break;
+ }
+}
+
+
+void thread_6_and_7_entry(ULONG thread_input)
+{
+
+UINT status;
+
+
+ /* This function is executed from thread 6 and thread 7. As the loop
+ below shows, these function compete for ownership of mutex_0. */
+ while(1)
+ {
+ puts("[Thread] : thread_6_and_7_entry is here!");
+ /* Increment the thread counter. */
+ if (thread_input == 6)
+ thread_6_counter++;
+ else
+ thread_7_counter++;
+
+ /* Get the mutex with suspension. */
+ status = tx_mutex_get(&mutex_0, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Get the mutex again with suspension. This shows
+ that an owning thread may retrieve the mutex it
+ owns multiple times. */
+ status = tx_mutex_get(&mutex_0, TX_WAIT_FOREVER);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Sleep for 2 ticks to hold the mutex. */
+ tx_thread_sleep(2);
+
+ /* Release the mutex. */
+ status = tx_mutex_put(&mutex_0);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+
+ /* Release the mutex again. This will actually
+ release ownership since it was obtained twice. */
+ status = tx_mutex_put(&mutex_0);
+
+ /* Check status. */
+ if (status != TX_SUCCESS)
+ break;
+ }
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/entry.S b/ports/risc-v64/gnu/example_build/erbium/entry.S
new file mode 100644
index 00000000..201f986e
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/entry.S
@@ -0,0 +1,50 @@
+/***************************************************************************
+ * 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.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
+ **************************************************************************/
+
+/* Reset entry for the Erbium example. Hart 0 sets up the global pointer,
+ the system stack and .bss, then calls main(). Any other hart is parked. */
+
+ .section .text.entry, "ax", @progbits
+ .align 2
+ .global _start
+ .extern main
+ .extern _sysstack_end
+ .extern _bss_start
+ .extern _bss_end
+_start:
+ csrr t0, mhartid
+ bnez t0, _park
+
+.option push
+.option norelax
+ la gp, __global_pointer$ // norelax keeps this load absolute
+.option pop
+
+ la sp, _sysstack_end
+
+ la t0, _bss_start // .bss is 16-byte aligned in link.lds
+ la t1, _bss_end
+_bss_clear:
+ bgeu t0, t1, _bss_done
+ sd zero, 0(t0)
+ addi t0, t0, 8
+ j _bss_clear
+_bss_done:
+
+ call main
+
+ /* The example never executes WFI; a parked hart spins. */
+_park:
+ j _park
diff --git a/ports/risc-v64/gnu/example_build/erbium/erbium_gnu.cmake b/ports/risc-v64/gnu/example_build/erbium/erbium_gnu.cmake
new file mode 100644
index 00000000..0d55dd89
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/erbium_gnu.cmake
@@ -0,0 +1,39 @@
+# /***************************************************************************
+# * 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.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
+# ***************************************************************************/
+
+# CMake toolchain file for the Erbium example.
+#
+# Erbium implements only part of the F extension, so the example and the
+# ThreadX library are built for the integer ISA with the soft-float lp64 ABI.
+#
+# Target ISA : rv64imc_zicsr_zifencei
+# ABI : lp64 (64-bit long/ptr, soft-float)
+# Code model : medany (MRAM at 0x40000000, peripherals above 2 GiB)
+#
+# The riscv64-unknown-elf toolchain installed by scripts/install_riscv.sh is
+# built for lp64d only, so the example links without libc and libgcc
+# (-nostdlib); a missing helper is a link error rather than an ABI mix.
+
+set(CMAKE_SYSTEM_NAME Generic)
+set(CMAKE_SYSTEM_PROCESSOR risc-v64)
+
+set(THREADX_ARCH "risc-v64")
+set(THREADX_TOOLCHAIN "gnu")
+set(ARCH_FLAGS "-g -march=rv64imc_zicsr_zifencei -mabi=lp64 -mcmodel=medany")
+set(CFLAGS "${ARCH_FLAGS}")
+set(ASFLAGS "${ARCH_FLAGS}")
+set(LDFLAGS "${ARCH_FLAGS}")
+
+include(${CMAKE_CURRENT_LIST_DIR}/../../../../../cmake/riscv64-unknown-elf.cmake)
diff --git a/ports/risc-v64/gnu/example_build/erbium/hwtimer.c b/ports/risc-v64/gnu/example_build/erbium/hwtimer.c
new file mode 100644
index 00000000..511bea59
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/hwtimer.c
@@ -0,0 +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.5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+#include "tx_api.h"
+#include "hwtimer.h"
+
+/* Naturally aligned 64-bit MMIO accesses are single loads and stores on
+ RV64; volatile keeps the compiler from caching or reordering them. */
+#define MTIME (*(volatile uint64_t *)ERBIUM_MTIME)
+#define MTIMECMP (*(volatile uint64_t *)ERBIUM_MTIMECMP)
+
+int hwtimer_init(void)
+{
+ MTIMECMP = MTIME + TICKNUM_PER_TIMER;
+ return 0;
+}
+
+int hwtimer_handler(void)
+{
+ /* Advance from the previous compare value, so trap latency does not
+ accumulate as tick drift. */
+ uint64_t next = MTIMECMP + TICKNUM_PER_TIMER;
+ uint64_t now = MTIME;
+
+ if (next <= now)
+ next = now + TICKNUM_PER_TIMER;
+
+ MTIMECMP = next;
+ return 0;
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/hwtimer.h b/ports/risc-v64/gnu/example_build/erbium/hwtimer.h
new file mode 100644
index 00000000..3b9bf4e3
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/hwtimer.h
@@ -0,0 +1,41 @@
+/***************************************************************************
+ * Copyright (c) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+#ifndef RISCV_HWTIMER_H
+#define RISCV_HWTIMER_H
+
+#include <stdint.h>
+
+/* Erbium machine timer. mtime and a single mtimecmp live in the CPU
+ register block; mtime_local_target selects which harts see the timer
+ interrupt and resets to all harts.
+
+ The mtime rate depends on the reference clock and time_config, which the
+ boot firmware programs. The default is the 2 MHz timebase used by the
+ Zephyr and NuttX Erbium ports; define ERBIUM_MTIME_FREQ_HZ when the
+ firmware selects another rate. */
+#define ERBIUM_MTIME 0x80F40200UL
+#define ERBIUM_MTIMECMP 0x80F40208UL
+
+#ifndef ERBIUM_MTIME_FREQ_HZ
+#define ERBIUM_MTIME_FREQ_HZ 2000000UL
+#endif
+
+#define TICKNUM_PER_TIMER (ERBIUM_MTIME_FREQ_HZ / TX_TIMER_TICKS_PER_SECOND)
+
+int hwtimer_init(void);
+int hwtimer_handler(void);
+
+#endif /* RISCV_HWTIMER_H */
diff --git a/ports/risc-v64/gnu/example_build/erbium/link.lds b/ports/risc-v64/gnu/example_build/erbium/link.lds
new file mode 100644
index 00000000..25ba9219
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/link.lds
@@ -0,0 +1,92 @@
+/***************************************************************************
+ * 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.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
+ **************************************************************************/
+
+/* Erbium memory layout.
+ *
+ * Address Size Usage
+ * ───────────── ──────── ──────────────────────────────────────
+ * 0x0200_0000 4 KiB System registers (SystemConfig at +0x08)
+ * 0x0200_4000 4 KiB UART0
+ * 0x4000_0000 512 B MRAM, reserved for the boot flow
+ * 0x4000_0200 ~16 MiB MRAM: code, data, stacks and free memory
+ * 0x80F4_0200 16 B mtime, mtimecmp
+ * 0xA000_0000 64 MiB PLIC
+ *
+ * MRAM is directly readable, writable and executable, so .data runs in
+ * place and needs no copy from a load address.
+ */
+
+OUTPUT_ARCH( "riscv" )
+ENTRY( _start )
+
+MEMORY
+{
+ MRAM (rwx) : ORIGIN = 0x40000200, LENGTH = 0x00FFFE00
+}
+
+PHDRS
+{
+ text PT_LOAD FLAGS(5); /* PF_R | PF_X */
+ data PT_LOAD FLAGS(6); /* PF_R | PF_W */
+}
+
+SECTIONS
+{
+ /* _start must stay at the load address, so the 4 KiB aligned trap
+ vector goes in the next output section rather than this one. */
+ .text.entry : {
+ KEEP(*(.text.entry))
+ } > MRAM :text
+
+ .text : {
+ KEEP(*(.text.trap))
+ *(.text .text.*)
+ } > MRAM :text
+
+ .rodata : {
+ . = ALIGN(16);
+ *(.srodata .srodata.*)
+ . = ALIGN(16);
+ *(.rodata .rodata.*)
+ } > MRAM :text
+
+ .data : {
+ . = ALIGN(16);
+ PROVIDE( __global_pointer$ = . + 0x800 );
+ *(.sdata .sdata.*)
+ . = ALIGN(16);
+ *(.data .data.*)
+ } > MRAM :data
+
+ .bss (NOLOAD) : {
+ . = ALIGN(16);
+ _bss_start = .;
+ *(.sbss .sbss.*)
+ . = ALIGN(16);
+ *(.bss .bss.*)
+ *(COMMON)
+ . = ALIGN(16);
+ _bss_end = .;
+ } > MRAM :data
+
+ .stack (NOLOAD) : {
+ . = ALIGN(16);
+ _sysstack_start = .;
+ . += 0x1000;
+ _sysstack_end = .;
+ } > MRAM :data
+
+ PROVIDE(_end = .);
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/plic.c b/ports/risc-v64/gnu/example_build/erbium/plic.c
new file mode 100644
index 00000000..9ca25f1b
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/plic.c
@@ -0,0 +1,118 @@
+/***************************************************************************
+ * 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.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
+ **************************************************************************/
+
+/* PLIC driver for Erbium, machine-mode context of the current hart. */
+
+#include "plic.h"
+#include <stddef.h>
+
+static irq_callback callbacks[MAX_CALLBACK_NUM];
+
+/* Source 0 is reserved and the six sources fit in the first enable word. */
+static int plic_source_valid(int irqno)
+{
+ return (irqno > 0) && (irqno < MAX_CALLBACK_NUM);
+}
+
+void plic_irq_enable(int irqno)
+{
+ volatile uint32_t *reg = (volatile uint32_t *)PLIC_MENABLE(riscv_get_core());
+ int intr_enable;
+
+ if (!plic_source_valid(irqno))
+ return;
+
+ intr_enable = riscv_mintr_save();
+ *reg = *reg | (1u << (unsigned int)irqno);
+ riscv_mintr_restore(intr_enable);
+}
+
+void plic_irq_disable(int irqno)
+{
+ volatile uint32_t *reg = (volatile uint32_t *)PLIC_MENABLE(riscv_get_core());
+ int intr_enable;
+
+ if (!plic_source_valid(irqno))
+ return;
+
+ intr_enable = riscv_mintr_save();
+ *reg = *reg & ~(1u << (unsigned int)irqno);
+ riscv_mintr_restore(intr_enable);
+}
+
+int plic_register_callback(int irqno, irq_callback callback)
+{
+ if (!plic_source_valid(irqno))
+ return -1;
+ callbacks[irqno] = callback;
+ return 0;
+}
+
+int plic_unregister_callback(int irqno)
+{
+ return plic_register_callback(irqno, NULL);
+}
+
+int plic_init(void)
+{
+ uintptr_t hart = riscv_get_core();
+
+ for (int i = 0; i < MAX_CALLBACK_NUM; i++)
+ callbacks[i] = NULL;
+
+ /* Do not depend on the reset state or on a prior boot stage: start
+ with every source disabled for this context. Drivers enable their
+ own sources. */
+ *(volatile uint32_t *)PLIC_MENABLE(hart) = 0;
+
+ /* The values the silicon hardwires; see plic.h. */
+ *(volatile uint32_t *)PLIC_MTHRESHOLD(hart) = 0;
+ for (int i = 1; i <= PLIC_NUM_SOURCES; i++)
+ *(volatile uint32_t *)PLIC_PRIORITY(i) = 1;
+
+ return 0;
+}
+
+int plic_claim(void)
+{
+ return (int)(*(volatile uint32_t *)PLIC_MCLAIM(riscv_get_core()));
+}
+
+void plic_complete(int irqno)
+{
+ *(volatile uint32_t *)PLIC_MCOMPLETE(riscv_get_core()) = (uint32_t)irqno;
+}
+
+int plic_irq_intr(void)
+{
+ int ret = -1;
+ int irqno = plic_claim();
+
+ /* Nothing pending: another context claimed it first. */
+ if (irqno == 0)
+ return 0;
+
+ if (!plic_source_valid(irqno))
+ {
+ plic_complete(irqno);
+ return -1;
+ }
+
+ if (callbacks[irqno] != NULL)
+ ret = (callbacks[irqno])(irqno);
+
+ plic_complete(irqno);
+ return ret;
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/plic.h b/ports/risc-v64/gnu/example_build/erbium/plic.h
new file mode 100644
index 00000000..ffc3a539
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/plic.h
@@ -0,0 +1,54 @@
+/***************************************************************************
+ * 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.5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+#ifndef RISCV_PLIC_H
+#define RISCV_PLIC_H
+
+#include "csr.h"
+#include <stdint.h>
+
+/* Erbium PLIC, RISC-V PLIC 1.0. Each hart has two contexts, machine mode
+ first, so hart h uses context 2h.
+
+ The silicon hardwires source priorities to 1 and context thresholds to
+ 0, so an enabled, pending source always interrupts. The et-platform
+ simulator implements them as writable registers that reset to 0, which
+ masks every source, so plic_init() programs the silicon values. There is
+ no priority control beyond that.
+
+ Sources: 1 MRAM, 2 QSPI, 3 UART, 4 system register, 5 xSPI, 6 GPIO. */
+#define PLIC 0xA0000000UL
+#define PLIC_PRIORITY(irqno) (PLIC + (unsigned long)(irqno) * 4UL)
+#define PLIC_MENABLE(hart) (PLIC + 0x2000UL + (hart) * 0x100UL)
+#define PLIC_MTHRESHOLD(hart) (PLIC + 0x200000UL + (hart) * 0x2000UL)
+#define PLIC_MCLAIM(hart) (PLIC + 0x200004UL + (hart) * 0x2000UL)
+#define PLIC_MCOMPLETE(hart) PLIC_MCLAIM(hart)
+
+#define PLIC_NUM_SOURCES 6
+#define MAX_CALLBACK_NUM (PLIC_NUM_SOURCES + 1)
+
+typedef int (*irq_callback)(int irqno);
+
+void plic_irq_enable(int irqno);
+void plic_irq_disable(int irqno);
+int plic_register_callback(int irqno, irq_callback callback);
+int plic_unregister_callback(int irqno);
+int plic_init(void);
+int plic_claim(void);
+void plic_complete(int irqno);
+
+int plic_irq_intr(void);
+
+#endif /* RISCV_PLIC_H */
diff --git a/ports/risc-v64/gnu/example_build/erbium/test/run_simulator_tests.sh b/ports/risc-v64/gnu/example_build/erbium/test/run_simulator_tests.sh
new file mode 100755
index 00000000..7d54a5f8
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/test/run_simulator_tests.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+##############################################################################
+# 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 Codex (GPT-6-Sol).
+# 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
+##############################################################################
+
+set -eu
+
+ERBIUM_EMU=${ERBIUM_EMU:?Set ERBIUM_EMU to the erbium_emu executable}
+TEST_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+BUILD_DIR=${BUILD_DIR:-"${TEST_DIR}/build-tests"}
+RUN_DIR=$(mktemp -d)
+trap 'rm -rf "$RUN_DIR"' EXIT HUP INT TERM
+failed=0
+
+cmake -S "$TEST_DIR" -B "$BUILD_DIR" -GNinja \
+ -DCMAKE_TOOLCHAIN_FILE="$TEST_DIR/erbium_gnu.cmake"
+cmake --build "$BUILD_DIR" --target test_uart_ticks test_plic_race
+
+for name in uart_ticks plic_race; do
+ case "$name" in
+ uart_ticks)
+ cycles=20000000
+ expected=TEST_UART_NO_LOST_TICKS
+ ;;
+ plic_race)
+ cycles=50000000
+ expected=TEST_PLIC_NO_LOST_UPDATE
+ ;;
+ esac
+ "$ERBIUM_EMU" -elf_load "$BUILD_DIR/test_$name" \
+ -reset_pc 0x40000200 -minions 1 -single_thread \
+ -max_cycles "$cycles" -uart_rx_file /dev/null \
+ -uart_tx_file "$RUN_DIR/$name.uart" > "$RUN_DIR/$name.run" 2>&1 || true
+ if grep -q -F "$expected" "$RUN_DIR/$name.uart" &&
+ grep -q -F 'max cycles reached' "$RUN_DIR/$name.run"; then
+ printf '%s: PASS\n' "$name"
+ else
+ printf '%s: FAIL: ' "$name"
+ grep -Eo 'TEST_[A-Z_]+' "$RUN_DIR/$name.uart" | tail -1 || true
+ failed=1
+ fi
+done
+
+exit "$failed"
diff --git a/ports/risc-v64/gnu/example_build/erbium/test/test_plic_race.c b/ports/risc-v64/gnu/example_build/erbium/test/test_plic_race.c
new file mode 100644
index 00000000..d6f2a3b5
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/test/test_plic_race.c
@@ -0,0 +1,90 @@
+/***************************************************************************
+ * 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 Codex (GPT-6-Sol).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ ***************************************************************************/
+
+#include "tx_api.h"
+#include "csr.h"
+#include "hwtimer.h"
+#include "plic.h"
+#include "uart.h"
+
+#define TEST_TIMER_INTERVAL 8u
+#define TEST_REQUIRED_INTERRUPTS 10000u
+
+static TX_THREAD test_thread;
+static UCHAR test_stack[2048];
+volatile unsigned int test_irq_count;
+volatile unsigned int test_expected_bit;
+
+/* Run the kernel with the board's normal interrupt setup. */
+int main(void)
+{
+ tx_kernel_enter();
+ return 0;
+}
+
+/* Compare the PLIC word and the ISR's expected bit atomically. */
+static int test_bit_matches(void)
+{
+ int enabled = riscv_mintr_save();
+ /* MISRA C:2012 Rule 11.4 deviation: the PLIC has a fixed MMIO address. */
+ uint32_t actual = *(volatile uint32_t *)PLIC_MENABLE(0u) & (1u << 2);
+ unsigned int expected = test_expected_bit;
+
+ riscv_mintr_restore(enabled);
+ return actual == (expected != 0u ? (1u << 2) : 0u);
+}
+
+/* Change source 1 while the timer ISR changes source 2. */
+static void test_entry(ULONG input)
+{
+ (void)input;
+ /* MISRA C:2012 Rule 11.4 deviation: the timer has a fixed MMIO address. */
+ *(volatile uint64_t *)ERBIUM_MTIMECMP =
+ *(volatile uint64_t *)ERBIUM_MTIME + TEST_TIMER_INTERVAL;
+
+ while (test_irq_count < TEST_REQUIRED_INTERRUPTS)
+ {
+ plic_irq_enable(1);
+ if (!test_bit_matches())
+ {
+ uart_puts("TEST_PLIC_LOST_UPDATE");
+ for (;;)
+ {
+ }
+ }
+ plic_irq_disable(1);
+ if (!test_bit_matches())
+ {
+ uart_puts("TEST_PLIC_LOST_UPDATE");
+ for (;;)
+ {
+ }
+ }
+ }
+
+ uart_puts("TEST_PLIC_NO_LOST_UPDATE");
+ for (;;)
+ {
+ }
+}
+
+/* Start one test thread. */
+void tx_application_define(void *first_unused_memory)
+{
+ (void)first_unused_memory;
+ (void)tx_thread_create(&test_thread, "plic test", test_entry, 0u,
+ test_stack, sizeof(test_stack), 1u, 1u,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/test/test_plic_trap.c b/ports/risc-v64/gnu/example_build/erbium/test/test_plic_trap.c
new file mode 100644
index 00000000..8dabb42b
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/test/test_plic_trap.c
@@ -0,0 +1,55 @@
+/***************************************************************************
+ * 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 Codex (GPT-6-Sol).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ ***************************************************************************/
+
+#include "tx_api.h"
+#include "hwtimer.h"
+#include "plic.h"
+#include "uart.h"
+#include <stdint.h>
+
+#define TEST_TIMER_INTERVAL 8u
+#define TEST_MCAUSE_TIMER (((uintptr_t)1u << 63) | 7u)
+
+extern volatile unsigned int test_irq_count;
+extern volatile unsigned int test_expected_bit;
+
+/* Toggle source 2 in timer context to preempt source 1 updates. */
+void trap_handler(uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
+{
+ (void)mepc;
+ (void)mtval;
+ if (mcause != TEST_MCAUSE_TIMER)
+ {
+ uart_puts("TEST_PLIC_UNEXPECTED_TRAP");
+ for (;;)
+ {
+ }
+ }
+
+ /* MISRA C:2012 Rule 11.4 deviation: the timer has a fixed MMIO address. */
+ *(volatile uint64_t *)ERBIUM_MTIMECMP =
+ *(volatile uint64_t *)ERBIUM_MTIME + TEST_TIMER_INTERVAL;
+ if (test_expected_bit == 0u)
+ {
+ plic_irq_enable(2);
+ test_expected_bit = 1u;
+ }
+ else
+ {
+ plic_irq_disable(2);
+ test_expected_bit = 0u;
+ }
+ test_irq_count++;
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/test/test_uart_ticks.c b/ports/risc-v64/gnu/example_build/erbium/test/test_uart_ticks.c
new file mode 100644
index 00000000..188af2e7
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/test/test_uart_ticks.c
@@ -0,0 +1,81 @@
+/***************************************************************************
+ * 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 Codex (GPT-6-Sol).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ ***************************************************************************/
+
+#include "tx_api.h"
+#include "uart.h"
+#include "hwtimer.h"
+
+#define TEST_TEXT_SIZE 160000u
+
+static TX_THREAD test_thread;
+static UCHAR test_stack[2048];
+static char test_text[TEST_TEXT_SIZE + 1u];
+
+/* Run the kernel with the board's normal interrupt setup. */
+int main(void)
+{
+ tx_kernel_enter();
+ return 0;
+}
+
+/* Print long enough to span several hardware timer intervals. */
+static void test_entry(ULONG input)
+{
+ uint64_t start;
+ uint64_t elapsed;
+ ULONG ticks_before;
+ ULONG ticks_after;
+ size_t i;
+
+ (void)input;
+ for (i = 0u; i < TEST_TEXT_SIZE; i++)
+ {
+ test_text[i] = 'X';
+ }
+ test_text[TEST_TEXT_SIZE] = '\0';
+
+ /* MISRA C:2012 Rule 11.4 deviation: the timer has a fixed MMIO address. */
+ start = *(volatile uint64_t *)ERBIUM_MTIME;
+ ticks_before = tx_time_get();
+ (void)uart_puts(test_text);
+ elapsed = *(volatile uint64_t *)ERBIUM_MTIME - start;
+ ticks_after = tx_time_get();
+
+ if (elapsed <= (2u * TICKNUM_PER_TIMER))
+ {
+ uart_puts("TEST_UART_TOO_SHORT");
+ }
+ else if ((ticks_after - ticks_before) < 2u)
+ {
+ uart_puts("TEST_UART_LOST_TICKS");
+ }
+ else
+ {
+ uart_puts("TEST_UART_NO_LOST_TICKS");
+ }
+
+ for (;;)
+ {
+ }
+}
+
+/* Start one test thread. */
+void tx_application_define(void *first_unused_memory)
+{
+ (void)first_unused_memory;
+ (void)tx_thread_create(&test_thread, "uart test", test_entry, 0u,
+ test_stack, sizeof(test_stack), 1u, 1u,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/trap.c b/ports/risc-v64/gnu/example_build/erbium/trap.c
new file mode 100644
index 00000000..d839e6ea
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/trap.c
@@ -0,0 +1,78 @@
+/***************************************************************************
+ * 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.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
+ **************************************************************************/
+
+/* Machine-mode trap dispatcher for the Erbium example. */
+
+#include "csr.h"
+#include "uart.h"
+#include "hwtimer.h"
+#include "plic.h"
+#include <stdint.h>
+#include <tx_api.h>
+
+#define MCAUSE_INT_BIT ((uintptr_t)1 << (__riscv_xlen - 1))
+#define MCAUSE_M_TIMER (MCAUSE_INT_BIT | 7u)
+#define MCAUSE_M_EXTERNAL (MCAUSE_INT_BIT | 11u)
+
+extern void _tx_timer_interrupt(void);
+
+static void print_hex(uintptr_t val)
+{
+ const char digits[] = "0123456789ABCDEF";
+
+ uart_putc('0');
+ uart_putc('x');
+ for (int i = (int)(sizeof(uintptr_t) * 2) - 1; i >= 0; i--)
+ uart_putc(digits[(val >> (i * 4)) & 0xFu]);
+ uart_putc('\r');
+ uart_putc('\n');
+}
+
+static void trap_halt(const char *reason, uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
+{
+ uart_puts(reason);
+ uart_puts("mcause:");
+ print_hex(mcause);
+ uart_puts("mepc:");
+ print_hex(mepc);
+ uart_puts("mtval:");
+ print_hex(mtval);
+
+ /* Spin rather than WFI, as everywhere else in this example. */
+ while (1)
+ ;
+}
+
+void trap_handler(uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
+{
+ if (mcause == MCAUSE_M_TIMER)
+ {
+ hwtimer_handler();
+ _tx_timer_interrupt();
+ }
+ else if (mcause == MCAUSE_M_EXTERNAL)
+ {
+ if (plic_irq_intr() != 0)
+ trap_halt("[INTERRUPT]: external interrupt not handled, halting", mcause, mepc, mtval);
+ }
+ else if ((mcause & MCAUSE_INT_BIT) != 0u)
+ {
+ trap_halt("[INTERRUPT]: unexpected interrupt, halting", mcause, mepc, mtval);
+ }
+ else
+ {
+ trap_halt("[EXCEPTION]: unhandled exception, halting", mcause, mepc, mtval);
+ }
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/tx_initialize_low_level.S b/ports/risc-v64/gnu/example_build/erbium/tx_initialize_low_level.S
new file mode 100644
index 00000000..fe0653ae
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/tx_initialize_low_level.S
@@ -0,0 +1,142 @@
+/***************************************************************************
+ * 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.5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+#include "csr.h"
+#include "tx_port.h"
+
+/* Erbium ignores mtvec base bits 11:1, so the trap vector must be 4 KiB
+ aligned. */
+ .section .text.trap, "ax", @progbits
+ .balign 4096
+/**************************************************************************/
+/* */
+/* FUNCTION RELEASE */
+/* */
+/* trap_entry RISC-V64/GNU */
+/* */
+/* DESCRIPTION */
+/* */
+/* This function is the machine-mode trap vector (direct mode). It */
+/* saves the interrupted context, calls trap_handler with mcause, */
+/* mepc and mtval, and leaves through the ThreadX context restore. */
+/* */
+/* INPUT */
+/* */
+/* None */
+/* */
+/* OUTPUT */
+/* */
+/* None */
+/* */
+/* CALLS */
+/* */
+/* _tx_thread_context_save Save interrupted context */
+/* trap_handler Dispatch the trap */
+/* _tx_thread_context_restore Restore or schedule a thread */
+/* */
+/* CALLED BY */
+/* */
+/* Hardware trap */
+/* */
+/**************************************************************************/
+ .global trap_entry
+ .extern trap_handler
+ .extern _tx_thread_context_save
+ .extern _tx_thread_context_restore
+trap_entry:
+ addi sp, sp, -TX_RISCV_TRAP_FRAME_SIZE // Interrupt frame from the port contract
+ sd x1, 28*8(sp) // Save RA before the call below overwrites it
+
+ call _tx_thread_context_save
+
+ csrr a0, mcause
+ csrr a1, mepc
+ csrr a2, mtval
+ addi sp, sp, -16 // 16 bytes keep sp 16-byte aligned at the call
+ sd ra, 0(sp)
+ call trap_handler
+ ld ra, 0(sp)
+ addi sp, sp, 16
+
+ call _tx_thread_context_restore
+ /* _tx_thread_context_restore does not return. */
+_trap_entry_hang:
+ j _trap_entry_hang
+
+
+ .section .text
+/**************************************************************************/
+/* */
+/* FUNCTION RELEASE */
+/* */
+/* _tx_initialize_low_level RISC-V64/GNU */
+/* */
+/* DESCRIPTION */
+/* */
+/* This function is responsible for any low-level processor */
+/* initialization, including setting up interrupt vectors, setting */
+/* up a periodic timer interrupt source, saving the system stack */
+/* pointer for use in ISR processing later, and finding the first */
+/* available RAM memory address for tx_application_define. */
+/* */
+/* INPUT */
+/* */
+/* None */
+/* */
+/* OUTPUT */
+/* */
+/* None */
+/* */
+/* CALLS */
+/* */
+/* board_init Initialize the peripherals */
+/* */
+/* CALLED BY */
+/* */
+/* _tx_initialize_kernel_enter ThreadX entry function */
+/* */
+/**************************************************************************/
+/* VOID _tx_initialize_low_level(VOID)
+{ */
+ .global _tx_initialize_low_level // Strong definition: must beat the port's weak default
+ .extern _end
+ .extern board_init
+ .extern _tx_thread_system_stack_ptr
+ .extern _tx_initialize_unused_memory
+_tx_initialize_low_level:
+ la t0, _tx_thread_system_stack_ptr
+ sd sp, 0(t0) // Save system stack pointer
+
+ la t0, _end // Pickup first free address
+ la t1, _tx_initialize_unused_memory
+ sd t0, 0(t1) // Save unused memory address
+
+ li t0, MSTATUS_MIE
+ csrrc zero, mstatus, t0 // Keep interrupts off until the scheduler runs
+ li t0, (MSTATUS_MPP_M | MSTATUS_MPIE)
+ csrrs zero, mstatus, t0 // mret stays in M-mode with interrupts enabled
+ li t0, (MIE_MTIE | MIE_MEIE)
+ csrrs zero, mie, t0 // Enable timer and external interrupts
+
+ addi sp, sp, -16 // 16 bytes keep sp 16-byte aligned at the call
+ sd ra, 0(sp)
+ call board_init
+ ld ra, 0(sp)
+ addi sp, sp, 16
+
+ la t0, trap_entry
+ csrw mtvec, t0 // Direct mode: trap_entry is 4 KiB aligned
+ ret
+/* } */
diff --git a/ports/risc-v64/gnu/example_build/erbium/uart.c b/ports/risc-v64/gnu/example_build/erbium/uart.c
new file mode 100644
index 00000000..1b8f2dfc
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/uart.c
@@ -0,0 +1,109 @@
+/***************************************************************************
+ * 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.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
+ **************************************************************************/
+
+/* Polling console driver for the Erbium UART (original Shakti integration).
+ *
+ * Offset Name Access Description
+ * ────── ──────────── ────── ─────────────────────────────────────
+ * 0x00 BaudReg R/W Divisor: clock / (16 * baud)
+ * 0x08 TxReg W Transmit data
+ * 0x10 RxReg R Receive data
+ * 0x18 StatusReg R FIFO and error status
+ * 0x20 DelayReg R/W Transmit delay
+ * 0x28 ControlReg R/W Character size, parity, stop bits
+ * 0x30 InterruptEn R/W Interrupt enables
+ * 0x40 Rx_Threshold R/W Receive FIFO threshold
+ *
+ * All registers are accessed as 32-bit words. The transmit and receive
+ * FIFOs hold 16 entries each.
+ */
+
+#include "uart.h"
+#include "csr.h"
+#include <stdint.h>
+
+#define UART_DIVISOR (ERBIUM_UART_CLOCK_HZ / (16UL * ERBIUM_UART_BAUD))
+
+#if (UART_DIVISOR == 0) || (UART_DIVISOR > 0xFFFF)
+#error "The Erbium UART baud divisor must be in the range 1..65535."
+#endif
+
+/* SystemConfig in the system register block gates the UART. */
+#define SYSREG_SYSTEM_CONFIG 0x02000008UL
+#define SYSTEM_CONFIG_UART_EN (1u << 6)
+
+#define UART_BAUD 0x00
+#define UART_TX 0x08
+#define UART_STATUS 0x18
+#define UART_DELAY 0x20
+#define UART_CONTROL 0x28
+#define UART_IEN 0x30
+#define UART_RX_THRESHOLD 0x40
+
+#define STATUS_TX_FULL (1u << 1)
+
+#define CONTROL_CHARSIZE_SHIFT 5 /* bits 10:5; stop bits and parity 0 */
+
+#define REG32(addr) (*(volatile uint32_t *)(addr))
+#define UART_REG(off) REG32(UART0 + (off))
+
+int uart_init(void)
+{
+ /* Ungate the UART. Read-modify-write keeps the watchdog and the
+ other peripheral enables as the boot flow left them. */
+ REG32(SYSREG_SYSTEM_CONFIG) = REG32(SYSREG_SYSTEM_CONFIG) | SYSTEM_CONFIG_UART_EN;
+
+ /* Polling only: no UART interrupts. */
+ UART_REG(UART_IEN) = 0;
+
+ /* 8 data bits, no parity, one stop bit. */
+ UART_REG(UART_BAUD) = (uint32_t)UART_DIVISOR;
+ UART_REG(UART_CONTROL) = 8u << CONTROL_CHARSIZE_SHIFT;
+ UART_REG(UART_DELAY) = 0;
+ UART_REG(UART_RX_THRESHOLD) = 0;
+
+ puts("[UART0] : Erbium UART initialized");
+ return 0;
+}
+
+int uart_putc(int ch)
+{
+ int intr_enable;
+
+ for (;;)
+ {
+ while ((UART_REG(UART_STATUS) & STATUS_TX_FULL) != 0u)
+ ;
+
+ intr_enable = riscv_mintr_save();
+ if ((UART_REG(UART_STATUS) & STATUS_TX_FULL) == 0u)
+ {
+ UART_REG(UART_TX) = (uint32_t)(ch & 0xFF);
+ riscv_mintr_restore(intr_enable);
+ return 1;
+ }
+ riscv_mintr_restore(intr_enable);
+ }
+}
+
+int uart_puts(const char *str)
+{
+ int i;
+ for (i = 0; str[i] != 0; i++)
+ uart_putc(str[i]);
+ uart_putc('\r');
+ uart_putc('\n');
+ return i;
+}
diff --git a/ports/risc-v64/gnu/example_build/erbium/uart.h b/ports/risc-v64/gnu/example_build/erbium/uart.h
new file mode 100644
index 00000000..74a6c1d0
--- /dev/null
+++ b/ports/risc-v64/gnu/example_build/erbium/uart.h
@@ -0,0 +1,40 @@
+/***************************************************************************
+ * 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.5).
+ * The AI-generated portions may be considered public domain (CC0-1.0)
+ * and not subject to the project's licence. The human contributor has
+ * reviewed and verified that the code is correct.
+ *
+ * SPDX-License-Identifier: MIT and CC0-1.0
+ **************************************************************************/
+
+#ifndef RISCV_UART_H
+#define RISCV_UART_H
+
+/* Erbium UART0: the original Shakti UART integration, with 32-bit registers
+ at eight-byte intervals. It is PLIC source 3. */
+#define UART0 0x02004000UL
+#define UART0_IRQ 3
+
+/* The UART input clock and the console rate. The defaults match the
+ public et-platform simulator; define these on the command line when the
+ boot firmware programs a different peripheral clock. */
+#ifndef ERBIUM_UART_CLOCK_HZ
+#define ERBIUM_UART_CLOCK_HZ 400000000UL
+#endif
+#ifndef ERBIUM_UART_BAUD
+#define ERBIUM_UART_BAUD 115200UL
+#endif
+
+#define puts uart_puts
+
+int uart_init(void);
+int uart_putc(int ch);
+int uart_puts(const char *str);
+
+#endif /* RISCV_UART_H */