summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrédéric Desbiens <[email protected]>2026-05-25 11:28:55 -0400
committerGitHub <[email protected]>2026-05-25 11:28:55 -0400
commit5513b1724adee994491b20a803d7c76a7cee9943 (patch)
tree497480a26614bfb7030b89702ea45743c2f438ec
parent9ccdd5d82248736274a4cc5cad5e13d2ffc50de7 (diff)
Added port for the OpenHW CORE-V MCU platform (#535)
Adds a complete ThreadX port for the OpenHW CORE-V MCU SoC, targeting the Digilent Nexys A7 FPGA board with an Ashling Opella-LD debug probe. New files --------- cmake/riscv64-gcc-rv32imc.cmake CMake toolchain file for riscv64-unknown-elf-gcc targeting rv32imc_zicsr/ilp32. ports/risc-v32/gnu/example_build/core_v_mcu/ -- Full BSP + demo application: Assembly crt0.S C runtime startup (BSS clear, GP/SP init, call main) vectors.S 32-entry vectored interrupt table at 0x1c000800 tx_initialize_low_level.S mtvec setup (vectored mode), stack/free-mem pointers BSP drivers (bsp/) system_core_v_mcu.c Top-level init and ISR dispatcher (isr_table[32]) irq.c PULP APB interrupt controller (enable/disable/mask) timer_irq.c PULP FC Timer -- 100 Hz tick from 10 MHz SOC clock fll.c Frequency Locked Loop -- 5 MHz to 50 MHz (FPGA) uart_driver.c UDMA UART channel 0 -- polled TX, non-blocking RX (8-bit transfer width) gpio.c PULP apb_gpiov2 driver: set/clear/toggle/direction by pin index; pad-mux via per-pad indexed registers at APB_SOC_CTRL + 0x400 + pad*4; full pinmux API (gpio_setpinmux, gpio_getpinmux, gpio_pin_set_dir, gpio_pin_read_status) i2c_master.c Polled UDMA I2C master adt7420.c ADT7420 temperature sensor driver string.c Freestanding memset/memcpy shim (no newlib) Headers (include/) Peripheral register maps, MMIO inlines, BSP API declarations, tx_user.h Application demo_threadx.c Two threads: LED[0] blink at 1 Hz (IO pad 11, GPIO pin 4, MUX=2) + UART heartbeat with startup banner ("Eclipse ThreadX for OpenHW CORE-V MCU vX.Y.Z.BBBBB") link.ld Linker script: .vectors@0x1c000800, .text@0x1c000880 CMakeLists.txt Build definition (references THREADX_ROOT) build.sh One-shot CMake+Ninja build script Tooling install_deps.sh Automates toolchain/OpenOCD dependency setup deploy.sh One-step GDB flashing via Ashling Opella-LD; gdb-multiarch fallback when riscv64-unknown-elf-gdb is absent; supports --wsl flag required by usbipd-win v5.x openocd-nexys-Ashling-Opella-LD.cfg OpenOCD config for Opella-LD over JTAG Tests (tests/) test_irq.c / test_timer.c Host-compiled unit tests (2/2 pass) mock/mmio_mock.* Software MMIO register map for host testing Documentation README.md Hardware overview, build, flash/debug, BSP API reference Architecture notes ------------------ - CV32E40P uses the PULP/PULPissimo interrupt controller (not CLINT); IRQ lines are masked via APB registers, not the mie CSR. - mtvec must be 256-byte aligned; vectors placed at 0x1c000800 (vectored mode). - Timer IRQ = line 10; dispatch via isr_table[mcause & 0x1f]. - Build: -march=rv32imc_zicsr -mabi=ilp32, -ffreestanding, -nodefaultlibs. - Verified: ELF 11 KB text, sections at correct addresses, unit tests pass. Third-party attributions ------------------------ BSP files derived from core-v-freertos (Apache-2.0): (c) 2019-2020 ETH Zurich and University of Bologna (c) 2020 GreenWaves Technologies (c) 2011-2014 Wind River Systems, Inc. (c) 2017 SiFive Inc. (crt0.S, BSD-2-Clause portions) All original copyright notices retained; see individual file headers. SPDX: Apache-2.0 AND MIT (crt0.S: (Apache-2.0 OR BSD-2-Clause) AND MIT). Co-authored-by: Copilot <[email protected]>
-rw-r--r--cmake/riscv64-gcc-rv32imc.cmake64
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/CMakeLists.txt65
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/README.md501
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/fll.c209
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/gpio.c128
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/i2c_master.c153
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/irq.c50
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/string.c64
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/system_core_v_mcu.c132
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/temp_sensor.c46
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/timer_irq.c58
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/bsp/uart_driver.c169
-rwxr-xr-xports/risc-v32/gnu/example_build/core_v_mcu/build.sh9
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/crt0.S48
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/demo_threadx.c134
-rwxr-xr-xports/risc-v32/gnu/example_build/core_v_mcu/deploy.sh142
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/gdb_init6
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/bits.h59
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/csr.h75
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/fll.h93
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/gpio.h68
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/i2c_master.h38
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/io.h61
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/irq.h64
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/memory_map.h39
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/properties.h43
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/pulp_mem_map.h103
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/string.h25
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/system_core_v_mcu.h29
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/temp_sensor.h29
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/timer.h120
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/timer_irq.h26
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/tx_user.h22
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/include/uart_driver.h27
-rwxr-xr-xports/risc-v32/gnu/example_build/core_v_mcu/install_deps.sh53
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/link.ld65
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/openocd-nexys-Ashling-Opella-LD.cfg41
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/tests/CMakeLists.txt28
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.c18
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.h45
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_irq.c66
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_timer.c53
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/tx_initialize_low_level.S63
-rw-r--r--ports/risc-v32/gnu/example_build/core_v_mcu/vectors.S25
-rw-r--r--ports/risc-v32/gnu/src/tx_thread_context_restore.S4
45 files changed, 3358 insertions, 2 deletions
diff --git a/cmake/riscv64-gcc-rv32imc.cmake b/cmake/riscv64-gcc-rv32imc.cmake
new file mode 100644
index 00000000..836ed1ce
--- /dev/null
+++ b/cmake/riscv64-gcc-rv32imc.cmake
@@ -0,0 +1,64 @@
+# /***************************************************************************
+# * 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 Copilot (Sonnet 4.6).
+# * 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 CORE-V MCU (CV32E40P, RV32IMC)
+#
+# Uses the riscv64-unknown-elf-gcc multi-lib toolchain (Ubuntu package
+# gcc-riscv64-unknown-elf) to cross-compile for a 32-bit RISC-V target.
+#
+# Target ISA : rv32imc_zicsr (integer, multiply, compressed, Zicsr)
+# ABI : ilp32 (32-bit int/long/ptr, no hardware FP)
+# Code model : medlow (addresses in [0, 2 GiB))
+
+set(CMAKE_SYSTEM_NAME Generic)
+set(CMAKE_SYSTEM_PROCESSOR riscv)
+
+set(THREADX_ARCH "risc-v32")
+set(THREADX_TOOLCHAIN "gnu")
+
+set(ARCH_FLAGS "-march=rv32imc_zicsr -mabi=ilp32 -mcmodel=medlow")
+set(CFLAGS "${ARCH_FLAGS}")
+set(ASFLAGS "${ARCH_FLAGS}")
+set(LDFLAGS "${ARCH_FLAGS}")
+
+# Toolchain binaries (riscv64-unknown-elf can target rv32 via multilib)
+set(CMAKE_C_COMPILER riscv64-unknown-elf-gcc)
+set(CMAKE_CXX_COMPILER riscv64-unknown-elf-g++)
+set(AS riscv64-unknown-elf-as)
+set(AR riscv64-unknown-elf-ar)
+set(OBJCOPY riscv64-unknown-elf-objcopy)
+set(OBJDUMP riscv64-unknown-elf-objdump)
+set(SIZE riscv64-unknown-elf-size)
+
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+
+# Use static library for compiler feature probing (no linker script yet)
+set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
+
+set(CMAKE_C_FLAGS "${CFLAGS}" CACHE INTERNAL "c compiler flags")
+set(CMAKE_CXX_FLAGS "${CFLAGS}" CACHE INTERNAL "cxx compiler flags")
+set(CMAKE_ASM_FLAGS "${ASFLAGS} -D__ASSEMBLER__" CACHE INTERNAL "asm compiler flags")
+set(CMAKE_EXE_LINKER_FLAGS "${LDFLAGS}" CACHE INTERNAL "exe link flags")
+
+set(CMAKE_C_FLAGS_DEBUG "-Og -g -ggdb3" CACHE INTERNAL "c debug flags")
+set(CMAKE_CXX_FLAGS_DEBUG "-Og -g -ggdb3" CACHE INTERNAL "cxx debug flags")
+set(CMAKE_ASM_FLAGS_DEBUG "-g -ggdb3" CACHE INTERNAL "asm debug flags")
+
+set(CMAKE_C_FLAGS_RELEASE "-Os" CACHE INTERNAL "c release flags")
+set(CMAKE_CXX_FLAGS_RELEASE "-Os" CACHE INTERNAL "cxx release flags")
+set(CMAKE_ASM_FLAGS_RELEASE "" CACHE INTERNAL "asm release flags")
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/CMakeLists.txt b/ports/risc-v32/gnu/example_build/core_v_mcu/CMakeLists.txt
new file mode 100644
index 00000000..eb6b4c89
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/CMakeLists.txt
@@ -0,0 +1,65 @@
+cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
+
+project(demo_threadx_core_v_mcu LANGUAGES C ASM)
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+
+set(CORE_V_MCU_DIR ${CMAKE_CURRENT_LIST_DIR})
+set(THREADX_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../..)
+set(TX_USER_FILE ${CORE_V_MCU_DIR}/include/tx_user.h)
+
+set(THREADX_ARCH "risc-v32")
+set(THREADX_TOOLCHAIN "gnu")
+add_subdirectory(${THREADX_ROOT} threadx)
+target_include_directories(threadx PRIVATE ${CORE_V_MCU_DIR}/include)
+
+set(SRCS
+ ${CORE_V_MCU_DIR}/crt0.S
+ ${CORE_V_MCU_DIR}/vectors.S
+ ${CORE_V_MCU_DIR}/tx_initialize_low_level.S
+ ${CORE_V_MCU_DIR}/bsp/irq.c
+ ${CORE_V_MCU_DIR}/bsp/timer_irq.c
+ ${CORE_V_MCU_DIR}/bsp/fll.c
+ ${CORE_V_MCU_DIR}/bsp/gpio.c
+ ${CORE_V_MCU_DIR}/bsp/uart_driver.c
+ ${CORE_V_MCU_DIR}/bsp/i2c_master.c
+ ${CORE_V_MCU_DIR}/bsp/temp_sensor.c
+ ${CORE_V_MCU_DIR}/bsp/system_core_v_mcu.c
+ ${CORE_V_MCU_DIR}/bsp/string.c
+ ${CORE_V_MCU_DIR}/demo_threadx.c
+)
+
+add_executable(demo_threadx ${SRCS})
+set_target_properties(demo_threadx PROPERTIES OUTPUT_NAME "demo_threadx.elf")
+
+target_include_directories(demo_threadx PRIVATE
+ ${CORE_V_MCU_DIR}/include
+ ${THREADX_ROOT}/common/inc
+ ${THREADX_ROOT}/ports/risc-v32/gnu/inc
+)
+
+target_link_libraries(demo_threadx PRIVATE threadx gcc)
+
+target_link_options(demo_threadx PRIVATE
+ -T${CORE_V_MCU_DIR}/link.ld
+ -nostartfiles
+ -nodefaultlibs
+ -Wl,--gc-sections
+ -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/demo_threadx.map
+)
+
+target_compile_options(demo_threadx PRIVATE
+ -Os
+ -g
+ -Wall
+ -Wextra
+ -ffreestanding
+ -ffunction-sections
+ -fdata-sections
+ -DTX_INCLUDE_USER_DEFINE_FILE
+)
+
+add_custom_command(TARGET demo_threadx POST_BUILD
+ COMMAND ${OBJCOPY} -O binary $<TARGET_FILE:demo_threadx> ${CMAKE_CURRENT_BINARY_DIR}/demo_threadx.bin
+ COMMENT "Generating demo_threadx.bin"
+)
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/README.md b/ports/risc-v32/gnu/example_build/core_v_mcu/README.md
new file mode 100644
index 00000000..d94759d0
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/README.md
@@ -0,0 +1,501 @@
+# ThreadX Port for CORE-V MCU
+
+This directory contains the Board Support Package (BSP) and demo application for
+running Eclipse ThreadX on the
+[CORE-V MCU](https://github.com/openhwgroup/core-v-mcu) — an open-source 32-bit
+RISC-V SoC based on the CV32E40P core (RV32IMC). The reference hardware is a
+[Digilent Nexys A7](https://digilent.com/reference/programmable-logic/nexys-a7/start)
+FPGA board running the synthesised CORE-V MCU bitstream.
+
+---
+
+## Table of Contents
+
+1. [Hardware overview](#1-hardware-overview)
+2. [Software prerequisites](#2-software-prerequisites)
+3. [Directory structure](#3-directory-structure)
+4. [Architecture notes](#4-architecture-notes)
+5. [Memory map](#5-memory-map)
+6. [Building](#6-building)
+7. [Flashing and debugging](#7-flashing-and-debugging)
+8. [BSP API reference](#8-bsp-api-reference)
+9. [Unit tests](#9-unit-tests)
+10. [Adapting to a different board](#10-adapting-to-a-different-board)
+11. [Setup and deployment scripts](#11-setup-and-deployment-scripts)
+
+---
+
+## 1. Hardware overview
+
+| Item | Detail |
+|------|--------|
+| CPU | CV32E40P — RV32IMC, in-order, 4-stage pipeline |
+| ISA | `rv32imc_zicsr` (integer, compressed, multiply/divide, CSR instructions) |
+| L2 SRAM | 512 KB at `0x1c000000` (code + data + stack) |
+| Interrupt controller | PULP APB interrupt controller (32 lines, **not** a standard CLINT) |
+| Timer | PULP FC Timer at `0x1A10B000`, 32 kHz reference clock |
+| UART | UDMA UART channel 0 at `0x1A102080` |
+| GPIO | PULP GPIO controller at `0x1A101000` |
+| FLL | Frequency Locked Loop — 5 MHz input (FPGA), 50 MHz output |
+| Debug probe | [Ashling Opella LD](https://www.ashling.com/ashling-opella-ld/) via JTAG |
+
+The CV32E40P does **not** implement the standard RISC-V CLINT (`mip`/`mie` per-source
+masking). Individual interrupt lines are enabled and cleared through PULP APB
+registers, and `mstatus.MIE` (bit 3) acts as the single global enable.
+
+---
+
+## 2. Software prerequisites
+
+Run `install_deps.sh` to install all dependencies in one step:
+
+```bash
+bash install_deps.sh
+```
+
+Or install individually:
+
+| Tool | Minimum version | Notes |
+|------|----------------|-------|
+| `riscv64-unknown-elf-gcc` | 13.x | `apt install gcc-riscv64-unknown-elf binutils-riscv64-unknown-elf` |
+| CMake | 3.15 | `apt install cmake` |
+| Ninja | 1.10 | `apt install ninja-build` |
+| OpenOCD | 0.12 | `apt install openocd` |
+| GDB | any multiarch | `apt install gdb-multiarch` — Ubuntu does not ship `riscv64-unknown-elf-gdb` |
+
+> **Note:** There is no `riscv32-unknown-elf-gcc` package required. The
+> `riscv64-unknown-elf-gcc` compiler generates RV32 code when passed
+> `-march=rv32imc_zicsr -mabi=ilp32`, as configured by the toolchain file.
+
+---
+
+## 3. Directory structure
+
+```
+core_v_mcu/
+├── bsp/ Board Support Package
+│ ├── fll.c Frequency Locked Loop driver
+│ ├── gpio.c GPIO driver
+│ ├── irq.c PULP APB interrupt controller driver
+│ ├── string.c Freestanding memset/memcpy shim
+│ ├── system_core_v_mcu.c Top-level BSP init and ISR dispatcher
+│ ├── timer_irq.c PULP FC Timer driver
+│ └── uart_driver.c UDMA UART driver
+├── include/ BSP headers
+│ ├── bits.h Bit-manipulation macros
+│ ├── csr.h CSR access macros
+│ ├── fll.h
+│ ├── gpio.h
+│ ├── io.h MMIO read/write inlines
+│ ├── irq.h
+│ ├── memory_map.h Peripheral base addresses
+│ ├── properties.h Clock/frequency constants
+│ ├── pulp_mem_map.h PULP peripheral register offsets
+│ ├── string.h Freestanding string shim header
+│ ├── system_core_v_mcu.h
+│ ├── timer.h
+│ ├── timer_irq.h
+│ ├── tx_user.h ThreadX compile-time configuration
+│ └── uart_driver.h
+├── tests/ Host-compiled unit tests (no cross-compiler needed)
+│ ├── mock/
+│ │ ├── mmio_mock.c Replaces io.h MMIO inlines with a software map
+│ │ └── mmio_mock.h
+│ ├── test_irq.c Tests for irq.c
+│ ├── test_timer.c Tests for timer_irq.c
+│ └── CMakeLists.txt
+├── crt0.S C runtime startup (clear BSS, call main)
+├── vectors.S 32-entry vectored interrupt table
+├── tx_initialize_low_level.S ThreadX platform init (mtvec, stack pointer)
+├── link.ld Linker script
+├── demo_threadx.c Demo application (blinky + UART heartbeat)
+├── CMakeLists.txt Build definition
+├── build.sh One-shot build script
+├── gdb_init GDB startup script
+└── openocd-nexys-Ashling-Opella-LD.cfg OpenOCD configuration
+```
+
+The ThreadX kernel sources are **not** duplicated here; they are referenced from the
+repository root via `THREADX_ROOT`.
+
+---
+
+## 4. Architecture notes
+
+### 4.1 Interrupt model
+
+The CV32E40P uses a PULP/PULPissimo-style interrupt controller rather than the
+standard RISC-V CLINT. Key differences:
+
+- **32 direct interrupt lines** connect to the core. `mcause` for an external
+ interrupt is `0x80000000 | N`, where `N` (bits 4–0) is the line number.
+- **Per-line masking** is done through APB registers in the PULP IRQ controller
+ (`IRQ_REG_MASK_SET_OFFSET` / `IRQ_REG_MASK_CLEAR_OFFSET`) — not through the `mie`
+ CSR.
+- **Global enable** is `mstatus.MIE` (bit 3). ThreadX manages this itself: the
+ scheduler sets MIE just before entering the WFI idle loop in
+ `_tx_thread_schedule`.
+
+ISR dispatch flow:
+
+```
+CPU takes interrupt
+ → PC jumps to mtvec base (vectored mode, bit 0 = 1)
+ → vectors.S entry for line N → tx_trap_entry
+ → tx_trap_entry saves registers, calls _tx_thread_context_save
+ → tx_trap_handler(mcause, mepc, mtval) dispatches via isr_table[mcause & 0x1f]
+ → ThreadX timer ISR (line 10) calls _tx_timer_interrupt
+ → _tx_thread_context_restore (never returns to tx_trap_entry)
+```
+
+### 4.2 `mtvec` alignment
+
+The CV32E40P requires the vector table base to be **256-byte aligned**. The linker
+script places `.vectors` at `0x1c000800`, which satisfies this. `mtvec` is written
+as `0x1c000801` (base + vectored-mode bit).
+
+### 4.3 ThreadX timer tick
+
+- Reference clock: 32 768 Hz (ARCHI_REF_CLOCK)
+- Tick rate: 100 Hz → compare value 327 (~0.1 % drift)
+- The timer auto-reloads on compare match (cycle mode)
+- `_tx_timer_interrupt` is registered in `isr_table[10]` during `system_init()`
+
+### 4.4 FLL and clock
+
+On the Nexys A7 FPGA, the FLL input is 5 MHz (`ARCHI_FPGA_FREQUENCY`). After
+`pi_fll_init()` the system clock is 50 MHz (`DEFAULT_SYSTEM_CLOCK`). The UART baud
+rate divisor is computed from this 50 MHz clock.
+
+### 4.5 Freestanding build
+
+The port builds with `-ffreestanding -nodefaultlibs` — no newlib or libc.
+`bsp/string.c` provides minimal `memset` and `memcpy` for ThreadX internal use.
+
+---
+
+## 5. Memory map
+
+| Region | Start | Size | Contents |
+|--------|-------|------|----------|
+| Vector table | `0x1c000800` | 128 B | 32 × 4-byte jump instructions to `tx_trap_entry` |
+| `.text` (boot address) | `0x1c000880` | — | Code + read-only data |
+| `.data` | after `.text` | — | Initialised data |
+| `.bss` | after `.data` | — | Zero-initialised data |
+| ThreadX free memory | after `.bss` | — | Heap pools, thread stacks |
+| Stack | top of L2 | 8 KB | Initial/idle stack grows downward from `0x1c080000` |
+
+L2 total: 512 KB (`0x1c000000`–`0x1c07ffff`).
+
+Peripheral base addresses (see `include/memory_map.h`):
+
+| Peripheral | Address |
+|------------|---------|
+| PULP IRQ controller | `0x1A109800` |
+| PULP FC Timer | `0x1A10B000` |
+| PULP GPIO | `0x1A101000` |
+| UDMA UART ch0 | `0x1A102080` |
+| FLL | `0x1A100000` |
+
+---
+
+## 6. Building
+
+### Quick start
+
+```bash
+cd ports/risc-v32/gnu/example_build/core_v_mcu
+bash build.sh
+```
+
+The script configures and builds with Ninja. The output is:
+
+```
+build/demo_threadx.elf — ELF with debug symbols
+build/demo_threadx.bin — raw binary for flashing
+build/demo_threadx.map — linker map
+```
+
+### Manual CMake invocation
+
+```bash
+THREADX_ROOT=$(realpath ../../../../..)
+cmake -B build -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="${THREADX_ROOT}/cmake/riscv64-gcc-rv32imc.cmake" \
+ .
+cmake --build build
+```
+
+### Toolchain file
+
+`${THREADX_ROOT}/cmake/riscv64-gcc-rv32imc.cmake` sets:
+
+```
+CMAKE_C_COMPILER riscv64-unknown-elf-gcc
+CMAKE_ASM_COMPILER riscv64-unknown-elf-gcc
+CMAKE_OBJCOPY riscv64-unknown-elf-objcopy
+CMAKE_C_FLAGS -march=rv32imc_zicsr -mabi=ilp32 -mcmodel=medlow
+```
+
+---
+
+## 7. Flashing and debugging
+
+### 7.1 Quick deploy (recommended)
+
+Use `deploy.sh` to automate the entire OpenOCD + GDB workflow in one command:
+
+```bash
+# Flash and run (leave target running after GDB exits):
+bash deploy.sh
+
+# Build first, then flash and run:
+bash deploy.sh --build
+
+# Flash and stop at main for interactive debugging:
+bash deploy.sh --debug
+
+# Override the ELF or the OpenOCD config:
+bash deploy.sh --elf path/to/my.elf --openocd-cfg path/to/other.cfg
+```
+
+> **First time only:** run `bash ../../../../../samplex-fd/OpenHW/scripts/setup_opella.sh`
+> (or the equivalent path) once to install the udev rule for the Opella LD.
+> See [Section 11](#11-setup-and-deployment-scripts) for details.
+
+### 7.2 Manual procedure
+
+#### Connect hardware
+
+1. Connect the Ashling Opella LD to the Nexys A7 JTAG header.
+2. Connect the USB-UART cable (CORE-V MCU UART0 mapped to USB micro-B on Nexys A7).
+3. Power on the board.
+
+#### Start OpenOCD
+
+```bash
+openocd -f openocd-nexys-Ashling-Opella-LD.cfg
+```
+
+Expected output ends with `Ready for Remote Connections`. OpenOCD listens on
+port 3333 (GDB) and 4444 (telnet).
+
+#### Flash and run with GDB
+
+In a second terminal:
+
+```bash
+riscv64-unknown-elf-gdb --command=gdb_init
+```
+
+`gdb_init` connects to OpenOCD, loads the ELF, sets a breakpoint at `main`, and
+continues. You should see the LED on pin 5 blinking at 1 Hz and the UART printing
+`ThreadX on CORE-V MCU` followed by periodic `.` heartbeats at 115 200 baud.
+
+#### UART monitor
+
+```bash
+minicom -b 115200 -D /dev/ttyUSBx # adjust device as needed
+```
+
+#### Useful GDB commands
+
+```
+(gdb) info threads — list ThreadX threads
+(gdb) thread 2 — switch to a thread
+(gdb) bt — backtrace
+(gdb) monitor halt — halt via OpenOCD
+(gdb) monitor resume — resume via OpenOCD
+```
+
+---
+
+## 8. BSP API reference
+
+### `system_core_v_mcu.h`
+
+```c
+void system_init(void);
+```
+Initialises FLL, UART, IRQ controller, and FC Timer. Must be called once from
+`main()` before `tx_kernel_enter()`.
+
+### `uart_driver.h`
+
+```c
+int uart_init(uint8_t uart_id, uint32_t baudrate, uint32_t periph_freq);
+void uart_write_byte(uint8_t uart_id, uint8_t c);
+int uart_read_byte(uint8_t uart_id); /* returns -1 if no data */
+void uart_write_str(uint8_t uart_id, const char *str);
+void uart_write_buf(uint8_t uart_id, const uint8_t *buf, uint32_t len);
+```
+
+`uart_id` 0 is the console UART. `uart_read_byte` is non-blocking.
+
+### `gpio.h`
+
+```c
+void gpio_init(void);
+void gpio_set_output(uint32_t pin_mask); /* configure pins as outputs */
+void gpio_set(uint32_t pin_mask); /* drive high */
+void gpio_clear(uint32_t pin_mask); /* drive low */
+void gpio_toggle(uint32_t pin_mask);
+```
+
+`pin_mask` is a bitmask where bit N corresponds to GPIO pin N.
+
+### `irq.h`
+
+```c
+void pulp_irq_init(void);
+void irq_enable(uint32_t irq_id);
+void irq_disable(uint32_t irq_id);
+void irq_clint_enable(void); /* set mstatus.MIE */
+void irq_clint_disable(void); /* clear mstatus.MIE */
+```
+
+`irq_id` values are defined as `IRQ_FC_EVT_*` in `irq.h`. The timer uses
+`IRQ_FC_EVT_TIMER0_LO` (= 10).
+
+### `timer_irq.h`
+
+```c
+void timer_irq_init(uint32_t ticks);
+void timer_irq_set_timeout(uint32_t ticks, bool enable);
+uint32_t timer_irq_cycle_get_32(void);
+```
+
+### Registering a custom ISR
+
+Add a function pointer to `isr_table` before calling `tx_kernel_enter()`:
+
+```c
+extern void (*isr_table[32])(void);
+
+static void my_irq_handler(void) { /* ... */ }
+
+/* In main() or tx_application_define(), before enabling the line: */
+isr_table[MY_IRQ_LINE] = my_irq_handler;
+irq_enable(MY_IRQ_LINE);
+```
+
+---
+
+## 9. Unit tests
+
+The tests compile for the **host** (no cross-compiler required) using a software
+MMIO mock that backs the peripheral register space with a `uint32_t` array.
+
+```bash
+cd tests
+cmake -B build -G Ninja
+cmake --build build
+./build/tx_core_v_mcu_tests
+```
+
+Expected output:
+
+```
+irq_tests: PASS
+timer_tests: PASS
+```
+
+The mock is enabled by defining `CORE_V_MCU_MMIO_MOCK` before including `io.h`.
+`mmio_mock.c` provides `mock_reg_read` / `mock_reg_write` helpers for assertions.
+
+---
+
+## 10. Adapting to a different board
+
+To port to a different CORE-V MCU carrier board:
+
+1. **Clock frequencies** — update `ARCHI_REF_CLOCK`, `ARCHI_FPGA_FREQUENCY`, and
+ `DEFAULT_SYSTEM_CLOCK` in `include/properties.h`.
+
+2. **Memory layout** — adjust `ORIGIN` and `LENGTH` for `L2` in `link.ld` and
+ update `__boot_address` if the boot ROM routes differently.
+
+3. **mtvec base** — if `.vectors` moves, update the address in `link.ld`. The
+ 256-byte alignment requirement is a hard CV32E40P constraint.
+
+4. **Debug probe** — replace `openocd-nexys-Ashling-Opella-LD.cfg` with a config
+ matching your probe (e.g., `interface jlink` for a J-Link).
+
+5. **GPIO pin** — `DEMO_LED_PIN` in `demo_threadx.c` selects the LED. Change to
+ match your board's LED assignment.
+
+---
+
+## 11. Setup and deployment scripts
+
+### 11.1 `setup_opella.sh` — one-time host setup
+
+Located in `samplex-fd/OpenHW/scripts/setup_opella.sh`. Run **once per machine**
+to grant user-space access to the Ashling Opella LD USB device.
+
+```bash
+# Native Linux:
+sudo bash samplex-fd/OpenHW/scripts/setup_opella.sh
+
+# WSL (also attempts usbipd-win USB forwarding):
+bash samplex-fd/OpenHW/scripts/setup_opella.sh
+```
+
+What it does:
+
+| Step | Native Linux | WSL |
+|------|-------------|-----|
+| Write `/etc/udev/rules.d/99-ashling-opella.rules` | ✓ | ✓ |
+| Add user to `plugdev` group | ✓ | ✓ |
+| Reload udev | ✓ | ✓ |
+| `usbipd.exe attach --wsl` (v3: `wsl attach`) | — | automatic |
+
+> **WSL — one-time bind (Windows admin required):**
+> Before the first attach, run this once in an **elevated (Run as Administrator) PowerShell** on the Windows host:
+> ```powershell
+> usbipd bind --hardware-id 0b6b:0040
+> ```
+> After binding, `setup_opella.sh` handles subsequent attaches automatically.
+
+> **WSL — if attach fails with "not USBIP capable" (usbipd-win v3 + Ubuntu 22.04+):**
+> Ubuntu 22.04 and 24.04 enable systemd by default. usbipd-win **v3** has a
+> known bug where it incorrectly reports the kernel as not USBIP capable when
+> systemd is running. **Upgrade usbipd-win to v4 or later:**
+> <https://github.com/dorssel/usbipd-win/releases>
+>
+> If you are on an older WSL2 kernel without USBIP support, first run:
+> ```powershell
+> wsl --update
+> wsl --shutdown
+> ```
+
+After running, **re-plug the probe**. On WSL, re-run the script after each WSL restart.
+
+### 11.2 `deploy.sh` — flash and run
+
+```bash
+# Flash and run (leave target running):
+bash deploy.sh
+
+# Build first, then flash:
+bash deploy.sh --build
+
+# Flash and stop at main for interactive debug session:
+bash deploy.sh --debug
+```
+
+The script starts OpenOCD in the background, waits for it to become ready,
+then drives `riscv64-unknown-elf-gdb` to load the ELF. OpenOCD is always
+stopped on exit (including Ctrl-C).
+
+---
+
+## Licence
+
+New files in this directory are © 2026 Eclipse ThreadX contributors, MIT.
+
+Files derived from
+[core-v-freertos](https://github.com/openhwgroup/core-v-freertos) retain their
+original Apache-2.0 copyright notices (ETH Zurich, GreenWaves Technologies, Wind
+River Systems, SiFive Inc.) and are dual-licensed `Apache-2.0 AND MIT` (or
+`(Apache-2.0 OR BSD-2-Clause) AND MIT` for `crt0.S`). See individual file headers.
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/fll.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/fll.c
new file mode 100644
index 00000000..46dce857
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/fll.c
@@ -0,0 +1,209 @@
+/***************************************************************************/
+/* Copyright 2020 GreenWaves Technologies
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "bits.h"
+#include "csr.h"
+#include "fll.h"
+#include "irq.h"
+#include "properties.h"
+#include "system_core_v_mcu.h"
+
+#define FLL_LOG2_MAXDCO 29U
+#define FLL_LOG2_REFCLK ARCHI_REF_CLOCK_LOG2
+#define FLL_LOG2_MAXM (FLL_LOG2_MAXDCO - FLL_LOG2_REFCLK)
+
+static volatile uint32_t flls_frequency[FLL_NUM];
+
+static uint32_t fll_max_u32(uint32_t a, uint32_t b)
+{
+ return (a > b) ? a : b;
+}
+
+static uint32_t fll_fl1(uint32_t value)
+{
+ return (uint32_t)(31U - (uint32_t)__builtin_clz(value));
+}
+
+static uint32_t fll_get_mult_div_from_frequency(uint32_t freq, uint32_t *mult,
+ uint32_t *div)
+{
+ uint32_t fref = FLL_REF_CLK;
+ uint32_t log2_m;
+ uint32_t d;
+ uint32_t m;
+ uint32_t fres;
+
+ if ((freq == 0U) || (mult == NULL) || (div == NULL))
+ {
+ if (mult != NULL)
+ {
+ *mult = 0U;
+ }
+ if (div != NULL)
+ {
+ *div = 0U;
+ }
+ return 0U;
+ }
+
+ if (freq > fref)
+ {
+ log2_m = fll_fl1(freq) - fll_fl1(fref);
+ }
+ else
+ {
+ log2_m = 0U;
+ }
+
+ if (FLL_LOG2_MAXM > log2_m)
+ {
+ d = fll_max_u32(1U, (FLL_LOG2_MAXM - log2_m) >> 1U);
+ }
+ else
+ {
+ d = 1U;
+ }
+
+ m = (freq << d) / fref;
+ fres = (fref * m + (1UL << (d - 1U))) >> d;
+
+ *mult = m;
+ *div = d + 1U;
+ return fres;
+}
+
+static uint32_t fll_get_frequency_from_mult_div(uint32_t mult, uint32_t div)
+{
+ uint32_t fref = FLL_REF_CLK;
+
+ if (div == 0U)
+ {
+ return fref * mult;
+ }
+
+ return (fref * mult) >> (div - 1U);
+}
+
+int pi_fll_set_frequency(fll_type_t which_fll, uint32_t frequency, int check)
+{
+ uint32_t mult;
+ uint32_t div;
+ uint32_t reg1;
+ uint32_t saved_irq;
+
+ (void)check;
+
+ if ((uint32_t)which_fll >= FLL_NUM)
+ {
+ return -1;
+ }
+
+ if (frequency == 0U)
+ {
+ return -1;
+ }
+
+ saved_irq = irq_clint_disable();
+ (void)fll_get_mult_div_from_frequency(frequency, &mult, &div);
+
+ reg1 = FLL_CTRL[which_fll].FLL_CONF1;
+ reg1 &= ~FLL_CTRL_CONF1_MULTI_FACTOR_MASK;
+ reg1 |= REG_SET(FLL_CTRL_CONF1_MULTI_FACTOR, mult);
+ reg1 &= ~FLL_CTRL_CONF1_CLK_OUT_DIV_MASK;
+ reg1 |= REG_SET(FLL_CTRL_CONF1_CLK_OUT_DIV, div);
+ FLL_CTRL[which_fll].FLL_CONF1 = reg1;
+
+ flls_frequency[which_fll] = frequency;
+ if (which_fll == FLL_SOC)
+ {
+ system_core_clock = frequency;
+ }
+
+ if ((saved_irq & MSTATUS_IE) != 0U)
+ {
+ (void)irq_clint_enable();
+ }
+
+ return (int)frequency;
+}
+
+void pi_fll_init(fll_type_t which_fll, uint32_t ret_state)
+{
+ uint32_t reg1;
+
+ if ((uint32_t)which_fll >= FLL_NUM)
+ {
+ return;
+ }
+
+ if (ret_state != 0U)
+ {
+ (void)pi_fll_get_frequency(which_fll, 1U);
+ return;
+ }
+
+ reg1 = FLL_CTRL[which_fll].FLL_CONF1;
+ if (REG_GET(FLL_CTRL_CONF1_MODE, reg1) == 0U)
+ {
+ uint32_t reg2 = FLL_CTRL[which_fll].FLL_CONF2;
+ uint32_t regint = FLL_CTRL[which_fll].FLL_INTEGRATOR;
+
+ reg2 &= ~FLL_CTRL_CONF2_ASSERT_CYCLES_MASK;
+ reg2 |= REG_SET(FLL_CTRL_CONF2_ASSERT_CYCLES, 0x6U);
+ reg2 &= ~FLL_CTRL_CONF2_LOCK_TOLERANCE_MASK;
+ reg2 |= REG_SET(FLL_CTRL_CONF2_LOCK_TOLERANCE, 0x50U);
+ FLL_CTRL[which_fll].FLL_CONF2 = reg2;
+
+ regint &= ~FLL_CTRL_INTEGRATOR_INT_PART_MASK;
+ regint |= REG_SET(FLL_CTRL_INTEGRATOR_INT_PART, 332U);
+ FLL_CTRL[which_fll].FLL_INTEGRATOR = regint;
+
+ reg1 &= ~FLL_CTRL_CONF1_OUTPUT_LOCK_EN_MASK;
+ reg1 |= REG_SET(FLL_CTRL_CONF1_OUTPUT_LOCK_EN, 1U);
+ reg1 &= ~FLL_CTRL_CONF1_MODE_MASK;
+ reg1 |= REG_SET(FLL_CTRL_CONF1_MODE, 1U);
+ FLL_CTRL[which_fll].FLL_CONF1 = reg1;
+ }
+
+ if (flls_frequency[which_fll] != 0U)
+ {
+ (void)pi_fll_set_frequency(which_fll, flls_frequency[which_fll], 0);
+ }
+ else
+ {
+ flls_frequency[which_fll] = fll_get_frequency_from_mult_div(
+ REG_GET(FLL_CTRL_CONF1_MULTI_FACTOR, reg1),
+ REG_GET(FLL_CTRL_CONF1_CLK_OUT_DIV, reg1));
+ }
+}
+
+int pi_fll_get_frequency(fll_type_t which_fll, uint8_t real)
+{
+ if ((uint32_t)which_fll >= FLL_NUM)
+ {
+ return -1;
+ }
+
+ if (real != 0U)
+ {
+ flls_frequency[which_fll] = fll_get_frequency_from_mult_div(
+ FLL_CTRL[which_fll].FLL_STATUS,
+ REG_GET(FLL_CTRL_CONF1_CLK_OUT_DIV, FLL_CTRL[which_fll].FLL_CONF1));
+ }
+
+ return (int)flls_frequency[which_fll];
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/gpio.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/gpio.c
new file mode 100644
index 00000000..ddeba5b5
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/gpio.c
@@ -0,0 +1,128 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <stdint.h>
+
+#include "pulp_mem_map.h"
+#include "io.h"
+#include "gpio.h"
+
+/* APB SOC CTRL pad-mux interface (apb_soc_ctrl v1.0.0):
+ * Each IO pad has a dedicated 32-bit register at base + 0x400 + pad_index*4.
+ * PWDATA[1:0] = mux function (0=sys, 1=perio, 2=apbio/GPIO, 3=fpgaio)
+ * PWDATA[13:8] = pad config (drive/pull, optional)
+ *
+ * FUNC_C (2) routes the pad to the GPIO (apbio) controller.
+ * For the standard GPIO pins: io_pad = gpio_pin + GPIO_IO_PAD_OFFSET (=7).
+ * Verified in pad_control.sv: apbio[N] ↔ io_pad[N+7] when pad_mux[N+7]==2. */
+#define APB_SOC_PADMUX_BASE 0x400U
+#define PINMUX_FUNC_C 2U
+#define GPIO_IO_PAD_OFFSET 7U
+
+static void gpio_pinmux_set_func_c(uint32_t pin_mask)
+{
+ for (uint32_t gpio_pin = 0U; gpio_pin < 32U; gpio_pin++)
+ {
+ if ((pin_mask & (1UL << gpio_pin)) == 0U)
+ {
+ continue;
+ }
+ uint32_t io_pad = gpio_pin + GPIO_IO_PAD_OFFSET;
+ uintptr_t reg = PULP_APB_SOC_CTRL_ADDR + APB_SOC_PADMUX_BASE
+ + io_pad * 4U;
+ writew(PINMUX_FUNC_C, reg);
+ }
+}
+
+/* Iterate over every set bit in pin_mask and call writew(pin_number, addr).
+ * The apb_gpiov2 SETGPIO/CLRGPIO/TOGGPIO/SETDIR registers are all
+ * pin-indexed: the written word encodes a pin number, not a bitmask. */
+static void gpio_foreach_pin(uint32_t pin_mask, uintptr_t reg_addr, uint32_t extra_bits)
+{
+ for (uint32_t pin = 0U; pin < 32U; pin++)
+ {
+ if ((pin_mask & (1UL << pin)) != 0U)
+ {
+ writew(pin | extra_bits, reg_addr);
+ }
+ }
+}
+
+void gpio_init(void)
+{
+ /* GPIO clocking is handled by platform boot code on CORE-V MCU. */
+}
+
+void gpio_set_output(uint32_t pin_mask)
+{
+ /* Route physical pads to the GPIO controller via the pad-mux (FUNC_C). */
+ gpio_pinmux_set_func_c(pin_mask);
+
+ /* Configure each pin as push-pull output via SETDIR. */
+ gpio_foreach_pin(pin_mask,
+ (uintptr_t)(PULP_GPIO_ADDR + GPIO_SETDIR_OFFSET),
+ GPIO_DIR_OUTPUT);
+}
+
+void gpio_toggle(uint32_t pin_mask)
+{
+ gpio_foreach_pin(pin_mask,
+ (uintptr_t)(PULP_GPIO_ADDR + GPIO_TOGGPIO_OFFSET),
+ 0U);
+}
+
+void gpio_set(uint32_t pin_mask)
+{
+ gpio_foreach_pin(pin_mask,
+ (uintptr_t)(PULP_GPIO_ADDR + GPIO_SETGPIO_OFFSET),
+ 0U);
+}
+
+void gpio_clear(uint32_t pin_mask)
+{
+ gpio_foreach_pin(pin_mask,
+ (uintptr_t)(PULP_GPIO_ADDR + GPIO_CLRGPIO_OFFSET),
+ 0U);
+}
+
+void gpio_setpinmux(uint8_t io_pad, uint8_t mux)
+{
+ uintptr_t reg = PULP_APB_SOC_CTRL_ADDR + APB_SOC_PADMUX_BASE
+ + (uintptr_t)io_pad * 4U;
+ writew((uint32_t)mux & 0x3U, reg);
+}
+
+uint8_t gpio_getpinmux(uint8_t io_pad)
+{
+ uintptr_t reg = PULP_APB_SOC_CTRL_ADDR + APB_SOC_PADMUX_BASE
+ + (uintptr_t)io_pad * 4U;
+ return (uint8_t)(readw(reg) & 0x3U);
+}
+
+void gpio_pin_set_dir(uint8_t pin, uint8_t dir)
+{
+ uint32_t val = (uint32_t)pin;
+ if (dir != 0U)
+ {
+ val |= GPIO_DIR_OUTPUT;
+ }
+ writew(val, (uintptr_t)(PULP_GPIO_ADDR + GPIO_SETDIR_OFFSET));
+}
+
+uint32_t gpio_pin_read_status(uint8_t pin)
+{
+ writew((uint32_t)pin, (uintptr_t)(PULP_GPIO_ADDR + GPIO_SETSEL_OFFSET));
+ return readw((uintptr_t)(PULP_GPIO_ADDR + GPIO_RDSTAT_OFFSET));
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/i2c_master.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/i2c_master.c
new file mode 100644
index 00000000..eb1461b9
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/i2c_master.c
@@ -0,0 +1,153 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <stddef.h>
+#include <stdint.h>
+
+#include "bits.h"
+#include "i2c_master.h"
+#include "io.h"
+#include "pulp_mem_map.h"
+
+#define UDMA_I2C_RX_SADDR_OFFSET 0x00U
+#define UDMA_I2C_RX_SIZE_OFFSET 0x04U
+#define UDMA_I2C_RX_CFG_OFFSET 0x08U
+#define UDMA_I2C_TX_SADDR_OFFSET 0x10U
+#define UDMA_I2C_TX_SIZE_OFFSET 0x14U
+#define UDMA_I2C_TX_CFG_OFFSET 0x18U
+#define UDMA_I2C_STATUS_OFFSET 0x20U
+#define UDMA_I2C_SETUP_OFFSET 0x24U
+
+#define UDMA_CTRL_CG_OFFSET 0x00U
+#define UDMA_CFG_EN_BIT BIT(4)
+#define UDMA_CFG_PENDING_BIT BIT(5)
+#define UDMA_I2C_STATUS_AL_BIT BIT(1)
+#define UDMA_I2C_SETUP_RESET_BIT BIT(0)
+#define I2C_MASTER_COUNT 2U
+#define I2C_TIMEOUT_ITERATIONS 1000000U
+
+#define I2C_CMD_CFG 0xE0U
+#define I2C_CMD_START 0x00U
+#define I2C_CMD_STOP 0x20U
+#define I2C_CMD_RD_ACK 0x40U
+#define I2C_CMD_RD_NACK 0x60U
+#define I2C_CMD_WR 0x80U
+#define I2C_CMD_WAIT 0xA0U
+#define I2C_CMD_RPT 0xC0U
+
+static uint8_t s_cmd_buf[32];
+static uint8_t s_clkdiv[I2C_MASTER_COUNT];
+
+static uintptr_t i2c_master_base(uint8_t id)
+{
+ return (id == 0U) ? (uintptr_t)UDMA_CH_ADDR_I2CM0 : (uintptr_t)UDMA_CH_ADDR_I2CM1;
+}
+
+int i2c_master_init(uint8_t id, uint32_t i2c_freq, uint32_t periph_freq)
+{
+ uint32_t cg;
+ uint32_t clk_div;
+ uintptr_t base;
+
+ if ((id >= I2C_MASTER_COUNT) || (i2c_freq == 0U) || (periph_freq == 0U))
+ {
+ return -1;
+ }
+
+ cg = readw((uintptr_t)(UDMA_CH_ADDR_CTRL + UDMA_CTRL_CG_OFFSET));
+ cg |= (id == 0U) ? UDMA_CTRL_I2CM0_CLKEN : UDMA_CTRL_I2CM1_CLKEN;
+ writew(cg, (uintptr_t)(UDMA_CH_ADDR_CTRL + UDMA_CTRL_CG_OFFSET));
+
+ clk_div = periph_freq / i2c_freq;
+ if (clk_div == 0U)
+ {
+ clk_div = 1U;
+ }
+ else if (clk_div > 255U)
+ {
+ clk_div = 255U;
+ }
+ s_clkdiv[id] = (uint8_t)clk_div;
+
+ base = i2c_master_base(id);
+ writew(UDMA_I2C_SETUP_RESET_BIT, base + UDMA_I2C_SETUP_OFFSET);
+ writew(0U, base + UDMA_I2C_SETUP_OFFSET);
+ writew(0U, base + UDMA_I2C_RX_CFG_OFFSET);
+ writew(0U, base + UDMA_I2C_TX_CFG_OFFSET);
+
+ return 0;
+}
+
+int i2c_master_read_reg(uint8_t id, uint8_t dev_addr7, uint8_t reg_addr,
+ uint8_t *buf, uint8_t len)
+{
+ uintptr_t base;
+ uint32_t timeout;
+ uint32_t cmd_len;
+ uint32_t status;
+
+ if ((id >= I2C_MASTER_COUNT) || (dev_addr7 > 0x7FU) || (buf == NULL) || (len == 0U))
+ {
+ return -1;
+ }
+
+ cmd_len = 0U;
+ s_cmd_buf[cmd_len++] = I2C_CMD_CFG;
+ s_cmd_buf[cmd_len++] = 0U;
+ s_cmd_buf[cmd_len++] = s_clkdiv[id];
+ s_cmd_buf[cmd_len++] = I2C_CMD_START;
+ s_cmd_buf[cmd_len++] = I2C_CMD_WR;
+ s_cmd_buf[cmd_len++] = (uint8_t)(dev_addr7 << 1U);
+ s_cmd_buf[cmd_len++] = I2C_CMD_WR;
+ s_cmd_buf[cmd_len++] = reg_addr;
+ s_cmd_buf[cmd_len++] = I2C_CMD_START;
+ s_cmd_buf[cmd_len++] = I2C_CMD_WR;
+ s_cmd_buf[cmd_len++] = (uint8_t)((dev_addr7 << 1U) | 1U);
+ if (len > 1U)
+ {
+ s_cmd_buf[cmd_len++] = I2C_CMD_RPT;
+ s_cmd_buf[cmd_len++] = (uint8_t)(len - 1U);
+ s_cmd_buf[cmd_len++] = I2C_CMD_RD_ACK;
+ }
+ s_cmd_buf[cmd_len++] = I2C_CMD_RD_NACK;
+ s_cmd_buf[cmd_len++] = I2C_CMD_STOP;
+ s_cmd_buf[cmd_len++] = I2C_CMD_WAIT;
+ s_cmd_buf[cmd_len++] = 0U;
+
+ base = i2c_master_base(id);
+ writew((uint32_t)(uintptr_t)buf, base + UDMA_I2C_RX_SADDR_OFFSET);
+ writew((uint32_t)len, base + UDMA_I2C_RX_SIZE_OFFSET);
+ writew(UDMA_CFG_EN_BIT, base + UDMA_I2C_RX_CFG_OFFSET);
+
+ writew((uint32_t)(uintptr_t)s_cmd_buf, base + UDMA_I2C_TX_SADDR_OFFSET);
+ writew(cmd_len, base + UDMA_I2C_TX_SIZE_OFFSET);
+ writew(UDMA_CFG_EN_BIT, base + UDMA_I2C_TX_CFG_OFFSET);
+
+ for (timeout = 0U; timeout < I2C_TIMEOUT_ITERATIONS; ++timeout)
+ {
+ status = readw(base + UDMA_I2C_STATUS_OFFSET);
+ if ((status & UDMA_I2C_STATUS_AL_BIT) != 0U)
+ {
+ return -1;
+ }
+ if (((readw(base + UDMA_I2C_TX_CFG_OFFSET) & UDMA_CFG_PENDING_BIT) == 0U) &&
+ ((readw(base + UDMA_I2C_RX_CFG_OFFSET) & UDMA_CFG_PENDING_BIT) == 0U))
+ {
+ return 0;
+ }
+ }
+
+ return -1;
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/irq.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/irq.c
new file mode 100644
index 00000000..cb79cc06
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/irq.c
@@ -0,0 +1,50 @@
+/***************************************************************************/
+/* Copyright (C) 2019 ETH Zurich and University of Bologna
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#include <stdint.h>
+
+#include "pulp_mem_map.h"
+#include "io.h"
+#include "irq.h"
+#include "csr.h"
+
+void irq_mask(uint32_t mask)
+{
+ writew(mask, (uintptr_t)(PULP_FC_IRQ_ADDR + IRQ_REG_MASK_OFFSET));
+}
+
+void irq_enable(uint32_t mask)
+{
+ writew(mask, (uintptr_t)(PULP_FC_IRQ_ADDR + IRQ_REG_MASK_SET_OFFSET));
+}
+
+void irq_disable(uint32_t mask)
+{
+ writew(mask, (uintptr_t)(PULP_FC_IRQ_ADDR + IRQ_REG_MASK_CLEAR_OFFSET));
+}
+
+uint32_t irq_clint_disable(void)
+{
+ return (uint32_t)csr_read_clear(CSR_MSTATUS, MSTATUS_IE);
+}
+
+uint32_t irq_clint_enable(void)
+{
+ return (uint32_t)csr_read_set(CSR_MSTATUS, MSTATUS_IE);
+}
+
+void pulp_irq_init(void)
+{
+ irq_disable(0xFFFFFFFFUL);
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/string.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/string.c
new file mode 100644
index 00000000..7ff699e7
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/string.c
@@ -0,0 +1,64 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <stddef.h>
+#include <stdint.h>
+
+#include "string.h"
+
+void *memcpy(void *dest, const void *src, size_t n)
+{
+ size_t index;
+ uint8_t *dst = (uint8_t *)dest;
+ const uint8_t *source = (const uint8_t *)src;
+
+ for (index = 0U; index < n; ++index)
+ {
+ dst[index] = source[index];
+ }
+
+ return dest;
+}
+
+void *memset(void *dest, int c, size_t n)
+{
+ size_t index;
+ uint8_t *dst = (uint8_t *)dest;
+ uint8_t value = (uint8_t)c;
+
+ for (index = 0U; index < n; ++index)
+ {
+ dst[index] = value;
+ }
+
+ return dest;
+}
+
+int memcmp(const void *lhs, const void *rhs, size_t n)
+{
+ size_t index;
+ const uint8_t *left = (const uint8_t *)lhs;
+ const uint8_t *right = (const uint8_t *)rhs;
+
+ for (index = 0U; index < n; ++index)
+ {
+ if (left[index] != right[index])
+ {
+ return (left[index] < right[index]) ? -1 : 1;
+ }
+ }
+
+ return 0;
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/system_core_v_mcu.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/system_core_v_mcu.c
new file mode 100644
index 00000000..f142c95d
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/system_core_v_mcu.c
@@ -0,0 +1,132 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <stddef.h>
+#include <stdint.h>
+
+#include "csr.h"
+#include "fll.h"
+#include "gpio.h"
+#include "irq.h"
+#include "properties.h"
+#include "system_core_v_mcu.h"
+#include "timer_irq.h"
+#include "tx_api.h"
+#include "uart_driver.h"
+
+extern void _tx_timer_interrupt(void);
+
+volatile uint32_t system_core_clock = DEFAULT_SYSTEM_CLOCK;
+volatile uint32_t last_trap_mcause;
+void (*isr_table[32])(void);
+
+static uint32_t uart_console_ready;
+
+static void uart_write_hex32(uint32_t value)
+{
+ static const char hex[] = "0123456789ABCDEF";
+ int shift;
+
+ uart_write_str(0U, "0x");
+ for (shift = 28; shift >= 0; shift -= 4)
+ {
+ uart_write_byte(0U, (uint8_t)hex[(value >> (uint32_t)shift) & 0xFU]);
+ }
+}
+
+void tx_undefined_irq_handler(void)
+{
+ if (uart_console_ready != 0U)
+ {
+ uart_write_str(0U, "\r\nUndefined IRQ\r\n");
+ }
+
+ /* MISRA deviation: intentional infinite loop used as a fault sink. */
+ for (;;)
+ {
+ }
+}
+
+void tx_timer_irq_handler(void)
+{
+ _tx_timer_interrupt();
+}
+
+void system_init(void)
+{
+ uint32_t i;
+
+ for (i = 0U; i < 32U; ++i)
+ {
+ isr_table[i] = tx_undefined_irq_handler;
+ }
+
+ isr_table[7U] = tx_timer_irq_handler;
+
+ for (i = 0U; i < ARCHI_NB_FLL; ++i)
+ {
+ pi_fll_init((fll_type_t)i, 0U);
+ }
+
+ pulp_irq_init();
+ (void)timer_irq_init(ARCHI_SOC_FREQUENCY / (uint32_t)TX_TIMER_TICKS_PER_SECOND);
+ /* CV32E40P routes the FC Timer LO to irq_i[7] (MTI, bit 7), which maps
+ * to mip[7]. IRQ_MASK in cv32e40p_cs_registers forces mie[10] to zero,
+ * so bit 7 is the correct enable bit. mcause will be 0x80000007. */
+ (void)csr_read_set(CSR_MIE, BIT(7));
+
+ gpio_init();
+ if (uart_init(0U, 115200U, ARCHI_FPGA_FREQUENCY) == 0)
+ {
+ uart_console_ready = 1U;
+ }
+}
+
+void tx_trap_handler(uint32_t mcause, uint32_t mepc, uint32_t mtval)
+{
+ last_trap_mcause = mcause;
+
+ if ((mcause & 0x80000000UL) != 0UL)
+ {
+ uint32_t irq_id = mcause & 0x1FUL;
+
+ if (isr_table[irq_id] != NULL)
+ {
+ isr_table[irq_id]();
+ }
+ else
+ {
+ tx_undefined_irq_handler();
+ }
+ }
+ else
+ {
+ if (uart_console_ready != 0U)
+ {
+ uart_write_str(0U, "\r\nTrap mcause=");
+ uart_write_hex32(mcause);
+ uart_write_str(0U, " mepc=");
+ uart_write_hex32(mepc);
+ uart_write_str(0U, " mtval=");
+ uart_write_hex32(mtval);
+ uart_write_str(0U, "\r\n");
+ }
+
+ /* MISRA deviation: intentional infinite loop used as a fault sink. */
+ for (;;)
+ {
+ }
+ }
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/temp_sensor.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/temp_sensor.c
new file mode 100644
index 00000000..6d70e943
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/temp_sensor.c
@@ -0,0 +1,46 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <stdint.h>
+
+#include "i2c_master.h"
+#include "properties.h"
+#include "temp_sensor.h"
+
+#define ADT7420_I2C_ID 1U
+#define ADT7420_ADDR7 0x4BU
+#define ADT7420_TEMP_REG 0x00U
+#define ADT7420_I2C_FREQ_HZ 200000U
+
+void temp_sensor_init(void)
+{
+ (void)i2c_master_init(ADT7420_I2C_ID, ADT7420_I2C_FREQ_HZ, ARCHI_FPGA_FREQUENCY);
+}
+
+int16_t temp_read_celsius_x10(void)
+{
+ uint8_t buf[2];
+ int16_t raw;
+ int16_t value;
+
+ if (i2c_master_read_reg(ADT7420_I2C_ID, ADT7420_ADDR7, ADT7420_TEMP_REG, &buf[0], 2U) != 0)
+ {
+ return INT16_MIN;
+ }
+
+ raw = (int16_t)(((uint16_t)buf[0] << 8U) | (uint16_t)buf[1]);
+ value = (int16_t)(raw >> 3);
+ return (int16_t)(((int32_t)value * 5) / 8);
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/timer_irq.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/timer_irq.c
new file mode 100644
index 00000000..9a085d9f
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/timer_irq.c
@@ -0,0 +1,58 @@
+/***************************************************************************/
+/* Copyright (C) 2019 ETH Zurich and University of Bologna
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#include <stdint.h>
+
+#include "pulp_mem_map.h"
+#include "io.h"
+#include "timer.h"
+#include "timer_irq.h"
+
+static uint32_t last_count;
+
+int timer_irq_init(uint32_t ticks)
+{
+ (void)timer_irq_set_timeout(ticks);
+
+ writew(TIMER_CFG_LO_ENABLE_MASK |
+ TIMER_CFG_LO_RESET_MASK |
+ TIMER_CFG_LO_MODE_MASK |
+ TIMER_CFG_LO_IRQEN_MASK,
+ (uintptr_t)(PULP_FC_TIMER_ADDR + TIMER_CFG_LO_OFFSET));
+
+ last_count = 0U;
+ return 0;
+}
+
+int timer_irq_set_timeout(uint32_t ticks)
+{
+ writew(1U, (uintptr_t)(PULP_FC_TIMER_ADDR + TIMER_RESET_LO_OFFSET));
+ writew(ticks, (uintptr_t)(PULP_FC_TIMER_ADDR + TIMER_CMP_LO_OFFSET));
+ last_count = 0U;
+ return 0;
+}
+
+uint32_t timer_irq_clock_elapsed(void)
+{
+ uint32_t current = timer_irq_cycle_get_32();
+ uint32_t elapsed = current - last_count;
+
+ last_count = current;
+ return elapsed;
+}
+
+uint32_t timer_irq_cycle_get_32(void)
+{
+ return readw((uintptr_t)(PULP_FC_TIMER_ADDR + TIMER_CNT_LO_OFFSET));
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/uart_driver.c b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/uart_driver.c
new file mode 100644
index 00000000..33b1ff15
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/bsp/uart_driver.c
@@ -0,0 +1,169 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <stddef.h>
+#include <stdint.h>
+
+#include "bits.h"
+#include "io.h"
+#include "pulp_mem_map.h"
+#include "uart_driver.h"
+
+#define UDMA_CTRL_CG_OFFSET 0x00U
+#define UDMA_CTRL_UART0_CLKEN BIT(0)
+
+#define UDMA_UART_RX_SADDR_OFFSET 0x00U
+#define UDMA_UART_RX_SIZE_OFFSET 0x04U
+#define UDMA_UART_RX_CFG_OFFSET 0x08U
+#define UDMA_UART_TX_SADDR_OFFSET 0x10U
+#define UDMA_UART_TX_SIZE_OFFSET 0x14U
+#define UDMA_UART_TX_CFG_OFFSET 0x18U
+#define UDMA_UART_STATUS_OFFSET 0x20U
+#define UDMA_UART_SETUP_OFFSET 0x24U
+#define UDMA_UART_VALID_OFFSET 0x30U
+#define UDMA_UART_DATA_OFFSET 0x34U
+
+#define UDMA_CFG_EN_BIT BIT(4)
+/* EN (bit 4) only; DATASIZE=0 for 8-bit transfers; 0x12 would set DATASIZE=1
+ * (16-bit) and cause the EN bit to never clear for single-byte buffers. */
+#define UDMA_UART_TX_CFG_ENABLE 0x10U
+#define UDMA_UART_WORDLEN_8_BITS 3U
+#define UDMA_UART_TX_ENABLE_BIT BIT(8)
+#define UDMA_UART_RX_ENABLE_BIT BIT(9)
+#define UDMA_UART_RX_POLLING_BIT BIT(4)
+#define UDMA_UART_RX_CLEAN_BIT BIT(5)
+#define UDMA_UART_DIV_SHIFT 16U
+#define UDMA_UART_VALID_DATA_BIT BIT(0)
+#define UDMA_UART_STATUS_TX_BUSY BIT(0)
+#define UDMA_UART_MAX_TRANSFER 0xFFFFU
+
+static uint8_t uart_tx_byte[N_UART];
+
+static uintptr_t uart_base(uint8_t uart_id)
+{
+ return (uintptr_t)(UDMA_CH_ADDR_UART + ((uintptr_t)uart_id * (uintptr_t)UDMA_CH_SIZE));
+}
+
+int uart_init(uint8_t uart_id, uint32_t baudrate, uint32_t periph_freq)
+{
+ uint32_t clk_div;
+ uint32_t setup;
+ uint32_t cg;
+
+ if ((uart_id >= N_UART) || (baudrate == 0U) || (periph_freq == 0U))
+ {
+ return -1;
+ }
+
+ cg = readw((uintptr_t)(UDMA_CH_ADDR_CTRL + UDMA_CTRL_CG_OFFSET));
+ cg |= (uint32_t)(UDMA_CTRL_UART0_CLKEN << uart_id);
+ writew(cg, (uintptr_t)(UDMA_CH_ADDR_CTRL + UDMA_CTRL_CG_OFFSET));
+
+ clk_div = periph_freq / baudrate;
+ if (clk_div == 0U)
+ {
+ clk_div = 1U;
+ }
+
+ setup = (clk_div << UDMA_UART_DIV_SHIFT) |
+ UDMA_UART_TX_ENABLE_BIT |
+ UDMA_UART_RX_ENABLE_BIT |
+ UDMA_UART_RX_POLLING_BIT |
+ UDMA_UART_RX_CLEAN_BIT |
+ (UDMA_UART_WORDLEN_8_BITS << 1U);
+ writew(setup, uart_base(uart_id) + UDMA_UART_SETUP_OFFSET);
+
+ writew(0U, uart_base(uart_id) + UDMA_UART_RX_SADDR_OFFSET);
+ writew(0U, uart_base(uart_id) + UDMA_UART_RX_SIZE_OFFSET);
+ writew(0U, uart_base(uart_id) + UDMA_UART_RX_CFG_OFFSET);
+
+ return 0;
+}
+
+void uart_write_byte(uint8_t uart_id, uint8_t c)
+{
+ if (uart_id >= N_UART)
+ {
+ return;
+ }
+
+ uart_tx_byte[uart_id] = c;
+ uart_write_buf(uart_id, &uart_tx_byte[uart_id], 1U);
+}
+
+int uart_read_byte(uint8_t uart_id)
+{
+ if (uart_id >= N_UART)
+ {
+ return -1;
+ }
+
+ if ((readw(uart_base(uart_id) + UDMA_UART_VALID_OFFSET) & UDMA_UART_VALID_DATA_BIT) == 0U)
+ {
+ return -1;
+ }
+
+ return (int)(readw(uart_base(uart_id) + UDMA_UART_DATA_OFFSET) & 0xFFU);
+}
+
+void uart_write_str(uint8_t uart_id, const char *str)
+{
+ if (str == NULL)
+ {
+ return;
+ }
+
+ while (*str != '\0')
+ {
+ uart_write_byte(uart_id, (uint8_t)*str);
+ ++str;
+ }
+}
+
+void uart_write_buf(uint8_t uart_id, const uint8_t *buf, uint32_t len)
+{
+ uintptr_t base_addr;
+ const uint8_t *current;
+ uint32_t remaining;
+
+ if ((uart_id >= N_UART) || (buf == NULL) || (len == 0U))
+ {
+ return;
+ }
+
+ base_addr = uart_base(uart_id);
+ current = buf;
+ remaining = len;
+
+ while (remaining > 0U)
+ {
+ uint32_t chunk = (remaining > UDMA_UART_MAX_TRANSFER) ? UDMA_UART_MAX_TRANSFER : remaining;
+
+ while ((readw(base_addr + UDMA_UART_STATUS_OFFSET) & UDMA_UART_STATUS_TX_BUSY) != 0U)
+ {
+ }
+
+ writew((uint32_t)(uintptr_t)current, base_addr + UDMA_UART_TX_SADDR_OFFSET);
+ writew(chunk, base_addr + UDMA_UART_TX_SIZE_OFFSET);
+ writew(UDMA_UART_TX_CFG_ENABLE, base_addr + UDMA_UART_TX_CFG_OFFSET);
+
+ while ((readw(base_addr + UDMA_UART_TX_CFG_OFFSET) & UDMA_CFG_EN_BIT) != 0U)
+ {
+ }
+
+ current += chunk;
+ remaining -= chunk;
+ }
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/build.sh b/ports/risc-v32/gnu/example_build/core_v_mcu/build.sh
new file mode 100755
index 00000000..fe78a8e6
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/build.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+THREADX_ROOT="${SCRIPT_DIR}/../../../../.."
+cmake -B "${SCRIPT_DIR}/build" \
+ -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE="${THREADX_ROOT}/cmake/riscv64-gcc-rv32imc.cmake" \
+ "${SCRIPT_DIR}"
+cmake --build "${SCRIPT_DIR}/build"
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/crt0.S b/ports/risc-v32/gnu/example_build/core_v_mcu/crt0.S
new file mode 100644
index 00000000..3c761bbe
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/crt0.S
@@ -0,0 +1,48 @@
+/***************************************************************************/
+/* Copyright 2020 ETH Zurich
+ * Copyright (c) 2017 SiFive Inc. All rights reserved.
+ * Copyright (c) 2019 ETH Zürich and University of Bologna
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0 (Apache-2.0)
+ * or the BSD 2-Clause License (BSD-2-Clause) at your option.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: (Apache-2.0 OR BSD-2-Clause) AND MIT
+ ***************************************************************************/
+
+ .section .text.start
+ .global _start
+ .type _start, @function
+
+_start:
+ .option push
+ .option norelax
+1: auipc gp, %pcrel_hi(__global_pointer$)
+ addi gp, gp, %pcrel_lo(1b)
+ .option pop
+
+ la sp, __stack_top
+
+ la a0, __vector_start
+ ori a0, a0, 1
+ csrw mtvec, a0
+
+ la t0, __bss_start
+ la t1, __bss_end
+2:
+ bgeu t0, t1, 3f
+ sw zero, 0(t0)
+ addi t0, t0, 4
+ j 2b
+3:
+ call main
+4:
+ wfi
+ j 4b
+
+ .size _start, .-_start
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/demo_threadx.c b/ports/risc-v32/gnu/example_build/core_v_mcu/demo_threadx.c
new file mode 100644
index 00000000..c7daac3c
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/demo_threadx.c
@@ -0,0 +1,134 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <stdint.h>
+
+#include "gpio.h"
+#include "system_core_v_mcu.h"
+#include "tx_api.h"
+#include "uart_driver.h"
+
+#define DEMO_STACK_SIZE 1024U
+#define DEMO_BYTE_POOL_SIZE 4096U
+/*
+ * LED[0] on Nexys A7 = IO pad 11 = GPIO/apbio pin 4 (MUX=2).
+ * Verified in the quick-start guide and the 2022 nexys-pin-table.csv used to
+ * synthesise the pre-built bitfile (io_pad = gpio_pin + 7).
+ */
+#define DEMO_LED_PIN 4U
+#define DEMO_LED_MASK (1UL << DEMO_LED_PIN)
+
+/* Assemble the version banner string at compile time from tx_api.h constants. */
+#define _TX_STR(x) #x
+#define TX_STR(x) _TX_STR(x)
+#define TX_VERSION_STRING \
+ "v" TX_STR(THREADX_MAJOR_VERSION) \
+ "." TX_STR(THREADX_MINOR_VERSION) \
+ "." TX_STR(THREADX_PATCH_VERSION) \
+ "." TX_STR(THREADX_BUILD_VERSION)
+
+static const char banner[] =
+ "\r\nEclipse ThreadX for OpenHW CORE-V MCU " TX_VERSION_STRING "\r\n"
+ "Copyright (c) 2026 Eclipse ThreadX Contributors\r\n\r\n";
+
+static TX_THREAD blinky_thread;
+static TX_THREAD uart_thread;
+static TX_BYTE_POOL byte_pool;
+static uint8_t memory_area[DEMO_BYTE_POOL_SIZE];
+
+static void demo_error_loop(void)
+{
+ /* MISRA deviation: intentional infinite loop used as a fault sink. */
+ for (;;)
+ {
+ }
+}
+
+static void blinky_thread_entry(ULONG arg)
+{
+ TX_PARAMETER_NOT_USED(arg);
+
+ /* Configure LED[0] as push-pull output (sets pad-mux to FUNC_C = 2). */
+ gpio_set_output((uint32_t)DEMO_LED_MASK);
+
+ for (;;)
+ {
+ gpio_set((uint32_t)DEMO_LED_MASK);
+ tx_thread_sleep(100U); /* 1 s on (100 ticks × 10 ms) */
+ gpio_clear((uint32_t)DEMO_LED_MASK);
+ tx_thread_sleep(100U); /* 1 s off */
+ }
+}
+
+static void uart_thread_entry(ULONG arg)
+{
+ TX_PARAMETER_NOT_USED(arg);
+
+ uart_write_str(0U, banner);
+ for (;;)
+ {
+ uart_write_str(0U, ".");
+ tx_thread_sleep(100U);
+ }
+}
+
+void tx_application_define(void *first_unused_memory)
+{
+ CHAR *stack_ptr = TX_NULL;
+ UINT status;
+
+ TX_PARAMETER_NOT_USED(first_unused_memory);
+
+ status = tx_byte_pool_create(&byte_pool, "demo pool", memory_area, DEMO_BYTE_POOL_SIZE);
+ if (status != TX_SUCCESS)
+ {
+ demo_error_loop();
+ }
+
+ status = tx_byte_allocate(&byte_pool, (VOID **)&stack_ptr, DEMO_STACK_SIZE, TX_NO_WAIT);
+ if (status != TX_SUCCESS)
+ {
+ demo_error_loop();
+ }
+
+ status = tx_thread_create(&blinky_thread, "blinky", blinky_thread_entry, 0U,
+ stack_ptr, DEMO_STACK_SIZE, 1U, 1U,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ if (status != TX_SUCCESS)
+ {
+ demo_error_loop();
+ }
+
+ status = tx_byte_allocate(&byte_pool, (VOID **)&stack_ptr, DEMO_STACK_SIZE, TX_NO_WAIT);
+ if (status != TX_SUCCESS)
+ {
+ demo_error_loop();
+ }
+
+ status = tx_thread_create(&uart_thread, "uart", uart_thread_entry, 0U,
+ stack_ptr, DEMO_STACK_SIZE, 2U, 2U,
+ TX_NO_TIME_SLICE, TX_AUTO_START);
+ if (status != TX_SUCCESS)
+ {
+ demo_error_loop();
+ }
+}
+
+int main(void)
+{
+ system_init();
+ tx_kernel_enter();
+ return 0;
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/deploy.sh b/ports/risc-v32/gnu/example_build/core_v_mcu/deploy.sh
new file mode 100755
index 00000000..2e22ec1e
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/deploy.sh
@@ -0,0 +1,142 @@
+#!/usr/bin/env 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 Copilot (Sonnet 4.6).
+# * The AI-generated portions may be considered public domain (CC0-1.0)
+# * and not subject to the project's licence.
+# *
+# * SPDX-License-Identifier: MIT AND CC0-1.0
+# ***************************************************************************/
+
+# deploy.sh -- build (optionally), flash, and run/debug the CORE-V MCU target
+# via the Ashling Opella LD debug probe.
+#
+# Usage:
+# bash deploy.sh [options]
+#
+# Options:
+# --build Run build.sh before flashing (default: skip).
+# --debug After loading, stop at main and leave GDB attached
+# (default: run free and exit GDB).
+# --elf <path> Override the ELF to flash
+# (default: build/demo_threadx.elf).
+# --openocd-cfg <p> Override the OpenOCD config file.
+#
+# Prerequisites:
+# * riscv64-unknown-elf-gdb and openocd must be on PATH.
+# * Run setup_opella.sh once to configure udev rules.
+# * The Nexys A7 board must be powered on.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+# Prefer a bare-metal RISC-V GDB; fall back to gdb-multiarch (Ubuntu apt default)
+if command -v riscv64-unknown-elf-gdb >/dev/null 2>&1; then
+ GDB="riscv64-unknown-elf-gdb"
+elif command -v gdb-multiarch >/dev/null 2>&1; then
+ GDB="gdb-multiarch"
+else
+ GDB="riscv64-unknown-elf-gdb" # will produce a clear error below
+fi
+OPENOCD_LOG="/tmp/openocd_demo_threadx.log"
+OPENOCD_PID=""
+OPENOCD_TIMEOUT=10
+
+DEFAULT_ELF="${SCRIPT_DIR}/build/demo_threadx.elf"
+DEFAULT_CFG="${SCRIPT_DIR}/openocd-nexys-Ashling-Opella-LD.cfg"
+
+OPT_BUILD=0
+OPT_DEBUG=0
+OPT_ELF=""
+OPT_CFG=""
+
+usage() { grep '^# ' "$0" | sed 's/^# //' | head -30; exit 0; }
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --build) OPT_BUILD=1; shift ;;
+ --debug) OPT_DEBUG=1; shift ;;
+ --elf) OPT_ELF="$2"; shift 2 ;;
+ --openocd-cfg) OPT_CFG="$2"; shift 2 ;;
+ -h|--help) usage ;;
+ *) echo "[ERROR] Unknown option: $1" >&2; usage ;;
+ esac
+done
+
+ELF="${OPT_ELF:-${DEFAULT_ELF}}"
+CFG="${OPT_CFG:-${DEFAULT_CFG}}"
+
+info() { echo "[INFO] $*"; }
+die() { echo "[ERROR] $*" >&2; exit 1; }
+
+stop_openocd() {
+ if [ -n "${OPENOCD_PID}" ] && kill -0 "${OPENOCD_PID}" 2>/dev/null; then
+ info "Stopping OpenOCD (PID ${OPENOCD_PID}) ..."
+ kill "${OPENOCD_PID}" 2>/dev/null || true
+ wait "${OPENOCD_PID}" 2>/dev/null || true
+ fi
+}
+
+trap stop_openocd EXIT INT TERM
+
+if [ "${OPT_BUILD}" -eq 1 ]; then
+ info "Running build.sh ..."
+ bash "${SCRIPT_DIR}/build.sh"
+fi
+
+[ -f "${ELF}" ] || die "ELF not found: ${ELF} (run with --build to build first)"
+[ -f "${CFG}" ] || die "OpenOCD config not found: ${CFG}"
+command -v openocd >/dev/null 2>&1 || die "openocd not found on PATH"
+command -v "${GDB}" >/dev/null 2>&1 || die "${GDB} not found on PATH"
+
+info "Starting OpenOCD with config: ${CFG}"
+info "Log: ${OPENOCD_LOG}"
+openocd -f "${CFG}" >"${OPENOCD_LOG}" 2>&1 &
+OPENOCD_PID=$!
+
+info "Waiting for OpenOCD to be ready (up to ${OPENOCD_TIMEOUT}s) ..."
+ELAPSED=0
+while ! grep -q "Ready for Remote Connections" "${OPENOCD_LOG}" 2>/dev/null; do
+ sleep 1
+ ELAPSED=$((ELAPSED + 1))
+ if [ "${ELAPSED}" -ge "${OPENOCD_TIMEOUT}" ]; then
+ echo "--- OpenOCD log ---" && cat "${OPENOCD_LOG}"
+ die "OpenOCD did not become ready within ${OPENOCD_TIMEOUT}s"
+ fi
+ if ! kill -0 "${OPENOCD_PID}" 2>/dev/null; then
+ echo "--- OpenOCD log ---" && cat "${OPENOCD_LOG}"
+ die "OpenOCD exited unexpectedly"
+ fi
+done
+info "OpenOCD is ready."
+
+GDB_INIT_FILE="/tmp/gdb_deploy_demo_threadx.gdb"
+printf 'set confirm off
+set remotetimeout 60
+file %s
+target extended-remote localhost:3333
+load
+' "${ELF}" > "${GDB_INIT_FILE}"
+
+if [ "${OPT_DEBUG}" -eq 0 ]; then
+ info "Flashing and running (detached) ..."
+ printf 'monitor resume\ndisconnect\nquit\n' >> "${GDB_INIT_FILE}"
+ info "ELF: ${ELF}"
+ riscv64-unknown-elf-size "${ELF}"
+ "${GDB}" --batch --command="${GDB_INIT_FILE}" 2>&1 \
+ | grep -v "keep_alive() was not invoked" \
+ | grep -v "GDB alive packet not sent"
+ info "Flash complete. Application is running on the target."
+ info "Serial console: minicom -b 115200 -D /dev/ttyUSBx"
+else
+ info "Flashing and stopping at main for interactive debug ..."
+ printf 'break main\ncontinue\n' >> "${GDB_INIT_FILE}"
+ info "ELF: ${ELF}"
+ riscv64-unknown-elf-size "${ELF}"
+ "${GDB}" --command="${GDB_INIT_FILE}"
+fi
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/gdb_init b/ports/risc-v32/gnu/example_build/core_v_mcu/gdb_init
new file mode 100644
index 00000000..718a20f1
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/gdb_init
@@ -0,0 +1,6 @@
+set arch riscv:rv32
+target extended-remote localhost:3333
+file build/demo_threadx.elf
+load
+break main
+continue
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/bits.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/bits.h
new file mode 100644
index 00000000..08871387
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/bits.h
@@ -0,0 +1,59 @@
+/***************************************************************************/
+/* Copyright (c) 2011-2014, Wind River Systems, Inc.
+ * Copyright 2020 ETH Zurich
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef BITS_H
+#define BITS_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/* Helper to pass an integer as a pointer or vice versa. */
+#define POINTER_TO_UINT(x) ((uintptr_t)(x))
+#define UINT_TO_POINTER(x) ((void *)(uintptr_t)(x))
+#define POINTER_TO_INT(x) ((intptr_t)(x))
+#define INT_TO_POINTER(x) ((void *)(intptr_t)(x))
+
+#if !(defined(__CHAR_BIT__) && defined(__SIZEOF_LONG__))
+#error Missing required predefined macros for BITS_PER_LONG calculation
+#endif
+
+#define BITS_PER_LONG (__CHAR_BIT__ * __SIZEOF_LONG__)
+#define GENMASK(h, l) \
+ (((~0UL) - (1UL << (l)) + 1UL) & (~0UL >> (BITS_PER_LONG - 1UL - (h))))
+
+#define KB(x) ((x) << 10)
+#define MB(x) (KB(x) << 10)
+#define GB(x) (MB(x) << 10)
+
+#define KHZ(x) ((x) * 1000)
+#define MHZ(x) (KHZ(x) * 1000)
+
+#ifndef BIT
+#ifdef _ASMLANGUAGE
+#define BIT(n) (1 << (n))
+#else
+#define BIT(n) (1UL << (n))
+#endif
+#endif
+
+#define WRITE_BIT(var, bit, set) \
+ ((var) = ((set) != 0) ? ((var) | BIT(bit)) : ((var) & ~BIT(bit)))
+
+#define BIT_MASK(n) (BIT(n) - 1UL)
+
+#define REG_SET(FIELD, v) (((uint32_t)(v) << FIELD##_SHIFT) & FIELD##_MASK)
+#define REG_GET(FIELD, v) (((uint32_t)(v) & FIELD##_MASK) >> FIELD##_SHIFT)
+
+#endif /* BITS_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/csr.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/csr.h
new file mode 100644
index 00000000..69c7763e
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/csr.h
@@ -0,0 +1,75 @@
+/***************************************************************************/
+/* Copyright (C) 2020 ETH Zurich and University of Bologna
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef CSR_H
+#define CSR_H
+
+#include "bits.h"
+
+#define CSR_MSTATUS 0x300
+#define CSR_MISA 0x301
+#define CSR_MIE 0x304
+#define CSR_MTVEC 0x305
+#define CSR_MSCRATCH 0x340
+#define CSR_MEPC 0x341
+#define CSR_MCAUSE 0x342
+#define CSR_MTVAL 0x343
+#define CSR_MIP 0x344
+#define CSR_PMPCFG0 0x3A0
+#define CSR_PMPADDR0 0x3B0
+#define CSR_MVENDORID 0xF11
+#define CSR_MARCHID 0xF12
+#define CSR_MIMPID 0xF13
+#define CSR_MHARTID 0xF14
+
+#define MSTATUS_IE BIT(3)
+
+#define __CSR_EXPAND(x) #x
+
+#ifndef csr_read
+#define csr_read(csr) \
+ ({ \
+ register unsigned long __val; \
+ __asm__ volatile("csrr %0, " __CSR_EXPAND(csr) : "=r"(__val) : : "memory"); \
+ __val; \
+ })
+#endif
+
+#ifndef csr_write
+#define csr_write(csr, val) \
+ ({ \
+ unsigned long __val = (unsigned long)(val); \
+ __asm__ volatile("csrw " __CSR_EXPAND(csr) ", %0" : : "rK"(__val) : "memory"); \
+ })
+#endif
+
+#ifndef csr_read_clear
+#define csr_read_clear(csr, val) \
+ ({ \
+ unsigned long __val = (unsigned long)(val); \
+ __asm__ volatile("csrrc %0, " __CSR_EXPAND(csr) ", %1" : "=r"(__val) : "rK"(__val) : "memory"); \
+ __val; \
+ })
+#endif
+
+#ifndef csr_read_set
+#define csr_read_set(csr, val) \
+ ({ \
+ unsigned long __val = (unsigned long)(val); \
+ __asm__ volatile("csrrs %0, " __CSR_EXPAND(csr) ", %1" : "=r"(__val) : "rK"(__val) : "memory"); \
+ __val; \
+ })
+#endif
+
+#endif /* CSR_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/fll.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/fll.h
new file mode 100644
index 00000000..011268df
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/fll.h
@@ -0,0 +1,93 @@
+/***************************************************************************/
+/* Copyright 2020 GreenWaves Technologies
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef FLL_H
+#define FLL_H
+
+#include <stdint.h>
+
+#include "properties.h"
+#include "pulp_mem_map.h"
+
+#define FLL_STATUS_OFFSET 0x000U
+#define FLL_CONF1_OFFSET 0x004U
+#define FLL_CONF2_OFFSET 0x008U
+#define FLL_INTEGRATOR_OFFSET 0x00CU
+
+typedef struct
+{
+ volatile uint32_t FLL_STATUS;
+ volatile uint32_t FLL_CONF1;
+ volatile uint32_t FLL_CONF2;
+ volatile uint32_t FLL_INTEGRATOR;
+} fll_ctrl_t;
+
+typedef enum _fll_type
+{
+ FLL_SOC = 0,
+ FLL_PERI = 1,
+ FLL_CLUSTER = 2
+} fll_type_t;
+
+typedef enum
+{
+ PI_FREQ_DOMAIN_FC = 0,
+ PI_FREQ_DOMAIN_CL = 1,
+ PI_FREQ_DOMAIN_PERIPH = 2
+} pi_freq_domain_e;
+
+#define FLL_CTRL_STATUS_MULTI_FACTOR_MASK 0xFFFFU
+#define FLL_CTRL_STATUS_MULTI_FACTOR_SHIFT 0U
+#define FLL_CTRL_CONF1_MULTI_FACTOR_MASK 0xFFFFU
+#define FLL_CTRL_CONF1_MULTI_FACTOR_SHIFT 0U
+#define FLL_CTRL_CONF1_DCO_INPUT_MASK 0x03FF0000U
+#define FLL_CTRL_CONF1_DCO_INPUT_SHIFT 16U
+#define FLL_CTRL_CONF1_CLK_OUT_DIV_MASK 0x3C000000U
+#define FLL_CTRL_CONF1_CLK_OUT_DIV_SHIFT 26U
+#define FLL_CTRL_CONF1_OUTPUT_LOCK_EN_MASK 0x40000000U
+#define FLL_CTRL_CONF1_OUTPUT_LOCK_EN_SHIFT 30U
+#define FLL_CTRL_CONF1_MODE_MASK 0x80000000U
+#define FLL_CTRL_CONF1_MODE_SHIFT 31U
+#define FLL_CTRL_CONF2_LOOPGAIN_MASK 0xFU
+#define FLL_CTRL_CONF2_LOOPGAIN_SHIFT 0U
+#define FLL_CTRL_CONF2_DEASSERT_CYCLES_MASK 0x3F0U
+#define FLL_CTRL_CONF2_DEASSERT_CYCLES_SHIFT 4U
+#define FLL_CTRL_CONF2_ASSERT_CYCLES_MASK 0xFC00U
+#define FLL_CTRL_CONF2_ASSERT_CYCLES_SHIFT 10U
+#define FLL_CTRL_CONF2_LOCK_TOLERANCE_MASK 0x0FFF0000U
+#define FLL_CTRL_CONF2_LOCK_TOLERANCE_SHIFT 16U
+#define FLL_CTRL_CONF2_CONF_CLK_SEL_MASK 0x20000000U
+#define FLL_CTRL_CONF2_CONF_CLK_SEL_SHIFT 29U
+#define FLL_CTRL_CONF2_OPEN_LOOP_MASK 0x40000000U
+#define FLL_CTRL_CONF2_OPEN_LOOP_SHIFT 30U
+#define FLL_CTRL_CONF2_DITHERING_MASK 0x80000000U
+#define FLL_CTRL_CONF2_DITHERING_SHIFT 31U
+#define FLL_CTRL_INTEGRATOR_FRACT_PART_MASK 0x0000FFC0U
+#define FLL_CTRL_INTEGRATOR_FRACT_PART_SHIFT 6U
+#define FLL_CTRL_INTEGRATOR_INT_PART_MASK 0x03FF0000U
+#define FLL_CTRL_INTEGRATOR_INT_PART_SHIFT 16U
+#define FLL_CTRL_SOC_FLL_CONV_MASK 0x1U
+#define FLL_CTRL_SOC_FLL_CONV_SHIFT 0U
+#define FLL_CTRL_CLUSTER_FLL_CONV_MASK 0x2U
+#define FLL_CTRL_CLUSTER_FLL_CONV_SHIFT 1U
+
+#define FLL_NUM ARCHI_NB_FLL
+#define FLL_REF_CLK ARCHI_REF_CLOCK
+#define FLL_CTRL ((volatile fll_ctrl_t *)PULP_FLL_ADDR)
+
+void pi_fll_init(fll_type_t which_fll, uint32_t ret_state);
+int pi_fll_set_frequency(fll_type_t which_fll, uint32_t frequency, int check);
+int pi_fll_get_frequency(fll_type_t which_fll, uint8_t real);
+
+#endif /* FLL_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/gpio.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/gpio.h
new file mode 100644
index 00000000..da5f6008
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/gpio.h
@@ -0,0 +1,68 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 GPIO_H
+#define GPIO_H
+
+#include <stdint.h>
+
+/* apb_gpiov2 register map (pin-indexed interface)
+ *
+ * Offset Name Access Description
+ * 0x000 SETGPIO W PWDATA[6:0] = pin number → drive pin high
+ * 0x004 CLRGPIO W PWDATA[6:0] = pin number → drive pin low
+ * 0x008 TOGGPIO W PWDATA[6:0] = pin number → toggle pin
+ * 0x010 PIN0 R bits [31:0] = sampled input state of pins 0-31
+ * 0x020 OUT0 R/W bits [31:0] = output state bitmask, pins 0-31
+ * 0x030 SETSEL W select pin for RDSTAT
+ * 0x034 RDSTAT R status of selected pin
+ * 0x038 SETDIR W PWDATA[6:0]=pin, PWDATA[25:24]=dir
+ * dir[0]=1 → push-pull output; dir[1]=1 → open-drain
+ * 0x03C SETINT W interrupt configuration
+ * 0x040 INTACK W interrupt acknowledge
+ */
+#define GPIO_SETGPIO_OFFSET 0x000U
+#define GPIO_CLRGPIO_OFFSET 0x004U
+#define GPIO_TOGGPIO_OFFSET 0x008U
+#define GPIO_PIN0_OFFSET 0x010U
+#define GPIO_OUT0_OFFSET 0x020U
+#define GPIO_SETSEL_OFFSET 0x030U
+#define GPIO_RDSTAT_OFFSET 0x034U
+#define GPIO_SETDIR_OFFSET 0x038U
+#define GPIO_SETINT_OFFSET 0x03CU
+#define GPIO_INTACK_OFFSET 0x040U
+
+/* Direction field in SETDIR: bits [25:24] of the written word.
+ * 0b01 = push-pull output enable; 0b10 = open-drain; 0b00 = input */
+#define GPIO_DIR_OUTPUT (1U << 24U)
+
+void gpio_init(void);
+void gpio_set_output(uint32_t pin_mask);
+void gpio_toggle(uint32_t pin_mask);
+void gpio_set(uint32_t pin_mask);
+void gpio_clear(uint32_t pin_mask);
+
+/* Pad-mux control (APB SOC CTRL v1.0.0).
+ * Each IO pad has a 32-bit register at SOC_CTRL_BASE + 0x400 + pad*4.
+ * bits[1:0] = mux function: 0=sys, 1=perio, 2=apbio/GPIO, 3=fpgaio. */
+void gpio_setpinmux(uint8_t io_pad, uint8_t mux);
+uint8_t gpio_getpinmux(uint8_t io_pad);
+
+/* Single-pin direction and status (for CLI use).
+ * dir: 0 = input, 1 = push-pull output. */
+void gpio_pin_set_dir(uint8_t pin, uint8_t dir);
+uint32_t gpio_pin_read_status(uint8_t pin);
+
+#endif /* GPIO_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/i2c_master.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/i2c_master.h
new file mode 100644
index 00000000..4a478b30
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/i2c_master.h
@@ -0,0 +1,38 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 I2C_MASTER_H
+#define I2C_MASTER_H
+
+#include <stdint.h>
+
+/* i2c_master_init: enable I2C channel clock gate, configure clock divider.
+ * id: 0 or 1 (I2CM0 or I2CM1)
+ * i2c_freq: desired I2C clock in Hz (e.g. 200000)
+ * periph_freq: UDMA peripheral clock in Hz (5000000 on CORE-V FPGA)
+ * Returns 0 on success, -1 on bad params. */
+int i2c_master_init(uint8_t id, uint32_t i2c_freq, uint32_t periph_freq);
+
+/* i2c_master_read_reg: read len bytes from register reg_addr of 7-bit addressed device.
+ * id: I2CM channel (0 or 1)
+ * dev_addr7: 7-bit I2C device address
+ * reg_addr: register address to read from
+ * buf: output buffer (must be in RAM, not stack if possible)
+ * len: number of bytes to read (1..255)
+ * Returns 0 on success, -1 on timeout or arbitration loss. */
+int i2c_master_read_reg(uint8_t id, uint8_t dev_addr7, uint8_t reg_addr,
+ uint8_t *buf, uint8_t len);
+
+#endif /* I2C_MASTER_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/io.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/io.h
new file mode 100644
index 00000000..02364544
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/io.h
@@ -0,0 +1,61 @@
+/***************************************************************************/
+/* Copyright 2020 ETH Zurich
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef IO_H
+#define IO_H
+
+#include <stdint.h>
+
+#ifndef CORE_V_MCU_MMIO_MOCK
+static inline void writeb(uint8_t val, uintptr_t addr)
+{
+ __asm__ volatile("sb %0, 0(%1)" : : "r"(val), "r"((volatile uint8_t *)addr));
+}
+
+static inline void writeh(uint16_t val, uintptr_t addr)
+{
+ __asm__ volatile("sh %0, 0(%1)" : : "r"(val), "r"((volatile uint16_t *)addr));
+}
+
+static inline void writew(uint32_t val, uintptr_t addr)
+{
+ __asm__ volatile("sw %0, 0(%1)" : : "r"(val), "r"((volatile uint32_t *)addr));
+}
+
+static inline uint8_t readb(const uintptr_t addr)
+{
+ uint8_t val;
+
+ __asm__ volatile("lb %0, 0(%1)" : "=r"(val) : "r"((const volatile uint8_t *)addr));
+ return val;
+}
+
+static inline uint16_t readh(const uintptr_t addr)
+{
+ uint16_t val;
+
+ __asm__ volatile("lh %0, 0(%1)" : "=r"(val) : "r"((const volatile uint16_t *)addr));
+ return val;
+}
+
+static inline uint32_t readw(const uintptr_t addr)
+{
+ uint32_t val;
+
+ __asm__ volatile("lw %0, 0(%1)" : "=r"(val) : "r"((const volatile uint32_t *)addr));
+ return val;
+}
+#endif /* CORE_V_MCU_MMIO_MOCK */
+
+#endif /* IO_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/irq.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/irq.h
new file mode 100644
index 00000000..aafe6226
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/irq.h
@@ -0,0 +1,64 @@
+/***************************************************************************/
+/* Copyright (C) 2019 ETH Zurich and University of Bologna
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef IRQ_H
+#define IRQ_H
+
+#include "pulp_mem_map.h"
+#include "io.h"
+#include "bits.h"
+
+#define IRQ_REG_MASK_OFFSET 0x000U
+#define IRQ_REG_MASK_SET_OFFSET 0x004U
+#define IRQ_REG_MASK_CLEAR_OFFSET 0x008U
+#define IRQ_REG_INT_OFFSET 0x00CU
+#define IRQ_REG_INT_SET_OFFSET 0x010U
+#define IRQ_REG_INT_CLEAR_OFFSET 0x014U
+#define IRQ_REG_ACK_OFFSET 0x018U
+#define IRQ_REG_ACK_SET_OFFSET 0x01CU
+#define IRQ_REG_ACK_CLEAR_OFFSET 0x020U
+#define IRQ_REG_FIFO_OFFSET 0x024U
+
+#define IRQ_FC_EVT_SW0 BIT(0)
+#define IRQ_FC_EVT_SW1 BIT(1)
+#define IRQ_FC_EVT_SW2 BIT(2)
+#define IRQ_FC_EVT_SW3 BIT(3)
+#define IRQ_FC_EVT_SW4 BIT(4)
+#define IRQ_FC_EVT_SW5 BIT(5)
+#define IRQ_FC_EVT_SW6 BIT(6)
+#define IRQ_FC_EVT_SW7 BIT(7)
+#define IRQ_FC_EVT_DMA_PE_EVT BIT(8)
+#define IRQ_FC_EVT_DMA_PE_IRQ BIT(9)
+#define IRQ_FC_EVT_TIMER0_LO BIT(10)
+#define IRQ_FC_EVT_TIMER0_HI BIT(11)
+#define IRQ_FC_EVT_PF BIT(12)
+#define IRQ_FC_EVT_CLK_REF BIT(14)
+#define IRQ_FC_EVT_GPIO BIT(15)
+#define IRQ_FC_EVT_ADV_TIMER0 BIT(17)
+#define IRQ_FC_EVT_ADV_TIMER1 BIT(18)
+#define IRQ_FC_EVT_ADV_TIMER2 BIT(19)
+#define IRQ_FC_EVT_ADV_TIMER3 BIT(20)
+#define IRQ_FC_EVT_SOC_EVT BIT(26)
+#define IRQ_FC_EVT_QUIRQE_ERROR BIT(29)
+#define IRQ_FC_EVT_PERIPH0 BIT(30)
+#define IRQ_FC_EVT_PERIPH1 BIT(31)
+
+void irq_mask(uint32_t mask);
+void irq_enable(uint32_t mask);
+void irq_disable(uint32_t mask);
+uint32_t irq_clint_disable(void);
+uint32_t irq_clint_enable(void);
+void pulp_irq_init(void);
+
+#endif /* IRQ_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/memory_map.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/memory_map.h
new file mode 100644
index 00000000..f3048367
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/memory_map.h
@@ -0,0 +1,39 @@
+/***************************************************************************/
+/* Copyright (C) 2019 ETH Zurich, University of Bologna and GreenWaves Technologies
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef MEMORY_MAP_H
+#define MEMORY_MAP_H
+
+#include "pulp_mem_map.h"
+
+#define L2_BASE 0x1C000000UL
+#define L2_SIZE 0x80000UL
+#define L2_SHARED_ADDR L2_BASE
+#define L2_SHARED_SIZE L2_SIZE
+#define ROM_ADDR 0x1A000000UL
+#define ROM_SIZE 0x00002000UL
+
+#define SOC_PERIPHERALS_ADDR PULP_SOC_PERIPHERALS_ADDR
+#define SOC_FLL_ADDR PULP_FLL_ADDR
+#define GPIO_ADDR PULP_GPIO_ADDR
+#define UDMA_CTRL_ADDR PULP_UDMA_ADDR
+#define APB_SOC_CTRL_ADDR PULP_APB_SOC_CTRL_ADDR
+#define ADV_TIMER_ADDR PULP_ADV_TIMER_ADDR
+#define SOC_EU_ADDR PULP_SOC_EU_ADDR
+#define FC_IRQ_ADDR PULP_FC_IRQ_ADDR
+#define FC_TIMER_ADDR PULP_FC_TIMER_ADDR
+#define FC_HWPE_ADDR PULP_FC_HWPE_ADDR
+#define STDOUT_ADDR PULP_STDOUT_ADDR
+
+#endif /* MEMORY_MAP_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/properties.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/properties.h
new file mode 100644
index 00000000..98473696
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/properties.h
@@ -0,0 +1,43 @@
+/***************************************************************************/
+/* Copyright (C) 2019 ETH Zurich, University of Bologna and GreenWaves Technologies
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef PROPERTIES_H
+#define PROPERTIES_H
+
+#define PULP
+
+/* Peripheral bus clock (clk_per): PER_CLK_PERIOD_NS=200 → 5 MHz */
+#define ARCHI_FPGA_FREQUENCY 5000000U
+/* FC/SOC clock (clk_soc): FC_CLK_PERIOD_NS=100 → 10 MHz */
+#define ARCHI_SOC_FREQUENCY 10000000U
+#define ARCHI_NUM_TIMER 1U
+#define ARCHI_NUM_FLL 2U
+
+#define ARCHI_REF_CLOCK_LOG2 15U
+#define ARCHI_REF_CLOCK (1U << ARCHI_REF_CLOCK_LOG2)
+
+#define ARCHI_NB_FLL 3U
+
+#define __RT_FLL_CL 2U
+#define __RT_FLL_PERIPH 1U
+#define __RT_FLL_FC 0U
+
+#define __RT_FREQ_DOMAIN_FC 0U
+#define __RT_FREQ_DOMAIN_CL 2U
+#define __RT_FREQ_DOMAIN_PERIPH 1U
+#define RT_FREQ_NB_DOMAIN 3U
+
+#define DEFAULT_SYSTEM_CLOCK 50000000U
+
+#endif /* PROPERTIES_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/pulp_mem_map.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/pulp_mem_map.h
new file mode 100644
index 00000000..32757d62
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/pulp_mem_map.h
@@ -0,0 +1,103 @@
+/***************************************************************************/
+/* Copyright (C) 2019 ETH Zurich and University of Bologna
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef PULP_MEM_MAP_H
+#define PULP_MEM_MAP_H
+
+#ifndef PULP_SOC_PERIPHERALS_ADDR
+#define PULP_SOC_PERIPHERALS_ADDR 0x1A100000UL
+#endif
+
+#ifndef PULP_FC_TIMER_SIZE
+#define PULP_FC_TIMER_SIZE 0x00000800UL
+#endif
+
+#ifndef PULP_FLL_OFFSET
+#define PULP_FLL_OFFSET 0x00000000UL
+#endif
+#ifndef PULP_GPIO_OFFSET
+#define PULP_GPIO_OFFSET 0x00001000UL
+#endif
+#ifndef PULP_UDMA_OFFSET
+#define PULP_UDMA_OFFSET 0x00002000UL
+#endif
+#ifndef PULP_APB_SOC_CTRL_OFFSET
+#define PULP_APB_SOC_CTRL_OFFSET 0x00004000UL
+#endif
+#ifndef PULP_ADV_TIMER_OFFSET
+#define PULP_ADV_TIMER_OFFSET 0x00005000UL
+#endif
+#ifndef PULP_SOC_EU_OFFSET
+#define PULP_SOC_EU_OFFSET 0x00006000UL
+#endif
+#ifndef PULP_FC_IRQ_OFFSET
+#define PULP_FC_IRQ_OFFSET 0x00009800UL
+#endif
+#ifndef PULP_FC_TIMER_OFFSET
+#define PULP_FC_TIMER_OFFSET 0x0000B000UL
+#endif
+#ifndef PULP_FC_HWPE_OFFSET
+#define PULP_FC_HWPE_OFFSET 0x0000C000UL
+#endif
+#ifndef PULP_STDOUT_OFFSET
+#define PULP_STDOUT_OFFSET 0x0000F000UL
+#endif
+#ifndef PULP_DEBUG_OFFSET
+#define PULP_DEBUG_OFFSET 0x00010000UL
+#endif
+
+#ifndef PULP_FLL_ADDR
+#define PULP_FLL_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_FLL_OFFSET)
+#endif
+#ifndef PULP_GPIO_ADDR
+#define PULP_GPIO_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_GPIO_OFFSET)
+#endif
+#ifndef PULP_UDMA_ADDR
+#define PULP_UDMA_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_UDMA_OFFSET)
+#endif
+#ifndef PULP_APB_SOC_CTRL_ADDR
+#define PULP_APB_SOC_CTRL_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_APB_SOC_CTRL_OFFSET)
+#endif
+#ifndef PULP_ADV_TIMER_ADDR
+#define PULP_ADV_TIMER_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_ADV_TIMER_OFFSET)
+#endif
+#ifndef PULP_SOC_EU_ADDR
+#define PULP_SOC_EU_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_SOC_EU_OFFSET)
+#endif
+#ifndef PULP_FC_IRQ_ADDR
+#define PULP_FC_IRQ_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_FC_IRQ_OFFSET)
+#endif
+#ifndef PULP_FC_TIMER_ADDR
+#define PULP_FC_TIMER_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_FC_TIMER_OFFSET)
+#endif
+#ifndef PULP_FC_HWPE_ADDR
+#define PULP_FC_HWPE_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_FC_HWPE_OFFSET)
+#endif
+#ifndef PULP_STDOUT_ADDR
+#define PULP_STDOUT_ADDR (PULP_SOC_PERIPHERALS_ADDR + PULP_STDOUT_OFFSET)
+#endif
+
+#define PULP_FLL_AREA_SIZE 0x00000010UL
+
+/* UDMA */
+#define UDMA_CH_ADDR_CTRL (PULP_UDMA_ADDR)
+#define UDMA_CH_ADDR_UART (PULP_UDMA_ADDR + 0x80U)
+#define UDMA_CH_ADDR_I2CM0 (PULP_UDMA_ADDR + 5U * UDMA_CH_SIZE)
+#define UDMA_CH_ADDR_I2CM1 (PULP_UDMA_ADDR + 6U * UDMA_CH_SIZE)
+#define UDMA_CTRL_I2CM0_CLKEN BIT(4)
+#define UDMA_CTRL_I2CM1_CLKEN BIT(5)
+#define UDMA_CH_SIZE (0x80U)
+#define N_UART (2U)
+
+#endif /* PULP_MEM_MAP_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/string.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/string.h
new file mode 100644
index 00000000..b5600000
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/string.h
@@ -0,0 +1,25 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 STRING_H
+#define STRING_H
+
+#include <stddef.h>
+
+void *memcpy(void *dest, const void *src, size_t n);
+void *memset(void *dest, int c, size_t n);
+int memcmp(const void *lhs, const void *rhs, size_t n);
+
+#endif /* STRING_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/system_core_v_mcu.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/system_core_v_mcu.h
new file mode 100644
index 00000000..4b445e31
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/system_core_v_mcu.h
@@ -0,0 +1,29 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 SYSTEM_CORE_V_MCU_H
+#define SYSTEM_CORE_V_MCU_H
+
+#include <stdint.h>
+
+extern void (*isr_table[32])(void);
+extern volatile uint32_t system_core_clock;
+
+void system_init(void);
+void tx_trap_handler(uint32_t mcause, uint32_t mepc, uint32_t mtval);
+void tx_timer_irq_handler(void);
+void tx_undefined_irq_handler(void);
+
+#endif /* SYSTEM_CORE_V_MCU_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/temp_sensor.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/temp_sensor.h
new file mode 100644
index 00000000..e94c589a
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/temp_sensor.h
@@ -0,0 +1,29 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 TEMP_SENSOR_H
+#define TEMP_SENSOR_H
+
+#include <stdint.h>
+
+/* Read ADT7420 temperature sensor on I2CM1 (Nexys A7 on-board sensor).
+ * Returns temperature in tenths of degrees Celsius (e.g. 213 = 21.3°C).
+ * Returns INT16_MIN on I2C error. */
+int16_t temp_read_celsius_x10(void);
+
+/* Initialize the temperature sensor I2C channel. Call once at startup. */
+void temp_sensor_init(void);
+
+#endif /* TEMP_SENSOR_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/timer.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/timer.h
new file mode 100644
index 00000000..ecfc0480
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/timer.h
@@ -0,0 +1,120 @@
+/***************************************************************************/
+/* Copyright (C) 2019 ETH Zurich and University of Bologna
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * Derived from core-v-freertos (https://github.com/openhwgroup/core-v-freertos)
+ * Original work licensed under the Apache License, Version 2.0.
+ * See https://www.apache.org/licenses/LICENSE-2.0
+ * Modifications licensed under MIT (https://opensource.org/licenses/MIT).
+ *
+ * AI Disclosure: Some portions generated or modified by Copilot (Sonnet 4.6).
+ *
+ * SPDX-License-Identifier: Apache-2.0 AND MIT
+ ***************************************************************************/
+
+#ifndef TIMER_H
+#define TIMER_H
+
+#include "bits.h"
+
+#define TIMER_CFG_LO_OFFSET 0x0U
+#define TIMER_CFG_HI_OFFSET 0x4U
+#define TIMER_CNT_LO_OFFSET 0x8U
+#define TIMER_CNT_HI_OFFSET 0xCU
+#define TIMER_CMP_LO_OFFSET 0x10U
+#define TIMER_CMP_HI_OFFSET 0x14U
+#define TIMER_START_LO_OFFSET 0x18U
+#define TIMER_START_HI_OFFSET 0x1CU
+#define TIMER_RESET_LO_OFFSET 0x20U
+#define TIMER_RESET_HI_OFFSET 0x24U
+
+#define TIMER_CFG_LO_ENABLE_BIT 0U
+#define TIMER_CFG_LO_ENABLE_WIDTH 1U
+#define TIMER_CFG_LO_ENABLE_MASK 0x1U
+#define TIMER_CFG_LO_RESET_BIT 1U
+#define TIMER_CFG_LO_RESET_WIDTH 1U
+#define TIMER_CFG_LO_RESET_MASK 0x2U
+#define TIMER_CFG_LO_IRQEN_BIT 2U
+#define TIMER_CFG_LO_IRQEN_WIDTH 1U
+#define TIMER_CFG_LO_IRQEN_MASK 0x4U
+#define TIMER_CFG_LO_IEM_BIT 3U
+#define TIMER_CFG_LO_IEM_WIDTH 1U
+#define TIMER_CFG_LO_IEM_MASK 0x8U
+#define TIMER_CFG_LO_MODE_BIT 4U
+#define TIMER_CFG_LO_MODE_WIDTH 1U
+#define TIMER_CFG_LO_MODE_MASK 0x10U
+#define TIMER_CFG_LO_ONE_S_BIT 5U
+#define TIMER_CFG_LO_ONE_S_WIDTH 1U
+#define TIMER_CFG_LO_ONE_S_MASK 0x20U
+#define TIMER_CFG_LO_PEN_BIT 6U
+#define TIMER_CFG_LO_PEN_WIDTH 1U
+#define TIMER_CFG_LO_PEN_MASK 0x40U
+#define TIMER_CFG_LO_CCFG_BIT 7U
+#define TIMER_CFG_LO_CCFG_WIDTH 1U
+#define TIMER_CFG_LO_CCFG_MASK 0x80U
+#define TIMER_CFG_LO_PVAL_BIT 8U
+#define TIMER_CFG_LO_PVAL_WIDTH 8U
+#define TIMER_CFG_LO_PVAL_MASK 0xFF00U
+#define TIMER_CFG_LO_CASC_BIT 31U
+#define TIMER_CFG_LO_CASC_WIDTH 1U
+#define TIMER_CFG_LO_CASC_MASK 0x80000000UL
+
+#define TIMER_CFG_HI_ENABLE_BIT 0U
+#define TIMER_CFG_HI_ENABLE_WIDTH 1U
+#define TIMER_CFG_HI_ENABLE_MASK 0x1U
+#define TIMER_CFG_HI_RESET_BIT 1U
+#define TIMER_CFG_HI_RESET_WIDTH 1U
+#define TIMER_CFG_HI_RESET_MASK 0x2U
+#define TIMER_CFG_HI_IRQEN_BIT 2U
+#define TIMER_CFG_HI_IRQEN_WIDTH 1U
+#define TIMER_CFG_HI_IRQEN_MASK 0x4U
+#define TIMER_CFG_HI_IEM_BIT 3U
+#define TIMER_CFG_HI_IEM_WIDTH 1U
+#define TIMER_CFG_HI_IEM_MASK 0x8U
+#define TIMER_CFG_HI_MODE_BIT 4U
+#define TIMER_CFG_HI_MODE_WIDTH 1U
+#define TIMER_CFG_HI_MODE_MASK 0x10U
+#define TIMER_CFG_HI_ONE_S_BIT 5U
+#define TIMER_CFG_HI_ONE_S_WIDTH 1U
+#define TIMER_CFG_HI_ONE_S_MASK 0x20U
+#define TIMER_CFG_HI_PEN_BIT 6U
+#define TIMER_CFG_HI_PEN_WIDTH 1U
+#define TIMER_CFG_HI_PEN_MASK 0x40U
+#define TIMER_CFG_HI_CLKCFG_BIT 7U
+#define TIMER_CFG_HI_CLKCFG_WIDTH 1U
+#define TIMER_CFG_HI_CLKCFG_MASK 0x80U
+
+#define TIMER_CNT_LO_CNT_LO_BIT 0U
+#define TIMER_CNT_LO_CNT_LO_WIDTH 32U
+#define TIMER_CNT_LO_CNT_LO_MASK 0xFFFFFFFFUL
+#define TIMER_CNT_HI_CNT_HI_BIT 0U
+#define TIMER_CNT_HI_CNT_HI_WIDTH 32U
+#define TIMER_CNT_HI_CNT_HI_MASK 0xFFFFFFFFUL
+#define TIMER_CMP_LO_CMP_LO_BIT 0U
+#define TIMER_CMP_LO_CMP_LO_WIDTH 32U
+#define TIMER_CMP_LO_CMP_LO_MASK 0xFFFFFFFFUL
+#define TIMER_CMP_HI_CMP_HI_BIT 0U
+#define TIMER_CMP_HI_CMP_HI_WIDTH 32U
+#define TIMER_CMP_HI_CMP_HI_MASK 0xFFFFFFFFUL
+
+#define TIMER_START_LO_STRT_LO_BIT 0U
+#define TIMER_START_LO_STRT_LO_WIDTH 1U
+#define TIMER_START_LO_STRT_LO_MASK 0x1U
+#define TIMER_START_HI_STRT_HI_BIT 0U
+#define TIMER_START_HI_STRT_HI_WIDTH 1U
+#define TIMER_START_HI_STRT_HI_MASK 0x1U
+#define TIMER_RESET_LO_RST_LO_BIT 0U
+#define TIMER_RESET_LO_RST_LO_WIDTH 1U
+#define TIMER_RESET_LO_RST_LO_MASK 0x1U
+#define TIMER_RESET_HI_RST_HI_BIT 0U
+#define TIMER_RESET_HI_RST_HI_WIDTH 1U
+#define TIMER_RESET_HI_RST_HI_MASK 0x1U
+
+struct pulp_timer
+{
+ unsigned int current_time;
+ unsigned int flags;
+ void *base;
+};
+
+#endif /* TIMER_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/timer_irq.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/timer_irq.h
new file mode 100644
index 00000000..11e0d76a
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/timer_irq.h
@@ -0,0 +1,26 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 TIMER_IRQ_H
+#define TIMER_IRQ_H
+
+#include <stdint.h>
+
+int timer_irq_init(uint32_t ticks);
+int timer_irq_set_timeout(uint32_t ticks);
+uint32_t timer_irq_clock_elapsed(void);
+uint32_t timer_irq_cycle_get_32(void);
+
+#endif /* TIMER_IRQ_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/tx_user.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/tx_user.h
new file mode 100644
index 00000000..cc104032
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/tx_user.h
@@ -0,0 +1,22 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 TX_USER_H
+#define TX_USER_H
+
+#define TX_TIMER_TICKS_PER_SECOND 100U
+#define TX_INCLUDE_USER_DEFINE_FILE
+
+#endif /* TX_USER_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/include/uart_driver.h b/ports/risc-v32/gnu/example_build/core_v_mcu/include/uart_driver.h
new file mode 100644
index 00000000..c1771093
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/include/uart_driver.h
@@ -0,0 +1,27 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 UART_DRIVER_H
+#define UART_DRIVER_H
+
+#include <stdint.h>
+
+int uart_init(uint8_t uart_id, uint32_t baudrate, uint32_t periph_freq);
+void uart_write_byte(uint8_t uart_id, uint8_t c);
+int uart_read_byte(uint8_t uart_id);
+void uart_write_str(uint8_t uart_id, const char *str);
+void uart_write_buf(uint8_t uart_id, const uint8_t *buf, uint32_t len);
+
+#endif /* UART_DRIVER_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/install_deps.sh b/ports/risc-v32/gnu/example_build/core_v_mcu/install_deps.sh
new file mode 100755
index 00000000..b4cd46a8
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/install_deps.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env 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 Copilot (Sonnet 4.6).
+# * The AI-generated portions may be considered public domain (CC0-1.0)
+# * and not subject to the project's licence.
+# *
+# * SPDX-License-Identifier: MIT AND CC0-1.0
+# ***************************************************************************/
+
+# install_deps.sh -- Install Linux build/debug dependencies for the CORE-V MCU ThreadX port.
+#
+# Supported: Ubuntu 20.04 / 22.04 / 24.04 and Debian derivatives.
+# Run as a regular user; sudo is invoked internally where needed.
+#
+# Usage:
+# bash install_deps.sh
+
+set -euo pipefail
+
+die() { echo "[ERROR] $*" >&2; exit 1; }
+info() { echo "[INFO] $*"; }
+
+command -v apt-get >/dev/null 2>&1 || die "This script requires apt-get (Ubuntu/Debian)."
+
+PACKAGES=(
+ cmake
+ ninja-build
+ gcc-riscv64-unknown-elf
+ binutils-riscv64-unknown-elf
+ gdb-multiarch
+ openocd
+ usbutils
+ git
+)
+
+info "Updating package lists ..."
+sudo apt-get update -qq
+
+info "Installing: ${PACKAGES[*]}"
+sudo apt-get install -y "${PACKAGES[@]}"
+
+info ""
+info "All dependencies installed."
+info ""
+info "Next steps:"
+info " 1. Run scripts/setup_opella.sh to configure udev rules for the Ashling Opella LD."
+info " 2. (WSL only) Follow the usbipd-win instructions printed by setup_opella.sh."
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/link.ld b/ports/risc-v32/gnu/example_build/core_v_mcu/link.ld
new file mode 100644
index 00000000..8ffb174e
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/link.ld
@@ -0,0 +1,65 @@
+OUTPUT_ARCH(riscv)
+ENTRY(_start)
+
+MEMORY
+{
+ L2 (rwx) : ORIGIN = 0x1c000000, LENGTH = 0x80000
+}
+
+SECTIONS
+{
+ PROVIDE(__boot_address = 0x1c000880);
+ __stack_size = DEFINED(__stack_size) ? __stack_size : 0x2000;
+ PROVIDE(__stack_size = __stack_size);
+
+ .vectors 0x1c000800 :
+ {
+ __vector_start = .;
+ KEEP(*(.vectors))
+ . = __vector_start + 0x80;
+ __vector_end = .;
+ } > L2
+
+ .text __boot_address :
+ {
+ *(.text.start)
+ *(.text .text.*)
+ *(.rodata .rodata.*)
+ . = ALIGN(4);
+ } > L2
+
+ .data :
+ {
+ __data_begin = .;
+ __sdata_begin = .;
+ *(.sdata .sdata.*)
+ *(.data .data.*)
+ . = ALIGN(4);
+ __data_end = .;
+ } > L2
+
+ .bss (NOLOAD) :
+ {
+ __bss_start = .;
+ *(.sbss .sbss.*)
+ *(.bss .bss.*)
+ *(COMMON)
+ . = ALIGN(4);
+ __bss_end = .;
+ _end = .;
+ } > L2
+
+ PROVIDE(__tx_free_memory_start = __bss_end);
+ PROVIDE(__global_pointer$ = MIN(__sdata_begin + 0x800,
+ MAX(__data_begin + 0x800, __bss_end - 0x800)));
+
+ __stack_top = ORIGIN(L2) + LENGTH(L2);
+
+ ASSERT(SIZEOF(.vectors) <= 128, "Vector table exceeds reserved 128 bytes")
+
+ /DISCARD/ :
+ {
+ *(.eh_frame)
+ *(.note.*)
+ }
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/openocd-nexys-Ashling-Opella-LD.cfg b/ports/risc-v32/gnu/example_build/core_v_mcu/openocd-nexys-Ashling-Opella-LD.cfg
new file mode 100644
index 00000000..db43eae5
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/openocd-nexys-Ashling-Opella-LD.cfg
@@ -0,0 +1,41 @@
+interface ftdi
+ftdi_device_desc "Opella-LD Debug Probe"
+ftdi_vid_pid 0x0B6B 0x0040
+ftdi_tdo_sample_edge falling
+ftdi_layout_init 0x0A68 0xFF7B
+ftdi_channel 0
+ftdi_layout_signal JTAGOE -ndata 0x0010
+ftdi_layout_signal nTRST -data 0x0020
+ftdi_layout_signal nSRST -data 0x0040
+ftdi_layout_signal SWD_EN -data 0x0100
+ftdi_layout_signal SWDIO_OE -data 0x0200
+ftdi_layout_signal LED -ndata 0x0800
+transport select jtag
+
+
+reset_config none
+
+adapter_khz 1000
+
+set _CHIPNAME riscv
+
+jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x10001C05
+
+set _TARGETNAME $_CHIPNAME.cpu
+target create $_TARGETNAME riscv -chain-position $_TARGETNAME
+
+gdb_report_data_abort enable
+gdb_report_register_access_error enable
+
+riscv set_reset_timeout_sec 120
+riscv set_command_timeout_sec 120
+
+# prefer to use sba for system bus access
+riscv set_prefer_sba on
+
+# dump jtag chain
+scan_chain
+
+init
+halt
+echo "Ready for Remote Connections" \ No newline at end of file
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/tests/CMakeLists.txt b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/CMakeLists.txt
new file mode 100644
index 00000000..6726981f
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/CMakeLists.txt
@@ -0,0 +1,28 @@
+cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
+project(core_v_mcu_tests C)
+set(CMAKE_C_STANDARD 99)
+set(CMAKE_C_STANDARD_REQUIRED ON)
+
+set(CORE_V_MCU_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
+
+add_executable(test_irq
+ ${CMAKE_CURRENT_LIST_DIR}/test_irq.c
+ ${CMAKE_CURRENT_LIST_DIR}/mock/mmio_mock.c
+)
+target_include_directories(test_irq PRIVATE
+ ${CMAKE_CURRENT_LIST_DIR}/mock
+ ${CORE_V_MCU_DIR}/include
+)
+
+add_executable(test_timer
+ ${CMAKE_CURRENT_LIST_DIR}/test_timer.c
+ ${CMAKE_CURRENT_LIST_DIR}/mock/mmio_mock.c
+)
+target_include_directories(test_timer PRIVATE
+ ${CMAKE_CURRENT_LIST_DIR}/mock
+ ${CORE_V_MCU_DIR}/include
+)
+
+enable_testing()
+add_test(NAME irq_tests COMMAND test_irq)
+add_test(NAME timer_tests COMMAND test_timer)
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.c b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.c
new file mode 100644
index 00000000..7852d5f1
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.c
@@ -0,0 +1,18 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 "mmio_mock.h"
+
+uint32_t mock_mmio[MOCK_MMIO_SIZE];
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.h b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.h
new file mode 100644
index 00000000..c8839e1e
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/mock/mmio_mock.h
@@ -0,0 +1,45 @@
+/***************************************************************************
+ * Copyright (C) 2026 Eclipse ThreadX contributors
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the MIT License which is available at
+ * https://opensource.org/licenses/MIT.
+ *
+ * AI Disclosure: This file was largely AI-generated by Copilot (Sonnet 4.6).
+ * 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 MMIO_MOCK_H
+#define MMIO_MOCK_H
+
+#include <stdint.h>
+#include <string.h>
+
+#define CORE_V_MCU_MMIO_MOCK 1
+#define MOCK_MMIO_SIZE 1024U
+
+extern uint32_t mock_mmio[MOCK_MMIO_SIZE];
+
+static inline void writew(uint32_t val, uintptr_t addr)
+{
+ if (addr < (uintptr_t)(MOCK_MMIO_SIZE * 4U))
+ {
+ mock_mmio[addr / 4U] = val;
+ }
+}
+
+static inline uint32_t readw(const uintptr_t addr)
+{
+ if (addr < (uintptr_t)(MOCK_MMIO_SIZE * 4U))
+ {
+ return mock_mmio[addr / 4U];
+ }
+
+ return 0U;
+}
+
+#endif /* MMIO_MOCK_H */
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_irq.c b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_irq.c
new file mode 100644
index 00000000..72c17f49
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_irq.c
@@ -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 Copilot (Sonnet 4.6).
+ * 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 <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mmio_mock.h"
+
+static uint32_t mock_mstatus = 0U;
+#define csr_read(csr) (mock_mstatus)
+#define csr_write(csr, val) do { mock_mstatus = (uint32_t)(val); } while (0)
+#define csr_read_clear(csr, val) ({ uint32_t _v = mock_mstatus; mock_mstatus &= ~(uint32_t)(val); _v; })
+#define csr_read_set(csr, val) ({ uint32_t _v = mock_mstatus; mock_mstatus |= (uint32_t)(val); _v; })
+
+#include "bits.h"
+#define PULP_SOC_PERIPHERALS_ADDR 0U
+#define PULP_FC_IRQ_OFFSET 0U
+#include "irq.h"
+#include "../../bsp/irq.c"
+
+static void test_irq_enable(void)
+{
+ memset(mock_mmio, 0, sizeof(mock_mmio));
+ irq_enable(BIT(10));
+ assert(mock_mmio[IRQ_REG_MASK_SET_OFFSET / 4U] == BIT(10));
+ printf("test_irq_enable: PASS\n");
+}
+
+static void test_irq_disable(void)
+{
+ memset(mock_mmio, 0, sizeof(mock_mmio));
+ irq_disable(BIT(10));
+ assert(mock_mmio[IRQ_REG_MASK_CLEAR_OFFSET / 4U] == BIT(10));
+ printf("test_irq_disable: PASS\n");
+}
+
+static void test_irq_clint_enable(void)
+{
+ mock_mstatus = 0U;
+ (void)irq_clint_enable();
+ assert((mock_mstatus & 0x8U) != 0U);
+ printf("test_irq_clint_enable: PASS\n");
+}
+
+int main(void)
+{
+ test_irq_enable();
+ test_irq_disable();
+ test_irq_clint_enable();
+ printf("All IRQ tests passed.\n");
+ return 0;
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_timer.c b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_timer.c
new file mode 100644
index 00000000..3e1499e7
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/tests/test_timer.c
@@ -0,0 +1,53 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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 <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "mmio_mock.h"
+#define PULP_SOC_PERIPHERALS_ADDR 0U
+#define PULP_FC_TIMER_OFFSET 0U
+
+#include "bits.h"
+#include "timer.h"
+#include "timer_irq.h"
+#include "../../bsp/timer_irq.c"
+
+static void test_timer_irq_init(void)
+{
+ uint32_t cfg;
+ int ret;
+
+ memset(mock_mmio, 0, sizeof(mock_mmio));
+ ret = timer_irq_init(327U);
+ assert(ret == 0);
+ assert(mock_mmio[TIMER_CMP_LO_OFFSET / 4U] == 327U);
+
+ cfg = mock_mmio[TIMER_CFG_LO_OFFSET / 4U];
+ assert((cfg & TIMER_CFG_LO_ENABLE_MASK) != 0U);
+ assert((cfg & TIMER_CFG_LO_IRQEN_MASK) != 0U);
+ assert((cfg & TIMER_CFG_LO_CCFG_MASK) != 0U);
+ assert((cfg & TIMER_CFG_LO_MODE_MASK) != 0U);
+ printf("test_timer_irq_init: PASS\n");
+}
+
+int main(void)
+{
+ test_timer_irq_init();
+ printf("All timer tests passed.\n");
+ return 0;
+}
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/tx_initialize_low_level.S b/ports/risc-v32/gnu/example_build/core_v_mcu/tx_initialize_low_level.S
new file mode 100644
index 00000000..3c5a3888
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/tx_initialize_low_level.S
@@ -0,0 +1,63 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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
+ **************************************************************************/
+
+ .section .data
+ .global __tx_free_memory_start
+__tx_free_memory_start:
+
+ .section .text.trap_entry
+ .align 4
+ .global tx_trap_entry
+ .extern _tx_thread_context_save
+ .extern _tx_thread_context_restore
+ .extern tx_trap_handler
+
+tx_trap_entry:
+ addi sp, sp, -128
+ sw ra, 112(sp)
+ call _tx_thread_context_save
+ csrr a0, mcause
+ csrr a1, mepc
+ csrr a2, mtval
+ addi sp, sp, -4
+ sw ra, 0(sp)
+ call tx_trap_handler
+ lw ra, 0(sp)
+ addi sp, sp, 4
+ call _tx_thread_context_restore
+1:
+ wfi
+ j 1b
+
+ .section .text
+ .global _tx_initialize_low_level
+ .weak _tx_initialize_low_level
+ .extern _tx_thread_system_stack_ptr
+ .extern _tx_initialize_unused_memory
+ .extern __vector_start
+
+_tx_initialize_low_level:
+ la t0, _tx_thread_system_stack_ptr
+ sw sp, 0(t0)
+
+ la t0, __tx_free_memory_start
+ la t1, _tx_initialize_unused_memory
+ sw t0, 0(t1)
+
+ la t0, __vector_start
+ ori t0, t0, 1
+ csrw mtvec, t0
+
+ ret
diff --git a/ports/risc-v32/gnu/example_build/core_v_mcu/vectors.S b/ports/risc-v32/gnu/example_build/core_v_mcu/vectors.S
new file mode 100644
index 00000000..f6a57d7a
--- /dev/null
+++ b/ports/risc-v32/gnu/example_build/core_v_mcu/vectors.S
@@ -0,0 +1,25 @@
+/***************************************************************************
+ * 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 Copilot (Sonnet 4.6).
+ * 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
+ **************************************************************************/
+
+ .section .vectors, "ax"
+ .option norvc
+ .align 2
+ .extern tx_trap_entry
+ .global __vector_start
+
+__vector_start:
+ .rept 32
+ jal zero, tx_trap_entry
+ .endr
diff --git a/ports/risc-v32/gnu/src/tx_thread_context_restore.S b/ports/risc-v32/gnu/src/tx_thread_context_restore.S
index 5fa80437..c20cbcdf 100644
--- a/ports/risc-v32/gnu/src/tx_thread_context_restore.S
+++ b/ports/risc-v32/gnu/src/tx_thread_context_restore.S
@@ -149,7 +149,7 @@ _tx_thread_context_restore:
/* Clear MPP/MPIE/MIE bits in t1 then set desired values. */
li t2, 0x1888 // MPP(0x1800) | MPIE(0x80) | MIE(0x08)
- li t3, 0x1800 // Set MPP to Machine mode (bits 12:11)
+ li t3, 0x1880 // Set MPP=Machine (0x1800) + MPIE=1 (0x80); mret sets MIE=MPIE
/* Construct new mstatus in t1: clear mask bits, set MPP/MPIE and optionally FP bit,
preserve everything except the bits we will modify. */
@@ -284,7 +284,7 @@ _tx_thread_no_preempt_restore:
csrr t1, mstatus
li t2, 0x1888 // MPP(0x1800) | MPIE(0x80) | MIE(0x08)
- li t3, 0x1880 // Set MPP=Machine(0x1800) + MPIE(0x80) so mret re-enables MIE
+ li t3, 0x1880 // Set MPP=Machine (0x1800) + MPIE=1 (0x80); mret sets MIE=MPIE
li t4, ~0x1888 // Clear mask for MPP/MPIE/MIE
and t1, t1, t4
or t1, t1, t3