Chapter 09 · CAN 总线 / 汽车电子

FreeRTOS + CAN

把 CAN 收发从裸机搬进 RTOS:ISR 只投递、任务做重活,用队列解耦收发,监控总线负载,并用临界区保护共享句柄,构建不丢帧的工程化架构。

课程进度56%

关键概念

裸机收发的问题
纯 while(1) 轮询里,若报文解析耗时长,会来不及取走 FIFO 中的报文(bxCAN FIFO 只有 3 帧深),造成溢出丢帧;若用阻塞式发送,主循环会卡在等待发送完成上,其他任务全部停摆。RTOS 通过任务调度和队列缓冲,把“接收”“解析”“发送”解耦,从根本上缓解这些问题。
ISR 只做最小工作
中断服务程序运行时会屏蔽同级及更低优先级中断,占用越久系统响应越差。CAN 接收中断里应只做一件事:把报文从硬件 FIFO 取出、塞进 RTOS 队列,立刻返回。所有解析、业务逻辑都交给普通任务在中断之外完成。这是嵌入式实时系统的黄金准则。
FromISR 后缀 API
FreeRTOS 中所有可在中断里调用的 API 都带 FromISR 后缀,如 xQueueSendFromISRxSemaphoreGiveFromISR。它们不会阻塞,且用一个出参 pxHigherPriorityTaskWoken 告知是否唤醒了更高优先级任务。普通版本 API(不带 FromISR)绝不能在中断里调用,否则会破坏内核状态导致崩溃。
队列 (Queue) 投递
FreeRTOS 队列是线程/中断安全的 FIFO 缓冲。ISR 用 xQueueSendFromISR 把报文拷贝进队列,接收任务用 xQueueReceive(可阻塞等待)取出。队列充当中断与任务之间的缓冲区,即使任务一时繁忙,报文也先排在队列里不丢,大大提高抗突发能力。
portYIELD_FROM_ISR
FromISR 调用唤醒了比当前被打断任务优先级更高的任务时,应在中断结束前请求一次上下文切换,让高优先级任务尽快运行。做法是把 xQueueSendFromISR 回填的 xHigherPriorityTaskWoken 传给 portYIELD_FROM_ISR()。忘记它不会崩,但会增加任务响应延迟。
总线负载率 (Bus Load)
单位时间内总线被占用的比例。每帧占用的位数 ≈ ID 与数据长度决定的固定开销(标准帧约 44+8×DLC 位,含填充位裕量)。负载率 = Σ(每帧位数 × 频率) / 波特率。工程上建议把负载率控制在 30%~50% 以下,为高优先级报文和错误重传留出余量;超过 70% 时低优先级报文可能长时间抢不到总线。
优先级翻转 (Priority Inversion)
高优先级任务因等待被低优先级任务持有的资源(如互斥锁)而被阻塞,期间中等优先级任务却抢占了低优先级任务,导致高优先级任务被“间接”拖延。FreeRTOS 的互斥量(Mutex)支持优先级继承缓解此问题:持锁的低优先级任务会临时提升到等待者的优先级。
临界区保护共享句柄
多个任务若同时调用 HAL_CAN_AddTxMessage 操作同一个 CAN 句柄,可能在填邮箱过程中被打断造成竞态。需用互斥量(xSemaphoreTake/Give)串行化访问,或用 taskENTER_CRITICAL() 短临界区保护。发送任务集中管理句柄、其他任务只往发送队列投递数据,是更简洁的做法。

ISR → 队列 → 任务 数据流

核心架构:中断只负责“搬运”,队列负责“缓冲”,任务负责“干活”。收发彻底解耦:

