Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
30 changes: 27 additions & 3 deletions configs/renode/expected_logs.txt
Original file line number Diff line number Diff line change
@@ -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
# --- 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
35 changes: 33 additions & 2 deletions linker/common.ld
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = .;
Expand Down
115 changes: 82 additions & 33 deletions scripts/run_renode_tests.sh
Original file line number Diff line number Diff line change
@@ -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}"

Expand All @@ -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 \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jeśli tutaj już była obsługa board, to czemu nie dodałeś odpowiedniego parametru?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The board parameter was already handled elsewhere; the real issue was that the VFS mounts failed pre-RTOS (see the Dmod_Mutex_New wrap fix in 04f0ac6), which is what prevented board modules from loading.

-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
Expand Down Expand Up @@ -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=$?
Expand Down
32 changes: 26 additions & 6 deletions scripts/verify_renode_logs.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#!/bin/bash
# Verify that expected log messages appear in Renode firmware execution
# Usage: verify_renode_logs.sh <log_file> <expected_logs_file>
#
# 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

Expand Down Expand Up @@ -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"

Expand Down
5 changes: 5 additions & 0 deletions src/arch/armv7/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
# ======================================================================
add_library(dmboot_arch STATIC
dmod_critical.c
dmod_mutex.c
)

target_link_libraries(dmboot_arch
PRIVATE
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})
36 changes: 36 additions & 0 deletions src/arch/armv7/dmod_mutex.c
Original file line number Diff line number Diff line change
@@ -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 <stdbool.h>

/* 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);
}
Loading