
简介STM32F103 CAN实例代码是一套面向嵌入式初学者的完整通信工程样例基于ARM Cortex-M3内核微控制器实现CAN总线收发功能适用于工业控制、汽车电子、物联网等多主通信场景。工程覆盖CAN控制器初始化、波特率与过滤器配置、数据帧与远程帧收发、错误处理、中断服务及回调机制等关键环节并使用标准外设库完成寄存器和邮箱管理可直接在Keil MDK中编译运行。压缩包共81个文件以28个C源文件和30个头文件为核心另含12个汇编启动文件、Keil工程配置uv2/plg/dep及doc/txt说明文档整体仅280KB目录层次清晰便于对照标准外设库与底层寄存器映射。示例代码对CAN协议状态机、邮箱机制和中断处理流程均有注释说明还涉及ISO 11898物理层特性和数据位定时等细节适合作为课程设计、毕业设计或产品原型的基础。已有2871人学习下载是入门STM32 CAN通信的实用参考资料。 STM32F103的CAN外设我用标准外设库v3.5实际调过不下二十个板子从刚开始的“照抄例程但通信就是不通”到后面能通过示波器一眼看出总线状态异常中间踩过的坑、查过的寄存器、翻过的中文参考手册攒了不少一手经验。这篇博文就把“STM32F103 CAN实例代码”这件事彻底讲透——从bxCAN的底层逻辑、波特率和过滤器的计算到可以直接抄作业的收发代码再到回环模式正常但正常模式发不出去这类高频故障的排查套路。如果你正在调CAN通信或者准备用F103做CAN节点开发这篇文章应该能帮你省掉好几天的弯路。1. 拿到需求先搞懂这些——CAN总线与bxCAN外设的底层逻辑1.1 为什么工业通信选CAN不选串口很多刚接触CAN的朋友第一个问题就是串口那么成熟RS485也够用干嘛非要上CAN这个问题的答案直接决定了你在写代码时对CAN外设的“敬畏程度”。CANController Area Network和串口最根本的区别在于串口是“点对点”的单主通信A发给BB不能同时发数据给A总线仲裁靠的是协议层约定物理层本身就比较脆弱。而CAN是真正的多主总线任何节点都能主动发数据出现冲突时靠非破坏性仲裁解决——ID小的报文优先发送优先级低的自动退避重发这个机制是硬件完成的不需要协议栈参与。放到工业场景里CAN的这个特性意味着什么我用一个自己经历过的例子说明有一个电机驱动器的项目原来用RS485组网主机轮询32个从机一个从机掉线整个轮询周期就得等超时效率很低。后来换成CAN速度提高到1Mbps电机的转速、温度、报警状态全部主动上报主机只需要处理“收到什么”不用关心“谁该回话”。这就是多主通信给系统架构带来的自由度。再加上CAN的双绞线差分信号、11位的报文ID优先级机制抗干扰能力远超单端串口在电机边上、逆变器旁边这种电磁环境恶劣的场景CAN基本是性价比最高的选择。1.2 STM32F103的bxCAN到底有哪些杀手锏STM32F103内部集成的CAN控制器叫bxCAN也就是Basic Extended CAN。它是CAN协议控制器负责把CPU要发的数据封装成标准帧或扩展帧、处理位时序、出错重发、接收滤波等等而真正的物理层电气转换还需要外接一个CAN收发器芯片比如TJA1050、SN65HVD230这类。bxCAN有3个让我觉得特别好用的特性。第一个是3个发送邮箱CPU可以连续把3帧数据塞给硬件硬件自动按优先级发送不需要CPU一帧一帧等这在轮询周期里能省下大量时间。第二个是2个接收FIFOFIFO0和FIFO1每个FIFO有3级硬件缓冲报文进来后自动排队CPU忙不过来时不会丢帧还支持FIFO溢出中断。第三个是28个过滤器组互联型才有更多F103基本是28个每个过滤器可以配置成列表模式或掩码模式把不关心的报文直接拦截在硬件层CPU根本不会被打扰。这一点在总线上报文很多的项目里价值巨大。理解这三点你就会发现CAN外设本质上是一个“自带排队列表的硬件通信协处理器”跟串口这种一个字节一个字节往外蹦完全不同。写代码的思路也要跟着变——初始化配置要精细数据处理要区分发送队列和接收队列不能拿写串口驱动的惯性来写CAN。2. 动手写代码前必须想清楚的三件事2.1 时钟树与引脚复用——PA11/PA12还是PB8/PB9STM32F103有两个CAN外设CAN1和CAN2但F103的中低端型号比如C8T6只有CAN1代码配置上也是CAN1为主。CAN1默认引脚是PA11RX和PA12TX这组引脚在F103上有个特别容易翻车的地方——它和USB共用。也就是说如果你用了带USB功能的F103型号而且USB也占用了PA11/PA12那CAN1就不能用默认引脚了得通过AFIO重映射到PB8RX和PB9TX。我当初第一次调就是没注意到这个共引脚的坑USB枚举成功但CAN死活进不了中断查了半天才想起来引脚冲突。引脚重映射的配置代码很简单但容易漏掉AFIO时钟使能RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE); // 如果要用PB8/PB9这行必开不用重映射的话PA11/PA12的GPIO配置要特别注意占空比。CAN的RX引脚要配置成上拉输入不能是浮空输入否则引脚悬空时电平不确定总线空闲状态可能被误判为显性电平导致bus off。TX引脚配置成推挽复用输出50MHz即可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); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入千万别用浮空 GPIO_Init(GPIOA, GPIO_InitStructure);2.2 波特率算对通信就成功了一半CAN通信最常见的故障就是波特率不匹配两个节点波特率不一样总线上全是错误帧。很多新手直接抄例程里的初始化参数改改但例程是1Mbps你项目要求500kbps改了Prescaler却忘了改BS1/BS2连上去就乱套。在STM32F103中CAN波特率计算公式是CANbps APB1时钟频率 / (CAN_Prescaler * (1 BS1 BS2))这里的BS1和BS2都以时间量子Tq为单位注意它们是从1开始计数的含义略有不同——标准库里的CAN_BS1_13tq表示物理上占13个Tq而SJW同步跳跃宽度是重同步时采样点可以往前或往后移动的最大时间量子数取值1~4Tq一般设1就够了。举个例子系统主频72MHzAPB1分频器配置为2那么APB1就是36MHz。如果要跑500kbps每个位的总时间应该是36MHz / 500kbps 72个时钟周期。设预分频器为4则每个Tq占4个时钟周期每个位总共需要72/4 18个Tq。再结合采样点要求——推荐采样点位于整个位时间的75%~85%我习惯取接近80%的位置所以BS1设为13TqBS2设为4Tq加上固定1Tq的同步段总共正好18Tq采样点位置就是(113)/18 ≈ 77.8%满足推荐范围。标准库VS的初始化代码是这样的CAN_InitStructure.CAN_Prescaler 4; // 预分频实际Tq 4个APB1周期 CAN_InitStructure.CAN_BS1 CAN_BS1_13tq; // 13 Tq CAN_InitStructure.CAN_BS2 CAN_BS2_4tq; // 4 Tq CAN_InitStructure.CAN_SJW CAN_SJW_1tq; // 重同步跳跃宽度1 Tq CAN_InitStructure.CAN_Mode CAN_Mode_Normal;我强烈建议你把自己的波特率参数写进表格里贴到工位上因为调试时最怕的就是两个板子软件版本不同、波特率配置不同还意识不到。常用的几组参数整理如下APB136MHz时目标波特率PrescalerBS1BS2采样点位置适用场景1Mbps37Tq4Tq72.7%近距离测试1Mbps29Tq8Tq55.6%一般不用采样点太靠前500kbps413Tq4Tq77.8%工业组网常用250kbps813Tq4Tq77.8%长距离稳定传输125kbps1613Tq4Tq77.8%楼宇、车辆诊断2.3 过滤器配置——accept code与acc mask到底怎么对应热词里有个“can通信acccode与accmask”这是SJA1000时代的叫法在STM32的bxCAN里对应的是CAN_FilterIdHigh/Low和CAN_FilterMaskIdHigh/Low。很多从8位机转过来的工程师会把这两个值搞混。简单说ID寄存器存放你想接收的报文IDMask寄存器存放对应位的屏蔽规则。Mask的某一位为1表示这一位必须和ID完全一致才接收为0表示这一位不关心什么值都能过。用生活类比ID等于你宿舍的门牌号Mask是“必须匹配的字段”比如“必须是3楼”但“楼层内的房间号不限制”那Mask里对应“3楼”的位就填1对应“房间号”的位就填0。实际项目中两种最常见的配置场景如下。场景一不过滤全部接收把Mask全部设为0ID随便写。标准库写法CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000;这种配置下总线上所有帧都会进FIFO适合初期调试、需要看总线上所有数据的情况。场景二只接收指定ID的标准帧比如只接收ID 0x123的标准帧。标准帧ID共11位放在FilterIdHigh的高11位FilterIdLow低5位是扩展帧的ID不用时填0。Mask对标准帧的11位ID全部置1即MaskIdHigh的高11位全为1CAN_FilterInitStructure.CAN_FilterIdHigh (0x123 5); CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x7FF0; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000;这里0x123 5是把11位ID挪到16位寄存器的高11位0x7FF0同样把11位全1放到高11位。这种精确过滤在实际组网里太重要了——比如电机驱动节点只关心主机发来的控制帧其他节点的状态帧一个都不想要那就在过滤器里精确匹配把中断次数降低一个数量级。3. 代码实现——库函数v3.5下一个能跑的CAN收发实例3.1 GPIO与CAN初始化按顺序来不出错我写CAN初始化时习惯严格按照“开启时钟 → 配置GPIO → 配置CAN模式/波特率 → 配置过滤器 → 使能中断可选”的顺序来。这个顺序不是随便定的因为bxCAN有些寄存器只有在初始化模式下才能写如果你在正常模式下强行配置寄存器写不进去代码又不报错排查起来非常痛苦。下面这段是针对F103 TJA1050收发器、标准库v3.5的完整初始化代码500kbps正常模式FIFO0中断接收void CAN_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 2. 配置引脚 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); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. CAN外设配置 CAN_DeInit(CAN1); CAN_StructInit(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; CAN_InitStructure.CAN_TXFP DISABLE; // 按ID优先级发送 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; 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; CAN_Init(CAN1, CAN_InitStructure); // 4. 过滤器这里配置成不过滤全收 CAN_FilterInitStructure.CAN_FilterNumber 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_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure); // 5. 中断配置 NVIC_InitStructure.NVIC_IRQChannel USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); }几个容易被忽略的点说明一下。CAN_ABOM自动离线恢复我在所有例程里都会置为ENABLE因为CAN节点一旦发生bus off如果软件没有处理外设会一直卡在离线状态。开启ABOM后硬件会自动等待128个总线空闲信号后恢复虽然不能完全替代软件做恢复逻辑但在大部分场景下已经足够兜底。CAN_NART是禁止自动重发我选择DISABLE即开启自动重发。CAN协议的精髓就是出错重发关闭自动重发反而是整个系统不稳定的根源不建议关。3.2 发送函数——3个邮箱和状态机怎么配合bxCAN发送有3个邮箱我们不需要自己管理“当前用哪个邮箱”标准库的CAN_Transmit()函数会自动从空邮箱中选一个来发送。但它只是“把帧提交给硬件”不代表“已经发到总线上”。真正想判断这一帧发没发出去得轮询CAN_TransmitStatus()函数返回的状态uint8_t CAN_SendFrame(CAN_TypeDef* CANx, uint32_t id, uint8_t* data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox 0; uint32_t timeout 0; TxMessage.StdId id; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len; for (int i 0; i len; i) { TxMessage.Data[i] data[i]; } mailbox CAN_Transmit(CANx, TxMessage); // 等待发送完成带超时 while (CAN_TransmitStatus(CANx, mailbox) ! CAN_TxStatus_Ok) { if (CAN_TransmitStatus(CANx, mailbox) CAN_TxStatus_Failed) { return 0; } if (timeout 0xFFFF) { // 超时保护 return 0; } } return 1; }我这里故意加了超时保护。实际项目中如果总线上有节点没接终端电阻或节点掉线发送邮箱可能一直拿不到ACK发送状态一直不是OK没有超时机制的话整个系统就卡死了。此外如果CAN_TransmitStatus()返回CAN_TxStatus_Failed说明发送始终失败常见原因是总线持续错误这时需要去检查总线物理层而不是继续重发。很多新手会犯一个错发送函数执行完就认为数据已经发出去了实际上它在发送队列里排队。如果要发送高频数据比如每1ms发一帧建议把发送函数的超时尽量缩短比如100us级别因为邮箱满了时就该丢弃实时性不高的帧而不是阻塞主循环。3.3 接收轮询与中断实际项目怎么取舍接收数据也有两种姿势轮询FIFO0或开启FMP0中断。我刚学CAN时习惯在主循环里查CAN_GetFlagStatus(CAN1, CAN_FLAG_FMP0)有数据就读取。这个写法简单直观但有个缺点如果主循环里有耗时操作比如刷屏、打印日志报文来了不能及时响应而且如果两次轮询之间来了超过3帧FIFO会自动溢出丢帧。生产级代码里我基本都改成了中断接收。上面初始化代码里已经开启了CAN_IT_FMP0中断对应的中断服务函数和接收处理逻辑如下void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 简单处理把ID和数据搬到全局变量 rx_frame.id RxMessage.StdId; rx_frame.len RxMessage.DLC; memcpy(rx_frame.data, RxMessage.Data, 8); CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }这里有个经验细节CAN_Receive一旦读取了FIFO0中的一帧FIFO指针会自动释放不需要额外清除标志。但如果你用了FMP0中断必须在中断服务里clear pending bit否则会反复进入中断。另外中断服务里尽量只做“数据搬运”把解析、协议处理放到主循环或低优先级任务里缩短关中断时间防止系统其他中断受影响。4. 实战中最容易翻车的几个细节——故障排查与经验4.1 回环模式正常、正常模式却发不出去怎么查这是CAN调试里最高频的问题。回环模式CAN_Mode_LoopBack下bxCAN内部把发送引脚和接收引脚直接短接不需要外部应答所以一帧数据自己发自己收测试通过并不代表物理链路是好的。切换到正常模式就发不出去说明问题出在物理层。我一般按下面这个顺序排查终端电阻查没查CAN总线两端需要各接一个120欧姆电阻。用万用表量CAN_H和CAN_L之间的直流电阻如果总线连着两个节点应该在60欧姆左右如果只有一个节点应该是120欧姆。阻值明显不对大概率终端电阻没接或接错。CAN_H和CAN_L有没有接反很多人把收发器的CANH接到了另一个板子的CANL上总线完全没法通信。这是接线问题但对错一眼看不太出来尤其是差分信号用同一颜色线缆时很容易搞混。用示波器量CAN_H和CAN_L对地电压正常工作时CAN_H约3.5V显性/2.5V隐性CAN_L约1.5V显性/2.5V隐性如果两个波形反过来了就是接反了。收发器芯片供电对不对TJA1050的VCC要求5V有的板子直接用了3.3V供电收发器虽然能工作但输出电平可能不达标导致远距离通信不稳定。我用过的SN65HVD230是3.3V供电但也不能混着和5V收发器一起用。总线上是否只有一个节点这其实是个很经典的坑——正常模式下单节点发送是无法成功的因为总线上没有任何节点回应ACK应答位bxCAN会认为发送失败并不断重发最终导致bus off。回环模式恰恰把ACK应答也短路了所以单节点测试用回环模式能成功。要测试正常模式至少得挂两个节点。波特率是否一致用示波器看波形并不直观最简单的办法是拿一块确认能工作的板子和它连一起用不同波特率测试如果某个波特率通了就说明另一块板的波特率没问题问题在硬件。4.2 示波器看CAN波形好坏怎么判断热词里那个“如何通过can总线波形判断通信的好坏”问得很好这也是一个合格嵌入式工程师必须掌握的技能。我可以分享几个实操性的判断方法。先看波形形态。CAN总线上的显性电平逻辑0会让总线产生压差隐性电平逻辑1是2.5V左右。标准波形上CAN_H在隐性时2.5V显性时被拉到3.5V左右CAN_L在隐性时2.5V显性时被拉到1.5V左右。如果示波器探头一端夹CAN_H、一端夹CAN_L差分模式看到的是显性时约2V的压差隐性时接近0V。波形好坏看的几个点边沿是否陡峭如果边沿不是干脆利落的跳变而是斜坡或圆弧说明总线容性负载太重或者节点过多通信速率上不去容易误码。毛刺和振铃在隐性到显性跳变后出现明显的过冲、振铃说明终端电阻匹配不好。振铃如果穿越了隐性/显性的电压阈值就会产生虚假的电平变化导致错误帧。显性位内的电平稳定度正常的显性电平在2V压差左右如果出现明显波动多半是总线供电不足或地线压差过大。顺便说个实用技巧调试CAN波形时不要只看单个CAN_H或CAN_L对地的波形最好用差分探头或把CH1接CAN_H、CH2接CAN_L观察两通道的电位差这样共模干扰会被抑制掉看到的才是真正的总线信号质量。4.3 错误状态错误计数器与Bus-Off恢复CAN控制器内部有发送错误计数器TEC和接收错误计数器REC两个计数器的值决定了节点的工作状态。正常状态TEC/REC都小于128节点正常通信。错误主动Error Active节点会正常发送错误标志。错误被动Error Passive计数器的值等于或高于128节点只能发被动错误标志且发送前需要等待一定的总线空闲时间。总线关闭Bus-OffTEC大于255节点被完全隔离不参与任何总线通信。怎么监控这些状态标准库里没有直接读TEC/REC的函数但可以通过中断判断。我常开CAN_IT_ERR、CAN_IT_BOF和CAN_IT_EPV中断在中断里读取CAN_GetITStatus()来判断节点状态。如果节点进了bus off虽然开了CAN_ABOM能自动恢复但最好还是在中断里做个记录日志方便定位是哪个节点在疯狂产生错误帧。我曾经遇到一个情况某个节点的CAN_H对地短路结果整个网络的通信质量全部下降其他节点也频繁报错。通过看错误中断频率和TEC的累积速度快速锁定了故障节点。这就是为什么我建议正式代码里把CAN错误中断日志记录下来出了问题时排查效率会高很多。5. 实例代码还能怎么扩展——从裸机驱动到上层应用5.1 从“收到就存”到“按协议解析”基础收发跑通之后下一个需要思考的问题就是数据怎么组织。CAN报文一帧最多8字节如果业务数据超过8字节就需要拆帧和组帧。比如一个电机驱动器的状态信息包括电流、转速、温度、故障码总共可能有12字节那就要拆成2帧发送并在帧头定义帧序号和总帧数。我自己的习惯是先定义一套简单的应用层协议类似这样帧ID方向数据段定义0x100主机→节点控制指令byte0使能byte1-2目标转速byte3模式0x200节点→主机状态回报byte0-1实际转速byte2温度byte3故障码0x300节点→主机故障报警byte0故障类型byte1故障等级这套协议的好处是每个节点只需要配置自己的过滤器比如节点1只接收0x100其他无关报文完全不会进入CPU减轻了软件负担也降低了错误数据干扰业务逻辑的概率。5.2 别急着上CAN FD先把手上的F103用到极致每次聊到CAN总会有人问CAN FDCAN with Flexible Data-rate。CAN FD相比传统CAN数据段速率更高单帧最多64字节数据还能带CRC校验看起来很强。但STM32F103的bxCAN并不支持CAN FD需要换G0/G4/H7系列或者带FD控制器的芯片。这里我建议如果你的产品还在用F103完全没有必要为了CAN FD去换主控因为CAN 2.0在50米内的工业组网里1Mbps的速率配上8字节的应用数据绝大多数场景都足够用了。另外如果项目里既有CAN又有RS485/RS232可以关注一下热词里提到的“can/rs 485复用差分接口电路”这种设计是让同一组差分线既支持CAN协议又支持RS485协议切换的时候只要改收发器的模式引脚即可。STM32的串口和CAN外设都支持切换只需修改GPIO复用和协议初始化实际项目中做过一次这种方案硬件成本能省不少但前提是通信协议要统一规划清楚。5.3 最后分享一个小技巧调试期多打印错误状态我每次给新板子调CAN都会在初始化之后加一个CAN_GetErrorCode()的读取函数把错误码通过串口打印出来。标准错误码分成了CAN_ERRORCODE_ACKErr、CAN_ERRORCODE_BitErr、CAN_ERRORCODE_CRCErr等自己根据错误码就能快速判断问题在哪个层级。错误码里出现ACKErr物理层正常但总线上没有节点应答多半是单节点测试或者对方没上电。出现BitErr或StuffErr一般和波特率不匹配、终端电阻异常有关重点查位定时配置。错误计数持续增长但通信偶尔成功大概率是干扰严重或总线过长考虑降波特率、加终端电阻匹配、检查地线布局。用上这个方法后我再也没有在CAN排查上浪费过超过半天时间。6. 结合经验做个实用总结——这套代码够不够用如果你把上面这些代码和排查思路吃透基本已经能应对STM32F103的绝大多数CAN开发场景。从引脚配置、波特率计算、过滤器掩码到发送邮箱的状态机和接收中断的搬数据逻辑这些都是能直接搬到项目里的骨架。后续根据业务场景往这个框架里塞自己的协议解析、状态机、报警处理就行。至于那些硬件层面的坑终端电阻、CAN_H/L接线、总线电容负载调试时多拿示波器和万用表量一量经验积累起来很快。根据我的个人经验CAN调试最大的门槛不是代码而是对物理层和协议层共同作用的直觉——多踩几次坑每次把波形和错误码记录下来你很快就会形成自己的排障思维模型。本文还有配套的精品资源点击获取