FEATURED · 精选文章

第五章:CAN 业务层开发,报文解析、心跳、超时处理,工程实战案例

发布时间 / 2026/8/25 15:58:10
来源 / 创域科博编辑部
栏目 / 资讯中心
第五章:CAN 业务层开发,报文解析、心跳、超时处理,工程实战案例 导读前面章节完成硬件、协议、底层驱动到这一章脱离简单的收发 Demo面向真实工程开发。实际项目不会只做简单的发报文打印需要处理报文解析、数据打包、节点心跳保活、通讯超时检测、多报文管理、异常容错。本章提供可直接落地的业务层框架、完整 C 代码附带机器人底盘模拟业务案例。前面底层代码完成能力可以发送报文、中断接收原始报文、检测总线硬件故障。 但是底层驱动存在短板接收回调在中断内部不适合做复杂解析运算收到报文之后没有缓存新报文会覆盖旧报文没有心跳机制无法判断远程节点掉线没有超时判断设备断线之后业务逻辑不会感知原始字节数据需要转换成实际物理量转速、电压、温度。解决方案在驱动和业务逻辑之间增加CAN 业务中间层使用环形缓冲区FIFO 队列把中断收到的报文搬运到主循环业务逻辑在主循环统一解析处理。5.1 CAN 报文环形接收队列设计中断回调只做一件事把收到的完整报文压入环形缓冲区快速退出中断报文解析、换算全部放到主循环执行。避免中断长时间占用防止系统卡顿、丢中断。环形队列结构体定义#ifndef CAN_BUSINESS_H #define CAN_BUSINESS_H #include stm32f1xx_hal.h //单条CAN报文结构体保存一帧完整信息 typedef struct { uint32_t id; //报文ID标准/扩展统一存放 uint8_t is_ext; //0标准帧1扩展帧 uint8_t is_remote; //0数据帧1远程帧 uint8_t dlc; //数据长度0~8 uint8_t data[8]; //报文数据 uint32_t rx_tick; //接收时刻系统滴答tick用于超时判断 }CanRxMsgTypeDef; //环形FIFO队列 #define CAN_RX_FIFO_SIZE 32 //最多缓存32帧报文根据项目调整 typedef struct { CanRxMsgTypeDef buf[CAN_RX_FIFO_SIZE]; uint16_t wr_index; //写指针中断中写入 uint16_t rd_index; //读指针主循环读取 }CanRxFifoTypeDef; extern CanRxFifoTypeDef g_can_rx_fifo; //对外接口 void Can_Rx_Fifo_Push(CanRxMsgTypeDef *msg); uint8_t Can_Rx_Fifo_Pop(CanRxMsgTypeDef *msg); #endif环形队列实现 can_business.c#include can_business.h CanRxFifoTypeDef g_can_rx_fifo {0}; /** * brief 压入报文到FIFO中断中调用执行要快 */ void Can_Rx_Fifo_Push(CanRxMsgTypeDef *msg) { uint16_t next_wr (g_can_rx_fifo.wr_index 1) % CAN_RX_FIFO_SIZE; //队列已满丢弃最老报文工程可加日志标记溢出 if(next_wr g_can_rx_fifo.rd_index) { return; } g_can_rx_fifo.buf[g_can_rx_fifo.wr_index] *msg; g_can_rx_fifo.wr_index next_wr; } /** * brief 主循环弹出一条报文 * retval 0成功1队列空 */ uint8_t Can_Rx_Fifo_Pop(CanRxMsgTypeDef *msg) { if(g_can_rx_fifo.rd_index g_can_rx_fifo.wr_index) { return 1; } *msg g_can_rx_fifo.buf[g_can_rx_fifo.rd_index]; g_can_rx_fifo.rd_index (g_can_rx_fifo.rd_index 1) % CAN_RX_FIFO_SIZE; return 0; }修改接收中断回调函数中断回调不再做 printf 打印只组装结构体压入 FIFO 队列。void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_buf[8] {0}; CanRxMsgTypeDef rx_msg {0}; if(HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx_header, rx_buf) ! HAL_OK) { return; } //填充报文结构体 if(rx_header.IDE CAN_ID_STD) { rx_msg.id rx_header.StdId; rx_msg.is_ext 0; } else { rx_msg.id rx_header.ExtId; rx_msg.is_ext 1; } rx_msg.is_remote (rx_header.RTR CAN_RTR_REMOTE) ? 1 : 0; rx_msg.dlc rx_header.DLC; for(uint8_t i0;irx_msg.dlc;i) { rx_msg.data[i] rx_buf[i]; } rx_msg.rx_tick HAL_GetTick(); //记录接收时间戳 Can_Rx_Fifo_Push(rx_msg); }工程重点中断回调中禁止使用 HAL_Delay禁止复杂运算禁止大量 printfprintf 内部有互斥锁中断调用极易死锁。5.2 主循环报文分发解析框架在 while (1) 循环中不断从 FIFO 取出报文根据 ID 执行对应的解析函数。模拟一个机器人底盘业务场景ID 0x201电机 1 状态报文转速、电流、故障码ID 0x202电机 2 状态报文ID 0x300节点心跳报文#include can_business.h //电机设备状态结构体 typedef struct { int16_t speed; //转速 int16_t current; //电流 uint8_t fault_code; //故障码 uint8_t online; //0掉线1在线 uint32_t last_rx_tick; //最后收到报文时间 }MotorDevTypeDef; MotorDevTypeDef motor1 {0}; MotorDevTypeDef motor2 {0}; uint32_t g_heartbeat_last_tick 0; #define DEV_TIMEOUT_MS 200 //200ms没有报文判定设备掉线 /** * brief 解析0x201电机1报文 * 报文格式DLC5 * data[0~1]:转速 int16 * data[2~3]:电流 int16 * data[4]:故障码 */ void Parse_Motor1(CanRxMsgTypeDef *msg) { motor1.speed (int16_t)((msg-data[0] 8) | msg-data[1]); motor1.current (int16_t)((msg-data[2] 8) | msg-data[3]); motor1.fault_code msg-data[4]; motor1.last_rx_tick msg-rx_tick; motor1.online 1; } /** * brief 解析0x202电机2报文 */ void Parse_Motor2(CanRxMsgTypeDef *msg) { motor2.speed (int16_t)((msg-data[0] 8) | msg-data[1]); motor2.current (int16_t)((msg-data[2] 8) | msg-data[3]); motor2.fault_code msg-data[4]; motor2.last_rx_tick msg-rx_tick; motor2.online 1; } /** * brief 解析全局心跳ID 0x300 */ void Parse_HeartBeat(CanRxMsgTypeDef *msg) { g_heartbeat_last_tick msg-rx_tick; } /** * brief CAN业务总处理函数放到主循环while(1)循环调用 */ void Can_Business_Process(void) { CanRxMsgTypeDef rx_msg; uint32_t now_tick HAL_GetTick(); //循环读取FIFO内所有报文 while(Can_Rx_Fifo_Pop(rx_msg) 0) { if(rx_msg.is_remote 1) { continue; //本项目忽略远程帧 } //根据ID分发解析 switch(rx_msg.id) { case 0x201: Parse_Motor1(rx_msg); break; case 0x202: Parse_Motor2(rx_msg); break; case 0x300: Parse_HeartBeat(rx_msg); break; default: break; } } //通讯超时检测判断节点掉线 if((now_tick - motor1.last_rx_tick) DEV_TIMEOUT_MS) { motor1.online 0; } if((now_tick - motor2.last_rx_tick) DEV_TIMEOUT_MS) { motor2.online 0; } }主循环调用示例while(1) { CAN_Hardware_Detect(); //第二章总线硬件故障检测 Can_Business_Process(); //业务报文解析超时检测 //业务逻辑读取电机状态 if(motor1.online 1) { //电机在线读取转速电流做控制逻辑 } else { //电机掉线执行保护逻辑停机报警 } HAL_Delay(1); }5.3 心跳包设计方案CAN 网络心跳包有两种工程方案方案 A节点主动定时上报心跳常用每个从节点周期性发送心跳报文 ID0x300报文内部携带设备 ID、设备状态。主控收到更新时间戳超过超时阈值标记设备离线。 示例主控定时发送心跳请求或者设备主动上报//100ms周期设备主动发送心跳 uint8_t hb_buf[2] {0x01,0x00}; CAN_Send_StdData(0x300,hb_buf,2);方案 B主控轮询远程帧主控周期性发送远程帧各个节点应答。缺点总线负载升高项目使用较少。工程注意超时时间设置要大于报文发送周期 2~3 倍。设备 100ms 发一次心跳超时设置 200‑300ms防止偶尔丢包误判掉线。5.4 业务数据打包发送示例控制电机下发转速指令。定义 ID0x101 为电机控制报文DLC4两个 int16 分别设置左右电机转速。/** * brief 下发电机转速指令 * param spd1 电机1转速 * param spd2 电机2转速 */ void Can_Set_Motor_Speed(int16_t spd1,int16_t spd2) { uint8_t tx_buf[4]; //大端模式高字节在前 tx_buf[0] (spd1 8) 0xFF; tx_buf[1] spd1 0xFF; tx_buf[2] (spd2 8) 0xFF; tx_buf[3] spd2 0xFF; CAN_Send_StdData(0x101, tx_buf,4); }调用示例//电机1转速 300电机2转速 -200 Can_Set_Motor_Speed(300, -200);重要约定CAN 没有规定大小端项目内必须统一字节序行业普遍采用大端高字节先发双方设备按照同一套协议文档解析。5.5 CAN 总线负载率概念总线负载率总线上有效报文占用时间 / 总时间百分比。负载率 30%通讯非常稳定推荐30%~60%可以工作70%总线压力大容易出现延迟、仲裁抢占高优先级报文会优先发送低优先级报文延迟严重90% 极易丢包、Bus‑Off。计算简单举例500K 波特率一帧标准数据帧大概 111bit每秒发送 100 帧占用比特 100*111 11100 bit。 总线理论比特 500000 bit/s负载率 11100 /500000 ≈2.22%。工程优化手段非关键报文降低发送周期不要无脑高频发包多数据尽量合并到一帧报文减少帧数量重要控制报文 ID 设置更小获取更高优先级。5.6 本章常见工程坑在中断回调做复杂解析、打印造成丢包、系统不稳定解决方案FIFO 队列搬运到主循环。没有超时检测设备拔掉之后程序依然认为设备在线不会触发保护。收发两端大小端不一致解析出来数值乱跳。报文 DLC 判断缺失收到 ID 正确但是 DLC 长度不对直接访问 data 数组造成越界。安全写法解析报文首先判断 msg-dlc 是否等于预期长度否则直接丢弃该帧。if(msg-dlc ! 5) return; //解析电机报文必须DLC等于5总线负载过高大量报文同时发送低 ID 抢占总线业务响应延迟。第五章到此结束。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