From 0f8ce1dfaf9536d9e6e863cb1756404ca9c67715 Mon Sep 17 00:00:00 2001 From: Tristen Pierson Date: Tue, 2 Jun 2026 10:23:58 -0400 Subject: [PATCH] feat: MISRA-C CI, stack usage analysis, hw benchmark configs REQ-BUILD-003: Add cppcheck MISRA C 2012 analysis CI job with fallback to built-in checks when the MISRA addon is unavailable. REQ-ARCH-040: Add hardware benchmark configuration for cycle-accurate PID benchmarking on nucleo_f446re and nucleo_h743zi using DWT CYCCNT. - Add misra job to CI workflow with suppression for known deviations - Add -fstack-usage to twister unit tests with 512-byte limit check - Upload .su files as CI artifacts alongside twister results - Create docs/safety/MISRA_DEVIATIONS.md documenting Rules 1.1, 11.3, 14.3, 20.9, 21.6 - Create tests/benchmarks/hw_benchmark/ with DWT CYCCNT timing, board overlays, testcase.yaml, and documentation Co-Authored-By: Oz --- .github/workflows/ci.yml | 65 +++- docs/safety/MISRA_DEVIATIONS.md | 29 ++ tests/benchmarks/hw_benchmark/CMakeLists.txt | 15 + tests/benchmarks/hw_benchmark/README.md | 107 +++++++ .../hw_benchmark/boards/nucleo_f446re.overlay | 2 + .../hw_benchmark/boards/nucleo_h743zi.overlay | 2 + tests/benchmarks/hw_benchmark/prj.conf | 6 + tests/benchmarks/hw_benchmark/sample.yaml | 10 + tests/benchmarks/hw_benchmark/src/main.c | 286 ++++++++++++++++++ tests/benchmarks/hw_benchmark/testcase.yaml | 13 + 10 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 docs/safety/MISRA_DEVIATIONS.md create mode 100644 tests/benchmarks/hw_benchmark/CMakeLists.txt create mode 100644 tests/benchmarks/hw_benchmark/README.md create mode 100644 tests/benchmarks/hw_benchmark/boards/nucleo_f446re.overlay create mode 100644 tests/benchmarks/hw_benchmark/boards/nucleo_h743zi.overlay create mode 100644 tests/benchmarks/hw_benchmark/prj.conf create mode 100644 tests/benchmarks/hw_benchmark/sample.yaml create mode 100644 tests/benchmarks/hw_benchmark/src/main.c create mode 100644 tests/benchmarks/hw_benchmark/testcase.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45a1724..9c55c1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,41 @@ jobs: done exit $missing + misra: + name: MISRA-C Analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install cppcheck + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cppcheck + + - name: Run cppcheck with MISRA C 2012 rules + run: | + # cppcheck's MISRA addon may not be available on ubuntu-latest; + # fall back to built-in checks if the addon fails. + if cppcheck --addon=misra --version 2>/dev/null; then + cppcheck --addon=misra \ + --suppress=misra-c2012-1.1 \ + --suppress=misra-c2012-20.9 \ + --suppress=misra-c2012-21.6 \ + -I include/ \ + -isystem tools/clang-tidy-stubs \ + --error-exitcode=1 \ + --inline-suppr \ + lib/*.c + else + echo "MISRA addon not available — falling back to built-in checks" + cppcheck \ + --enable=all \ + --suppress=missingIncludeSystem \ + -I include/ \ + -isystem tools/clang-tidy-stubs \ + --error-exitcode=1 \ + --inline-suppr \ + lib/*.c + fi + zephyr: name: Zephyr Twister Tests runs-on: ubuntu-latest @@ -170,13 +205,33 @@ jobs: ~/zephyr-sdk/setup.sh -c # Unit tests run as native executables on native_sim. + # -fstack-usage emits .su files for stack analysis. - name: Twister — unit tests run: | west twister \ -T app/tests/unit \ -p native_sim \ --inline-logs -v \ - -O twister-out/unit + -O twister-out/unit \ + -- -DEXTRA_CFLAGS=-fstack-usage + + - name: Check stack usage (max 512 bytes per function) + run: | + echo "--- Stack usage analysis ---" + err=0 + while IFS= read -r sufile; do + while IFS=$'\t' read -r func bytes type; do + if [ "${bytes:-0}" -gt 512 ] 2>/dev/null; then + echo "STACK EXCEEDED: $sufile: $func $bytes $type" + err=1 + fi + done < "$sufile" + done < <(find twister-out/unit -name '*.su' 2>/dev/null) + if [ $err -ne 0 ]; then + echo "::error::One or more functions exceed 512-byte stack limit" + exit 1 + fi + echo "All functions within 512-byte stack limit" # Benchmarks execute on native_sim; Twister captures timing output # as an artifact. pass/fail is gated on the final log line regex. @@ -203,3 +258,11 @@ jobs: with: name: twister-results path: twister-out/ + + - name: Upload stack usage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: stack-usage-reports + path: twister-out/**/*.su + if-no-files-found: ignore diff --git a/docs/safety/MISRA_DEVIATIONS.md b/docs/safety/MISRA_DEVIATIONS.md new file mode 100644 index 0000000..2ffe9cc --- /dev/null +++ b/docs/safety/MISRA_DEVIATIONS.md @@ -0,0 +1,29 @@ +# MISRA C 2012 — Known Deviations + +This document records intentional deviations from MISRA C:2012 rules in +the arbiter engine source code (`lib/` and `include/`). Each deviation +is tracked here with a rationale and scope so that safety audits can +reference this file as the single source of truth. + +## Deviation Table + +| Rule | Category | Rationale | Location | +|------|----------|-----------|----------| +| 1.1 — C11 required | Required | Zephyr RTOS requires C11 (e.g. `_Static_assert`, `_Atomic`, ``). The arbiter engine targets Zephyr exclusively and cannot compile as C90/C99. | All translation units | +| 20.9 — `` input/output | Required | Zephyr system headers (``, ``) are included via angle-bracket syntax and resolved by the Zephyr build system. These are not standard-library I/O headers. | All `#include ` | +| 21.6 — Standard I/O functions | Required | The `LOG_INF`, `LOG_ERR`, `LOG_DBG` macros expand to `printf`-style formatting internally. The arbiter engine never calls `printf`/`fprintf` directly; all logging goes through Zephyr's `LOG_*` API, which may be compiled out via `CONFIG_LOG=n`. | `lib/arbiter_engine.c`, `lib/arbiter_eval.c`, `lib/arbiter_trace.c` | +| 11.3 — Cast between pointer to object and pointer to different object type | Required | The blob loader casts `const uint8_t *` to `const struct arbiter_model *` when deserialising a compiled model blob. The cast is guarded by alignment checks and a SHA-256 hash verification before any field access. | `lib/arbiter_blob.c` — `ARBITER_blob_load()` | +| 14.3 — Controlling expression is always true/false | Required | The `unlikely()` and `likely()` branch-hint macros (from ``) wrap conditions in `__builtin_expect()`. Static analysers may report the inner expression as invariant. These are intentional performance annotations, not dead code. | `lib/arbiter_eval.c`, `lib/arbiter_fact_store.c` | + +## Process + +- New deviations must be added to this table **before** the corresponding + code is merged. +- Each entry must include the MISRA rule number, its category + (Required / Advisory / Mandatory), a rationale, and the affected + file(s). +- The CI pipeline runs `cppcheck --addon=misra` with `--suppress` flags + matching the rules listed above. Adding a new suppression in CI + without a corresponding entry here is a policy violation. +- This file is referenced by `docs/COMPLIANCE.md` and the safety case + documentation in `safety/`. diff --git a/tests/benchmarks/hw_benchmark/CMakeLists.txt b/tests/benchmarks/hw_benchmark/CMakeLists.txt new file mode 100644 index 0000000..0e0cced --- /dev/null +++ b/tests/benchmarks/hw_benchmark/CMakeLists.txt @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(hw_benchmark) + +# Reuse the PID benchmark's compiled model tables. +target_sources(app PRIVATE + src/main.c + ${CMAKE_CURRENT_LIST_DIR}/../pid_benchmark/src/arbiter_model.c +) + +target_include_directories(app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/../pid_benchmark/src +) diff --git a/tests/benchmarks/hw_benchmark/README.md b/tests/benchmarks/hw_benchmark/README.md new file mode 100644 index 0000000..d17e98b --- /dev/null +++ b/tests/benchmarks/hw_benchmark/README.md @@ -0,0 +1,107 @@ +# Hardware PID Benchmark + +Cycle-accurate PID benchmark targeting real Cortex-M hardware. +Uses the ARM DWT CYCCNT register for cycle-exact measurement on +Cortex-M targets, and falls back to `clock_gettime` on `native_sim`. + +## Supported Boards + +- **nucleo_f446re** — STM32F446RE (Cortex-M4, 180 MHz) +- **nucleo_h743zi** — STM32H743ZI (Cortex-M7, 480 MHz) +- **native_sim** — Host execution (nanosecond timing) + +## Building + +```sh +# For real hardware +west build -b nucleo_f446re tests/benchmarks/hw_benchmark +west build -b nucleo_h743zi tests/benchmarks/hw_benchmark + +# For simulation +west build -b native_sim tests/benchmarks/hw_benchmark +``` + +## Flashing + +Connect the Nucleo board via USB (ST-Link) and run: + +```sh +west flash +``` + +Ensure your udev rules are configured for ST-Link. On Linux: + +```sh +# /etc/udev/rules.d/99-stlink.rules +SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374b", MODE="0666" +``` + +## Capturing Results + +Open a serial terminal at 115200 baud on the board's virtual COM port: + +```sh +# Linux — typical device path for Nucleo boards +picocom -b 115200 /dev/ttyACM0 + +# Or use west +west espressif monitor # (for ESP boards) +# For STM32 Nucleo, use any serial terminal on the ST-Link VCP +``` + +Press the board's reset button to re-run the benchmark. + +### Expected Output + +``` +[00:00:00.000,000] hw_bench: === HW PID Benchmark === +[00:00:00.000,000] hw_bench: Timer unit: cycles +[00:00:00.000,000] hw_bench: Iterations: 10000 (warmup: 500) +[00:00:00.xxx,xxx] hw_bench: --- Hand-coded PID --- +[00:00:00.xxx,xxx] hw_bench: Total: NNNN cycles (NN cycles/tick) +[00:00:00.xxx,xxx] hw_bench: RAM (struct): 24 bytes +[00:00:00.xxx,xxx] hw_bench: --- arbiter Engine PID --- +[00:00:00.xxx,xxx] hw_bench: Total: NNNN cycles (NN cycles/tick) +[00:00:00.xxx,xxx] hw_bench: RAM (ctx): NNN bytes +[00:00:00.xxx,xxx] hw_bench: === Comparison === +[00:00:00.xxx,xxx] hw_bench: Engine overhead: NN% (NN vs NN cycles/tick) +[00:00:00.xxx,xxx] hw_bench: HW Benchmark complete +``` + +## Running via Twister + +```sh +# Build-only test (no hardware required) +west twister -T tests/benchmarks/hw_benchmark -p native_sim + +# With real hardware (requires connected board and runner configured) +west twister -T tests/benchmarks/hw_benchmark -p nucleo_f446re --device-testing +``` + +## How It Works + +### DWT CYCCNT (Cortex-M) + +The Data Watchpoint and Trace (DWT) unit provides a 32-bit free-running +cycle counter (`CYCCNT`). At typical Cortex-M clock speeds: + +- **180 MHz (F446RE)**: wraps every ~23.8 seconds +- **480 MHz (H743ZI)**: wraps every ~8.9 seconds + +The benchmark completes well within these limits. + +### Measurement Methodology + +1. **Warmup**: 500 iterations to stabilize caches and branch predictors. +2. **Measured window**: 10,000 iterations of the PID loop. +3. **Both implementations** (hand-coded and arbiter engine) use identical + inputs and produce identical control outputs. +4. The cycle count delta is divided by the iteration count for per-tick cost. + +### ROM Comparison + +ROM cannot be measured at runtime. After building, compare `.elf` sizes: + +```sh +arm-none-eabi-size build/zephyr/zephyr.elf +``` diff --git a/tests/benchmarks/hw_benchmark/boards/nucleo_f446re.overlay b/tests/benchmarks/hw_benchmark/boards/nucleo_f446re.overlay new file mode 100644 index 0000000..85b702c --- /dev/null +++ b/tests/benchmarks/hw_benchmark/boards/nucleo_f446re.overlay @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +/* Minimal overlay for STM32F446RE — no board-specific DTS changes needed. */ diff --git a/tests/benchmarks/hw_benchmark/boards/nucleo_h743zi.overlay b/tests/benchmarks/hw_benchmark/boards/nucleo_h743zi.overlay new file mode 100644 index 0000000..7c07603 --- /dev/null +++ b/tests/benchmarks/hw_benchmark/boards/nucleo_h743zi.overlay @@ -0,0 +1,2 @@ +/* SPDX-License-Identifier: MIT */ +/* Minimal overlay for STM32H743ZI — no board-specific DTS changes needed. */ diff --git a/tests/benchmarks/hw_benchmark/prj.conf b/tests/benchmarks/hw_benchmark/prj.conf new file mode 100644 index 0000000..5ee3e66 --- /dev/null +++ b/tests/benchmarks/hw_benchmark/prj.conf @@ -0,0 +1,6 @@ +CONFIG_ARBITER=y +CONFIG_ARBITER_HW_ACCEL=y +CONFIG_TIMING_FUNCTIONS=y +CONFIG_LOG=y +CONFIG_PRINTK=y +CONFIG_MAIN_STACK_SIZE=4096 diff --git a/tests/benchmarks/hw_benchmark/sample.yaml b/tests/benchmarks/hw_benchmark/sample.yaml new file mode 100644 index 0000000..417e9aa --- /dev/null +++ b/tests/benchmarks/hw_benchmark/sample.yaml @@ -0,0 +1,10 @@ +sample: + name: Hardware PID Benchmark + description: | + Cycle-accurate PID benchmark targeting real Cortex-M hardware. + Uses DWT CYCCNT on Cortex-M targets and Zephyr timing API + on native_sim for comparative measurement. + platforms: + - nucleo_f446re + - nucleo_h743zi + - native_sim diff --git a/tests/benchmarks/hw_benchmark/src/main.c b/tests/benchmarks/hw_benchmark/src/main.c new file mode 100644 index 0000000..6f8a701 --- /dev/null +++ b/tests/benchmarks/hw_benchmark/src/main.c @@ -0,0 +1,286 @@ +/* SPDX-License-Identifier: MIT */ + +/** + * Hardware PID Benchmark — cycle-accurate timing on Cortex-M + * + * On Cortex-M targets (nucleo_f446re, nucleo_h743zi) this uses the + * Data Watchpoint and Trace (DWT) unit's CYCCNT register for cycle-exact + * measurement. On native_sim it falls back to Zephyr's timing API. + * + * Reuses the PID model and hand-coded baseline from pid_benchmark. + */ + +#include +#include +#include +#include "arbiter_model.h" + +LOG_MODULE_REGISTER(hw_bench, LOG_LEVEL_INF); + +/* ------------------------------------------------------------------ */ +/* DWT cycle counter (Cortex-M only) */ +/* ------------------------------------------------------------------ */ + +#if defined(CONFIG_CPU_CORTEX_M) + +/* CoreDebug and DWT registers — ARMv7-M / ARMv8-M */ +#define DWT_CTRL_REG (*(volatile uint32_t *)0xE0001000) +#define DWT_CYCCNT_REG (*(volatile uint32_t *)0xE0001004) +#define DEM_CR_REG (*(volatile uint32_t *)0xE000EDFC) +#define DEM_CR_TRCENA (1UL << 24) +#define DWT_CTRL_CYCCNTENA (1UL) + +static inline void dwt_init(void) +{ + DEM_CR_REG |= DEM_CR_TRCENA; + DWT_CYCCNT_REG = 0; + DWT_CTRL_REG |= DWT_CTRL_CYCCNTENA; +} + +static inline uint32_t dwt_cycles(void) +{ + return DWT_CYCCNT_REG; +} + +#define HW_TIMER_INIT() dwt_init() +#define HW_TIMER_GET() dwt_cycles() +#define HW_TIMER_UNIT "cycles" + +#else /* native_sim / other */ + +/* Fall back to clock_gettime for nanosecond resolution */ +struct bench_ts { long tv_sec; long tv_nsec; }; +extern int clock_gettime(int, struct bench_ts *); + +static inline uint64_t native_ns(void) +{ + struct bench_ts ts; + + clock_gettime(4 /* CLOCK_MONOTONIC_RAW */, &ts); + return (uint64_t)(unsigned long)ts.tv_sec * 1000000000ULL + + (uint64_t)(unsigned long)ts.tv_nsec; +} + +static uint64_t native_start; + +#define HW_TIMER_INIT() do { native_start = 0; } while (0) +#define HW_TIMER_GET() ((uint32_t)(native_ns() & 0xFFFFFFFFUL)) +#define HW_TIMER_UNIT "ns" + +#endif /* CONFIG_CPU_CORTEX_M */ + +/* ------------------------------------------------------------------ */ +/* Hand-coded PID (baseline) — same as pid_benchmark */ +/* ------------------------------------------------------------------ */ + +struct hand_pid { + int32_t kp; /* x1000 */ + int32_t ki; /* x1000 */ + int32_t kd; /* x1000 */ + int32_t error_prev; + int32_t i_term; + int32_t output; +}; + +static void hand_pid_init(struct hand_pid *__restrict p) +{ + p->kp = 2500; + p->ki = 100; + p->kd = 800; + p->error_prev = 0; + p->i_term = 0; + p->output = 0; +} + +static void hand_pid_tick(struct hand_pid *__restrict p, int32_t setpoint, + int32_t process_value) +{ + int32_t error = setpoint - process_value; + int32_t p_term = (int32_t)(((int64_t)error * p->kp) / 1000); + + p->i_term += (int32_t)(((int64_t)error * p->ki) / 10000); + + int32_t d_raw = error - p->error_prev; + int32_t d_term = (int32_t)(((int64_t)d_raw * p->kd) / 1000); + + int32_t raw = p_term + p->i_term + d_term; + + if (raw > 1000) { + raw = 1000; + } else if (raw < -1000) { + raw = -1000; + } + + if (raw >= 1000 || raw <= -1000) { + if (p->i_term > 500000) { + p->i_term = 500000; + } else if (p->i_term < -500000) { + p->i_term = -500000; + } + } + + p->output = raw; + p->error_prev = error; +} + +/* ------------------------------------------------------------------ */ +/* Fact index aliases */ +/* ------------------------------------------------------------------ */ + +#define F_KD ARBITER_FACT_GAIN_KD +#define F_KI ARBITER_FACT_GAIN_KI +#define F_KP ARBITER_FACT_GAIN_KP +#define F_DT_MS ARBITER_FACT_IN_DT_MS +#define F_ENABLE ARBITER_FACT_IN_ENABLE +#define F_PROCESS_VALUE ARBITER_FACT_IN_PROCESS_VALUE +#define F_SENSOR_VALID ARBITER_FACT_IN_SENSOR_VALID +#define F_SETPOINT ARBITER_FACT_IN_SETPOINT +#define F_OUTPUT ARBITER_FACT_PID_OUTPUT + +/* ------------------------------------------------------------------ */ +/* Benchmark parameters */ +/* ------------------------------------------------------------------ */ + +#define BENCH_ITERATIONS 10000 +#define WARMUP_ITERATIONS 500 + +static struct ARBITER_ctx arbiter_ctx; + +void app_update_actuator(void) +{ + /* No-op for benchmark */ +} + +/* ------------------------------------------------------------------ */ +/* Main */ +/* ------------------------------------------------------------------ */ + +int main(void) +{ + LOG_INF("=== HW PID Benchmark ==="); + LOG_INF("Timer unit: %s", HW_TIMER_UNIT); + LOG_INF("Iterations: %d (warmup: %d)", BENCH_ITERATIONS, + WARMUP_ITERATIONS); + + HW_TIMER_INIT(); + + /* ----- Hand-coded PID ----- */ + struct hand_pid hpid; + + hand_pid_init(&hpid); + + int32_t plant_h = 0; + + for (int i = 0; i < WARMUP_ITERATIONS; i++) { + hand_pid_tick(&hpid, 10000, plant_h); + plant_h += hpid.output / 10; + } + + hand_pid_init(&hpid); + plant_h = 0; + + uint32_t t0 = HW_TIMER_GET(); + + for (int i = 0; i < BENCH_ITERATIONS; i++) { + hand_pid_tick(&hpid, 10000, plant_h); + plant_h += hpid.output / 10; + } + + uint32_t t1 = HW_TIMER_GET(); + uint32_t hand_total = t1 - t0; + uint32_t hand_per_tick = hand_total / BENCH_ITERATIONS; + + LOG_INF("--- Hand-coded PID ---"); + LOG_INF(" Total: %u %s (%u %s/tick)", hand_total, HW_TIMER_UNIT, + hand_per_tick, HW_TIMER_UNIT); + LOG_INF(" RAM (struct): %u bytes", (unsigned)sizeof(struct hand_pid)); + + /* ----- arbiter engine PID ----- */ + int ret = ARBITER_init(&arbiter_ctx, &ARBITER_generated_model); + + if (ret != ARBITER_OK) { + LOG_ERR("arbiter init failed: %d", ret); + return ret; + } + + ARBITER_set_i32(&arbiter_ctx, F_KP, 2500); + ARBITER_set_i32(&arbiter_ctx, F_KI, 100); + ARBITER_set_i32(&arbiter_ctx, F_KD, 800); + ARBITER_set_bool(&arbiter_ctx, F_ENABLE, true); + ARBITER_set_bool(&arbiter_ctx, F_SENSOR_VALID, true); + ARBITER_set_u32(&arbiter_ctx, F_DT_MS, 10); + + int32_t plant_z = 0; + + for (int i = 0; i < WARMUP_ITERATIONS; i++) { + ARBITER_set_i32(&arbiter_ctx, F_SETPOINT, 10000); + ARBITER_set_i32(&arbiter_ctx, F_PROCESS_VALUE, plant_z); + ARBITER_set_timestamp(&arbiter_ctx, F_PROCESS_VALUE, + (uint32_t)i * 10); + + struct ARBITER_snapshot snap; + struct ARBITER_result result; + + ARBITER_snapshot_begin(&arbiter_ctx, &snap); + ARBITER_eval(&ARBITER_generated_model, &snap, &result, NULL); + plant_z += arbiter_ctx.fact_values[F_OUTPUT].value / 10; + } + + ARBITER_init(&arbiter_ctx, &ARBITER_generated_model); + ARBITER_set_i32(&arbiter_ctx, F_KP, 2500); + ARBITER_set_i32(&arbiter_ctx, F_KI, 100); + ARBITER_set_i32(&arbiter_ctx, F_KD, 800); + ARBITER_set_bool(&arbiter_ctx, F_ENABLE, true); + ARBITER_set_bool(&arbiter_ctx, F_SENSOR_VALID, true); + ARBITER_set_u32(&arbiter_ctx, F_DT_MS, 10); + plant_z = 0; + + uint32_t t2 = HW_TIMER_GET(); + + for (int i = 0; i < BENCH_ITERATIONS; i++) { + ARBITER_set_i32(&arbiter_ctx, F_SETPOINT, 10000); + ARBITER_set_i32(&arbiter_ctx, F_PROCESS_VALUE, plant_z); + ARBITER_set_timestamp(&arbiter_ctx, F_PROCESS_VALUE, + (uint32_t)(WARMUP_ITERATIONS + i) * 10); + + struct ARBITER_snapshot snap; + struct ARBITER_result result; + + ARBITER_snapshot_begin(&arbiter_ctx, &snap); + ARBITER_eval(&ARBITER_generated_model, &snap, &result, NULL); + plant_z += arbiter_ctx.fact_values[F_OUTPUT].value / 10; + } + + uint32_t t3 = HW_TIMER_GET(); + uint32_t arb_total = t3 - t2; + uint32_t arb_per_tick = arb_total / BENCH_ITERATIONS; + + LOG_INF("--- arbiter Engine PID ---"); + LOG_INF(" Total: %u %s (%u %s/tick)", arb_total, HW_TIMER_UNIT, + arb_per_tick, HW_TIMER_UNIT); + LOG_INF(" RAM (ctx): %u bytes", + (unsigned)sizeof(struct ARBITER_ctx)); + LOG_INF(" Model: %u facts, %u rules, %u conditions, %u expressions", + ARBITER_generated_model.fact_count, + ARBITER_generated_model.rule_count, + ARBITER_generated_model.condition_count, + ARBITER_generated_model.expr_count); + + /* ----- Comparison ----- */ + uint32_t overhead_pct = 0; + + if (hand_per_tick > 0) { + overhead_pct = ((arb_per_tick - hand_per_tick) * 100) + / hand_per_tick; + } + + LOG_INF("=== Comparison ==="); + LOG_INF(" Engine overhead: %u%% (%u vs %u %s/tick)", + overhead_pct, arb_per_tick, hand_per_tick, HW_TIMER_UNIT); + LOG_INF(" RAM overhead: %u bytes (ctx) vs %u bytes (struct)", + (unsigned)sizeof(struct ARBITER_ctx), + (unsigned)sizeof(struct hand_pid)); + LOG_INF("HW Benchmark complete"); + + return 0; +} diff --git a/tests/benchmarks/hw_benchmark/testcase.yaml b/tests/benchmarks/hw_benchmark/testcase.yaml new file mode 100644 index 0000000..5254fcf --- /dev/null +++ b/tests/benchmarks/hw_benchmark/testcase.yaml @@ -0,0 +1,13 @@ +tests: + benchmark.arbiter.hw_pid: + tags: arbiter benchmark hardware + platform_allow: + - nucleo_f446re + - nucleo_h743zi + - native_sim + harness: console + extra_args: CONF_FILE=prj.conf + harness_config: + type: one_line + regex: + - "HW Benchmark complete"