Archive STM32 firmware to legacy/stm32/: - src/, include/, lib/USB_CDC/, platformio.ini, test stubs, flash_firmware.py - test/test_battery_adc.c, test_hw_button.c, test_pid_schedule.c, test_vesc_can.c, test_can_watchdog.c - USB_CDC_BUG.md Rename: stm32_protocol → esp32_protocol, mamba_protocol → balance_protocol, stm32_cmd_node → esp32_cmd_node, stm32_cmd_params → esp32_cmd_params, stm32_cmd.launch.py → esp32_cmd.launch.py, test_stm32_protocol → test_esp32_protocol, test_stm32_cmd_node → test_esp32_cmd_node Content cleanup across all files: - Mamba F722S → ESP32-S3 BALANCE - BlackPill → ESP32-S3 IO - STM32F722/F7xx → ESP32-S3 - stm32Mode/Version/Port → esp32Mode/Version/Port - STM32 State/Mode labels → ESP32 State/Mode - Jetson Nano → Jetson Orin Nano Super - /dev/stm32 → /dev/esp32 - stm32_bridge → esp32_bridge - STM32 HAL → ESP-IDF docs/SALTYLAB.md: - Update "Drone FC Details" to describe ESP32-S3 BALANCE board (Waveshare ESP32-S3 Touch LCD 1.28) - Replace verbose "Self-Balancing Control" STM32 section with brief note pointing to SAUL-TEE-SYSTEM-REFERENCE.md TEAM.md: Update Embedded Firmware Engineer role to ESP32-S3 / ESP-IDF No new functionality — cleanup only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
225 lines
6.5 KiB
C
225 lines
6.5 KiB
C
/*
|
|
* test_can_watchdog.c — Unit tests for CAN bus watchdog and error recovery (Issue #694).
|
|
*
|
|
* Build (host, no hardware):
|
|
* gcc -I include -I test/stubs -DTEST_HOST \
|
|
* -o /tmp/test_can_watchdog src/can_driver.c test/test_can_watchdog.c
|
|
*
|
|
* All timing driven through now_ms argument passed to can_driver_watchdog_tick().
|
|
* ESR state injected via can_driver_inject_esr().
|
|
*/
|
|
|
|
/* Include implementation directly (pulls stm32 stub via -I test/stubs) */
|
|
#include "../src/can_driver.c"
|
|
|
|
/* ---- Test framework ---- */
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
static int g_pass = 0;
|
|
static int g_fail = 0;
|
|
|
|
#define ASSERT(cond, msg) \
|
|
do { \
|
|
if (cond) { g_pass++; } \
|
|
else { g_fail++; printf("FAIL [%s:%d] %s\n", __FILE__, __LINE__, msg); } \
|
|
} while (0)
|
|
|
|
/* HAL_GetTick stub */
|
|
uint32_t HAL_GetTick(void) { return 0u; }
|
|
|
|
/* Reset driver state between tests */
|
|
static void _reset(void)
|
|
{
|
|
can_driver_inject_esr(0u);
|
|
can_driver_init();
|
|
}
|
|
|
|
/* ---- Tests ---- */
|
|
|
|
static void test_nominal_no_errors(void)
|
|
{
|
|
_reset();
|
|
can_error_state_t st = can_driver_watchdog_tick(0u);
|
|
ASSERT(st == CAN_ERR_NOMINAL, "clean ESR: state is NOMINAL");
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.restart_count == 0u, "nominal: restart_count 0");
|
|
ASSERT(wd.busoff_count == 0u, "nominal: busoff_count 0");
|
|
}
|
|
|
|
static void test_errwarn_detection(void)
|
|
{
|
|
_reset();
|
|
can_driver_inject_esr(CAN_ESR_EWGF);
|
|
can_error_state_t st = can_driver_watchdog_tick(1000u);
|
|
ASSERT(st == CAN_ERR_WARNING, "EWGF set: state is WARNING");
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.errwarn_count == 1u, "errwarn_count incremented");
|
|
}
|
|
|
|
static void test_errwarn_cleared_returns_nominal(void)
|
|
{
|
|
_reset();
|
|
/* Enter warning state */
|
|
can_driver_inject_esr(CAN_ESR_EWGF);
|
|
can_driver_watchdog_tick(100u);
|
|
|
|
/* Clear ESR - should de-escalate */
|
|
can_driver_inject_esr(0u);
|
|
can_error_state_t st = can_driver_watchdog_tick(200u);
|
|
ASSERT(st == CAN_ERR_NOMINAL, "ESR cleared: de-escalate to NOMINAL");
|
|
}
|
|
|
|
static void test_errpassive_detection(void)
|
|
{
|
|
_reset();
|
|
can_driver_inject_esr(CAN_ESR_EPVF);
|
|
can_error_state_t st = can_driver_watchdog_tick(500u);
|
|
ASSERT(st == CAN_ERR_ERROR_PASSIVE, "EPVF set: state is ERROR_PASSIVE");
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.errpassive_count == 1u, "errpassive_count incremented");
|
|
}
|
|
|
|
static void test_busoff_detection(void)
|
|
{
|
|
_reset();
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
can_error_state_t st = can_driver_watchdog_tick(1000u);
|
|
ASSERT(st == CAN_ERR_BUS_OFF, "BOFF set: state is BUS_OFF");
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.busoff_count == 1u, "busoff_count incremented");
|
|
ASSERT(wd.busoff_ms == 1000u, "busoff_ms recorded");
|
|
}
|
|
|
|
static void test_busoff_sets_stats_bus_off(void)
|
|
{
|
|
_reset();
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
can_driver_watchdog_tick(500u);
|
|
|
|
can_stats_t cs;
|
|
can_driver_get_stats(&cs);
|
|
ASSERT(cs.bus_off == 1u, "bus-off: stats.bus_off = 1");
|
|
}
|
|
|
|
static void test_busoff_no_restart_before_timeout(void)
|
|
{
|
|
_reset();
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
can_driver_watchdog_tick(0u);
|
|
|
|
/* Call again just before CAN_WDOG_RESTART_MS */
|
|
can_error_state_t st = can_driver_watchdog_tick(CAN_WDOG_RESTART_MS - 1u);
|
|
ASSERT(st == CAN_ERR_BUS_OFF, "before restart timeout: still BUS_OFF");
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.restart_count == 0u, "no restart yet");
|
|
}
|
|
|
|
static void test_busoff_auto_restart_after_timeout(void)
|
|
{
|
|
_reset();
|
|
/* Inject bus-off at t=0 */
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
can_driver_watchdog_tick(0u);
|
|
|
|
/* After restart timeout, HAL_CAN_Stop/Start clears s_test_esr internally */
|
|
can_error_state_t st = can_driver_watchdog_tick(CAN_WDOG_RESTART_MS);
|
|
ASSERT(st == CAN_ERR_NOMINAL, "after restart timeout: state is NOMINAL");
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.restart_count == 1u, "restart_count incremented to 1");
|
|
ASSERT(wd.busoff_ms == 0u, "busoff_ms cleared after restart");
|
|
}
|
|
|
|
static void test_busoff_stats_cleared_after_restart(void)
|
|
{
|
|
_reset();
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
can_driver_watchdog_tick(0u);
|
|
can_driver_watchdog_tick(CAN_WDOG_RESTART_MS);
|
|
|
|
can_stats_t cs;
|
|
can_driver_get_stats(&cs);
|
|
ASSERT(cs.bus_off == 0u, "after restart: stats.bus_off cleared");
|
|
}
|
|
|
|
static void test_busoff_count_only_increments_once_per_event(void)
|
|
{
|
|
_reset();
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
/* Call tick multiple times while still in bus-off */
|
|
can_driver_watchdog_tick(0u);
|
|
can_driver_watchdog_tick(10u);
|
|
can_driver_watchdog_tick(50u);
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.busoff_count == 1u, "repeated BOFF ticks: count stays 1");
|
|
}
|
|
|
|
static void test_multiple_restart_cycles(void)
|
|
{
|
|
_reset();
|
|
/* Cycle 1 */
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
can_driver_watchdog_tick(0u);
|
|
can_driver_watchdog_tick(CAN_WDOG_RESTART_MS); /* restart */
|
|
|
|
/* Cycle 2: inject bus-off again */
|
|
can_driver_inject_esr(CAN_ESR_BOFF);
|
|
can_driver_watchdog_tick(CAN_WDOG_RESTART_MS + 1u);
|
|
can_driver_watchdog_tick(CAN_WDOG_RESTART_MS * 2u + 1u); /* restart */
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.restart_count == 2u, "two restart cycles: restart_count = 2");
|
|
ASSERT(wd.busoff_count == 2u, "two restart cycles: busoff_count = 2");
|
|
}
|
|
|
|
static void test_tec_rec_extracted(void)
|
|
{
|
|
_reset();
|
|
/* Inject ESR with TEC=0xAB (bits 23:16) and REC=0xCD (bits 31:24) */
|
|
uint32_t fake_esr = (0xABu << 16) | (0xCDu << 24);
|
|
can_driver_inject_esr(fake_esr);
|
|
can_driver_watchdog_tick(100u);
|
|
|
|
can_wdog_t wd;
|
|
can_driver_get_wdog(&wd);
|
|
ASSERT(wd.tec == 0xABu, "TEC extracted from ESR correctly");
|
|
ASSERT(wd.rec == 0xCDu, "REC extracted from ESR correctly");
|
|
}
|
|
|
|
/* ---- main ---- */
|
|
int main(void)
|
|
{
|
|
printf("=== test_can_watchdog ===\n");
|
|
|
|
test_nominal_no_errors();
|
|
test_errwarn_detection();
|
|
test_errwarn_cleared_returns_nominal();
|
|
test_errpassive_detection();
|
|
test_busoff_detection();
|
|
test_busoff_sets_stats_bus_off();
|
|
test_busoff_no_restart_before_timeout();
|
|
test_busoff_auto_restart_after_timeout();
|
|
test_busoff_stats_cleared_after_restart();
|
|
test_busoff_count_only_increments_once_per_event();
|
|
test_multiple_restart_cycles();
|
|
test_tec_rec_extracted();
|
|
|
|
printf("\nResults: %d passed, %d failed\n", g_pass, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|