
做嵌入式这些年回过头看STM32F103的CAN通信确实是很多人遇到的第一个中等难度外设串口很快调通了I2C、SPI也能照葫芦画瓢跑起来一上CAN就开始冒各种奇奇怪怪的问题。CAN本身不难难的是资料太乱——网上能找到的教程一半是纯寄存器版一半是HAL库版真正基于标准外设库、从建工程到双机收发、还把坑点讲透的文章很少。这篇文章就把我这些年用标准库在F103上做CAN通信的完整经验串一遍从硬件连接、初始化配置、收发测试到中断与过滤器最后附上几个实际项目中的故障排查记录代码直接用标准外设库V3.5可以直接抄。先说清楚这篇文章适合谁正在用STM32F103标准库做CAN通信、但卡在初始化到底该怎么配为什么回环能通、组网不通过滤器怎么设才能只收我想要的帧这些问题上的朋友。如果你已经在用HAL库也可以对照着看配置思路完全一致只是API名字换了。1. 动手之前的硬准备硬件电路、工程模板和工具链1.1 F103的CAN外设到底长什么样STM32F103的CAN控制器是bxCANBasic Extended CAN它本身不负责电平转换MCU的CAN控制器引脚输出的是逻辑电平必须外接CAN收发器芯片比如TJA1050、MCP2551、SN65HVD230才能连到CANH和CANL两根差分线上。大多数F103型号只带一个CAN控制器也就是CAN1默认引脚映射在PA11CAN1_RX和PA12CAN1_TX。只有部分大容量型号像STM32F103ZET6、VET6才同时有CAN1和CAN2。这里有一个非常容易踩的坑PA11和PA12同时是USB的D-和D引脚如果你的板子上USB和CAN共用这两个脚信号是冲突的不是这里乱就是那里乱。解决办法是用重映射功能把CAN1挪到PB8RX和PB9TX后面我会专门再讲这个。还有一点新手容易忽略CAN外设挂载在APB1总线上。F103在72MHz主频下APB1时钟默认是36MHz这是后面所有波特率计算的基础。如果系统时钟配置变了APB1不再是36MHz你按36MHz算出来的波特率全是错的通信表现就是两边都发但谁都收不到这种问题最隐蔽。1.2 收发器选型和电路上三个不能省的点收发器芯片的选择直接影响调试体验。TJA1050和MCP2551是5V供电SN65HVD230是3.3V供电。F103的IO逻辑是3.3V接5V供电的TJA1050时要注意TXD输入引脚可以直接由3.3V驱动这个没问题但RXD输出的高电平接近5V需要确认MCU对应引脚是不是5V容忍FT引脚。PA11和PA12本身是FT引脚所以直接用没问题。如果你把CAN重映射到PB8/PB9也要查一下数据手册确认这两个脚是FT通常也是。电路上有三个点我每个项目都会检查一遍终端电阻CAN总线两端各需要1个120Ω电阻。如果你只是两块板子短距离对接每个板子都带120Ω那总线等效电阻是60Ω也能正常工作。但如果以后要扩展节点尽量用只有最远端两块板接120Ω的方式避免总线负载过大。共地两个节点之间除了CANH、CANLGND一定要连在一起。CAN收发器输出的是差分信号但收发器本身需要参考地不共地时经常出现单独收发都正常一接上另一个节点就乱码。收发器工作模式使能脚TJA1050有S脚高电平进入静默模式只能收不能发SN65HVD230的RS脚也有类似作用。很多现成模块已经把S脚拉低但自己画板子时特别容易忘忘掉的表现就是能收到数据但发不出去。还有一点关于CAN模块供电不建议把USB-CAN分析仪或某个CAN模块的电源当作板子的主供电来源。调试时模块和主板共用一个电源没问题但正式项目里各节点该独立供电就独立供电电源干扰最能在CAN这种差分总线上引起间歇性故障。1.3 标准库工程模板的最小化组织方式标准库工程不需要从零开始。我的做法是直接复制一个之前调通的F103工程模板然后把CAN需要的文件补进去stm32f10x_can.c、stm32f10x_gpio.c、stm32f10x_rcc.c如果要做串口打印调试再加上stm32f10x_usart.c。如果之前的工程模板还没有stm32f10x_can.c去标准外设库V3.5的src目录里拷贝同时把对应头文件的路径加进Include路径。新工程下载调试时如果遇到DAP下载失败先检查BOOT0跳线是不是接到了1正常跑用户程序要接0以及SWD两个脚是不是被代码复用成了普通IO。我见过有人把PA13/PA14重映射成别的功能结果下一次就下不进程序了只能按住复位键抢时间下载。这个跟CAN本身没关系但新手很容易把两件事混在一起搞了半天以为是CAN配置问题。2. 标准库初始化背后从GPIO到波特率每一步的原理推算2.1 时钟和GPIO三个使能缺一不可CAN要跑起来第一步是打开时钟。CAN1挂在APB1上所以要开RCC_APB1Periph_CAN1GPIOA挂在APB2上要开RCC_APB2Periph_GPIOA如果用了重映射还必须开RCC_APB2Periph_AFIO并且调用重映射函数。这三个使能缺一个现象都不一样缺CAN时钟初始化返回失败缺GPIO时钟引脚没反应缺AFIO时钟重映射不生效发出去的数据还是从PA12走。GPIO的模式选择也容易犹豫。CAN_TX发送脚要配成GPIO_Mode_AF_PP也就是复用推挽输出因为CAN控制器输出信号要经过GPIO复用功能送到引脚。CAN_RX接收脚配成GPIO_Mode_IPU上拉输入就行也有项目用GPIO_Mode_IN_FLOATING浮空输入实测都行我更习惯上拉输入抗干扰好一点。标准库的代码写出来就是这样的GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 如果使用重映射把CAN1的引脚换到PB8/PB9 // GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; // CAN1_RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; // CAN1_TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);如果你用重映射把GPIOA改成GPIOB、引脚改成GPIO_Pin_8和GPIO_Pin_9再取消那行GPIO_PinRemapConfig的注释。2.2 CAN初始化结构体每个字段到底在说什么CAN初始化结构体CAN_InitTypeDef里有十来个字段看着多但真正需要关心的就两类波特率相关和模式相关。波特率相关字段是CAN_Prescaler分频系数、CAN_BS1、CAN_BS2、CAN_SJW。CAN的每一位bit在总线上由若干个时间量子Tq组成典型结构是1个SYNC_SEG BS1段 BS2段CAN_SJW是同步跳转宽度跟波特率无关一般取1Tq就行。波特率的完整计算公式是波特率 APB1时钟 / (CAN_Prescaler × (1 CAN_BS1 CAN_BS2))这里1 CAN_BS1 CAN_BS2就是1位占用的总Tq数。以F103默认的36MHz APB1时钟为例常用波特率可以这么配目标波特率PrescalerBS1BS2实际波特率采样点1Mbps4711Mbps88.9%500kbps4134500kbps77.8%250kbps2134250kbps77.8%125kbps4134125kbps77.8%注意BS1和BS2的可取值范围BS1的寄存器位宽是4位实际支持1到16个TqBS2的位宽是3位实际支持1到8个Tq。不要在配置时超过这个范围。采样点的概念很多人不重视它表示接收方在1位时间内的哪个时刻采样总线电平。CAN总线布线如果比较长、或者分支多信号跳变沿会有回波采样点太靠近位起点容易采到不稳定区域。工业上推荐75%到85%上表里77.8%就是比较稳的选择。模式相关字段里我只在真正需要时才打开这几个CAN_ABOM自动离线恢复这个建议打开后面第五部分会详细说总线出错导致离线后它能自动恢复CAN_TXFP是发送优先级控制置1时按发送请求顺序排队置0时按报文ID优先级排队一般项目保持0即可CAN_NART置1会禁止自动重发一般不打开让控制器自动重发更省心。标准库初始化CAN外设的完整代码CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_TTCM DISABLE; // 时间触发通信模式 CAN_InitStructure.CAN_ABOM ENABLE; // 自动离线管理 CAN_InitStructure.CAN_AWUM ENABLE; // 自动唤醒模式 CAN_InitStructure.CAN_NART DISABLE; // 非自动重传模式关闭 CAN_InitStructure.CAN_RFLM DISABLE; // 接收FIFO锁定模式关闭 CAN_InitStructure.CAN_TXFP DISABLE; // 发送FIFO优先级按ID优先级 CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack; // 先配回环模式 CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_13tq; CAN_InitStructure.CAN_BS2 CAN_BS2_4tq; CAN_InitStructure.CAN_Prescaler 4; // 36MHz / (4 * 18) 500kbps CAN_Init(CAN1, CAN_InitStructure);很多朋友在这段代码里纠结CAN_SJW_1tq后面能不能换成数字答案是不要换直接用标准库定义好的宏宏的值和寄存器字段是对应好的你自己写个1很可能就错位了。2.3 过滤器初始化第一次调试就全收通了再收窄CAN控制器本身就带硬件滤波过滤器的配置逻辑是一堆报文从总线上过来先经过过滤器筛选只有通过筛选的报文才能进接收FIFO软件才能读到。第一次调CAN的时候我强烈建议先配一个全不过滤的过滤器也就是不管收到什么帧都收。这样能先把收发链路验证通再回来设过滤规则否则一旦收不到数据你根本分不清是发送问题还是过滤太严。全收过滤器的配置如下CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; // 使用过滤器0 CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);这里的关键是掩码掩码位为0表示这一位不关心为1才表示这一位必须和ID寄存器对应位一致。所以把掩码全配成0就是所有ID的帧都收。实际工作中我见过有人把这个逻辑记反配了半天只收得到固定一帧其他全丢还以为是总线问题。3. 收发测试完整流程环回、双机链路和第一轮故障清单3.1 先做回环测试把怀疑范围圈在MCU内部调试CAN通信我的顺序永远是先回环LoopBack、再点对点双机、最后才上多节点。回环模式是CAN控制器内部的逻辑自检发送的数据不进收发器、不上总线而是直接从控制器内部绕回到接收邮箱。这个模式下即使板子上根本没接收发器芯片也能完成发送-接收的完整验证。回环模式只需要把上一节初始化代码里的CAN_Mode_LoopBack配好然后写个简单的发送函数和一接收检查逻辑。uint8_t CAN1_SendFrame(uint16_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; TxMessage.StdId id; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; // 标准帧 TxMessage.RTR CAN_RTR_Data; // 数据帧 TxMessage.DLC len; for (uint8_t i 0; i len; i) { TxMessage.Data[i] data[i]; } mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox CAN_TxStatus_NoMailBox) { return 1; // 三个发送邮箱都满了请求失败 } // 等待发送完成实际项目里建议加超时 while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) { } return 0; }注意CAN_Transmit的返回值如果分配到了邮箱返回邮箱号0、1、2如果三个发送邮箱全满返回CAN_TxStatus_NoMailBox。有些老资料会把这个返回值当是否发送成功来判断这是不对的CAN_Transmit只是把报文提交给硬件去发真正发完要查CAN_TransmitStatus。接收端在回环测试里直接轮询FIFOCanRxMsg RxMessage; if (CAN_MessagePending(CAN1, CAN_FIFO0) 0) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // RxMessage.StdId 就是发出去的IDRxMessage.Data里就是数据 }回环通过说明MCU内部的CAN控制器、初始化配置、发送接收流程都是好的。这时候如果后面接上收发器还不通问题就锁定在外部电路或差分总线上排查范围一下子小了很多。3.2 切到Normal模式做真正的双机收发回环验证通过后把初始化里的CAN_Mode_LoopBack改成CAN_Mode_Normal重新上电然后两套板子接线板A的CANH接板B的CANH板A的CANL接板B的CANL两块板的GND连一起总线两端各保留一个120Ω终端电阻发送端代码不用动接收端用一个最简单的轮询就行。跑起来以后A板每500ms发一帧B板收到后通过串口打印ID、长度和几个字节的数据。我第一次在这个环节就遇到个经典问题回环一切正常一上总线就收不到。查了半天发现是收板子的终端电阻没焊总线波形反射严重。所以后来我调试双机时第一步永远是拿示波器在CANH和CANL之间看有没有清晰的差分方波没有方波就先别怀疑代码。这里再强调一遍共地的问题。两块板子如果各自用独立的开关电源供电VCC之间不连通那就必须把GND连起来。CAN收发器内部的比较器是以本地GND为参考的不共地就等于收发器的参考电平对不上轻则数据错误重则直接损伤收发器。3.3 第一轮收发失败我通常按这个清单排查新手在回环通了、总线不通这个阶段最容易心态爆炸因为问题不在代码逻辑里。我的排查顺序是固定的现象优先检查点回环正常双机不通收发器供电、CANH/CANL是否接反、GND是否连通能发能收但数据偶尔错终端电阻缺失、总线距离过长、采样点位置两边都发都收不到波特率是否一致、APB1时钟是否真的36MHz能收到对方的数据但自己发不出去收发器S/STBY脚是不是被拉高了进了静默模式上电后第一帧丢失收发器上电复位时间比MCU长发送前延时几十毫秒这个清单我打印出来贴在工位上后面几个项目排查问题基本都能命中前四行。4. 中断接收与多节点过滤从能通到好用的进阶细节4.1 中断接收配置别把CAN_Bus_Off中断和FIFO中断搞混轮询接收在验证时够用但正式项目里谁也不会让CPU死等一个FIFO。CAN的中断接收配置分成两步第一步使能中断源第二步配置NVIC优先级。对FIFO0接收来说要打开的是CAN_IT_FMP0FIFO0有报文挂起。有的朋友会打开CAN_IT_FF0或CAN_IT_FOV0这俩分别是FIFO0满和FIFO0溢出中断不是正常接收用的。NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);中断服务函数名字是固定的F103标准库里CAN1的FIFO0接收中断向量对应USB_LP_CAN1_RX0_IRQHandler这个名字不要自己改改了就链接不到中断向量表上。void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 把RxMessage拷贝到自己的接收缓冲区置标志位 // 不要在中断里做耗时的串口打印、flash写入等操作 CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }一个细节FMP0这种中断标志在FIFO数据被读走后硬件会硬件清除但为了代码统一规范我仍然会调用CAN_ClearITPendingBit实测不会出问题。4.2 过滤器实战精确收一个ID和收一组ID的写法回环测试通过之后就可以把过滤器从全收改成只收我想要的。先看只收标准帧ID0x123的情况CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh (0x123 5) 0xFFE0; CAN_FilterInitStructure.CAN_FilterIdLow 0; CAN_FilterInitStructure.CAN_FilterMaskIdHigh (0x7FF 5) 0xFFE0; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0;这里最核心的是左移5位的操作。原因一句话就能说清在32位过滤器的寄存器里标准ID的11位被放在了高16位寄存器的bit[15:5]上所以要把ID左移5位正好对齐。掩码也一样0x7FF表示11位全部要匹配再左移5位对齐到对应位置。如果只想收一组ID就借助掩码把不想管的位写成0。比如ID范围0x310到0x31F也就是高6位固定低5位可变掩码高16位写(0x7E0 5)ID高16位写(0x310 5)这样0x310到0x31F就都能进了。这个逻辑用熟了后面做多节点协议拆分会很舒服。4.3 多节点共存时最容易忽略的邮箱和优先级问题F103的CAN有3个发送邮箱和2个接收FIFO每个FIFO能缓冲3个报文。这意味着发送时连续CAN_Transmit三次以内不会阻塞但如果你一次突发几十帧就要做好发送邮箱满的重试逻辑。我见过一个项目在循环里连续发送不对CAN_TxStatus_NoMailBox做处理结果偶发丢帧查了很久才发现是邮箱满了新报文直接被丢弃。多节点组网时CAN的仲裁机制会自动处理总线竞争ID越小优先级越高。标准库里默认CAN_TXFP DISABLE就是按ID优先级仲裁。如果你希望所有节点按先到先发的顺序发再考虑把CAN_TXFP置1否则保持默认最稳。5. 三次实际项目中的玄学问题排查全过程5.1 波特率算得明明对可就是收不到有次做双机调试波特率配500k两边程序都是从同一份模板改的理论上不可能不一致但死活不通。我把代码看了三遍波特率没算错过滤器也是全收。后来拿示波器量波形发现CAN_TX引脚上的位宽跟500k对不上一个位明显比2微秒宽很多。顺着这个线索查下去才发现问题出在时钟源上。那批板子用的是12MHz的外部晶振但工程模板是按8MHz晶振做的SystemInit里的倍频配置是基于8MHz算的。实际系统主频根本不是72MHzAPB1自然也不是36MHz波特率跟着全偏了。这个坑的教训是拿到一个工程模板第一件事别急着写CAN先确认板子的外部晶振频率和SystemInit的配置是否匹配。用串口打印SystemCoreClock或者用PA8的MCO功能把系统时钟输出到示波器上看一眼比什么都管用。CAN波特率这种东西差1%都可能通信失败何况是差了那么大。5.2 回环正常接上TJA1050就是不通另一个项目回环测试一切正常一旦把CAN_Mode改成Normal同样代码就是不通。排查第一步检查了收发器供电5V正常接线也是对的CANH接CANHCANL接CANL。用示波器点CAN_TX引脚MCU侧波形是有的但点到TJA1050那一侧的CANH-CANL差分波形什么也没有。最后发现问题出在TJA1050的S引脚上。那批板子自作主张把S脚通过10k电阻上拉到5V等于收发器一直工作在静默模式只收不发。把S脚直接接地之后波形立刻出来了。这类问题极具迷惑性因为能收到数据往往让人误以为收发器完全正常忘了静默模式是能收不能发。如果你用的收发器也有模式选择引脚上电后用万用表量一下这个脚的电平确认它处在正常收发模式。5.3 通信一段时间后偶发掉线查到最后是bus-off恢复的问题还有一次是设备长时间运行后偶发掉线重启后又正常。刚开始怀疑是干扰因为现场有电机启停但只要把设备放回实验室跑一天也没问题。后来我在代码里周期打印CAN的错误寄存器发现掉线前错误计数器一路飙升直到进入bus-off状态之后控制器就完全退出了总线不再参与通信。标准库里有个CAN_ABOM字段全称是自动离线管理Auto Bus-Off Management。如果这个字段没打开CAN控制器进入bus-off后不会自动恢复必须软件重新初始化CAN外设才能重新上线。打开CAN_ABOM后控制器会在适当时候自动回到总线项目最终稳定了下来。这段经历让我养成了一个习惯初始化CAN时CAN_ABOM一定设为ENABLE。同时代码里加一个周期性的错误状态检查用CAN_GetFlagStatus(CAN1, CAN_FLAG_BOF)判断是否发生过bus-off方便现场远程定位问题。5.4 PA11和PB8之间的重映射选择前面说过PA11/PA12和USB引脚冲突的问题单独拿出来再说一次是因为我确实见过有人在这里绕了很久。现象是板子同时用了USB虚拟串口和CAN通信两个功能互相干扰CAN数据偶尔错USB也经常枚举失败。这不是芯片bug而是引脚复用打架了。正确的做法是启用CAN1重映射把CAN1_RX挪到PB8、CAN1_TX挪到PB9同时把GPIO初始化里的GPIOA换成GPIOB并在初始化前调用GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE)。还有一个容易漏的点重映射需要打开AFIO时钟也就是开头说的RCC_APB2Periph_AFIO。这个时钟忘了开重映射配置写进去也不会生效。如果板子上PB8和PB9已经接了其他功能那就只能重新画板把USB和CAN分开了。用一个USB转串口芯片把调试口和CAN功能剥离开是更省心的方案。最后分享一个我从CAN调试中得出的习惯每次改完配置只改一个变量然后回环测试一遍再上总线测试。这个看起来笨的方法在排除玄学问题时其实最有效率。手边常备USB-CAN分析仪能把总线上实际跑的ID和数据都抓出来比在代码里猜快得多。等到项目稳定了再把波特率表、过滤器配置和错误处理逻辑沉淀成一套标准模板后面所有产品的CAN通信初始化都能半小时搞定。