做STM32的CAN通讯,十有八九会在过滤器这儿卡一下。我第一次调F103的bxCAN时,收发器、波特率、中断都配好了,发端数据照发,收端就是进不了接收中断。把过滤器关掉之后又反过来,总线上所有节点的帧全都涌进FIFO,程序大部分时间都在处理垃圾ID。后来把CAN_FxR1、CAN_FxR2、FMR、FS1R、FFA1R、FA1R这几个寄存器逐个手动配置,又用两块板子在真实总线上连着验证了一晚上,才算把STM32 CAN通讯过滤器寄存器的行为彻底摸清。这篇就从寄存器角度,把这些配置逻辑和验证结果完整记录下来,给正在调过滤器的朋友一份可以直接照着做的参考。
1. 为什么明明一行配置能解决的问题,我还是要翻开寄存器手册
很多教程里过滤器就一句话:调HAL_CAN_ConfigFilter,参数抄一下,完事。但实际项目里,一旦出现“过滤器配了好像没配”“只收到一部分帧”“CAN1和CAN2互相抢过滤器”这类问题,不搞懂寄存器就只能反复试错。
1.1 过滤器在CAN接收链路里的位置
bxCAN外设收到一帧报文后,不是直接扔进FIFO等CPU读,而是先过一次硬件过滤器。过滤器的作用就是根据标识符(标准帧的11位ID或扩展帧的29位ID)决定这帧要不要进FIFO、进FIFO0还是FIFO1。
链路大致是这样:总线电平 -> CAN内核解析 -> 验收过滤器 -> FIFO0/FIFO1 -> 中断或查询读取。过滤器位于内核和FIFO之间,匹配不上的帧在硬件层面就被丢掉了,CPU根本感知不到。这也正是过滤器的价值:总线上几百个ID,你只关心其中两三个,如果全收进来再靠软件判断,中断频率会高得离谱,还可能因为FIFO溢出丢掉真正关心的帧。
1.2 只有寄存器能解释清楚的几个场景
我用HAL或者标准库配过滤器,遇到过三类问题,每一类最后都得回到寄存器才能定位:
- 过滤器配置成功,但某个bank就是不生效。回读FA1R才发现激活位没写进去,或者FMR的FINIT位没有按正确时序操作。
- CAN1和CAN2同时用,结果CAN2一个过滤器都收不到。因为CAN2使用的bank范围和CAN1重叠,没有正确设置FMR里的CAN2SB。
- Bootloader和App都要重建过滤规则。直接调HAL封装函数当然能初始化,但切换过程中寄存器处于什么状态、哪些bank被占,必须看得懂FMR/FA1R。
这类问题只看库函数的参数列表,永远找不到答案。寄存器才是底层唯一的事实标准。
1.3 本文的实验方法
后面的内容都基于STM32F103C8T6的bxCAN外设,两块板子通过TJA1050收发器实际对接,普通模式,500kbps。F105、F407这些带bxCAN的芯片逻辑一致;使用FDCAN的G系列/H7系列不要直接照抄,FDCAN的过滤器组织形式完全不同。
2. 从两组32位寄存器到六个控制寄存器:过滤器组的完整地图
STM32F103的bxCAN里有28个过滤器组,编号0到27。每个过滤器组内部有两个32位寄存器:CAN_FxR1和CAN_FxR2(x是组号),在标准外设库里对应CANx->sFilterRegister[bank].FR1和.FR2。这两个寄存器装的就是ID或掩码,而它工作在什么模式、什么位宽、挂哪个FIFO、有没有激活,由另外五个控制寄存器决定。
2.1 一个过滤器组内部:CAN_FxR1和CAN_FxR2的分工
一个过滤器组本质上就是一个8字节的存储单元,可以按两种位宽来用:
- 32位位宽:两个寄存器合起来表示完整的一条过滤规则。列表模式时,FR1和FR2各存一个ID,两个槽位;掩码模式时,FR1存期望ID,FR2存掩码。
- 16位位宽:FR1的高16位、低16位,FR2的高16位、低16位,一共四个16位槽位。列表模式可以放四条ID,掩码模式可以放两组“ID+掩码”。
一句话概括资源:一个组能装几条规则,取决于你是列表模式还是掩码模式,以及有没有把寄存器拆成16位槽。
2.2 五个控制寄存器各自管什么
28个过滤器组在内存里连续排列,每组固定占用8字节。第一组的FR1偏移是0x40,FR2是0x44,下一组直接加8,所以第n组的FR1偏移就是0x40 + n*8,FR2再加4。调试器里手工改寄存器时,这个偏移关系很有用。
过滤器组怎么工作,由下面这几个寄存器控制:
| 寄存器 | 英文全称 | 每个bit对应一个过滤器组,置1/置0的含义 |
|---|---|---|
| FMR | Filter Master Register | bit0=FINIT写保护;bits13:8=CAN2SB,CAN2起始组号 |
| FM1R | Filter Mode Register | 1=列表模式,0=掩码模式 |
| FS1R | Filter Scale Register | 1=32位位宽,0=16位位宽 |
| FFA1R | Filter FIFO Assignment Register | 1=关联FIFO1,0=关联FIFO0 |
| FA1R | Filter Activation Register | 1=激活该过滤器组,0=禁用 |
也就是说,你配置第7组过滤器时,就是操作第7个bit。比如想让第7组工作在32位列表模式并挂FIFO1,就要把FS1R的bit7置1、FM1R的bit7置1、FFA1R的bit7置1,最后FA1R的bit7也置1让它生效。
2.3 FINIT的写保护:改配置前必须跨过的门槛
FMR寄存器里最容易被忽略的就是bit0,FINIT。这款CAN控制器对过滤器相关的寄存器有一道写保护:只有FINIT等于1时,FM1R、FS1R、FFA1R、FA1R、CAN_FxR1、CAN_FxR2这些寄存器才允许被修改。FINIT等于0时,你往这些寄存器写值,写进去的等于空气。
标准库初始化函数内部会自动处理这个时序,但如果你在调试器里直接改寄存器做实验,或者自己写寄存器操作函数,就必须手动控制。正常顺序只有两步:先把FMR的FINIT置1,再配置其他寄存器;全部写完,最后把FINIT清0,配置才会在硬件层面真正生效。后面实验配置函数里我会把这个时序写进去。
2.4 CAN1和CAN2之间怎么分bank
F103的bxCAN比较特殊:28个过滤器组是CAN1和CAN2共用的,CAN1可以全用,CAN2只能用其中一部分。划分依据就是FMR寄存器里的CAN2SB字段,它指定CAN2从第几个组开始用。
比如设置CAN2SB=14,那0到13号组归CAN1,14到27号组归CAN2。CAN2SB复位默认值是0,这在只用一个CAN时没问题,但如果你初始化完CAN2发现过滤器完全不工作,第一反应就应该是检查CAN2SB有没有设置,而不是去调掩码。
3. 掩码模式、列表模式、标准帧、扩展帧:把过滤逻辑拆成位运算
过滤器寄存器里存的东西不神秘,本质就是位比较。只是这个位比较有几个参数容易绕晕,拆开看就清晰了。
3.1 列表模式:拿ID去和两个槽位挨个比
列表模式下的规则可以理解成一张名单。报文进来后,硬件把报文的标识符拿过来,和FR1、FR2里存的值逐一比较,只要完全等于其中一个,就放行。
这里说的“完全等于”是寄存器值的完全相等。标准帧ID是11位,扩展帧ID是29位,它们在寄存器里的摆放方式不同,所以配置时要把ID先转换成对应的寄存器值。FR1和FR2在列表模式下都是ID槽位,掩码概念不存在。比如我想只收两个标准帧ID:0x123和0x456,那FR1写标准帧0x123的寄存器值,FR2写标准帧0x456的寄存器值。
3.2 掩码模式:用1表示必须匹配
掩码模式把FR2从“第二个ID槽位”改成了“掩码”。掩码的每一位含义是:1代表这一位必须与FR1里对应位相同,0代表这一位无所谓。
举个例子。FR1的ID填0x100,FR2的掩码填0x7F8。0x7F8换算成二进制是bit10到bit3全为1,bit2到bit0全为0。这意味着报文ID的bit10到bit3必须等于0x100的这些位,而低3位不参与比较。所以0x100、0x103、0x107都能进,0x0FF进不来,因为它的高8位不对。这就是掩码模式做批量接收的基本思路。
掩码计算是过滤器配置里最容易出错的地方,我自己的习惯是先在纸上把二进制写出来,别直接在十进制里心算。比如“收0x100到0x107”这类需求,0x100到0x107的区别只在bit2到bit0,掩码就是0x7F8。
3.3 标准帧和扩展帧在过滤器寄存器里的不同摆法
这是很多人栽跟头的核心原因。发送邮箱的ID寄存器里,标准帧ID是左移21位放进高位的,而过滤器寄存器里的标准帧ID是在低位对齐,直接放在bit10到bit0,扩展帧ID则是左移3位后存放在寄存器中。也就是说,你发送时用的ID数值不能直接拿来填过滤器,必须做一次换算。
我在实验里用到的换算规则:
- 标准帧ID:过滤器值 = 标准ID本身,也就是
(uint32_t)std_id & 0x7FF。 - 扩展帧ID:过滤器值 =
((uint32_t)ext_id << 3) | 0x04,其中左移3位是为29位ID腾出位置,bit2置1表示这是扩展帧。
对应的掩码:
- 标准帧常用掩码:0x7FF,表示11位ID全比较。
- 扩展帧常用掩码:0xFFFFFFFC,表示29位ID和IDE位全部参与比较,最低两位不关心。
3.4 模式选择的决策建议
实际项目里我一般按下面这张表来选:
| 使用场景 | 推荐配置 | 理由 |
|---|---|---|
| 只收一个确定的ID | 32位列表模式 | FR1和FR2各放一个,还能顺带收第二个 |
| 收一段连续的ID范围 | 32位掩码模式 | 用一个掩码覆盖整段,不用堆多个过滤器组 |
| 同时收标准帧和扩展帧 | 32位掩码模式同组放宽匹配 | 掩码不覆盖帧类型位,靠软件再判断 |
| 过滤器组不够用 | 16位位宽 | 把一组拆成四个槽位,但配置时要格外小心 |
| CAN1和CAN2都有接收需求 | 先分bank,再各配各的规则 | 避免两组CAN互相覆盖 |
4. 实验验证:四种典型过滤配置在真实总线上跑一遍
原理归原理,最终还是要看实际效果。这一章的四个实验我都在两块STM32F103C8T6对接的真实总线上跑过,配置函数和结果都记录下来。
4.1 实验平台与基础初始化
两块板子都带TJA1050电平转换芯片,接线很简单:CANH接CANH,CANL接CANL,GND共地。短距离测试,两边都保留120Ω终端电阻也不会出问题。波特率统一配500kbps。
先初始化GPIO和CAN控制器。下面这段是标准外设库的写法,重点是APB1时钟36MHz下,预分频4、BS1=9tq、BS2=8tq,1+9+8=18tq,算出500kbps:
void CAN1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_PinRemapConfig(GPIO_Remap1_CAN1, DISABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); CAN_InitStructure.CAN_TTCM = DISABLE; CAN_InitStructure.CAN_ABOM = ENABLE; CAN_InitStructure.CAN_AWUM = DISABLE; CAN_InitStructure.CAN_NART = DISABLE; CAN_InitStructure.CAN_RFLM = DISABLE; CAN_InitStructure.CAN_TXFP = DISABLE; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_9tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_8tq; CAN_InitStructure.CAN_Prescaler = 4; CAN_Init(CAN1, &CAN_InitStructure); }注意CAN控制器初始化完成后要先做一次自测:发送一条报文,再确认能收到,确认收发通路没问题,再开始配置过滤器。
4.2 过滤配置函数:直接操作寄存器版本
我封了一个直接操作寄存器的函数,后面实验全部调用它。mode传0表示掩码模式,传1表示列表模式;scale传0表示16位,传1表示32位;fifo传0或1表示挂FIFO0还是FIFO1:
void CAN_FilterRegConfig(CAN_TypeDef *CANx, uint32_t bank, uint32_t mode, uint32_t scale, uint32_t fifo, uint32_t fr1, uint32_t fr2) { CANx->FMR |= CAN_FMR_FINIT; // 进入初始化模式,解除写保护 if (mode == 1) CANx->FM1R |= (1UL << bank); else CANx->FM1R &= ~(1UL << bank); if (scale == 1) CANx->FS1R |= (1UL << bank); else CANx->FS1R &= ~(1UL << bank); if (fifo == 1) CANx->FFA1R |= (1UL << bank); else CANx->FFA1R &= ~(1UL << bank); CANx->sFilterRegister[bank].FR1 = fr1; CANx->sFilterRegister[bank].FR2 = fr2; CANx->FA1R |= (1UL << bank); // 激活该过滤器组 CANx->FMR &= ~CAN_FMR_FINIT; // 退出初始化模式,配置生效 }这个函数必须先进入FINIT初始化模式再改寄存器,最后激活并退出。如果觉得某次配置没生效,第一步就回读这几个寄存器看看,而不是去怀疑总线。
发消息的辅助函数:
void SendStd(uint16_t id) { CanTxMsg TxMsg; TxMsg.StdId = id; TxMsg.ExtId = 0; TxMsg.IDE = CAN_Id_Standard; TxMsg.RTR = CAN_RTR_Data; TxMsg.DLC = 1; TxMsg.Data[0] = 0xAA; while (CAN_Transmit(CAN1, &TxMsg) == CAN_TxStatus_Failed); } void SendExt(uint32_t id) { CanTxMsg TxMsg; TxMsg.StdId = 0; TxMsg.ExtId = id; TxMsg.IDE = CAN_Id_Extended; TxMsg.RTR = CAN_RTR_Data; TxMsg.DLC = 1; TxMsg.Data[0] = 0xBB; while (CAN_Transmit(CAN1, &TxMsg) == CAN_TxStatus_Failed); }接收端用查询方式读FIFO,打印收到的ID和来源FIFO:
CanRxMsg RxMsg; if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0) { CAN_Receive(CAN1, CAN_FIFO0, &RxMsg); if (RxMsg.IDE == CAN_Id_Standard) printf("[FIFO0] Std 0x%03X\r\n", RxMsg.StdId); else printf("[FIFO0] Ext 0x%08X\r\n", RxMsg.ExtId); } if (CAN_MessagePending(CAN1, CAN_FIFO1) > 0) { CAN_Receive(CAN1, CAN_FIFO1, &RxMsg); if (RxMsg.IDE == CAN_Id_Standard) printf("[FIFO1] Std 0x%03X\r\n", RxMsg.StdId); else printf("[FIFO1] Ext 0x%08X\r\n", RxMsg.ExtId); }4.3 实验一:列表模式只收标准帧0x123
配置第0组过滤器,32位列表模式,挂FIFO0:
CAN_FilterRegConfig(CAN1, 0, 1, 1, 0, 0x0123, 0x0123);发送端依次发送标准帧ID:0x123、0x124、0x100、0x456。每帧间隔100ms。接收端串口实际输出只有0x123,其余三帧全部被硬件丢弃。这说明列表模式下FR1和FR2就是两个精确匹配槽位。
实验记录:
| 发送ID | 期望结果 | 实际结果 |
|---|---|---|
| 0x123 | 收到 | 收到 |
| 0x124 | 丢弃 | 丢弃 |
| 0x100 | 丢弃 | 丢弃 |
| 0x456 | 丢弃 | 丢弃 |
4.4 实验二:掩码模式收下0x100到0x107一组ID
同样用第0组,切换成32位掩码模式。FR1填0x100,FR2填0x7F8。0x7F8的bit10到bit3是1,低3位是0,表示ID高8位必须等于0x100的高8位,低3位任意:
CAN_FilterRegConfig(CAN1, 0, 0, 1, 0, 0x0100, 0x07F8);发送端依次发送:0x0FF、0x100、0x103、0x107、0x108、0x7FF。接收端只收到0x100、0x103、0x107。0x108虽然只比0x107大1,但bit3已经变了,被掩码挡住,这正是掩码按位比较的特点。
| 发送ID | 期望结果 | 实际结果 |
|---|---|---|
| 0x0FF | 丢弃 | 丢弃 |
| 0x100 | 收到 | 收到 |
| 0x103 | 收到 | 收到 |
| 0x107 | 收到 | 收到 |
| 0x108 | 丢弃 | 丢弃 |
| 0x7FF | 丢弃 | 丢弃 |
4.5 实验三:列表模式接收扩展帧ID 0x1234567
扩展帧29位ID左移3位再参与过滤。先算好寄存器值,0x1234567左移3位等于0x91A2B38,再加上bit2的IDE标志位,得到0x91A2B3C:
uint32_t ext_mask_val = (0x1234567UL << 3) | 0x04UL; CAN_FilterRegConfig(CAN1, 0, 1, 1, 0, ext_mask_val, ext_mask_val);发送端发送扩展帧:0x1234567、0x1234568、0x2234567。接收端只收到0x1234567。注意发送时接收打印要用RxMsg.ExtId,不是StdId。
| 发送扩展ID | 期望结果 | 实际结果 |
|---|---|---|
| 0x1234567 | 收到 | 收到 |
| 0x1234568 | 丢弃 | 丢弃 |
| 0x2234567 | 丢弃 | 丢弃 |
4.6 实验四:两个过滤器组分别绑定FIFO0和FIFO1
第0组列表模式收标准帧0x222,挂FIFO1;第1组掩码模式收0x300到0x307,挂FIFO0:
CAN_FilterRegConfig(CAN1, 0, 1, 1, 1, 0x0222, 0x0222); CAN_FilterRegConfig(CAN1, 1, 0, 1, 0, 0x0300, 0x07F8);发送端交替发送0x222、0x300、0x304。接收端串口输出分别带上了[FIFO1]和[FIFO0]前缀,说明FFA1R的FIFO分配位工作正常。这个实验验证的不只是过滤,还有多优先级场景下的FIFO分流能力,比如高优先级报文进FIFO1、普通报文进FIFO0,后续读邮箱时可以直接按FIFO处理。
5. 实测中踩过的坑和排查思路
配置过滤器本身不复杂,真正复杂的是出错以后怎么定位。下面几个问题全是我在实验过程中真实碰到过的,按坑的典型度排个序。
5.1 坑1:没置FINIT就把配置项当摆设
有一次我在调试器里直接改FM1R,想看看列表模式效果,改了bit0为1,然后读回FM1R,发现还是0。一开始以为是硬件坏了,后来才意识到FMR的FINIT还是0,整个过滤器配置区是写保护的。
这个坑在调试器里特别容易踩,因为你写寄存器时不会报错,寄存器也不变,看起来就像没写一样。正确操作是先写CANx->FMR |= CAN_FMR_FINIT;,配置完再清掉。而且要注意,配置过程和退出必须在同一个临界区内完成,中间不要被中断打断去收发CAN帧,否则可能出现配置写到一半就开始过滤的竞态。
5.2 坑2:配置都对了,就是忘了FA1R激活
第二个高频坑是FS1R、FM1R、FFA1R、FR1、FR2全填好了,FIFO里就是没数据。排查到最后发现FA1R对应位还是0。
FA1R的每个位是过滤器的总开关。这个寄存器太不起眼,很容易在配置列表里漏掉。我的建议是把“激活”这一步写进配置函数里,而不是在主程序里调用完再单独激活。上面封装的CAN_FilterRegConfig函数最后一行就是激活,这样就不会漏。
5.3 坑3:32位掩码模式下把第二个寄存器当成ID填
列表模式用习惯了以后,切到掩码模式会下意识在FR2里也填一个期望ID。但掩码模式下FR2是掩码,不是ID槽位。如果你期望收两个特定ID,然后只改模式不改FR2,FR2就会变成另一个ID,结果两个ID都收不到,或者只收到一个,非常迷惑。
掩码模式下的FR2填的是位比较规则,1表示这几位必须匹配,0表示忽略。填值之前先把二进制在纸上写出来,别用十进制估算。
5.4 坑4:CAN2过滤器组被CAN1占光
只用CAN1的时候,28个组随便用,根本不会想到bank分配问题。一旦把CAN2也初始化了,CAN2的过滤器却怎么配都不生效,十有八九是FMR里CAN2SB没设。
复位后CAN2SB默认是0,意味着CAN2默认从第0组开始用,这和CAN1完全重叠。正常做法是在初始化CAN2过滤器之前,把CAN2SB设为14,让CAN2用第14到第27组。代码里就是修改FMR寄存器的高位:
CANx->FMR = (CANx->FMR & ~(0x3FUL << 8)) | (14UL << 8);CAN2只占14个组,CAN1用0到13,分配完双方互不干扰。
5.5 快速判断过滤器是否生效的三步法
最后给一个我自己一直在用的排查套路。不管过滤器配置成什么样,先别急着怀疑总线、波特率、中断,按下面三步走:
第一步,把过滤器临时配成全接收。掩码模式,FR1填0,FR2填0,掩码全0意味着所有位都不比较,任何帧都能进。这时如果还收不到,问题就不在过滤器,而是总线或者CAN初始化。
第二步,改成目标过滤规则,发送一个期望ID,看FIFO有没有数据。没有的话回读寄存器:
printf("FM1R=%08X FS1R=%08X FFA1R=%08X FA1R=%08X\r\n", CAN1->FM1R, CAN1->FS1R, CAN1->FFA1R, CAN1->FA1R); printf("FR1=%08X FR2=%08X\r\n", CAN1->sFilterRegister[0].FR1, CAN1->sFilterRegister[0].FR2);对比回读值和预期,通常能立刻发现问题,要么FINIT没退出,要么FA1R没置位,要么FR2算错。
第三步,发送一个不期望的邻近ID,确认FIFO里的计数不增加。这一步很多人会忽略,但“该收的收进来”和“不该收的挡在外面”是两件事,只验证一半不算完。
我自己现在配过滤器,已经习惯先把估算好的寄存器值写在注释里,再写配置代码。因为掩码这种东西,隔一个晚上再看就会觉得当时算错了。把计算过程留档,既能自检,也方便同事接手。