Added an Erbium example to the RISC-V64 GNU port

ThreadX had no board support for Erbium, the OpenHW Foundation CORE-ET RISC-V platform
that Zephyr and NuttX support. Its hart implements only part of the F extension, needs a
4 KiB aligned mtvec and uses the original Shakti UART.

The example runs the standard demo on hart 0 in machine mode. It builds the library and
the demo for rv64imc_zicsr_zifencei with the soft-float lp64 ABI and links without libc
or libgcc, so the pinned riscv64-unknown-elf toolchain can build it. It programs the
machine timer, a polling UART console and the PLIC, writing the source priorities and
threshold that silicon hardwires because the simulator resets them to 0. No shared file
changes.

The demo built without warnings and ran on erbium_emu from et-platform 836a4ab, with all
eight threads reporting and five thread 0 wakeups in 100M cycles, which matches the 2
MHz tick. Separate test images took five PLIC UART interrupts through the ThreadX ISR
path and halted with mcause 2 on an illegal instruction. check_ai_disclosure.sh and
check_ports.sh passed.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>
This commit is contained in:
Afonso Oliveira
2026-09-30 10:49:55 -04:00
committed by Frédéric Desbiens
parent 0a52df8f87
commit 7212b366ae
17 changed files with 1489 additions and 0 deletions
@@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
project(demo_threadx_erbium LANGUAGES C ASM)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(ERBIUM_DIR ${CMAKE_CURRENT_LIST_DIR})
set(THREADX_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../..)
add_subdirectory(${THREADX_ROOT} threadx)
add_executable(demo_threadx
${ERBIUM_DIR}/entry.S
${ERBIUM_DIR}/tx_initialize_low_level.S
${ERBIUM_DIR}/board.c
${ERBIUM_DIR}/hwtimer.c
${ERBIUM_DIR}/plic.c
${ERBIUM_DIR}/trap.c
${ERBIUM_DIR}/uart.c
${ERBIUM_DIR}/demo_threadx.c
)
set_target_properties(demo_threadx PROPERTIES
OUTPUT_NAME "demo_threadx.elf"
LINK_DEPENDS ${ERBIUM_DIR}/link.lds
)
target_include_directories(demo_threadx PRIVATE ${ERBIUM_DIR})
target_link_libraries(demo_threadx PRIVATE threadx)
target_compile_options(demo_threadx PRIVATE
-Wall
-ffreestanding
-ffunction-sections
-fdata-sections
)
target_link_options(demo_threadx PRIVATE
-T${ERBIUM_DIR}/link.lds
-nostdlib
-Wl,--gc-sections
-Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/demo_threadx.map
)
@@ -0,0 +1,125 @@
# ThreadX example for Erbium
This directory runs the standard ThreadX demo on one hart of Erbium, the
16-hart RISC-V platform of the OpenHW Foundation CORE-ET project, using the
RISC-V64 GNU port in machine mode. The public `et-platform` functional
simulator, `erbium_emu`, is the reference target.
## Hardware used
| Item | Address | Notes |
|------|---------|-------|
| Hart 0 | - | RV64IMC, machine mode; other harts are parked |
| MRAM | `0x4000_0200` | 16 MiB window; the first 512 bytes belong to the boot flow |
| System registers | `0x0200_0000` | `SystemConfig` bit 6 enables the UART |
| UART0 | `0x0200_4000` | Original Shakti integration, polling, 115200 8N1 |
| Machine timer | `0x80F4_0200` | `mtime`, with `mtimecmp` at `+0x08` |
| PLIC | `0xA000_0000` | Machine-mode context of hart 0; sources 1 to 6 |
## Port notes
* **Soft float.** Erbium implements only part of the F extension: divide,
square root and conversions between single precision and 64-bit integers
raise an emulation exception. The example and the ThreadX library are
built for `rv64imc_zicsr_zifencei` with the `lp64` ABI, so the port saves
no FP state. `csr.h` rejects a build with F enabled.
* **No C library.** The `riscv64-unknown-elf` toolchain installed by
`scripts/install_riscv.sh` ships `lp64d` libraries only, so the example
links with `-nostdlib` and `board.c` provides `memset`. A missing runtime
helper shows up as a link error, not as a mix of ABIs.
* **No WFI.** The example never executes `wfi`, like the Zephyr Erbium
port. `TX_USE_WFI_IDLE` stays undefined, so the scheduler spins while
idle, and parked harts and fatal-trap halts spin as well.
* **Trap vector.** Erbium ignores `mtvec` base bits 11:1, so the trap
vector must be 4 KiB aligned. `trap_entry` is placed in its own 4 KiB
aligned section and used in direct mode.
* **PLIC.** The silicon hardwires source priorities to 1 and thresholds to
0. The simulator models them as writable registers that reset to 0, so
`plic_init()` writes the hardwired values. Drivers register a callback and
enable their source with `plic_irq_enable()`.
* **Clocks.** The UART divisor assumes a 400 MHz input clock and the tick
assumes a 2 MHz `mtime` rate, the values used by the Zephyr and NuttX
Erbium ports. If the boot firmware programs other rates, define
`ERBIUM_UART_CLOCK_HZ` and `ERBIUM_MTIME_FREQ_HZ` when you build.
* **UART.** The console only polls. `uart_init()` sets the UART enable bit
in `SystemConfig` with a read-modify-write, which leaves the watchdog
setting as the boot flow left it.
## Building
Install the toolchain with `scripts/install_riscv.sh`, or put another
`riscv64-unknown-elf` GCC on `PATH`, then run:
```bash
./build.sh
```
This configures `CMakeLists.txt` with `erbium_gnu.cmake`, builds
`libthreadx.a` for the soft-float ABI and links `build/demo_threadx.elf`.
Set `BUILD_DIR` to build elsewhere.
## Running on the simulator
Build `erbium_emu` from
[et-platform](https://github.com/aifoundry-org/et-platform). The example was
tested at revision `836a4ab600e93c3059bb58c898edbc37744cd8d0`:
```bash
git clone https://github.com/aifoundry-org/et-platform.git
cd et-platform
git checkout 836a4ab600e93c3059bb58c898edbc37744cd8d0
cmake -S erbium-hal -B build-hal -DCMAKE_INSTALL_PREFIX="$PWD/install"
cmake --build build-hal --target install
cmake -S sw-sysemu -B build-emu -GNinja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS=-Wno-error=unused-result \
-DCMAKE_PREFIX_PATH="$PWD/install"
cmake --build build-emu --target erbium_emu
```
At that revision a release build with a distribution GCC that enables
`_FORTIFY_SOURCE` stops on an unused `write()` result in the UART model;
the `CMAKE_CXX_FLAGS` setting above keeps that warning from being an error.
Load the ELF and start hart 0 at the MRAM load address, bypassing the boot
ROM:
```bash
erbium_emu -elf_load build/demo_threadx.elf -reset_pc 0x40000200 \
-minions 1 -single_thread -max_cycles 100000000 \
-uart_rx_file /dev/null -uart_tx_file uart.log
cat uart.log
```
The log starts with `[UART0] : Erbium UART initialized`, followed by lines
from all eight demo threads. The simulator ends with a `max cycles reached`
error once the cycle budget is spent; judge the run by the UART log, not by
the exit status. With the default clocks one ThreadX tick is 2,000,000
simulated cycles, so the 100,000,000-cycle run shows `thread_0` five times.
The simulator verifies register accesses, the trap flow and the console.
It does not verify baud timing or the timer rate of real silicon.
## Files
| File | Purpose |
|------|---------|
| `entry.S` | Reset entry: parks other harts, sets `gp`, stack and `.bss` |
| `tx_initialize_low_level.S` | Trap vector and `_tx_initialize_low_level` |
| `trap.c` | Dispatches the timer, the PLIC and fatal exceptions |
| `board.c` | `board_init()` and `memset` |
| `plic.c`, `plic.h` | PLIC driver for the hart's machine-mode context |
| `hwtimer.c`, `hwtimer.h` | Periodic tick from `mtime` and `mtimecmp` |
| `uart.c`, `uart.h` | Polling console |
| `demo_threadx.c` | The standard ThreadX demo |
| `link.lds` | MRAM layout |
| `erbium_gnu.cmake` | Toolchain file: `rv64imc_zicsr_zifencei`, `lp64` |
| `CMakeLists.txt`, `build.sh` | Build of the library and the demo |
## References
* [Erbium processor](https://github.com/openhwfoundation/core-et-erbium)
* [UART registers](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/uart.md)
* [CPU memory map](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/cpu_mm.md)
* [Platform interrupts](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/interrupts.md)
* [CPU subsystem](https://github.com/openhwfoundation/core-et-erbium/blob/325b32b7efaa2c2ab9c91001c89d3f49a4740826/doc/cpu_subsystem.md)
* [et-platform simulator](https://github.com/aifoundry-org/et-platform)
@@ -0,0 +1,49 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
#include "plic.h"
#include "hwtimer.h"
#include "uart.h"
#include <stdint.h>
#include <stddef.h>
/* The example links without a C library; ThreadX needs memset. */
void *memset(void *des, int c, size_t n)
{
unsigned char *target = des;
for (size_t i = 0; i < n; i++)
target[i] = (unsigned char)c;
return des;
}
int board_init(void)
{
int ret;
ret = plic_init();
if (ret)
return ret;
ret = uart_init();
if (ret)
return ret;
ret = hwtimer_init();
if (ret)
return ret;
return 0;
}
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
##############################################################################
# Copyright (c) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
##############################################################################
# Build the ThreadX library and the Erbium demo, build/demo_threadx.elf.
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
BUILD_DIR=${BUILD_DIR:-"${SCRIPT_DIR}/build"}
cmake -S "${SCRIPT_DIR}" -B "${BUILD_DIR}" -GNinja \
-DCMAKE_TOOLCHAIN_FILE="${SCRIPT_DIR}/erbium_gnu.cmake"
cmake --build "${BUILD_DIR}" --target demo_threadx
printf 'Built %s\n' "${BUILD_DIR}/demo_threadx.elf"
@@ -0,0 +1,66 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/* Machine-mode CSR definitions and helpers for the Erbium example. */
#ifndef RISCV_CSR_H
#define RISCV_CSR_H
/* The Erbium FPU implements only part of the F extension: single-precision
divide, square root and conversions to and from 64-bit integers raise an
emulation exception that this example does not handle. Build for the
integer ISA with the soft-float lp64 ABI. */
#ifdef __riscv_flen
#error "The Erbium example requires a soft-float build: use -march=rv64imc_zicsr_zifencei -mabi=lp64."
#endif
/* Machine Status Register, mstatus */
#define MSTATUS_MIE (1L << 3) /* machine-mode interrupt enable */
#define MSTATUS_MPIE (1L << 7) /* previous MIE */
#define MSTATUS_MPP_M (3L << 11) /* previous mode: machine */
/* Machine Interrupt Enable, mie */
#define MIE_MTIE (1L << 7) /* machine timer */
#define MIE_MEIE (1L << 11) /* machine external */
#ifndef __ASSEMBLER__
#include <stdint.h>
static inline uintptr_t riscv_get_core(void)
{
uintptr_t x;
__asm__ volatile("csrr %0, mhartid" : "=r" (x));
return x;
}
/* Clear mstatus.MIE and return whether it was set. */
static inline int riscv_mintr_save(void)
{
uintptr_t x;
__asm__ volatile("csrrci %0, mstatus, %1" : "=r" (x) : "i" (MSTATUS_MIE) : "memory");
return (x & MSTATUS_MIE) != 0;
}
/* Set mstatus.MIE again if riscv_mintr_save found it set. */
static inline void riscv_mintr_restore(int enabled)
{
if (enabled)
__asm__ volatile("csrsi mstatus, %0" : : "i" (MSTATUS_MIE) : "memory");
}
#endif /* __ASSEMBLER__ */
#endif /* RISCV_CSR_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/* Reset entry for the Erbium example. Hart 0 sets up the global pointer,
the system stack and .bss, then calls main(). Any other hart is parked. */
.section .text.entry, "ax", @progbits
.align 2
.global _start
.extern main
.extern _sysstack_end
.extern _bss_start
.extern _bss_end
_start:
csrr t0, mhartid
bnez t0, _park
.option push
.option norelax
la gp, __global_pointer$ // norelax keeps this load absolute
.option pop
la sp, _sysstack_end
la t0, _bss_start // .bss is 16-byte aligned in link.lds
la t1, _bss_end
_bss_clear:
bgeu t0, t1, _bss_done
sd zero, 0(t0)
addi t0, t0, 8
j _bss_clear
_bss_done:
call main
/* The example never executes WFI; a parked hart spins. */
_park:
j _park
@@ -0,0 +1,39 @@
# /***************************************************************************
# * Copyright (c) 2026 Eclipse ThreadX contributors
# *
# * This program and the accompanying materials are made available under the
# * terms of the MIT License which is available at
# * https://opensource.org/licenses/MIT.
# *
# * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
# * The AI-generated portions may be considered public domain (CC0-1.0)
# * and not subject to the project's licence. The human contributor has
# * reviewed and verified that the code is correct.
# *
# * SPDX-License-Identifier: MIT and CC0-1.0
# ***************************************************************************/
# CMake toolchain file for the Erbium example.
#
# Erbium implements only part of the F extension, so the example and the
# ThreadX library are built for the integer ISA with the soft-float lp64 ABI.
#
# Target ISA : rv64imc_zicsr_zifencei
# ABI : lp64 (64-bit long/ptr, soft-float)
# Code model : medany (MRAM at 0x40000000, peripherals above 2 GiB)
#
# The riscv64-unknown-elf toolchain installed by scripts/install_riscv.sh is
# built for lp64d only, so the example links without libc and libgcc
# (-nostdlib); a missing helper is a link error rather than an ABI mix.
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR risc-v64)
set(THREADX_ARCH "risc-v64")
set(THREADX_TOOLCHAIN "gnu")
set(ARCH_FLAGS "-g -march=rv64imc_zicsr_zifencei -mabi=lp64 -mcmodel=medany")
set(CFLAGS "${ARCH_FLAGS}")
set(ASFLAGS "${ARCH_FLAGS}")
set(LDFLAGS "${ARCH_FLAGS}")
include(${CMAKE_CURRENT_LIST_DIR}/../../../../../cmake/riscv64-unknown-elf.cmake)
@@ -0,0 +1,42 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
#include "tx_api.h"
#include "hwtimer.h"
/* Naturally aligned 64-bit MMIO accesses are single loads and stores on
RV64; volatile keeps the compiler from caching or reordering them. */
#define MTIME (*(volatile uint64_t *)ERBIUM_MTIME)
#define MTIMECMP (*(volatile uint64_t *)ERBIUM_MTIMECMP)
int hwtimer_init(void)
{
MTIMECMP = MTIME + TICKNUM_PER_TIMER;
return 0;
}
int hwtimer_handler(void)
{
/* Advance from the previous compare value, so trap latency does not
accumulate as tick drift. */
uint64_t next = MTIMECMP + TICKNUM_PER_TIMER;
uint64_t now = MTIME;
if (next <= now)
next = now + TICKNUM_PER_TIMER;
MTIMECMP = next;
return 0;
}
@@ -0,0 +1,41 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
#ifndef RISCV_HWTIMER_H
#define RISCV_HWTIMER_H
#include <stdint.h>
/* Erbium machine timer. mtime and a single mtimecmp live in the CPU
register block; mtime_local_target selects which harts see the timer
interrupt and resets to all harts.
The mtime rate depends on the reference clock and time_config, which the
boot firmware programs. The default is the 2 MHz timebase used by the
Zephyr and NuttX Erbium ports; define ERBIUM_MTIME_FREQ_HZ when the
firmware selects another rate. */
#define ERBIUM_MTIME 0x80F40200UL
#define ERBIUM_MTIMECMP 0x80F40208UL
#ifndef ERBIUM_MTIME_FREQ_HZ
#define ERBIUM_MTIME_FREQ_HZ 2000000UL
#endif
#define TICKNUM_PER_TIMER (ERBIUM_MTIME_FREQ_HZ / TX_TIMER_TICKS_PER_SECOND)
int hwtimer_init(void);
int hwtimer_handler(void);
#endif /* RISCV_HWTIMER_H */
@@ -0,0 +1,92 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/* Erbium memory layout.
*
* Address Size Usage
* ───────────── ──────── ──────────────────────────────────────
* 0x0200_0000 4 KiB System registers (SystemConfig at +0x08)
* 0x0200_4000 4 KiB UART0
* 0x4000_0000 512 B MRAM, reserved for the boot flow
* 0x4000_0200 ~16 MiB MRAM: code, data, stacks and free memory
* 0x80F4_0200 16 B mtime, mtimecmp
* 0xA000_0000 64 MiB PLIC
*
* MRAM is directly readable, writable and executable, so .data runs in
* place and needs no copy from a load address.
*/
OUTPUT_ARCH( "riscv" )
ENTRY( _start )
MEMORY
{
MRAM (rwx) : ORIGIN = 0x40000200, LENGTH = 0x00FFFE00
}
PHDRS
{
text PT_LOAD FLAGS(5); /* PF_R | PF_X */
data PT_LOAD FLAGS(6); /* PF_R | PF_W */
}
SECTIONS
{
/* _start must stay at the load address, so the 4 KiB aligned trap
vector goes in the next output section rather than this one. */
.text.entry : {
KEEP(*(.text.entry))
} > MRAM :text
.text : {
KEEP(*(.text.trap))
*(.text .text.*)
} > MRAM :text
.rodata : {
. = ALIGN(16);
*(.srodata .srodata.*)
. = ALIGN(16);
*(.rodata .rodata.*)
} > MRAM :text
.data : {
. = ALIGN(16);
PROVIDE( __global_pointer$ = . + 0x800 );
*(.sdata .sdata.*)
. = ALIGN(16);
*(.data .data.*)
} > MRAM :data
.bss (NOLOAD) : {
. = ALIGN(16);
_bss_start = .;
*(.sbss .sbss.*)
. = ALIGN(16);
*(.bss .bss.*)
*(COMMON)
. = ALIGN(16);
_bss_end = .;
} > MRAM :data
.stack (NOLOAD) : {
. = ALIGN(16);
_sysstack_start = .;
. += 0x1000;
_sysstack_end = .;
} > MRAM :data
PROVIDE(_end = .);
}
@@ -0,0 +1,112 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/* PLIC driver for Erbium, machine-mode context of the current hart. */
#include "plic.h"
#include <stddef.h>
static irq_callback callbacks[MAX_CALLBACK_NUM];
/* Source 0 is reserved and the six sources fit in the first enable word. */
static int plic_source_valid(int irqno)
{
return (irqno > 0) && (irqno < MAX_CALLBACK_NUM);
}
void plic_irq_enable(int irqno)
{
volatile uint32_t *reg = (volatile uint32_t *)PLIC_MENABLE(riscv_get_core());
if (!plic_source_valid(irqno))
return;
*reg = *reg | (1u << (unsigned int)irqno);
}
void plic_irq_disable(int irqno)
{
volatile uint32_t *reg = (volatile uint32_t *)PLIC_MENABLE(riscv_get_core());
if (!plic_source_valid(irqno))
return;
*reg = *reg & ~(1u << (unsigned int)irqno);
}
int plic_register_callback(int irqno, irq_callback callback)
{
if (!plic_source_valid(irqno))
return -1;
callbacks[irqno] = callback;
return 0;
}
int plic_unregister_callback(int irqno)
{
return plic_register_callback(irqno, NULL);
}
int plic_init(void)
{
uintptr_t hart = riscv_get_core();
for (int i = 0; i < MAX_CALLBACK_NUM; i++)
callbacks[i] = NULL;
/* Do not depend on the reset state or on a prior boot stage: start
with every source disabled for this context. Drivers enable their
own sources. */
*(volatile uint32_t *)PLIC_MENABLE(hart) = 0;
/* The values the silicon hardwires; see plic.h. */
*(volatile uint32_t *)PLIC_MTHRESHOLD(hart) = 0;
for (int i = 1; i <= PLIC_NUM_SOURCES; i++)
*(volatile uint32_t *)PLIC_PRIORITY(i) = 1;
return 0;
}
int plic_claim(void)
{
return (int)(*(volatile uint32_t *)PLIC_MCLAIM(riscv_get_core()));
}
void plic_complete(int irqno)
{
*(volatile uint32_t *)PLIC_MCOMPLETE(riscv_get_core()) = (uint32_t)irqno;
}
int plic_irq_intr(void)
{
int ret = -1;
int irqno = plic_claim();
/* Nothing pending: another context claimed it first. */
if (irqno == 0)
return 0;
if (!plic_source_valid(irqno))
{
plic_complete(irqno);
return -1;
}
if (callbacks[irqno] != NULL)
ret = (callbacks[irqno])(irqno);
plic_complete(irqno);
return ret;
}
@@ -0,0 +1,54 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
#ifndef RISCV_PLIC_H
#define RISCV_PLIC_H
#include "csr.h"
#include <stdint.h>
/* Erbium PLIC, RISC-V PLIC 1.0. Each hart has two contexts, machine mode
first, so hart h uses context 2h.
The silicon hardwires source priorities to 1 and context thresholds to
0, so an enabled, pending source always interrupts. The et-platform
simulator implements them as writable registers that reset to 0, which
masks every source, so plic_init() programs the silicon values. There is
no priority control beyond that.
Sources: 1 MRAM, 2 QSPI, 3 UART, 4 system register, 5 xSPI, 6 GPIO. */
#define PLIC 0xA0000000UL
#define PLIC_PRIORITY(irqno) (PLIC + (unsigned long)(irqno) * 4UL)
#define PLIC_MENABLE(hart) (PLIC + 0x2000UL + (hart) * 0x100UL)
#define PLIC_MTHRESHOLD(hart) (PLIC + 0x200000UL + (hart) * 0x2000UL)
#define PLIC_MCLAIM(hart) (PLIC + 0x200004UL + (hart) * 0x2000UL)
#define PLIC_MCOMPLETE(hart) PLIC_MCLAIM(hart)
#define PLIC_NUM_SOURCES 6
#define MAX_CALLBACK_NUM (PLIC_NUM_SOURCES + 1)
typedef int (*irq_callback)(int irqno);
void plic_irq_enable(int irqno);
void plic_irq_disable(int irqno);
int plic_register_callback(int irqno, irq_callback callback);
int plic_unregister_callback(int irqno);
int plic_init(void);
int plic_claim(void);
void plic_complete(int irqno);
int plic_irq_intr(void);
#endif /* RISCV_PLIC_H */
@@ -0,0 +1,78 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/* Machine-mode trap dispatcher for the Erbium example. */
#include "csr.h"
#include "uart.h"
#include "hwtimer.h"
#include "plic.h"
#include <stdint.h>
#include <tx_api.h>
#define MCAUSE_INT_BIT ((uintptr_t)1 << (__riscv_xlen - 1))
#define MCAUSE_M_TIMER (MCAUSE_INT_BIT | 7u)
#define MCAUSE_M_EXTERNAL (MCAUSE_INT_BIT | 11u)
extern void _tx_timer_interrupt(void);
static void print_hex(uintptr_t val)
{
const char digits[] = "0123456789ABCDEF";
uart_putc('0');
uart_putc('x');
for (int i = (int)(sizeof(uintptr_t) * 2) - 1; i >= 0; i--)
uart_putc(digits[(val >> (i * 4)) & 0xFu]);
uart_putc('\r');
uart_putc('\n');
}
static void trap_halt(const char *reason, uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
{
uart_puts(reason);
uart_puts("mcause:");
print_hex(mcause);
uart_puts("mepc:");
print_hex(mepc);
uart_puts("mtval:");
print_hex(mtval);
/* Spin rather than WFI, as everywhere else in this example. */
while (1)
;
}
void trap_handler(uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
{
if (mcause == MCAUSE_M_TIMER)
{
hwtimer_handler();
_tx_timer_interrupt();
}
else if (mcause == MCAUSE_M_EXTERNAL)
{
if (plic_irq_intr() != 0)
trap_halt("[INTERRUPT]: external interrupt not handled, halting", mcause, mepc, mtval);
}
else if ((mcause & MCAUSE_INT_BIT) != 0u)
{
trap_halt("[INTERRUPT]: unexpected interrupt, halting", mcause, mepc, mtval);
}
else
{
trap_halt("[EXCEPTION]: unhandled exception, halting", mcause, mepc, mtval);
}
}
@@ -0,0 +1,142 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
#include "csr.h"
#include "tx_port.h"
/* Erbium ignores mtvec base bits 11:1, so the trap vector must be 4 KiB
aligned. */
.section .text.trap, "ax", @progbits
.balign 4096
/**************************************************************************/
/* */
/* FUNCTION RELEASE */
/* */
/* trap_entry RISC-V64/GNU */
/* */
/* DESCRIPTION */
/* */
/* This function is the machine-mode trap vector (direct mode). It */
/* saves the interrupted context, calls trap_handler with mcause, */
/* mepc and mtval, and leaves through the ThreadX context restore. */
/* */
/* INPUT */
/* */
/* None */
/* */
/* OUTPUT */
/* */
/* None */
/* */
/* CALLS */
/* */
/* _tx_thread_context_save Save interrupted context */
/* trap_handler Dispatch the trap */
/* _tx_thread_context_restore Restore or schedule a thread */
/* */
/* CALLED BY */
/* */
/* Hardware trap */
/* */
/**************************************************************************/
.global trap_entry
.extern trap_handler
.extern _tx_thread_context_save
.extern _tx_thread_context_restore
trap_entry:
addi sp, sp, -TX_RISCV_TRAP_FRAME_SIZE // Interrupt frame from the port contract
sd x1, 28*8(sp) // Save RA before the call below overwrites it
call _tx_thread_context_save
csrr a0, mcause
csrr a1, mepc
csrr a2, mtval
addi sp, sp, -16 // 16 bytes keep sp 16-byte aligned at the call
sd ra, 0(sp)
call trap_handler
ld ra, 0(sp)
addi sp, sp, 16
call _tx_thread_context_restore
/* _tx_thread_context_restore does not return. */
_trap_entry_hang:
j _trap_entry_hang
.section .text
/**************************************************************************/
/* */
/* FUNCTION RELEASE */
/* */
/* _tx_initialize_low_level RISC-V64/GNU */
/* */
/* DESCRIPTION */
/* */
/* This function is responsible for any low-level processor */
/* initialization, including setting up interrupt vectors, setting */
/* up a periodic timer interrupt source, saving the system stack */
/* pointer for use in ISR processing later, and finding the first */
/* available RAM memory address for tx_application_define. */
/* */
/* INPUT */
/* */
/* None */
/* */
/* OUTPUT */
/* */
/* None */
/* */
/* CALLS */
/* */
/* board_init Initialize the peripherals */
/* */
/* CALLED BY */
/* */
/* _tx_initialize_kernel_enter ThreadX entry function */
/* */
/**************************************************************************/
/* VOID _tx_initialize_low_level(VOID)
{ */
.global _tx_initialize_low_level // Strong definition: must beat the port's weak default
.extern _end
.extern board_init
.extern _tx_thread_system_stack_ptr
.extern _tx_initialize_unused_memory
_tx_initialize_low_level:
la t0, _tx_thread_system_stack_ptr
sd sp, 0(t0) // Save system stack pointer
la t0, _end // Pickup first free address
la t1, _tx_initialize_unused_memory
sd t0, 0(t1) // Save unused memory address
li t0, MSTATUS_MIE
csrrc zero, mstatus, t0 // Keep interrupts off until the scheduler runs
li t0, (MSTATUS_MPP_M | MSTATUS_MPIE)
csrrs zero, mstatus, t0 // mret stays in M-mode with interrupts enabled
li t0, (MIE_MTIE | MIE_MEIE)
csrrs zero, mie, t0 // Enable timer and external interrupts
addi sp, sp, -16 // 16 bytes keep sp 16-byte aligned at the call
sd ra, 0(sp)
call board_init
ld ra, 0(sp)
addi sp, sp, 16
la t0, trap_entry
csrw mtvec, t0 // Direct mode: trap_entry is 4 KiB aligned
ret
/* } */
@@ -0,0 +1,106 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
/* Polling console driver for the Erbium UART (original Shakti integration).
*
* Offset Name Access Description
* ────── ──────────── ────── ─────────────────────────────────────
* 0x00 BaudReg R/W Divisor: clock / (16 * baud)
* 0x08 TxReg W Transmit data
* 0x10 RxReg R Receive data
* 0x18 StatusReg R FIFO and error status
* 0x20 DelayReg R/W Transmit delay
* 0x28 ControlReg R/W Character size, parity, stop bits
* 0x30 InterruptEn R/W Interrupt enables
* 0x40 Rx_Threshold R/W Receive FIFO threshold
*
* All registers are accessed as 32-bit words. The transmit and receive
* FIFOs hold 16 entries each.
*/
#include "uart.h"
#include "csr.h"
#include <stdint.h>
#define UART_DIVISOR (ERBIUM_UART_CLOCK_HZ / (16UL * ERBIUM_UART_BAUD))
#if (UART_DIVISOR == 0) || (UART_DIVISOR > 0xFFFF)
#error "The Erbium UART baud divisor must be in the range 1..65535."
#endif
/* SystemConfig in the system register block gates the UART. */
#define SYSREG_SYSTEM_CONFIG 0x02000008UL
#define SYSTEM_CONFIG_UART_EN (1u << 6)
#define UART_BAUD 0x00
#define UART_TX 0x08
#define UART_STATUS 0x18
#define UART_DELAY 0x20
#define UART_CONTROL 0x28
#define UART_IEN 0x30
#define UART_RX_THRESHOLD 0x40
#define STATUS_TX_FULL (1u << 1)
#define CONTROL_CHARSIZE_SHIFT 5 /* bits 10:5; stop bits and parity 0 */
#define REG32(addr) (*(volatile uint32_t *)(addr))
#define UART_REG(off) REG32(UART0 + (off))
int uart_init(void)
{
/* Ungate the UART. Read-modify-write keeps the watchdog and the
other peripheral enables as the boot flow left them. */
REG32(SYSREG_SYSTEM_CONFIG) = REG32(SYSREG_SYSTEM_CONFIG) | SYSTEM_CONFIG_UART_EN;
/* Polling only: no UART interrupts. */
UART_REG(UART_IEN) = 0;
/* 8 data bits, no parity, one stop bit. */
UART_REG(UART_BAUD) = (uint32_t)UART_DIVISOR;
UART_REG(UART_CONTROL) = 8u << CONTROL_CHARSIZE_SHIFT;
UART_REG(UART_DELAY) = 0;
UART_REG(UART_RX_THRESHOLD) = 0;
puts("[UART0] : Erbium UART initialized");
return 0;
}
static inline void uart_putc_nolock(int ch)
{
while ((UART_REG(UART_STATUS) & STATUS_TX_FULL) != 0u)
;
UART_REG(UART_TX) = (uint32_t)(ch & 0xFF);
}
int uart_putc(int ch)
{
int intr_enable = riscv_mintr_save();
uart_putc_nolock(ch);
riscv_mintr_restore(intr_enable);
return 1;
}
int uart_puts(const char *str)
{
int i;
int intr_enable = riscv_mintr_save();
for (i = 0; str[i] != 0; i++)
uart_putc_nolock(str[i]);
uart_putc_nolock('\r');
uart_putc_nolock('\n');
riscv_mintr_restore(intr_enable);
return i;
}
@@ -0,0 +1,40 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
**************************************************************************/
#ifndef RISCV_UART_H
#define RISCV_UART_H
/* Erbium UART0: the original Shakti UART integration, with 32-bit registers
at eight-byte intervals. It is PLIC source 3. */
#define UART0 0x02004000UL
#define UART0_IRQ 3
/* The UART input clock and the console rate. The defaults match the
public et-platform simulator; define these on the command line when the
boot firmware programs a different peripheral clock. */
#ifndef ERBIUM_UART_CLOCK_HZ
#define ERBIUM_UART_CLOCK_HZ 400000000UL
#endif
#ifndef ERBIUM_UART_BAUD
#define ERBIUM_UART_BAUD 115200UL
#endif
#define puts uart_puts
int uart_init(void);
int uart_putc(int ch);
int uart_puts(const char *str);
#endif /* RISCV_UART_H */