
简介面向 STM32 开发者的 CAN 总线实战资料包以 stm32f103RE 为核心的收发例程为主线覆盖回环测试与双机通信两种典型调试方式适合嵌入式入门工程师、单片机学习者以及需要快速验证 CAN 通信的开发者使用。压缩包采用 RAR 格式体积约 9.5MB便于本地保存和按需解压资料内包含完整的 CAN 总线协议背景说明从 ISO11898/ISO11519 标准化进程到博世公司的开发历程均有梳理能帮助读者在跑通例程的同时理解报文发送、接收以及错误处理等底层机制。目前已有 609 人学习下载属于轻量但实用的通信协议参考包拿到后既能直接编译烧录进行回环自检也能借助双机例程观察实际节点间的数据交互结合协议文档边调边查对建立 CAN 总线的整体认知和提升调试能力很有帮助。1. 为什么 stm32f103RE 的 CAN 总线收发必须先吃透 CAN 总线协议写 stm32f103RE 的 CAN 总线收发最容易卡住的不是寄存器而是对 CAN 总线协议里位同步和采样点的理解。串口只要波特率一致就能通信但 CAN 还要保证每个节点在位时间的同一时刻采样采样点偏差大时线长一增加就出现周期性错误帧。另一个反直觉的点是很多新手上手都把“收到数据”作为目标最后发现过滤器默认配置把所有帧都挡在了外设门口。这篇把 CAN 总线协议的帧结构、bxCAN 外设的初始化、标准库收发函数和调试手法串在一起按“协议—实现—发送—接收—排错—技巧”推进。刚入门的能照抄工程用过 CAN 的老手也能在过滤器和位定时参数上找到点新东西。2. CAN 总线协议要点帧格式、仲裁和位定时2.1 标准帧、扩展帧、远程帧用 15 分钟分清三件事CAN 总线协议报文在物理链路上是一个差分信号串但逻辑上可以拆成多个域。最常用的是标准帧CAN 2.0A标识符 11 位加上控制场里的 DLC最长带 8 字节数据扩展帧CAN 2.0B把标识符扩展到 29 位适合节点多、业务 ID 需要分层的总线。远程帧则没有数据场只是向对方请求数据实际项目中用得少但 RTR 位要理解因为它在仲裁时会影响优先级。从帧头到帧尾依次是 SOF、仲裁场、控制场、数据场、CRC 场、ACK 场、EOF。仲裁场里最关键的三个位是 ID、IDE 和 RTR。IDE 在当前帧是标准帧时写 0是扩展帧时写 1RTR 在当前帧是远程帧时写 1。这些位不只在报文里表达语义还会直接参与总线仲裁所以过滤器配置时也需要把它们当成“ID 的一部分”来匹配。很多人把 DLC 当成数据长度其实 DLC 是数据场字节数的编码。标准帧的 DLC 可以是 0 到 8超过 8 的值在普通数据帧里没有合法语义有些 CAN 分析仪会报格式错误。CRC 场则是 15 位 CRC 加一个隐性分隔符它只覆盖前面的帧部分不覆盖 ACK 场。理解 CRC 的作用范围对后期调试很有帮助如果总线上一直报 CRC 错误问题大概率出在物理层信号质量或采样点偏移而不是应用层把数据写错了。2.2 仲裁靠“隐形”和“显性”谁显性谁先走CAN 总线协议最优雅的设计是 CSMA/CR多个节点同时发送时总线通过电平仲裁决定谁先发。显性位逻辑值是 0隐形位逻辑值是 1显性可以覆盖隐性。一个节点在发送 ID 的每一位时都会同时读取总线电平如果自己发隐形位但读到显性位就退出发送改做接收。所以 ID 越小越优先数据场里的字节计数无法决定优先级这对安排实时性要求高的报文很重要。举一个具体的仲裁场景节点 A 发 ID 0x100节点 B 发 ID 0x200。A 的 ID 二进制是 00100000000B 是 01000000000。仲裁时从最高位开始逐位比较第二位 A 是 0B 是 1A 发出显性位B 读到总线是显性而自己准备发隐形B 立刻退出。这个过程的代价是仲裁失败的节点会认为发送未完成但不会报告错误因为它一直在观察总线状态知道是正常仲裁丢了随后启动重发。接收端不需要仲裁但必须从位流里提取时钟。这也是为什么 CAN 不能像 UART 那样只写波特率还要配置同步段、传播段、相位段。采样点就是在一个位时间里接收器真正锁存电平的位置一般建议放在 75% 到 85% 之间靠近位中间可以躲避信号跳变和振铃。如果采样点太靠前或靠后遇到总线线缆过长、终端电阻不匹配时会在同一个位里采到两个错误电平最后表现为随机丢帧和持续重发。2.3 位定时参数波特率不等于采样点bxCAN 的位时间由同步段固定 1 TQ、传播段和相位段组成。在标准外设库中用 CAN_BS1 和 CAN_BS2 表达实际 tq 数量是寄存器值加 1 或直接选择可枚举的 CAN_BS1_8tp 等。波特率计算公式为Baud PCLK1 / (CAN_Prescaler × (1 CAN_BS1 CAN_BS2))采样点 (1 CAN_BS1) / (1 CAN_BS1 CAN_BS2)STM32F103RE 的 APB1 默认 36MHz下面是一组可直接用的参数示例目标波特率PrescalerBS1BS2实际波特率采样点1Mbps3831.0 Mbps75%500kbps683500 kbps75%250kbps1283250 kbps75%125kbps2483125 kbps75%以 500kbps 为例初始化时这样设置CAN_InitTypeDef can; can.CAN_Prescaler 6; can.CAN_BS1 CAN_BS1_8tp; can.CAN_BS2 CAN_BS2_3tp; can.CAN_SJW CAN_SJW_1tq; can.CAN_Mode CAN_Mode_Normal; CAN_Init(CAN1, can);CAN_SJW 是同步跳转宽度用于重新同步在不同节点晶振有偏差时可以吸收误差。这里设 1tq 已经能覆盖常见场景。如果总线很长或节点很多可以按采样点需求重新计算 BS1 和 BS2不一定照抄表中数字。常见误区是把 Prescaler 改成 5得到的就不是 500kbps如果两端的采样点又不同报文能发出去但偶发 ACK 错误和 CRC 错误看寄存器才找得到原因。3. 在 stm32f103RE 上把 CAN 收发例程跑通从初始化到中断接收3.1 引脚复用与时钟先让 PA11/PA12 变成 CAN 的双绞线stm32f103RE 的 CAN1 默认复用引脚是 PA11 (RX) 和 PA12 (TX)。配置时先把 APB2 上的 GPIOA 时钟使能再开 APB1 上的 CAN1 时钟。注意 CAN1 在 APB1不是 APB2这个很多朋友会因为看不透而找不到 CAN 寄存器。复用功能要设成 AFIO 推挽输出打开 PMUX 之后再用 GPIO_Init 配置 PA12 为复用推挽PA11 为上拉输入。然后再调用 CAN_Init 把外设模式、位时序写入。下面是一段初始化流程中与 GPIO 相关的代码GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); gpio.GPIO_Pin GPIO_Pin_12; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_11; gpio.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, gpio);PA12 作为 CAN 发送脚必须配置成复用推挽这样才能把 CAN 控制器的输出送到引脚上。PA11 作为接收脚配成上拉输入是因为 CAN 收发器在总线隐性时输出高电平内部上拉可以避免引脚悬空带来的误触发。如果用了带收发器的模块比如 TJA1050其实收发器内部已经有了终端和上下拉但 MCU 端依然建议保留上拉防止收发器断电时引脚浮空。3.2 过滤器和模式先“什么都收”再只收业务 ID调试阶段过滤器可以设成不屏蔽任何位。代码如下CAN_FilterInitTypeDef filter; filter.CAN_FilterNumber 0; filter.CAN_FilterMode CAN_FilterMode_IdMask; filter.CAN_FilterScale CAN_FilterScale_32bit; filter.CAN_FilterIdHigh 0x0000; filter.CAN_FilterIdLow 0x0000; filter.CAN_FilterMaskIdHigh 0x0000; filter.CAN_FilterMaskIdLow 0x0000; filter.CAN_FilterFIFOAssignment CAN_FIFO0; filter.CAN_FilterActivation ENABLE; CAN_FilterInit(filter);掩码全 0 表示比较位自动通过所以总线上所有帧都会被放进 FIFO0。只看代码的前三行很多人忘记使能 FilterActivation结果过滤器模组没生效依然一帧都收不到。确认收发链路通了之后再把掩码改出来例如只收 ID 0x1F0 的标准帧让寄存器高 16 位保存 STID[10:0]低 16 位的 bit2 保存 IDE 位。filter.CAN_FilterIdHigh (0x1F0 5); filter.CAN_FilterIdLow 0x0000; // MaskHigh 保留 STID 的 11 位MaskLow 的 bit2 保留 IDE filter.CAN_FilterMaskIdHigh 0xFFE0; filter.CAN_FilterMaskIdLow 0x0004;这段代码的关键在移位标准 ID 在 32 位寄存器里位于 bit31:21向右数 16 位正好落在高十六位的 bit15:5。直接0x1F0 5得到 0x3E00寄存器能直接认。MaskLow 设 0x0004表示必须匹配 IDE0也就是标准帧扩展帧进不来。远程帧的 RTR 位也在低十六位如果业务里不想收远程帧可以再让 bit1 也参与匹配。3.3 发送例程三个邮箱与状态码CAN1 有 3 个发送邮箱标准外设库把发送封装成了CAN_Transmit。写发送例程前先定义CanTxMsg需要填充 ID、IDE、RTR、DLC 和 Data。下面是一个阻塞式发送函数带超时uint8_t CAN_Send_Frame(uint16_t id, uint8_t *data, uint8_t len) { CanTxMsg tx; tx.StdId id; tx.IDE CAN_Id_Standard; tx.RTR CAN_RTR_Data; tx.DLC len; memcpy(tx.Data, data, len); uint8_t mailbox CAN_Transmit(CAN1, tx); if (mailbox CAN_TxStatus_NoMailBox) return 0; uint32_t timeout 0xFFFF; while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) { if (--timeout 0) return 0; } return 1; }CAN_Transmit返回的 mailbox 是 0/1/2 号邮箱编号不是成功标志。要区分两种情况没有可用邮箱会返回CAN_TxStatus_NoMailBox发送失败也可能是在 ACK 阶段等不到接收节点发回应答位。如果对不同优先级报文有要求也可以在调用前用CAN_TransmitPending查询邮箱占用把高优先级报文塞进空闲邮箱。此函数虽简单但能规避总线拥堵时邮箱全满的死等问题。返回值含义可参考下表返回值含义常见原因CAN_TxStatus_Ok发送成功且进入完成状态正常CAN_TxStatus_Pending等待 ACK 或发送中总线上没有其他节点CAN_TxStatus_Failed发送失败总线错误仲裁丢失或错误退出CAN_TxStatus_NoMailBox三邮箱满发送太连续降低发送频率其中Pending状态最容易迷惑人程序里看到一直 pending第一反应是总线坏了但很多时候只是总线上除发送节点外没有第二个初始化完成的节点。CAN 协议要求每个节点在 ACK 槽对合法帧做应答哪怕数据不是发给自己的因此发送节点如果收不到 ACK就认为帧没有被正确接收从而重发并引发错误计数器增长。3.4 接收例程FIFO0 中断逐字节解释stm32f103RE 的接收路径核心是两个 FIFO有报文时 FIFO0 置非空同时可以触发中断。初始化时用CAN_ITConfig开启CAN_IT_FMP0再配置 NVIC。如果使用标准外设库中断服务函数名要对应启动文件里的USB_LP_CAN1_RX0_IRQHandlerHAL 库里则改为CAN1_RX0_IRQHandler。void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg rx; CAN_Receive(CAN1, CAN_FIFO0, rx); // 业务处理rx.StdId, rx.DLC, rx.Data[] if (rx.IDE CAN_Id_Standard) { // 按 StdId 分发 } }在 CAN_Receive 之前不需要手动清中断挂起位标准外设库在读 FIFO 时会自动清除 FIFO0 的报文段但中断使能位由你控制。FIFO 溢出时需要额外留意CAN_IT_FOV它表示 FIFO 已满而又有新报文到达会丢最旧还是最新的报文取决于配置。多数例程把CAN_Receive写在中断里处理逻辑短没有问题如果业务里有耗时操作先进环形缓冲区再在 main 里解析数据避免中断阻塞太久让 CAN 分析仪那边同步产生溢出错误。4. 调试与排错从“发不出去”到“一帧不丢”4.1 先回环不管线上有没有其他节点代码里将 CAN_Mode 设为 CAN_Mode_LoopBack启用后数据会从内部回环到接收邮箱不需要外部应答也能产生发送完成中断。这时如果还能收到自己发的报文说明时钟/GPIO/CAN外设链路正常。回环有几个好处不受总线终端电阻影响不依赖总线是否有其他节点也方便计算真实的波特率采样点。但要注意回环模式下发送并不依赖总线上的 ACK因为内部显性位直接反馈给自身所以即使外部总线断开发送也显示成功这是区别于普通模式的最大一点。实际操作时把初始化结构体里的CAN_Mode从CAN_Mode_Normal临时改成CAN_Mode_LoopBack保留过滤器放行所有帧的配置然后循环发送一帧 ID 0x123、长度 8 的测试数据。如果接收 FIFO 能读出同样的 8 字节说明 MCU 侧的软件链路完全正常。回环通过后再改回普通模式接上收发器和另一个节点继续排查总线侧的物理问题。很多工程师跳过这步直接量波形浪费时间在错误方向上。4.2 用 CAN 分析仪抓帧看 ACK 和错误帧在无法肉眼判断时用 USB CAN 分析仪或者逻辑分析仪是不错的手段。抓出总线波形后先数一位的时间反向算出波特率再观察一则标准帧的 ACK 槽。如果 ACK 部分一直是隐性说明发送节点没有收到任何接收节点的应答。因为 CAN 总线协议规定每个正常接收帧的节点都要在 ACK 槽拉一个显性位哪怕这个帧不是给自己的也必须应答。如果总线上只有一个节点发送就会因为等不到 ACK 不断重发。示波器在排查物理层时更直接把探头夹在 CAN_H 和 GND、CAN_L 和 GND 之间正常收发时能看到约 2V 到 3.5V 之间的差分跳变如果只有共模电压没有跳变大概率是收发器没有上电或发送引脚配成了普通推挽而不是复用推挽。另一个常见问题是忘了接终端电阻在短线调试时不明显线长超过几十厘米后波形上的振铃会让人怀疑采样点其实只要在总线两端各接一个 120Ω 电阻就能稳定下来。4.3 查询 ESR 寄存器把硬件错误翻译成人话CAN1 错误状态寄存器里有 EWGF、EPVF、BOFF 和 LEC[2:0]。LEC 是一个三位的错误代码软件可以这样读uint8_t lec (CAN1-ESR 4) 0x7; switch (lec) { case 1: // 位填充错误 case 2: // 格式错误 case 3: // 应答错误 case 4: // 位显性错误 case 5: // 位隐性错误 case 6: // CRC 错误 }把这些状态和错误代码列到表里LEC 值含义排查方向0无错误正常1位填充错误总线波形畸变、波特率偏差2格式错误报文截断、ID 范围不对3应答错误总线上没有其他节点或 ACK 被屏蔽4位显性错误发送显性位时读到隐性终端电阻消失5位隐性错误发送隐性位时读到显性短路、多节点冲突6CRC 错误数据链路被干扰采样点偏移如果 LEC 一直是回答错误最有可能就是总线上只有一个节点在发或者另一端没有正确初始化 CAN 外设。如果在波形正常时依然报错就复制一套相同的波特率参数配合示波器测量两端的位长度确保误差小于 0.5%。对于多节点系统特别要注意两个节点的晶振独立振荡如果用的是内部 RC 振荡器而不锁外部晶振采样点偏差会在长时间运行后积累成偶发错误这种情况建议把 CAN 外设时钟源切成外部 HSE 的 PLL 输出。5. 进阶技巧过滤表复用、动态波特率、负载率自检5.1 用一条公式动态计算波特率参数在大型嵌入式软件里我一般会写一个小工具在初始化时根据目标波特率自动规划分频和 BS1/BS2。先枚举 PCLK1然后扫描预分频器让总时间单元数尽量接近整数并优先满足采样点在 75%85% 之间。这样可以避免人工推算不同主频下的配置。注意 STM32F103 的 CAN 外设如果使用外部 8M 晶振和 PLL 倍频APB1 主频可能不是标准的 36MHz万用表测不出来但用分频自动计算能躲过不少坑。5.2 让一个滤波器同时管理多个 ID当总线上的业务 ID 数量多于滤波器组时可以用列表模式而不是屏蔽模式。列表模式每个滤波器组可以精确匹配两个 ID屏蔽模式允许一个组匹配“ID 加上通配符”。一个常见技巧是把相同优先级的报警帧放进同一组用掩码屏蔽掉 ID 的低 6 位让同一个滤波器组放行 0x1000x13F 之间的数据。注意扩展帧的 IDE 位也要算进掩码否则标准帧和扩展帧可能在同一个滤波器里互相串入。5.3 用发送计数估算总线负载率总线负载率的简单验证方法是统计单位时间内实际发送成功的报文数量乘以每帧需要的位数量除以波特率。比如 1Mbps 总线上每 10ms 发 8 字节标准帧一帧大约 110 位一分钟约 660k 位负载率只有 1%。当场站数增加、周期报文叠加时建议保持负载率低 70%这个可以用接收 FIFO 的溢出次数做辅助判断。如果 FIFO 溢出跑得比报文频率还快就是应用层接收太慢不是总线带宽不够。本文还有配套的精品资源点击获取