FreeRTOS 下 CAN 收发架构 ═══════════════════════════════════════════════════════════════ 总线报文到达 │ ▼ ┌──────────────────┐ 中断上下文(越短越好)CAN RX ISR │ GetRxMessage 取出 │ RxFifo0Callback │──────────┐ └──────────────────┘ │ xQueueSendFromISR ▼ ┌─────────────────┐ │ RX 队列 │ 缓冲,抗突发 │ (深度 16 帧) │ └────────┬────────┘ │ xQueueReceive(阻塞) ▼ ┌──────────────────┐ 任务上下文(可从容解析)RX 处理任务 │ 解析业务、更新状态 │ CAN_Rx_Task │──┐ └──────────────────┘ │ 需要回复时 → xQueueSend ▼ ┌─────────────────┐ │ TX 队列 │ └────────┬────────┘ │ xQueueReceive ▼ ┌──────────────────┐ 独占 CAN 句柄发送TX 发送任务 │ AddTxMessage │ CAN_Tx_Task │──────────▶ 总线 └──────────────────┘
为什么把发送也交给专门任务?

让唯一的“TX 发送任务”独占 CAN 句柄,其他任务想发报文就往 TX 队列投递数据帧。这样天然避免了多任务并发写同一句柄的竞态,无需到处加锁;发送任务还能统一做流控、重试、优先级排序。这是“单一所有者”模式,比到处 xSemaphoreTake 更清晰、更不易出错。

完整代码:双任务 + ISR 投递队列

下面是一套可直接套用的骨架:接收中断投递到 RX 队列,RX 任务解析后按需生成回复投到 TX 队列,TX 任务独占句柄发送。

/* can_rtos.c — FreeRTOS + bxCAN 工程化收发 */
#include "FreeRTOS.h"
#include "queue.h"
#include "task.h"
#include "stm32f4xx_hal.h"

extern CAN_HandleTypeDef hcan1;

/* 队列里传递的报文结构 */
typedef struct {
  uint32_t id;
  uint8_t  dlc;
  uint8_t  data[8];
} CanFrame_t;

static QueueHandle_t s_rxQueue;   /* 接收队列 */
static QueueHandle_t s_txQueue;   /* 发送队列 */

/* ── 中断回调:只取报文 + 投递,绝不做重活 ── */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
  CAN_RxHeaderTypeDef h;
  CanFrame_t f;
  BaseType_t woken = pdFALSE;

  if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &h, f.data) == HAL_OK) {
    f.id  = h.StdId;
    f.dlc = h.DLC;
    /* FromISR 版本:非阻塞、中断安全 */
    xQueueSendFromISR(s_rxQueue, &f, &woken);
  }
  /* 若唤醒了更高优先级任务,请求一次切换 */
  portYIELD_FROM_ISR(woken);
}

/* ── RX 任务:从容解析,需要时生成回复 ── */
void CAN_Rx_Task(void *arg) {
  CanFrame_t rx;
  for (;;) {
    /* 阻塞等待,队列空时任务让出 CPU,零轮询开销 */
    if (xQueueReceive(s_rxQueue, &rx, portMAX_DELAY) == pdTRUE) {
      if (rx.id == 0x100) {
        /* 收到查询帧 → 组回复帧投到 TX 队列 */
        CanFrame_t tx = { .id = 0x101, .dlc = 2 };
        tx.data[0] = 0x5A; tx.data[1] = 0xA5;
        xQueueSend(s_txQueue, &tx, 0);
      }
    }
  }
}

/* ── TX 任务:独占句柄,串行发送,无竞态 ── */
void CAN_Tx_Task(void *arg) {
  CanFrame_t tx;
  CAN_TxHeaderTypeDef hdr = {0};
  uint32_t mbox;
  hdr.IDE = CAN_ID_STD; hdr.RTR = CAN_RTR_DATA;

  for (;;) {
    if (xQueueReceive(s_txQueue, &tx, portMAX_DELAY) == pdTRUE) {
      hdr.StdId = tx.id;
      hdr.DLC   = tx.dlc;
      /* 邮箱满则等待,直到有空闲(简单退避)*/
      while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) == 0)
        vTaskDelay(1);
      HAL_CAN_AddTxMessage(&hcan1, &hdr, tx.data, &mbox);
    }
  }
}

