saltylab-firmware/test/test_can_watchdog.c
sl-firmware f600aa98e7 feat: CAN bus watchdog and error recovery (Issue #694)
- CAN1_SCE_IRQHandler: detects bus-off/error-passive/error-warning from ESR
- can_driver_watchdog_tick(): polls ESR each cycle, auto-restarts after CAN_WDOG_RESTART_MS (200ms)
- can_wdog_t: tracks restart_count, busoff_count, errpassive_count, errwarn_count, tec, rec
- JLink TLM code 0x8F (JLINK_TLM_CAN_WDOG) with jlink_send_can_wdog_tlm()
- main.c: calls watchdog_tick() each loop, sends CAN wdog TLM at 1 Hz
- TEST_HOST: inject_esr() stub + busoff_pending flag fixes t=0 sentinel ambiguity
- test/test_can_watchdog.c: 23 unit tests, all pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:47:09 -04:00

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;
}