From 080129b0d514aa602d8518bfe62f99e55dfac71d Mon Sep 17 00:00:00 2001 From: acetaffy Date: Sun, 11 Oct 2026 16:01:34 +0800 Subject: [PATCH] fix: recover CAN bus-off and expire stalled transmissions --- A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c | 122 +++++++++++++++++++++++- tests/test_can_health.py | 123 +++++++++++++++++++++++++ 固件使用说明.md | 6 ++ 3 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 tests/test_can_health.py diff --git a/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c b/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c index a433a2c..42a3cfd 100644 --- a/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c +++ b/A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c @@ -65,6 +65,7 @@ typedef struct #define ADC_REPORT_PERIOD_MS 500U #define PWM_REPORT_PERIOD_MS 20U #define GPIO_REPORT_PERIOD_MS 1000U +#define CAN_TX_TIMEOUT_MS 100U #define CAN_HEARTBEAT_PERIOD_MS 100U #define DAC_CAN_TIMEOUT_MS 100U #define PWM_TIMEOUT_MS 1000U @@ -129,6 +130,26 @@ static char uart_line[UART_LINE_SIZE]; static uint8_t uart_line_len; static uint8_t assi_lamp_state; +/* Read through SWD; counters are observations, not a bus capture. */ +typedef struct +{ + uint32_t last_esr; + uint32_t bus_off_events; + uint32_t recovery_events; + uint32_t passive_events; + uint32_t tx_queued; + uint32_t tx_completed; + uint32_t tx_failed; + uint32_t tx_unobserved; + uint32_t tx_dropped; + uint32_t abort_requests; + uint32_t abort_failures; +} CanDiagnostics; +static volatile CanDiagnostics can_diagnostics[2]; +static uint32_t can_tx_started_ms[2][3]; +static bool can_tx_tracked[2][3]; +static bool can_tx_abort_requested[2][3]; + /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ @@ -167,6 +188,7 @@ static uint32_t ReadAdcInputMillivolts(void); static uint32_t WheelSpeedMmPerSecond(uint32_t frequency_hz); static uint32_t GetTimerClockHz(const TIM_HandleTypeDef *htim); static void InitCaptureChannels(void); +static void ServiceCanHealth(CAN_HandleTypeDef *hcan); static void ConfigureCanRuntime(CAN_HandleTypeDef *hcan); static void StartCanBus(CAN_HandleTypeDef *hcan, uint32_t filter_bank); static void SendCanFrame(CAN_HandleTypeDef *hcan, @@ -237,6 +259,8 @@ int main(void) /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ + ServiceCanHealth(&hcan1); + ServiceCanHealth(&hcan2); PollUart(); PollCanBus(&hcan1, "CAN1"); PollCanBus(&hcan2, "CAN2"); @@ -366,7 +390,7 @@ static void MX_CAN1_Init(void) hcan1.Init.TimeSeg1 = CAN_BS1_1TQ; hcan1.Init.TimeSeg2 = CAN_BS2_4TQ; hcan1.Init.TimeTriggeredMode = DISABLE; - hcan1.Init.AutoBusOff = DISABLE; + hcan1.Init.AutoBusOff = ENABLE; hcan1.Init.AutoWakeUp = DISABLE; hcan1.Init.AutoRetransmission = DISABLE; hcan1.Init.ReceiveFifoLocked = DISABLE; @@ -403,7 +427,7 @@ static void MX_CAN2_Init(void) hcan2.Init.TimeSeg1 = CAN_BS1_1TQ; hcan2.Init.TimeSeg2 = CAN_BS2_4TQ; hcan2.Init.TimeTriggeredMode = DISABLE; - hcan2.Init.AutoBusOff = DISABLE; + hcan2.Init.AutoBusOff = ENABLE; hcan2.Init.AutoWakeUp = DISABLE; hcan2.Init.AutoRetransmission = DISABLE; hcan2.Init.ReceiveFifoLocked = DISABLE; @@ -1294,12 +1318,74 @@ static void InitCaptureChannels(void) } } +static void ServiceCanHealth(CAN_HandleTypeDef *hcan) +{ + uint32_t bus = (hcan == &hcan1) ? 0U : 1U; + volatile CanDiagnostics *diag = &can_diagnostics[bus]; + uint32_t now = HAL_GetTick(); + uint32_t esr = hcan->Instance->ESR; + uint32_t previous = diag->last_esr; + + if (((esr & CAN_ESR_BOFF) != 0U) && ((previous & CAN_ESR_BOFF) == 0U)) + { + diag->bus_off_events++; + } + if (((esr & CAN_ESR_BOFF) == 0U) && ((previous & CAN_ESR_BOFF) != 0U)) + { + diag->recovery_events++; + } + if (((esr & CAN_ESR_EPVF) != 0U) && ((previous & CAN_ESR_EPVF) == 0U)) + { + diag->passive_events++; + } + diag->last_esr = esr; + + for (uint32_t index = 0U; index < 3U; index++) + { + uint32_t mailbox = 1UL << index; + if (!can_tx_tracked[bus][index]) + { + continue; + } + if (HAL_CAN_IsTxMessagePending(hcan, mailbox) == 0U) + { + /* TXOK is hardware completion, unlike HAL_OK when enqueuing. */ + if ((hcan->Instance->TSR & (CAN_TSR_TXOK0 << (8U * index))) != 0U) + { + diag->tx_completed++; + } + else + { + diag->tx_failed++; + } + can_tx_tracked[bus][index] = false; + can_tx_abort_requested[bus][index] = false; + } + else if (!can_tx_abort_requested[bus][index] && + (((esr & CAN_ESR_BOFF) != 0U) || + ((uint32_t)(now - can_tx_started_ms[bus][index]) >= CAN_TX_TIMEOUT_MS))) + { + /* Discard stale reports/ACKs; abort is asynchronous and never waits. */ + if (HAL_CAN_AbortTxRequest(hcan, mailbox) == HAL_OK) + { + can_tx_abort_requested[bus][index] = true; + diag->abort_requests++; + } + else + { + diag->abort_failures++; + } + } + } +} + static void ConfigureCanRuntime(CAN_HandleTypeDef *hcan) { hcan->Init.Prescaler = 4U; hcan->Init.SyncJumpWidth = CAN_SJW_1TQ; hcan->Init.TimeSeg1 = CAN_BS1_5TQ; hcan->Init.TimeSeg2 = CAN_BS2_2TQ; + hcan->Init.AutoBusOff = ENABLE; hcan->Init.AutoRetransmission = ENABLE; if (HAL_CAN_Init(hcan) != HAL_OK) @@ -1340,6 +1426,16 @@ static void SendCanFrame(CAN_HandleTypeDef *hcan, { CAN_TxHeaderTypeDef tx_header = {0}; uint32_t mailbox = 0U; + uint32_t bus = (hcan == &hcan1) ? 0U : 1U; + + /* Account for completion before HAL can reuse a free mailbox. */ + ServiceCanHealth(hcan); + if (((hcan->Instance->ESR & CAN_ESR_BOFF) != 0U) || + (HAL_CAN_GetTxMailboxesFreeLevel(hcan) == 0U)) + { + can_diagnostics[bus].tx_dropped++; + return; + } tx_header.StdId = std_id; tx_header.ExtId = 0U; @@ -1348,7 +1444,27 @@ static void SendCanFrame(CAN_HandleTypeDef *hcan, tx_header.DLC = (length <= 8U) ? length : 8U; tx_header.TransmitGlobalTime = DISABLE; - (void)HAL_CAN_AddTxMessage(hcan, &tx_header, (uint8_t *)data, &mailbox); + if (HAL_CAN_AddTxMessage(hcan, &tx_header, (uint8_t *)data, &mailbox) != HAL_OK) + { + can_diagnostics[bus].tx_dropped++; + return; + } + for (uint32_t index = 0U; index < 3U; index++) + { + if (mailbox == (1UL << index)) + { + /* A completion can race the HAL mailbox selection. Do not guess TXOK. */ + if (can_tx_tracked[bus][index]) + { + can_diagnostics[bus].tx_unobserved++; + } + can_tx_started_ms[bus][index] = HAL_GetTick(); + can_tx_tracked[bus][index] = true; + can_tx_abort_requested[bus][index] = false; + break; + } + } + can_diagnostics[bus].tx_queued++; } static void BroadcastCanFrame(uint16_t std_id, const uint8_t *data, uint8_t length) diff --git a/tests/test_can_health.py b/tests/test_can_health.py new file mode 100644 index 0000000..356ad7f --- /dev/null +++ b/tests/test_can_health.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Run the actual firmware CAN service/send functions against a host HAL stub.""" +from pathlib import Path +import subprocess +import tempfile + +source = (Path(__file__).resolve().parents[1] / + 'A11_F407_SDC_ACQ_DAC_CubeMX/Src/main.c').read_text() + +def function(name): + start = source.index('static void ' + name + '(', source.index('static void ServiceCanHealth(CAN_HandleTypeDef *hcan)\n{')) + brace = source.index('{', start) + depth = 1 + end = brace + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + +state_start = source.index('typedef struct\n{\n uint32_t last_esr;') +state_end = source.index('static bool can_tx_abort_requested[2][3];', state_start) +state_end += len('static bool can_tx_abort_requested[2][3];') +harness = r''' +#include +#include +#include +#include +#include +#define CAN_TX_TIMEOUT_MS 100U +#define CAN_ESR_BOFF 4U +#define CAN_ESR_EPVF 2U +#define CAN_TSR_TXOK0 2U +#define CAN_ID_STD 0U +#define CAN_RTR_DATA 0U +#define DISABLE 0U +#define HAL_OK 0U +#define HAL_ERROR 1U +typedef struct { uint32_t ESR, TSR, pending; } CAN_TypeDef; +typedef struct { CAN_TypeDef *Instance; } CAN_HandleTypeDef; +typedef struct { uint32_t StdId, ExtId, IDE, RTR, DLC, TransmitGlobalTime; } CAN_TxHeaderTypeDef; +static CAN_TypeDef regs[2]; +static CAN_HandleTypeDef hcan1 = {®s[0]}, hcan2 = {®s[1]}; +static uint32_t now, abort_calls, add_result, abort_result; +static bool race_completion; +static uint32_t HAL_GetTick(void) { return now; } +static uint32_t HAL_CAN_IsTxMessagePending(CAN_HandleTypeDef *h, uint32_t mask) { + return (h->Instance->pending & mask) != 0; +} +static uint32_t HAL_CAN_GetTxMailboxesFreeLevel(CAN_HandleTypeDef *h) { + if (race_completion) { h->Instance->pending = 0; race_completion = false; } + return 3 - __builtin_popcount(h->Instance->pending); +} +static uint32_t HAL_CAN_AbortTxRequest(CAN_HandleTypeDef *h, uint32_t mask) { + (void)h; (void)mask; abort_calls++; return abort_result; +} +static uint32_t HAL_CAN_AddTxMessage(CAN_HandleTypeDef *h, const CAN_TxHeaderTypeDef *hdr, + uint8_t *data, uint32_t *mailbox) { + (void)hdr; (void)data; + if (add_result) return add_result; + for (uint32_t i = 0; i < 3; i++) if (!(h->Instance->pending & (1U << i))) { + *mailbox = 1U << i; h->Instance->pending |= *mailbox; + h->Instance->TSR &= ~(CAN_TSR_TXOK0 << (8 * i)); return HAL_OK; + } + return HAL_ERROR; +} +''' +harness += source[state_start:state_end] + '\n' +harness += function('ServiceCanHealth') + '\n' + function('SendCanFrame') +harness += r''' +static void reset(void) { + memset(regs, 0, sizeof regs); + memset((void *)can_diagnostics, 0, sizeof can_diagnostics); + memset(can_tx_tracked, 0, sizeof can_tx_tracked); + memset(can_tx_abort_requested, 0, sizeof can_tx_abort_requested); + now = abort_calls = add_result = abort_result = 0; race_completion = false; +} +static void send(CAN_HandleTypeDef *h) { uint8_t data[8] = {0}; SendCanFrame(h, 0x6A0, data, 8); } +int main(void) { + reset(); send(&hcan1); + assert(can_diagnostics[0].tx_queued == 1 && can_diagnostics[0].tx_completed == 0); + regs[0].pending = 0; regs[0].TSR = CAN_TSR_TXOK0; + ServiceCanHealth(&hcan1); ServiceCanHealth(&hcan1); + assert(can_diagnostics[0].tx_completed == 1); /* counted only once */ + send(&hcan1); regs[0].pending = 0; ServiceCanHealth(&hcan1); + assert(can_diagnostics[0].tx_failed == 1); /* HAL_OK is not delivery */ + reset(); send(&hcan2); now = 99; ServiceCanHealth(&hcan2); assert(abort_calls == 0); + now = 100; ServiceCanHealth(&hcan2); ServiceCanHealth(&hcan2); + assert(abort_calls == 1 && can_diagnostics[1].abort_requests == 1); + send(&hcan1); assert(can_diagnostics[0].tx_queued == 1); /* other bus independent */ + regs[1].pending = 0; ServiceCanHealth(&hcan2); + assert(can_diagnostics[1].tx_failed == 1); + reset(); now = UINT32_MAX - 49; send(&hcan1); now = 49; + ServiceCanHealth(&hcan1); assert(abort_calls == 0); + now = 50; ServiceCanHealth(&hcan1); assert(abort_calls == 1); /* tick wrap */ + reset(); send(&hcan1); regs[0].ESR = CAN_ESR_BOFF | CAN_ESR_EPVF; + send(&hcan1); ServiceCanHealth(&hcan1); + assert(can_diagnostics[0].bus_off_events == 1 && can_diagnostics[0].passive_events == 1); + assert(can_diagnostics[0].tx_dropped == 1 && abort_calls == 1); + regs[0].pending = 0; regs[0].ESR = 0; send(&hcan1); + assert(can_diagnostics[0].recovery_events == 1 && can_diagnostics[0].tx_queued == 2); + reset(); send(&hcan1); send(&hcan1); send(&hcan1); send(&hcan1); + assert(can_diagnostics[0].tx_queued == 3 && can_diagnostics[0].tx_dropped == 1); + reset(); add_result = HAL_ERROR; send(&hcan1); + assert(can_diagnostics[0].tx_dropped == 1 && !can_tx_tracked[0][0]); + reset(); send(&hcan1); now = 100; abort_result = HAL_ERROR; ServiceCanHealth(&hcan1); + assert(can_diagnostics[0].abort_failures == 1 && !can_tx_abort_requested[0][0]); + abort_result = HAL_OK; ServiceCanHealth(&hcan1); assert(can_diagnostics[0].abort_requests == 1); + reset(); send(&hcan1); race_completion = true; send(&hcan1); + assert(can_diagnostics[0].tx_unobserved == 1 && can_diagnostics[0].tx_queued == 2); + /* Validate success bit selection for mailboxes 1 and 2. */ + reset(); send(&hcan1); send(&hcan1); send(&hcan1); + regs[0].pending = 0; regs[0].TSR = (CAN_TSR_TXOK0 << 8) | (CAN_TSR_TXOK0 << 16); + ServiceCanHealth(&hcan1); + assert(can_diagnostics[0].tx_completed == 2 && can_diagnostics[0].tx_failed == 1); + puts("CAN health tests passed"); +} +''' +with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + (root / 'test.c').write_text(harness) + subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', + '-fsanitize=undefined', str(root / 'test.c'), '-o', str(root / 'test')], check=True) + subprocess.run([str(root / 'test')], check=True) diff --git a/固件使用说明.md b/固件使用说明.md index 9592f72..086eac9 100644 --- a/固件使用说明.md +++ b/固件使用说明.md @@ -74,6 +74,12 @@ ADC=12300mV WheelSpeed=LF:0.289m/s RF:0.577m/s CAN1 和 CAN2 使用相同协议,所有主动上报均分别发送到两条总线。使用 11 位标准数据帧、低字节在前(little-endian)。CAN 正常发帧必须存在至少一个其他节点 ACK;单独接 CAN 分析仪且未开启 ACK 时,发送邮箱可能不会完成。 +两路 CAN 均开启硬件自动 Bus-Off 恢复(ABOM)。总线重新满足恢复条件后,无需 MCU RESET 或整车断电即可重新通信;恢复功能不能消除线束、终端、节点或位时序造成的错误。 + +主循环分别维护两路发送邮箱:Bus-Off 时停止入队并请求取消未完成帧;正常状态下超过 100 ms 仍未完成的帧也会被异步取消,避免未连接的 CAN2 或缺少 ACK 时积压旧上报和 DAC 应答。邮箱满或入队失败的帧直接丢弃并统计,不阻塞另一条 CAN,也不等待取消完成。周期上报在后续周期重新生成;DAC 应答不保证重发。 + +可通过 STLINK/GDB 读取 `can_diagnostics[0]`(CAN1)和 `[1]`(CAN2):`last_esr` 为最近的硬件错误状态,`bus_off_events` / `recovery_events` / `passive_events` 为主循环观察到的状态转换次数,`tx_queued` 为入队数,`tx_completed` 为硬件 TXOK 完成数,`tx_failed` 为未成功完成数(含取消),`tx_unobserved` 为在邮箱重新分配前未能采到完成结果的帧数,`tx_dropped` 为未入队数,`abort_requests` / `abort_failures` 为取消请求成功/失败次数。计数上电清零,按 uint32 回绕;轮询可能漏掉短暂状态转换。ESR 的 TEC 字段只有发送错误计数的低 8 位,Bus-Off 时不能将它当作完整计数。TXOK 仍需外部抓包验证报文内容和持续通信。 + | ID | 名称 | 周期/触发 | 数据 | |---:|---|---|---| | `0x6A0` | 心跳 | 每 100 ms | `A1 01 seq_L seq_H uptime_0..3` |