/* ── 初始化:建队列、建任务 ── */
void CAN_RTOS_Init(void) {
  s_rxQueue = xQueueCreate(16, sizeof(CanFrame_t));
  s_txQueue = xQueueCreate(16, sizeof(CanFrame_t));

  HAL_CAN_Start(&hcan1);
  HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

  /* TX 任务优先级略高于 RX,保证回复及时发出 */
  xTaskCreate(CAN_Rx_Task, "canrx", 256, NULL, 3, NULL);
  xTaskCreate(CAN_Tx_Task, "cantx", 256, NULL, 4, NULL);
}

总线负载率计算与监控

估算并周期性上报总线负载,是判断网络是否健康的重要指标。下面用一个统计任务累加收发帧数,按秒换算负载率:

/* 标准数据帧位数估算:SOF+ID+控制+CRC+ACK+EOF ≈ 44 位固定
   + 8*DLC 数据位,再留 ~15% 位填充裕量 */
static inline uint32_t FrameBits(uint8_t dlc) {
  uint32_t bits = 44 + 8 * dlc;
  return bits + bits / 6;   /* 约 +16% 填充位裕量 */
}

volatile uint32_t g_bits_this_sec = 0;   /* 每收发一帧累加 */

void BusLoad_Task(void *arg) {
  const uint32_t baud = 500000;   /* 500 kbps */
  for (;;) {
    vTaskDelay(pdMS_TO_TICKS(1000));
    uint32_t bits;
    taskENTER_CRITICAL();      /* 短临界区读清计数 */
    bits = g_bits_this_sec;
    g_bits_this_sec = 0;
    taskEXIT_CRITICAL();
    uint32_t load = bits * 100 / baud;  /* 百分比 */
    printf("总线负载: %lu%%\n", load);
    if (load > 70)
      printf("警告:负载过高,低优先级报文可能被饿死\n");
  }
}
FromISR 与优先级翻转的注意点

① FromISR 后缀不可省:在中断里误用 xQueueSend(无 FromISR)会破坏内核调度状态,表现为偶发死机,极难调试。凡是中断上下文,一律用 FromISR 版本并处理 xHigherPriorityTaskWoken
② 用 Mutex 而非二值信号量护共享资源:若确实要在多任务间共享 CAN 句柄,用互斥量(xSemaphoreCreateMutex)而非普通信号量,因为 Mutex 支持优先级继承,可缓解优先级翻转。
③ 中断优先级要低于 RTOS 阈值:能调用 FreeRTOS API 的中断,其优先级数值必须 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY(在 Cortex-M 上数值越大优先级越低),否则 FromISR 调用会触发断言。

看门狗与任务活性

生产系统应配硬件独立看门狗(IWDG)。喂狗动作不要放在中断里,而应由一个低优先级“监护任务”执行,并要求各关键任务定期上报“我还活着”标志;只有所有关键任务都活着时监护任务才喂狗。这样一旦某个任务因死锁、优先级翻转或队列耗尽而卡死,看门狗超时会触发复位,避免节点“假死”仍占着总线 ID 却不回应,拖垮整车网络。

本章小结

裸机收发的痛点是解析耗时导致 FIFO(仅深 3)溢出丢帧、阻塞发送卡死主循环。RTOS 方案的核心是三段解耦:ISR 只搬运HAL_CAN_GetRxMessage 取帧后 xQueueSendFromISR 投队列,再 portYIELD_FROM_ISR),队列做缓冲(抗突发、任务忙时报文不丢),任务干重活(RX 任务 xQueueReceive 阻塞取帧解析,TX 任务独占句柄串行发送)。“单一所有者”的 TX 任务模式天然免除多任务写句柄的竞态。总线负载率 = Σ(每帧位数×频率)/波特率,建议 ≤50%、超 70% 报警。工程要点:中断里一律用 FromISR 后缀 API 并处理 xHigherPriorityTaskWoken;共享资源用支持优先级继承的 Mutex 防优先级翻转;能调 RTOS API 的中断优先级须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY;再配独立看门狗由监护任务基于各任务活性喂狗,防节点假死。

⚒️ 配套开发者工具箱 · JSON / 正则 / 时间戳 / Cron 等 17 款免费在线工具