4. CAN标识符与滤波:标准帧与扩展帧的深度解析
各位同学,今天我们来聊聊CAN总线里一个绕不开的话题——标识符与滤波。说实话,我在飞控开发初期,就被ID分配坑过好几次。你想想看,一架无人机上十几个CAN节点,如果ID冲突或者滤波没配好,那总线上的数据就跟菜市场一样乱。
这一节,我会把标准帧(11位ID)和扩展帧(29位ID)的区别、ID分配策略,以及硬件滤波和软件滤波的实现,掰开了揉碎了讲清楚。嗯,咱们开始吧。
4.1 标准帧 vs 扩展帧:11位ID与29位ID
CAN总线有两种帧格式:标准帧和扩展帧。它们的核心区别,说白了就是ID的长度不同。
| 特性 | 标准帧(Base Frame) | 扩展帧(Extended Frame) |
|---|---|---|
| ID位数 | 11位 | 29位 |
| ID范围 | 0x000 ~ 0x7FF(2048个) | 0x00000000 ~ 0x1FFFFFFF(约5.3亿个) |
| 仲裁场长度 | 12位(ID + RTR) | 32位(ID + SRR + IDE + RTR) |
| 总线利用率 | 较高(帧头短) | 较低(帧头多2字节) |
| 兼容性 | 所有CAN控制器都支持 | 需控制器支持CAN 2.0B |
关键点来了:标准帧的ID优先级更高。为什么?因为扩展帧在仲裁时,IDE位(标识符扩展位)是显性(0),而标准帧的IDE位是隐性(1)。所以标准帧永远比扩展帧先抢到总线。我在做飞控时,就把心跳包、紧急指令这些高优先级消息,全部用标准帧发送。
我的经验:飞控系统中,建议把实时性要求高的消息(如电机控制指令、IMU数据)用标准帧,配置类、日志类消息用扩展帧。这样既能保证关键数据的低延迟,又能利用扩展帧的丰富ID空间做设备管理。
4.2 ID分配策略:别让总线变成战场
ID分配,是CAN总线设计的第一步,也是最容易出问题的一步。我见过一个项目,因为ID分配没规划好,导致两个传感器同时发送相同ID的数据,接收端根本分不清谁是谁。
这里我分享一套在飞控项目中验证过的分配策略:
- 按优先级分配:ID值越小,优先级越高。把紧急消息(如急停、故障上报)放在0x000~0x0FF区间。
- 按功能分组:比如0x100~0x1FF给传感器,0x200~0x2FF给执行器,0x300~0x3FF给控制器。
- 按节点编号分配:每个CAN节点分配一个唯一编号,ID = 功能基地址 + 节点编号。这样调试时一眼就能看出是谁发的。
- 预留扩展空间:别把ID用满,留20%的余量给未来升级。
避坑指南:我曾经在项目中把所有ID都按顺序排好,结果后来加了一个新传感器,发现ID区间已经用完了。最后不得不重新分配,改了一堆代码。所以,ID分配一定要留余量,而且最好用宏定义或枚举,别写死数字。
4.3 硬件滤波:让CAN控制器帮你干活
硬件滤波,说白了就是让CAN控制器在硬件层面直接过滤掉不关心的消息,减少CPU的负担。飞控系统里,CPU资源很宝贵,能省一点是一点。
大多数CAN控制器(如STM32的bxCAN、SJA1000)都支持两种滤波模式:
- 标识符列表模式:只接收ID完全匹配的消息。适合节点只关心少数几个ID的场景。
- 标识符掩码模式:通过掩码指定哪些位必须匹配,哪些位可以忽略。适合需要接收一组ID的场景。
举个例子,假设飞控主控需要接收所有传感器数据(ID范围0x100~0x1FF),用掩码模式配置如下:
// 以STM32 bxCAN为例
// 滤波器组0,掩码模式,32位宽度
CAN_FilterInitStructure.CAN_FilterNumber = 0;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
// 期望ID:0x100(高16位),掩码:0x1FF(只匹配高9位)
CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0100 >> 3; // 右移3位对齐
CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x01FF >> 3;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
这样配置后,只有ID在0x100~0x1FF范围内的消息才会进入FIFO,CPU只需要处理这些消息就行。
注意:硬件滤波虽然高效,但灵活性有限。比如你想同时接收0x100~0x1FF和0x300~0x3FF两组不连续的ID,单个滤波器可能搞不定。这时候就需要多个滤波器组合,或者用软件滤波来兜底。
4.4 软件滤波:最后的防线
软件滤波,就是在中断或任务里,对收到的消息再做一次ID判断。虽然会消耗CPU,但胜在灵活。
我一般这样用软件滤波:
- 硬件滤波做粗筛,过滤掉90%不关心的消息。
- 软件滤波做精筛,处理那些硬件滤波搞不定的复杂场景。
代码实现很简单:
// 软件滤波函数
uint8_t SoftwareFilter(uint32_t id, uint8_t ide) {
// 只接收标准帧,且ID在0x100~0x1FF或0x300~0x3FF
if (ide == CAN_ID_STD) {
if ((id >= 0x100 && id <= 0x1FF) ||
(id >= 0x300 && id <= 0x3FF)) {
return 1; // 接收
}
}
return 0; // 丢弃
}
// 在CAN接收中断中调用
void CAN_RX_IRQHandler(void) {
CanRxMsg rxMsg;
CAN_Receive(CAN_FIFO0, &rxMsg);
if (SoftwareFilter(rxMsg.ExtId, rxMsg.IDE)) {
// 处理消息
ProcessMessage(&rxMsg);
}
}
我的建议:软件滤波不要做太复杂的逻辑,否则中断服务程序执行时间过长,会影响其他中断的响应。如果滤波规则很复杂,可以考虑用查表法,把允许的ID列表放在一个数组里,用二分查找来判断。
4.5 知识体系总览
为了让大家更直观地理解这一节的内容,我画了一张图,把标准帧与扩展帧的区别、ID分配策略、硬件滤波与软件滤波的关系串起来。
这张图把这一节的核心逻辑都串起来了。你从上往下看,先搞清楚标准帧和扩展帧的区别,然后规划好ID怎么分配,最后根据需求选择硬件滤波还是软件滤波。嗯,思路就清晰了。
4.6 总结
这一节的内容,说白了就是三件事:
- 标准帧和扩展帧怎么选——高优先级用标准帧,复杂场景用扩展帧。
- ID怎么分配——按优先级、按功能、留余量,别拍脑袋。
- 滤波怎么配——硬件滤波做粗筛,软件滤波做精筛,两者结合最稳妥。
我在飞控项目里,一直用这套方法,没出过ID冲突的问题。你按照这个思路来,也能少踩坑。
公众号:蓝海资料掘金营,微信deep3321