简介:面向嵌入式初学者的STM32 CAN通信经典例程包,基于ARM Cortex-M内核与标准外设库实现,适合学习CAN协议在单片机上的实际落地。压缩包共93个文件,以h头文件与c源文件为主,并含Keil/EWARM工程配置、链接脚本及readme说明,整体303KB,目录结构清晰,便于直接打开调试。例程覆盖CAN外设初始化、GPIO与时钟配置、标准与扩展帧收发、滤波器设置及中断处理,并附完整标准外设库驱动,可快速理解CAN两线差分通信与优先级仲裁机制。已有978人学习,适合自动化、汽车电子及工业控制方向的开发者作为设计参考,帮助掌握多设备可靠通信的工程实现细节。 最近在整理资料的时候翻出一个老项目,里面有一份很经典的STM32 CAN通信例程,压缩包名字就叫“STM32的CAN通信例程,很经典,可供学习设计参考.zip”。说来惭愧,当年我就是靠这份例程把CAN总线彻底跑通的,后来做电池管理系统、工业设备通信,甚至给毕业设计学生讲CAN,底子都是从这里打下的。所以今天想把这个例程的要点拆开来讲一讲,包括协议理解、代码结构、波特率计算、过滤器配置,还有调试时最容易踩的坑。无论你是刚接触CAN的新手,还是想快速上手STM32 CAN开发的工程师,这篇内容应该都能给你省不少时间。
1. 这个例程到底在讲什么:CAN通信的核心价值与整体设计
1.1 为什么CAN在嵌入式里经久不衰
CAN(Controller Area Network)总线是博世在1986年搞出来的串行通信协议,最早是为汽车电子设计的。三十多年过去了,它依然是工业控制、医疗设备、机器人、储能系统中主流的现场总线之一。原因很简单:CAN是差分信号,抗干扰能力极强;多主结构,任何节点都能主动发报文;有完善的仲裁机制,优先级高的报文先发,不会冲突;还有CRC校验和错误处理机制,数据可靠性高。我一直觉得CAN的设计哲学很像一个文明的多方对话现场——每个人都有发言权,但身份高的人优先讲话,如果有人听错了,大家会同步重讲,直到信息一致。
对比一下别的通信方式:UART是点对点,想要多点通信得自己搞组网逻辑;RS485虽然能挂多节点,但主机轮询式的半双工机制效率不高,而且没有内置的硬件仲裁。CAN等于把这些常用痛点都在底层解决了,所以不少MCU甚至直接把CAN外设叫bxCAN(Basic Extended CAN),STM32就是这样。
1.2 经典例程的模块划分与学习路线
对于这份“很经典”的例程,我印象中它的代码结构并不复杂,但五脏俱全。一般就是main.c、can.c、can.h,加上stm32f10x_can.c标准库文件,有些版本还会有中断处理函数。代码流程基本是:初始化CAN外设(GPIO、波特率、过滤器)→ 进入正常模式 → 主循环发送报文 → 中断接收报文。
这个设计相比直接把收发逻辑全塞在main里的写法,优势非常明显:初始化与业务处理分离,CAN底层配置一次搞定,收发接口清晰,方便套用到自己的项目里。我建议新手拿到例程后,不要太急着改功能,先按下面这条线路走一遍:
- 第一步,烧录例程,用环回模式(LoopBack)验证收发逻辑,不用接外部设备也能跑通;
- 第二步,把环回模式改成正常模式,用两块开发板对接,或者接到USB-CAN分析仪上,实测报文收发;
- 第三步,对照数据手册和代码注释,把波特率计算、过滤器掩码这几段彻底弄明白;
- 第四步,在此基础上增加中断接收、错误处理、多节点组网,逐步变成自己的工程模板。
2. 核心细节:bxCAN配置中容易忽视的关键参数
2.1 引脚与时钟:CAN外设的“地基”
很多人一上来就急着配置CAN的波特率,结果通信死活不通,回过头来才发现GPIO和时钟根本没配对。STM32F103的CAN外设复用引脚有两个映射组:默认复用功能是PA11(RX)、PA12(TX),重映射之后是PB8(RX)、PB9(TX)。你要是用了重映射,不开启AFIO重映射时钟,RX/TX就不会连接到CAN外设上,这是非常常见的低级而又隐蔽的问题。
时钟方面,CAN1外设挂载在APB1总线上,时钟频率一般是36MHz(如果系统时钟为72MHz)。注意:STM32的CAN外设具有“时钟魔术”机制——它内部的位时序单元要求输入时钟是APB1时钟,但波特率分频器则以该时钟为基础进行分频。API函数里的CAN_InitStructure.CAN_Prescaler就是分频系数,这个值直接决定波特率。我在代码里看到很多人喜欢把分频系数写成4或者5,然后发现通信不上,其实就是没算清楚APB1时钟频率和想要的目标波特率之间的关系。
提示:配置CAN前务必确认RCC是否开启了CAN外设时钟(在标准库中就是RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE))。少开这行,CAN寄存器全读出来都是0。
2.2 波特率配置:位时间与SJW的计算逻辑
CAN总线的波特率不是像串口那样随便填一个数值就能行的,它需要把一个位时间(Bit Time)拆成多个时间段,然后由分频时钟来驱动。STM32 bxCAN的位时间由三部分组成:同步段(SYNC_SEG)、时间段1(BS1)和时间段2(BS2)。协议规定同步段固定为1个时间量子(tq),BS1和BS2各占若干个tq,可配置范围是1到16(实际ST限制1到15)。跳变宽度(SJW)影响的是重同步时的最大补偿范围,通常取1,除非总线时钟误差较大时才调大。
最终波特率的计算公式为:
波特率 = APB1时钟 / (CAN_Prescaler × (1 + BS1 + BS2))举个例子,假设APB1=36MHz,我想得到500kbps的波特率。常见的搭配是:CAN_Prescaler=4,BS1=8,BS2=7,这样实际波特率 = 36MHz / (4 × (1+8+7)) = 36MHz / 64 = 562.5kHz,这不满足要求。正确的计算思路是先确定采样点。我习惯把采样点控制在75%到80%之间,在500kbps时,如果每秒有20个时间量子,设置BS1=13、BS2=6,采样点就是 (1 + 13) / 20 = 70%,如果设置BS1=14、BS2=5,采样点就是75%。然后再反推分频系数:
500kbps = 36MHz / (Prescaler × 20) → Prescaler = 3.6,不满足整数所以 36MHz 下严谨来说做不到严格的500kbps,需要调整分频值或略微调整位时间。很多工程师没意识到这一点,会导致不同节点的等效波特率不完全一致,总线偶发性丢帧。经典例程中如果注释写着500kbps,通常用的分频是9、BS1=7、BS2=6,实际波特率 = 36M / (9×14) = 285.7kbps?不对,仔细算一下:9×14=126,36M/126≈285.7k,这显然不是500k。所以要按自己板子的时钟源核对,不能照抄。
注意:两个CAN节点通信必须满足三个一致——CAN波特率一致、采样点尽量一致、位时间参数都落在协议允许范围内。否则短距离通信可能正常,距离一长或总线负载一高就出错。
2.3 过滤器与验收屏蔽:报文选择性接收的核心
过滤器的设计是CAN例程学习和实际项目开发中的分水岭。如果你不需要选择性地接收报文,把过滤器设为掩码全0,也就是全部报文都接收,开发调试没问题。但如果是正经项目,这个配置就是埋雷。
STM32F103的bxCAN有28个过滤器组,每个过滤器组可以工作在列表模式或掩码模式。列表模式适合匹配精确ID:能收到指定ID的报文;掩码模式适合匹配一段范围的ID:掩码位为1的位必须精确匹配,为0的位可以忽略。以标准帧为例,如果我想只接收ID为0x123的标准帧,把过滤器ID设为0x123,屏蔽寄存器设为全0x7FF(11位全必须匹配)就可以。如果我想接收ID为0x120到0x12F的所有报文,屏蔽寄存器就要把低4位设为忽略。
我曾经遇到一个工程问题:一台设备总线狂收垃圾帧,查了半天发现是过滤器配成了全部接收模式,MCU不得不在中断里处理上百个无用消息,直接拖垮主循环。后来把过滤器掩码精确化,CPU占用率立刻降了30%。所以例程里看起来不起眼的十几行过滤器代码,其实是整个接收策略的核心,值得反复研究。
3. 实操过程:从初始化到收发报文的完整走读
3.1 初始化流程与GPIO配置
把这个经典例程翻出来,一步一步走一遍。首先看main函数里的初始化部分,第一步配置的是GPIO时钟和CAN时钟。以最常用的STM32F103C8T6为例,PA11和PA12作为CAN_RX和CAN_TX。推挽复用输出。很多人问为什么不用开漏模式,因为CAN收发器(比如TJA1050)内部有电平转换,MCU侧的TX脚作为输出端只负责给出高低电平信号,推挽模式没有问题;RX脚配置为上拉输入。
接下来是CAN初始化结构体的配置。经典例程一般是这样写的:
CAN_InitStructure.CAN_TTCM = DISABLE; // 关闭时间触发通信模式 CAN_InitStructure.CAN_ABOM = ENABLE; // 自动离线管理,总线关闭后自动恢复 CAN_InitStructure.CAN_AWUM = DISABLE; // 自动唤醒模式 CAN_InitStructure.CAN_NART = DISABLE; // 非自动重传,为ENABLE时发送失败不重传 CAN_InitStructure.CAN_RFLM = DISABLE; // 关闭接收FIFO锁定模式 CAN_InitStructure.CAN_TXFP = DISABLE; // 关闭发送FIFO优先级,由报文ID决定优先级 CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler = 4; CAN_Init(CAN1, &CAN_InitStructure);这里有一个细节特别值得新人琢磨:CAN_ABOM(自动离线管理)。当CAN控制器进入Bus Off状态(发送错误计数器超过255)后,硬件需要等待128个11位隐性位才能恢复。如果不开启ABOM,你用软件方式恢复会很麻烦;开启了之后硬件会自动处理恢复流程。经典例程里把这位置ENABLE,我认为非常正确,值得推广到所有工业项目里。
3.2 报文发送与中断接收的标准写法
发送报文的核心是填充CanTxMsg结构体,然后调用CAN_Transmit函数。一个标准的发送函数是这样:
uint8_t CAN_Send_Msg(uint32_t id, uint8_t *msg, uint8_t len) { CanTxMsg TxMessage; uint8_t i; TxMessage.StdId = id; // 标准标识符 TxMessage.ExtId = 0; // 扩展标识符不用 TxMessage.IDE = CAN_Id_Standard; // 标准帧 TxMessage.RTR = CAN_RTR_Data; // 数据帧 TxMessage.DLC = len; // 数据长度 for (i = 0; i < len; i++) TxMessage.Data[i] = msg[i]; return CAN_Transmit(CAN1, &TxMessage); }这里最容易出错的是DLC(数据长度码),CAN协议里数据段的长度上限是8字节,很多新手会填超过8,结果发送失败或数据被截断。虽然CAN FD扩展到了64字节,但STM32F103的经典CAN外设并不支持CAN FD,在例程里老老实实用8字节以内就好。
接收部分,经典例程采用中断接收。初始化时需要配置接收FIFO0的消息挂起中断:
CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);然后在中断服务函数中识别中断标志,读取FIFO:
void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); // 处理接收到的数据 // 根据 RxMessage.StdId 做分发 } }关于中断函数名,需要特别说明:不同型号的向量表不同,比如STM32F103ZET6和STM32F103C8T6共用USB_LP_CAN1_RX0_IRQHandler这个名字;而部分互联型产品可能是CAN1_RX0_IRQHandler。写代码前一定要对照启动文件里的中断向量表核对,否则中断服务函数永远进不去。
3.3 环回模式实测:没有硬件也能验证代码
我刚接触CAN时手里只有一块板子,没有CAN分析仪,也没有第二块开发板,这时候环回模式救了大命。环回模式把CAN控制器内部的发送输出直接环回到接收输入,内置收发器不参与外部总线通信。也就是说,你发一帧报文,自己就能在接收FIFO里收到这帧。
在例程里把CAN_Mode从CAN_Mode_Normal改成CAN_Mode_LoopBack,其他配置不变。主循环里发送一个ID为0x001的报文,然后在中断或轮询中读取接收FIFO,如果数据和自己发出去的一致,说明bxCAN外设的发送/接收路径、过滤器配置、中断机制都没问题。等把环回模式跑通了,再切回正常模式接到实际总线上,可以少踩很多坑。
实操提示:用环回模式调试时,建议把过滤器临时设为“全部接收”(掩码全0),先排除过滤器干扰。等收发通了,再逐渐把过滤器收紧,最后测试精确掩码场景。
4. 常见问题与排查技巧实录
4.1 连不上总线、收不到数据?先测波形
CAN通信不成功时,我建议不要先怀疑代码,先测量CAN_H和CAN_L之间的差分波形。用示波器探头夹在CAN_H和CAN_L上,正常通信时你会看到一个约2V的共模电压水平上有明显的差分摆幅。如果波形是一条直线没有任何跳变,大概率是硬件链路没打通;如果波形乱跳但接收端收不到,可能是波特率不一致。
没有示波器的场合,用逻辑分析仪也可以抓CAN报文,现在不少逻辑分析仪软件都支持CAN协议解析。直接把CAN_H接到逻辑分析仪通道,设置好波特率就能看到报文ID和数据,排查效率非常高。我强烈建议每一个做CAN开发的人手里至少有一个USB-CAN分析仪,其实不贵,但能省下的调试时间远超它的价格。
4.2 波特率不同步导致的Bus Off
总线关闭是CAN节点最令人头疼的问题之一。现象是代码跑着跑着就发不出报文了,错误计数器一路飙升,最终节点脱离总线。大多数情况下这是一个典型的波特率配置错误引发的——位时间配置错误或分频值算错,导致本节点的位时序和其他节点不一致。总线上只要有一个节点时序不对,就会持续产生错误帧,形成雪崩效应。
排查步骤我总结成一张表,方便对照:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 发送超时,TXERR计数持续增加 | APB1时钟频率与分频值不匹配 | 核对RCC时钟树,重新计算Prescaler |
| 总线上只有本节点,无法与其他设备通信 | 没有接终端电阻(120Ω) | 确认总线两端各接一个120Ω电阻 |
| 能发出报文但立即收到错误帧 | 采样点偏差过大 | 调整BS1/BS2比例,采样点设为75%左右 |
| 发送一次后不再工作,状态寄存器显示BusOff | ABOM未使能 | 开启CAN_ABOM实现自动恢复 |
| 接收到错误帧或CRC错误 | 波特率不匹配 | 用CAN分析仪对比双方参数 |
4.3 过滤器配置错误导致丢帧
有一个典型案例:项目中节点需要接收ID范围0x200到0x2FF的所有报文,我把掩码寄存器设成了0x700,看起来低8位全部忽略,理论上是对的。但实际运行中发现0x100到0x1FF的报文也混进来了,因为这组报文的低8位满足被忽略的条件,而高3位又没有挡住。后来我把掩码调整成0x7F8,把高3位精确匹配为0x2,低8位忽略,报文过滤就精确了。
过滤器这块的经验是:先明确业务上必须接收的ID范围,再反推掩码值;掩码中为1的位是必匹配位,为0的位是“不关心”位。如果条件允许,优先使用列表模式配置多个精确ID,比掩码模式更直观、更不容易误收。
4.4 中断进不去或程序卡死
中断进不去有一个被忽略的常见原因:CAN1的接收中断在STM32F103上对应的是USB LP CAN1 RX0中断向量,但如果你在其他地方对USB中断做了处理,两者可能冲突。这种问题在同时使用USB和CAN的场景下非常考验向量表分配,需要阅读数据手册中的中断向量表说明,必要时采用查询方式接收CAN数据,避开冲突。
程序卡死则多发生在中断处理时间过长的情况下。CAN中断里塞了耗时操作,比如printf串口打印、Flash读写、长延时,会导致中断函数一直占用CPU,主循环无法执行,看起来就是程序卡死。我的建议是中断里只做报文拷贝和置标志位,数据解析和业务逻辑放到主循环处理。
5. 调试工具与扩展思路:从例程到项目落地的最后一公里
5.1 用好调试工具:CAN分析仪与示波器的分工
很多人在开发CAN项目时只依赖MCU代码里的串口打印,这实在太慢了。CAN分析仪是必备利器,它能让你看到总线上每一帧报文的时间戳、ID、数据、帧类型,还能模拟发送报文,是双机通信调试的照妖镜。示波器负责物理层验证,特别是检查CAN_H和CAN_L的共模电压、波形上升下降沿、位时间宽度。波形是判断通信质量的重要依据:正常情况下每位波形清晰,差分幅值在1.5V到3V之间;如果波形失真或边沿过缓,要考虑总线长度、支线长度、终端电阻等因素。
在经典例程的后续实践中,我建议在初始化完成后主动读取CAN_GetFlagStatus(CAN_FLAG_TSR)等发送状态标志,并输出CAN_GetErrorCode()的错误代码,用串口打印出来,这样调试时能快速定位是发送失败还是接收错误。
5.2 后续扩展:多节点组网与错误处理机制
一旦把例程跑通,下一步就是往工程化方向进化。比如多节点组网时要合理分配CAN ID优先级,ID越小优先级越高,重要报文(如急停、故障报警)分配小ID;要设计心跳报文机制,每个节点周期上报在线状态;要考虑错误恢复机制,把CAN_ABOM、错误中断、被动错误检测全部用上。
我自己在项目里的通用模板是:每个节点有一个唯一ID,周期性发送状态帧,事件触发发送数据帧,接收方通过过滤器只接收与自己相关的报文。接收缓冲区做成FIFO环形队列,避免中断频繁操作队列造成数据互相覆盖。这些能力都可以从经典例程的基础上逐层叠加,而不需要推倒重来。
5.3 学习建议:怎么把这份例程吃透并变成自己的
最后分享一点个人经验。拿到这份例程后,不要满足于“能收到数据、能发数据”这个状态,一定要亲手做三件事:第一,重算一遍波特率,用不同APB1时钟值代入公式,直到不查表也能随口算出结果;第二,改过滤器配置,分别验证精确接收、范围接收、全部接收三种模式对中断频率和CPU占用率的影响;第三,故意制造错误,比如把波特率调错、拔掉终端电阻,观察错误计数器和总线的反应。这三件事做完,你对CAN的理解会比光看十遍教程都要扎实。
我当年踩过最深的坑就是盲目照搬例程,结果换了一块主频不同、APB1分频不同的开发板,通信死活就是不通,最后被逼着翻数据手册自己推演了一遍波特率才解决问题。从那以后,我拿到别人的CAN代码,第一反应永远是先看时钟配置和位时序参数,而不是急着往功能里钻。希望这篇内容也能帮你少走这段弯路。
本文还有配套的精品资源,点击获取