Added simulator tests for Erbium interrupt regressions

Long polling UART writes can mask timer interrupts across several tick periods.
PLIC enable-word updates can overwrite a change made by an interrupt handler.
The board example had no simulator tests for either case.

Added two simulator images and a runner. One compares hardware timer progress with
ThreadX ticks after a long print. The other toggles one PLIC source in timer
context while a thread changes another. Both tests fail on the PR code and pass
with the corresponding local fixes.

GCC 15.2.0 and Ninja built both images. erbium_emu reproduced both failures;
control runs passed both tests. Port consistency checks passed. No silicon run.

Assisted-by: Codex (GPT-6-Sol) <noreply@openai.com>
This commit is contained in:
Frédéric Desbiens
2026-09-30 10:49:55 -04:00
parent 7212b366ae
commit f19d8c267d
5 changed files with 306 additions and 0 deletions
@@ -41,3 +41,28 @@ target_link_options(demo_threadx PRIVATE
-Wl,--gc-sections
-Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/demo_threadx.map
)
function(add_erbium_test name source handler)
add_executable(${name}
${ERBIUM_DIR}/entry.S
${ERBIUM_DIR}/tx_initialize_low_level.S
${ERBIUM_DIR}/board.c
${ERBIUM_DIR}/hwtimer.c
${ERBIUM_DIR}/plic.c
${ERBIUM_DIR}/uart.c
${source}
${handler}
)
target_include_directories(${name} PRIVATE ${ERBIUM_DIR})
target_link_libraries(${name} PRIVATE threadx)
target_compile_options(${name} PRIVATE -ffreestanding)
target_link_options(${name} PRIVATE
-T${ERBIUM_DIR}/link.lds
-nostdlib
-Wl,--gc-sections
)
set_target_properties(${name} PROPERTIES LINK_DEPENDS ${ERBIUM_DIR}/link.lds)
endfunction()
add_erbium_test(test_uart_ticks ${ERBIUM_DIR}/test/test_uart_ticks.c ${ERBIUM_DIR}/trap.c)
add_erbium_test(test_plic_race ${ERBIUM_DIR}/test/test_plic_race.c ${ERBIUM_DIR}/test/test_plic_trap.c)
@@ -0,0 +1,55 @@
#!/bin/sh
##############################################################################
# Copyright (c) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Codex (GPT-6-Sol).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0
##############################################################################
set -eu
ERBIUM_EMU=${ERBIUM_EMU:?Set ERBIUM_EMU to the erbium_emu executable}
TEST_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
BUILD_DIR=${BUILD_DIR:-"${TEST_DIR}/build-tests"}
RUN_DIR=$(mktemp -d)
trap 'rm -rf "$RUN_DIR"' EXIT HUP INT TERM
failed=0
cmake -S "$TEST_DIR" -B "$BUILD_DIR" -GNinja \
-DCMAKE_TOOLCHAIN_FILE="$TEST_DIR/erbium_gnu.cmake"
cmake --build "$BUILD_DIR" --target test_uart_ticks test_plic_race
for name in uart_ticks plic_race; do
case "$name" in
uart_ticks)
cycles=20000000
expected=TEST_UART_NO_LOST_TICKS
;;
plic_race)
cycles=50000000
expected=TEST_PLIC_NO_LOST_UPDATE
;;
esac
"$ERBIUM_EMU" -elf_load "$BUILD_DIR/test_$name" \
-reset_pc 0x40000200 -minions 1 -single_thread \
-max_cycles "$cycles" -uart_rx_file /dev/null \
-uart_tx_file "$RUN_DIR/$name.uart" > "$RUN_DIR/$name.run" 2>&1 || true
if grep -q -F "$expected" "$RUN_DIR/$name.uart" &&
grep -q -F 'max cycles reached' "$RUN_DIR/$name.run"; then
printf '%s: PASS\n' "$name"
else
printf '%s: FAIL: ' "$name"
grep -Eo 'TEST_[A-Z_]+' "$RUN_DIR/$name.uart" | tail -1 || true
failed=1
fi
done
exit "$failed"
@@ -0,0 +1,90 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Codex (GPT-6-Sol).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
***************************************************************************/
#include "tx_api.h"
#include "csr.h"
#include "hwtimer.h"
#include "plic.h"
#include "uart.h"
#define TEST_TIMER_INTERVAL 8u
#define TEST_REQUIRED_INTERRUPTS 10000u
static TX_THREAD test_thread;
static UCHAR test_stack[2048];
volatile unsigned int test_irq_count;
volatile unsigned int test_expected_bit;
/* Run the kernel with the board's normal interrupt setup. */
int main(void)
{
tx_kernel_enter();
return 0;
}
/* Compare the PLIC word and the ISR's expected bit atomically. */
static int test_bit_matches(void)
{
int enabled = riscv_mintr_save();
/* MISRA C:2012 Rule 11.4 deviation: the PLIC has a fixed MMIO address. */
uint32_t actual = *(volatile uint32_t *)PLIC_MENABLE(0u) & (1u << 2);
unsigned int expected = test_expected_bit;
riscv_mintr_restore(enabled);
return actual == (expected != 0u ? (1u << 2) : 0u);
}
/* Change source 1 while the timer ISR changes source 2. */
static void test_entry(ULONG input)
{
(void)input;
/* MISRA C:2012 Rule 11.4 deviation: the timer has a fixed MMIO address. */
*(volatile uint64_t *)ERBIUM_MTIMECMP =
*(volatile uint64_t *)ERBIUM_MTIME + TEST_TIMER_INTERVAL;
while (test_irq_count < TEST_REQUIRED_INTERRUPTS)
{
plic_irq_enable(1);
if (!test_bit_matches())
{
uart_puts("TEST_PLIC_LOST_UPDATE");
for (;;)
{
}
}
plic_irq_disable(1);
if (!test_bit_matches())
{
uart_puts("TEST_PLIC_LOST_UPDATE");
for (;;)
{
}
}
}
uart_puts("TEST_PLIC_NO_LOST_UPDATE");
for (;;)
{
}
}
/* Start one test thread. */
void tx_application_define(void *first_unused_memory)
{
(void)first_unused_memory;
(void)tx_thread_create(&test_thread, "plic test", test_entry, 0u,
test_stack, sizeof(test_stack), 1u, 1u,
TX_NO_TIME_SLICE, TX_AUTO_START);
}
@@ -0,0 +1,55 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Codex (GPT-6-Sol).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
***************************************************************************/
#include "tx_api.h"
#include "hwtimer.h"
#include "plic.h"
#include "uart.h"
#include <stdint.h>
#define TEST_TIMER_INTERVAL 8u
#define TEST_MCAUSE_TIMER (((uintptr_t)1u << 63) | 7u)
extern volatile unsigned int test_irq_count;
extern volatile unsigned int test_expected_bit;
/* Toggle source 2 in timer context to preempt source 1 updates. */
void trap_handler(uintptr_t mcause, uintptr_t mepc, uintptr_t mtval)
{
(void)mepc;
(void)mtval;
if (mcause != TEST_MCAUSE_TIMER)
{
uart_puts("TEST_PLIC_UNEXPECTED_TRAP");
for (;;)
{
}
}
/* MISRA C:2012 Rule 11.4 deviation: the timer has a fixed MMIO address. */
*(volatile uint64_t *)ERBIUM_MTIMECMP =
*(volatile uint64_t *)ERBIUM_MTIME + TEST_TIMER_INTERVAL;
if (test_expected_bit == 0u)
{
plic_irq_enable(2);
test_expected_bit = 1u;
}
else
{
plic_irq_disable(2);
test_expected_bit = 0u;
}
test_irq_count++;
}
@@ -0,0 +1,81 @@
/***************************************************************************
* Copyright (c) 2026 Eclipse ThreadX contributors
*
* This program and the accompanying materials are made available under the
* terms of the MIT License which is available at
* https://opensource.org/licenses/MIT.
*
* AI Disclosure: This file was largely AI-generated by Codex (GPT-6-Sol).
* The AI-generated portions may be considered public domain (CC0-1.0)
* and not subject to the project's licence. The human contributor has
* reviewed and verified that the code is correct.
*
* SPDX-License-Identifier: MIT and CC0-1.0
***************************************************************************/
#include "tx_api.h"
#include "uart.h"
#include "hwtimer.h"
#define TEST_TEXT_SIZE 160000u
static TX_THREAD test_thread;
static UCHAR test_stack[2048];
static char test_text[TEST_TEXT_SIZE + 1u];
/* Run the kernel with the board's normal interrupt setup. */
int main(void)
{
tx_kernel_enter();
return 0;
}
/* Print long enough to span several hardware timer intervals. */
static void test_entry(ULONG input)
{
uint64_t start;
uint64_t elapsed;
ULONG ticks_before;
ULONG ticks_after;
size_t i;
(void)input;
for (i = 0u; i < TEST_TEXT_SIZE; i++)
{
test_text[i] = 'X';
}
test_text[TEST_TEXT_SIZE] = '\0';
/* MISRA C:2012 Rule 11.4 deviation: the timer has a fixed MMIO address. */
start = *(volatile uint64_t *)ERBIUM_MTIME;
ticks_before = tx_time_get();
(void)uart_puts(test_text);
elapsed = *(volatile uint64_t *)ERBIUM_MTIME - start;
ticks_after = tx_time_get();
if (elapsed <= (2u * TICKNUM_PER_TIMER))
{
uart_puts("TEST_UART_TOO_SHORT");
}
else if ((ticks_after - ticks_before) < 2u)
{
uart_puts("TEST_UART_LOST_TICKS");
}
else
{
uart_puts("TEST_UART_NO_LOST_TICKS");
}
for (;;)
{
}
}
/* Start one test thread. */
void tx_application_define(void *first_unused_memory)
{
(void)first_unused_memory;
(void)tx_thread_create(&test_thread, "uart test", test_entry, 0u,
test_stack, sizeof(test_stack), 1u, 1u,
TX_NO_TIME_SLICE, TX_AUTO_START);
}