FreeRTOS + CAN
把 CAN 收发从裸机搬进 RTOS:ISR 只投递、任务做重活,用队列解耦收发,监控总线负载,并用临界区保护共享句柄,构建不丢帧的工程化架构。
把 CAN 收发从裸机搬进 RTOS:ISR 只投递、任务做重活,用队列解耦收发,监控总线负载,并用临界区保护共享句柄,构建不丢帧的工程化架构。
FromISR 后缀,如 xQueueSendFromISR、xSemaphoreGiveFromISR。它们不会阻塞,且用一个出参 pxHigherPriorityTaskWoken 告知是否唤醒了更高优先级任务。普通版本 API(不带 FromISR)绝不能在中断里调用,否则会破坏内核状态导致崩溃。xQueueSendFromISR 把报文拷贝进队列,接收任务用 xQueueReceive(可阻塞等待)取出。队列充当中断与任务之间的缓冲区,即使任务一时繁忙,报文也先排在队列里不丢,大大提高抗突发能力。FromISR 调用唤醒了比当前被打断任务优先级更高的任务时,应在中断结束前请求一次上下文切换,让高优先级任务尽快运行。做法是把 xQueueSendFromISR 回填的 xHigherPriorityTaskWoken 传给 portYIELD_FROM_ISR()。忘记它不会崩,但会增加任务响应延迟。HAL_CAN_AddTxMessage 操作同一个 CAN 句柄,可能在填邮箱过程中被打断造成竞态。需用互斥量(xSemaphoreTake/Give)串行化访问,或用 taskENTER_CRITICAL() 短临界区保护。发送任务集中管理句柄、其他任务只往发送队列投递数据,是更简洁的做法。核心架构:中断只负责“搬运”,队列负责“缓冲”,任务负责“干活”。收发彻底解耦:
让唯一的“TX 发送任务”独占 CAN 句柄,其他任务想发报文就往 TX 队列投递数据帧。这样天然避免了多任务并发写同一句柄的竞态,无需到处加锁;发送任务还能统一做流控、重试、优先级排序。这是“单一所有者”模式,比到处 xSemaphoreTake 更清晰、更不易出错。
下面是一套可直接套用的骨架:接收中断投递到 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 后缀不可省:在中断里误用 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;再配独立看门狗由监护任务基于各任务活性喂狗,防节点假死。