diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 40689c2..65de27a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,8 +139,8 @@ jobs: build/dmboot.map retention-days: 30 - test-renode: - name: Test Renode Simulation + test-renode-stm32f746g-disco: + name: Renode emulation test (stm32f746g-disco) runs-on: ubuntu-latest container: image: chocotechnologies/dmboot:1.0.0 @@ -164,15 +164,42 @@ jobs: run: | git submodule init git submodule update --recursive - - - name: Run Renode emulation tests + + - name: Configure cmake for stm32f746g-disco with Renode emulation + run: | + cmake -DCMAKE_BUILD_TYPE=Debug \ + -DBOARD=stm32f746g-disco \ + -DDMBOOT_EMULATION=ON \ + -S . \ + -B build + + - name: Build firmware for stm32f746g-disco + run: | + cmake --build build --config Debug + + - name: Install firmware for Renode + run: | + cmake --build build --target install-firmware + ls -lh build/renode_firmware.elf + + - name: Run Renode emulation and verify firmware logs run: | - ./scripts/run_renode_tests.sh + ./scripts/run_renode_tests.sh --skip-build . build + + - name: Upload Renode logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: renode-logs-stm32f746g-disco + path: | + build/connect.log + build/monitor.log + retention-days: 7 build-all: name: All builds completed runs-on: ubuntu-latest - needs: [build-cmake, test-renode] + needs: [build-cmake, test-renode-stm32f746g-disco] permissions: contents: read diff --git a/configs/renode/expected_logs.txt b/configs/renode/expected_logs.txt index ead1457..162ac94 100644 --- a/configs/renode/expected_logs.txt +++ b/configs/renode/expected_logs.txt @@ -1,5 +1,29 @@ # Expected log messages from DMOD-Boot firmware running in Renode -# Each line is a pattern that must appear in the monitor-gdb output -# Lines starting with # are comments and empty lines are ignored +# on the stm32f746g-disco board configuration. +# +# This test catches the hard-fault that occurred when .dmod.inputs was not +# correctly copied to RAM during startup. A corrupt .dmod.inputs caused +# Dmod_ConnectApi to write garbage function pointers into loaded modules' +# .dmod.outputs sections, leading to an immediate hard-fault whenever a +# loaded driver (e.g. dmgpio via dmdevfs) tried to log a message. +# +# With the fix applied the boot sequence is: +# 1. Heap initialised +# 2. modules.dmp loaded, dmell enabled +# 3. Filesystems mounted (/configs, /dev) -- requires the dmvfs pre-RTOS +# mutex fallback fix so that mounts succeed before vTaskStartScheduler +# 4. FreeRTOS scheduler starts +# 5. dmell loads and configures board drivers (dmgpio, dmclk, etc.) +# -- this is where the hard-fault used to occur with the corrupt +# .dmod.inputs linker bug +# +# Lines starting with ! must NOT appear in the log (crash guard). +# The pattern matched is everything after the leading '!'. +# All other non-empty, non-comment lines must appear in the log. -DMOD-Boot started \ No newline at end of file +# --- crash guard: a hard-fault means something is still broken --- +!HardFault_Handler invoked! + +# --- positive checks: firmware must reach these milestones --- +Heap initialized +DMOD-Boot started diff --git a/linker/common.ld b/linker/common.ld index 05da257..aea2bd6 100644 --- a/linker/common.ld +++ b/linker/common.ld @@ -149,9 +149,40 @@ SECTIONS . = ALIGN(4); *(.data .data.* .gnu.linkonce.d.*) - } > ram AT > rom + /* + * Include DMOD input/output sections inside .data so that the startup + * data copy (from __data_init_start__ to __data_end__) correctly handles + * them. Placing these sections outside .data with "> ram AT > rom" would + * create VMA alignment gaps (the ". = ALIGN(16)" below) that have no + * corresponding LMA gaps, causing the startup copy to corrupt the .inputs + * section and produce garbage function-pointer values at runtime + * (e.g. Dmod_EnterCritical = 0xd3aefbda → hard fault). + * + * When the subsections live inside a single .data section the linker + * inserts the same alignment padding in both the VMA (RAM) and LMA (ROM) + * representations of the section, so the byte-for-byte startup copy + * remains correct. + */ + + . = ALIGN(16); + __inputs_start = .; + PROVIDE(__dmod_inputs_start = .); + KEEP(*(.dmod.inputs)) + __inputs_end = .; + PROVIDE(__dmod_inputs_end = .); + __inputs_size = __inputs_end - __inputs_start; + PROVIDE(__dmod_inputs_size = __inputs_end - __inputs_start); + + . = ALIGN(16); + __outputs_start = .; + PROVIDE(__dmod_outputs_start = .); + KEEP(*(.dmod.outputs)) + __outputs_end = .; + PROVIDE(__dmod_outputs_end = .); + __outputs_size = __outputs_end - __outputs_start; + PROVIDE(__dmod_outputs_size = __outputs_end - __outputs_start); - INCLUDE dmod-system.ld + } > ram AT > rom . = ALIGN(4); __data_end__ = .; diff --git a/scripts/run_renode_tests.sh b/scripts/run_renode_tests.sh index 33bf1bd..fc9517e 100755 --- a/scripts/run_renode_tests.sh +++ b/scripts/run_renode_tests.sh @@ -1,21 +1,38 @@ #!/bin/bash # run_renode_tests.sh - Run Renode emulation tests for dmod-boot # -# This script reproduces the Renode CI tests locally. -# It builds the firmware with emulation mode, starts Renode, runs monitor-gdb -# to capture firmware logs, and verifies the expected log messages. +# This script builds the stm32f746g-disco firmware with Renode emulation mode, +# starts Renode, runs monitor-gdb to capture firmware logs, and verifies that +# the expected log messages appear. In particular it catches the hard-fault +# that occurs when .dmod.inputs is not correctly copied to RAM at startup +# (which prevents Dmod_EnterCritical and other output function pointers from +# being connected when modules are loaded at runtime). # # Usage: -# ./scripts/run_renode_tests.sh [SOURCE_DIR [BUILD_DIR]] +# ./scripts/run_renode_tests.sh [OPTIONS] [SOURCE_DIR [BUILD_DIR]] # -# SOURCE_DIR Path to the project root. -# Defaults to the parent directory of this script. -# BUILD_DIR Path to the build directory. -# Defaults to SOURCE_DIR/build. +# --skip-build Skip the cmake configure + build steps. Use this when the +# firmware has already been built (e.g. in a prior CI step) +# and only the Renode emulation part needs to be run. +# SOURCE_DIR Path to the project root. +# Defaults to the parent directory of this script. +# BUILD_DIR Path to the build directory. +# Defaults to SOURCE_DIR/build. set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +SKIP_BUILD=0 +POSITIONAL=() +for arg in "$@"; do + case "$arg" in + --skip-build) SKIP_BUILD=1 ;; + *) POSITIONAL+=("$arg") ;; + esac +done +set -- "${POSITIONAL[@]}" + SOURCE_DIR="${1:-$(cd "$SCRIPT_DIR/.." && pwd)}" BUILD_DIR="${2:-$SOURCE_DIR/build}" @@ -29,37 +46,63 @@ MONITOR_TIMEOUT=30 echo "==============================================" echo " dmod-boot Renode emulation tests" +echo " Board: $BOARD" echo "==============================================" echo "Source dir : $SOURCE_DIR" echo "Build dir : $BUILD_DIR" echo "Board : $BOARD" +echo "Skip build : $SKIP_BUILD" echo "" -# ------------------------------------------------------- -# Step 1 – Build firmware with emulation mode enabled -# ------------------------------------------------------- -echo "[1/4] Building firmware with emulation mode enabled..." -cmake -DCMAKE_BUILD_TYPE=Debug \ - -DBOARD="$BOARD" \ - -DDMBOOT_EMULATION=ON \ - -S "$SOURCE_DIR" \ - -B "$BUILD_DIR" -cmake --build "$BUILD_DIR" --config Debug -echo "✓ Build completed" -echo "" - -# ------------------------------------------------------- -# Step 2 – Verify install-firmware target -# ------------------------------------------------------- -echo "[2/4] Testing install-firmware target..." -cmake --build "$BUILD_DIR" --target install-firmware -if [ ! -f "$BUILD_DIR/renode_firmware.elf" ]; then - echo "✗ renode_firmware.elf not found after install-firmware" - exit 1 +# Cleanup helper – kill any lingering background processes on exit +cleanup() { + if [ -n "$CONNECT_PID" ] && ps -p "$CONNECT_PID" > /dev/null 2>&1; then + kill "$CONNECT_PID" 2>/dev/null || true + fi + if [ -n "$MONITOR_PID" ] && ps -p "$MONITOR_PID" > /dev/null 2>&1; then + kill "$MONITOR_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +if [ "$SKIP_BUILD" -eq 0 ]; then + # ------------------------------------------------------- + # Step 1 – Build firmware with emulation mode enabled + # ------------------------------------------------------- + echo "[1/4] Building firmware (BOARD=$BOARD, DMBOOT_EMULATION=ON)..." + cmake -DCMAKE_BUILD_TYPE=Debug \ + -DBOARD="$BOARD" \ + -DDMBOOT_EMULATION=ON \ + -S "$SOURCE_DIR" \ + -B "$BUILD_DIR" + cmake --build "$BUILD_DIR" --config Debug + echo "✓ Build completed" + echo "" + + # ------------------------------------------------------- + # Step 2 – Verify install-firmware target + # ------------------------------------------------------- + echo "[2/4] Testing install-firmware target..." + cmake --build "$BUILD_DIR" --target install-firmware + if [ ! -f "$BUILD_DIR/renode_firmware.elf" ]; then + echo "✗ renode_firmware.elf not found after install-firmware" + exit 1 + fi + ls -lh "$BUILD_DIR/renode_firmware.elf" + echo "✓ install-firmware target works correctly" + echo "" +else + # --skip-build: just verify the firmware exists + echo "[1/4] Skipping build (--skip-build specified)" + if [ ! -f "$BUILD_DIR/renode_firmware.elf" ]; then + echo "✗ renode_firmware.elf not found at $BUILD_DIR/renode_firmware.elf" + echo " Run without --skip-build or build the firmware first." + exit 1 + fi + ls -lh "$BUILD_DIR/renode_firmware.elf" + echo "✓ Firmware found" + echo "" fi -ls -lh "$BUILD_DIR/renode_firmware.elf" -echo "✓ install-firmware target works correctly" -echo "" # ------------------------------------------------------- # Step 3 – Start Renode in the background @@ -89,13 +132,19 @@ MONITOR_LOG="$BUILD_DIR/monitor.log" timeout "$MONITOR_TIMEOUT" cmake --build "$BUILD_DIR" --target monitor-gdb > "$MONITOR_LOG" 2>&1 & MONITOR_PID=$! -# Give the firmware time to produce log output +# Give the firmware time to boot and produce log output +echo "Waiting ${MONITOR_TIMEOUT}s for firmware to boot..." sleep "$MONITOR_TIMEOUT" echo "Monitor output:" cat "$MONITOR_LOG" echo "" +# Also show the Renode connect log for diagnostics +echo "--- connect.log (last 40 lines) ---" +tail -40 "$CONNECT_LOG" || true +echo "" + # Verify expected log messages bash "$VERIFY_SCRIPT" "$MONITOR_LOG" "$EXPECTED_LOGS" VERIFY_STATUS=$? diff --git a/scripts/verify_renode_logs.sh b/scripts/verify_renode_logs.sh index 5ae443f..11a8c9e 100755 --- a/scripts/verify_renode_logs.sh +++ b/scripts/verify_renode_logs.sh @@ -1,6 +1,14 @@ #!/bin/bash # Verify that expected log messages appear in Renode firmware execution # Usage: verify_renode_logs.sh +# +# Lines in the expected_logs_file: +# - Empty lines and lines starting with # are ignored. +# - Lines starting with ! are "must NOT appear" patterns; the pattern +# being checked is the text after the leading '!'. +# Example: !HardFault_Handler invoked! +# (checks that the text "HardFault_Handler invoked!" is absent) +# - All other lines are "must appear" patterns. set -e @@ -33,13 +41,25 @@ while IFS= read -r expected_log || [ -n "$expected_log" ]; do if [ -z "$expected_log" ] || [[ "$expected_log" =~ ^#.* ]]; then continue fi - - echo -n "Checking for: '$expected_log' ... " - if grep -q "$expected_log" "$LOG_FILE"; then - echo "✓ FOUND" + + # Lines starting with ! are "must NOT appear" patterns + if [[ "$expected_log" =~ ^!.* ]]; then + pattern="${expected_log:1}" + echo -n "Checking absent: '$pattern' ... " + if grep -q "$pattern" "$LOG_FILE"; then + echo "✗ FOUND (should be absent)" + ALL_FOUND=false + else + echo "✓ ABSENT" + fi else - echo "✗ NOT FOUND" - ALL_FOUND=false + echo -n "Checking for: '$expected_log' ... " + if grep -q "$expected_log" "$LOG_FILE"; then + echo "✓ FOUND" + else + echo "✗ NOT FOUND" + ALL_FOUND=false + fi fi done < "$EXPECTED_LOGS_FILE" diff --git a/src/arch/armv7/CMakeLists.txt b/src/arch/armv7/CMakeLists.txt index 08c5fa0..3a579d7 100644 --- a/src/arch/armv7/CMakeLists.txt +++ b/src/arch/armv7/CMakeLists.txt @@ -3,6 +3,7 @@ # ====================================================================== add_library(dmboot_arch STATIC dmod_critical.c + dmod_mutex.c ) target_link_libraries(dmboot_arch @@ -10,4 +11,8 @@ target_link_libraries(dmboot_arch dmod ) +# Wrap Dmod_Mutex_New so the pre-RTOS guard is applied to all callers +# (e.g. dmvfs) that are linked into the final firmware image. +target_link_options(dmboot_arch INTERFACE -Wl,--wrap=Dmod_Mutex_New) + add_subdirectory(${DMBOOT_ARCH_FAMILY}) \ No newline at end of file diff --git a/src/arch/armv7/dmod_mutex.c b/src/arch/armv7/dmod_mutex.c new file mode 100644 index 0000000..494000e --- /dev/null +++ b/src/arch/armv7/dmod_mutex.c @@ -0,0 +1,36 @@ +/** + * @brief Pre-RTOS guard for Dmod_Mutex_New (ARMv7-M) + * + * The dmosi bridge library provides a strong Dmod_Mutex_New that calls + * dmosi_mutex_create(), which in turn calls pvPortMalloc() + + * xSemaphoreCreateRecursiveMutex(). Both succeed before vTaskStartScheduler() + * because FreeRTOS heap allocation does not require the scheduler. However, + * dmosi_mutex_lock() returns -ENOTSUP when !dmosi_is_started(), so any caller + * that obtains a non-NULL mutex handle before the scheduler starts and then + * tries to lock it will fail. + * + * This wrapper (enabled via -Wl,--wrap=Dmod_Mutex_New) returns NULL when the + * scheduler has not yet been started. NULL causes callers such as dmvfs to + * fall back to Dmod_EnterCritical / Dmod_ExitCritical (interrupt-disable + * critical sections), which work correctly both before and after RTOS start. + * + * After vTaskStartScheduler() the wrapper forwards to the real implementation + * so proper recursive RTOS mutexes are created as usual. + */ + +#include + +/* Resolved at link time from dmosi_freertos */ +extern bool dmosi_is_started(void); + +/* The real (unwrapped) symbol comes from the dmosi bridge library */ +extern void* __real_Dmod_Mutex_New(bool Recursive); + +void* __wrap_Dmod_Mutex_New(bool Recursive) +{ + if (!dmosi_is_started()) + { + return NULL; + } + return __real_Dmod_Mutex_New(Recursive); +}