简介:这是围绕STM32F103C8T6微控制器CAN总线收发功能的完整演示工程,主要面向嵌入式开发初学者,也适合需要快速接入工业控制网络的工程师参考,能够直观理解CAN多主通信、错误检测与自动重传机制。压缩包共110个文件,包括31个头文件、29个C源文件、Keil工程文件以及编译过程产生的目标文件、调试信息与链接脚本,整体大小仅1.65MB,工程结构紧凑,方便直接打开查看各项配置。代码演示了从时钟使能、GPIO复用、波特率与帧格式初始化,到发送与接收函数完成数据交互,再到中断方式处理位错误、格式错误等异常的标准流程,关键步骤都有注释,便于逐段学习;同时附带编译产物,可快速烧录到开发板观察效果。目前已有5435人学习下载,适合希望通过实际工程快速掌握STM32 CAN通信,并在此基础上实现双机或更多节点数据交换的开发者。
1. 项目概述与方案选型思路
1.1 为什么是CAN总线,为什么是F103C8T6
先说说这个项目让我最兴奋的地方。CAN总线这个东西,做嵌入式的迟早要碰,尤其是在车载、工业控制、医疗设备这些场景里,基本是躲不掉的。STM32F103C8T6作为一颗经典到不能再经典的MCU,片上集成了CAN外设,而且这颗芯片在淘宝上十来块钱就能买到最小系统板,配合一个TJA1050收发器芯片,几十块钱就能把CAN通信环境搭起来。这个组合对于学习CAN协议、验证CAN通信逻辑、甚至做小型分布式控制原型,性价比高得离谱。
我知道有朋友会问:现在新出的MCU那么多,为什么还守着F103这个老古董?答案很实在:F103的资料是全互联网最全的,遇到任何问题都能搜到答案,而且它的CAN外设在所有STM32家族里算是比较典型的,把F103的CAN搞明白了,以后换到F407、H750甚至G0系列,上手成本极低。更值得注意的是,这颗芯片的国产替代型号(如GD32F103C8T6、MM32F103等)在市场上已经很成熟,如果你是在做产品选型,用国产替代芯片可以把单颗成本打到5块钱以内,代码基本不用改,这个账算下来非常划算。
1.2 项目需求拆解
这个项目的核心需求可以拆成三层:
- 第一层:理解CAN协议原理。CAN是差分信号传输,两根线CANH和CANL,靠电平差来传数据,抗干扰能力远强于UART的TTL电平。协议层面有两层:物理层的电平定义和显性/隐性位仲裁,数据链路层的帧格式、仲裁机制、错误处理。这些不搞清楚,后面调试会一头雾水。
- 第二层:把STM32的CAN外设跑起来。包括GPIO复用配置、CAN初始化、滤波器设置、报文发送和接收中断处理。
- 第三层:实现两块板子之间的可靠收发。这涉及到终端电阻匹配、波特率计算、错误状态监测、总线竞争仲裁等实际工程问题。
我当时做这个项目的时候,目标很明确:用两块F103C8T6最小系统板,各接一个TJA1050收发器,一块发一块收,把CAN报文正确传过去,然后在调试中把遇到的各种坑记录下来。这篇文章就是完整的过程记录,适合刚接触CAN、想用F103快速上手的朋友直接照着抄。
2. CAN总线核心原理与F103的CAN外设
2.1 差分信号与总线电平
CAN总线之所以在工业现场这么受欢迎,核心就是它的物理层设计。两根线缠在一起走差分信号,CANH和CANL之间的电压差决定了总线电平状态:
- 显性位(Dominant):CANH拉到3.5V,CANL拉到1.5V,差分电压约2V,逻辑上对应0。
- 隐性位(Recessive):CANH和CANL都被拉到2.5V,差分电压约0V,逻辑上对应1。
显性位可以覆盖隐性位,这是CAN总线仲裁机制的基础。也就是说,如果两个节点同时发数据,谁先发显性位谁就赢得仲裁,这个机制让CAN不需要像RS-485那样靠主机轮询,真正实现了多主通信。
STM32F103C8T6的CAN控制器是符合CAN 2.0B规范的,支持标准帧(11位标识符)和扩展帧(29位标识符),最高速率1Mbps。片上集成了3个发送邮箱和2个接收FIFO,配合硬件过滤功能,可以大幅减轻CPU负担。不过要注意,F103的CAN外设和USB共用一块SRAM,两者不能同时使用,用USB的时候CAN就没法用了,这是这颗芯片的一个小限制。
2.2 帧格式:标准帧与扩展帧
CAN报文的核心结构是数据帧。标准帧和扩展帧的区别只在ID长度上:标准帧11位ID,可以分配2048个不同标识;扩展帧29位ID,标识空间大得多。实际项目里,如果节点数量不多,标准帧完全够用。
一个标准数据帧长这样:
- 帧起始:1个显性位,表示一帧开始。
- 仲裁场:11位ID + RTR位(远程帧请求位)+ IDE位(标识符扩展位)。
- 控制场:DLC(数据长度代码),表示后面数据场有几个字节,0-8。
- 数据场:0-8字节数据,这就是真正要传的内容。
- CRC场:15位CRC校验,发方计算,收方校验,错就报错。
- ACK场:接收节点在ACK位发送显性位,告诉发方“我收到了”。
- 帧结束:7个隐性位。
F103的CAN外设在硬件上处理了大部分流程,你需要做的就是:给发送邮箱填入ID、数据长度和数据,然后请求发送;接收侧则直接去读接收FIFO拿数据。协议栈和错误处理这些脏活累活,硬件都替你扛了。
2.3 F103 CAN外设的结构特点
F103的CAN外设内部结构有几个关键模块值得说一下:
- CAN控制器内核:负责协议解析、位时序同步、错误管理,这个部分对用户基本透明。
- 发送邮箱:3个,可以缓存3条待发报文。硬件会按优先级自动调度发送,优先级由ID决定,ID越小优先级越高。
- 接收FIFO:2个,每个深度3级。FIFO0和FIFO1可以通过过滤器选择进入哪个FIFO。
- 过滤器组:28个(F103C8T6是28个),每个过滤器可以配置为掩码模式或列表模式。这是CAN外设跟UART最不一样的地方,也是新手最容易困惑的地方。
过滤器的逻辑用一句话解释:硬件在接收报文时,根据你预设的规则,决定哪些报文进入FIFO、哪些直接丢弃。比如你只关心ID为0x123的报文,就可以配置一个列表模式过滤器,只放行ID等于0x123的帧。这在多节点总线上非常重要,不然CPU要被无关中断淹死。
3. 硬件搭建与接线实操
3.1 最小硬件清单
做CAN通信实验,硬件成本完全可以控制在50块钱以内:
- STM32F103C8T6最小系统板 x2(十几块钱一块,顺便用国产替代芯片测试一下兼容性也可以)
- TJA1050 CAN收发器模块 x2(淘宝搜“TJA1050模块”,几块钱一个,一般都带6N137光耦隔离,不过低速短距离测试直接供电就行)
- 双绞线两根(CANH和CANL各一根),长度一米就够
- 120欧姆终端电阻 x2(买模块一般会送,如果没送,自己去电子市场买几个贴片电阻也行)
- USB-TTL串口模块 x1(用来观察串口打印调试信息)
- 杜邦线若干
基本接线逻辑是:STM32的CAN_TX引脚(PA12)接TJA1050的TXD,CAN_RX引脚(PA11)接TJA1050的RXD,收发器模块的CANH和CANL分别接到总线上的CANH和CANL。注意两块板子的CANH接CANH、CANL接CANL,不能交叉,交叉了物理层就是错的,收发肯定失败。
3.2 终端电阻的关键作用
CAN总线两端必须各接一个120欧姆终端电阻,这是新手最常忽略但影响最大的一个点。原因在于CAN的隐性位是靠收发器的差分电压输出实现的,如果没有终端电阻,信号在总线末端会发生反射,导致波形畸变,尤其在波特率比较高(500k以上)的时候,反射信号可能直接把显性位吃掉,表现为通信时好时坏、偶发错误帧。
终端电阻的接法:总线最两端的节点各接一个120欧电阻。如果只有两个节点在通信,那就在每个节点上各接一个。如果你的收发器模块上已经焊了120欧电阻,你就省事了,直接用它就行。但这里有个坑:如果总线上挂了很多模块,每个模块都自带终端电阻,那总线的等效阻抗会偏低,导致信号幅值不够,反而通信故障。所以接多个节点时,要检查哪些模块带了终端电阻,只保留最两端的那两个。
3.3 供电和共地问题
两块板子之间的电源处理也要注意。如果你用的是两个独立的USB口供电,那两边的GND必须连在一起。CAN收发器输出的差分信号虽然不需要共地才能通信(理论上差分信号可以完全隔离),但收发器本身的供电和逻辑参考还是需要共地的,不共地的话,TJA1050的逻辑电平参考点不一致,容易出现误码。
如果是短距离桌面测试,最简单的办法是:两块板子都从一个USB HUB取电,或者直接把两块板的GND用杜邦线连起来。如果以后做长距离现场总线,就应该考虑用隔离CAN收发器模块(带DC-DC隔离的那种),彻底断开两个节点之间的地环路,抗干扰能力会强很多。
3.4 国产替代芯片验证
顺便说一句,我在测试中用了一片GD32F103C8T6替换原厂芯片,在这套CAN例程上直接烧录编译好的hex文件,跑起来一点问题都没有。国产F103在CAN外设的寄存器布局跟原厂完全一致,这也就意味着你不用担心换芯片要改代码的问题。做产品的话,这一颗芯片能省好几块钱成本,量大非常可观。
4. CAN初始化与收发程序实现
4.1 标准库还是HAL库
F103的CAN开发环境选择,我推荐直接上HAL库。虽然标准库的代码更简洁直观,但从长期角度看,ST已经停止标准库的更新了,而HAL库在后续所有STM32系列上都是通用的。你花一天时间学会HAL库的CAN初始化流程,以后换任何型号都能无缝切换。
用STM32CubeMX配置工程是最稳妥的路径:选芯片型号F103C8T6,打开CAN1外设,配置PA11为CAN_RX、PA12为CAN_TX,然后设置波特率。这里有个关键参数:CAN外设的时钟源,F103的CAN挂在APB1总线上,APB1时钟最大36MHz。波特率配置涉及三个参数:分频系数(Prescaler)、时间段1(Time Segment 1)、时间段2(Time Segment 2),它们共同决定了一个位时间分成多少份。
波特率计算公式:
CAN波特率 = APB1时钟频率 / (Prescaler x (1 + BS1 + BS2))比如APB1时钟36MHz,Prescaler=4,BS1=9,BS2=2,那波特率就是36000000 / (4 x (1 + 9 + 2)) = 750000,即750kbps。这里BS1和BS2的单位是时间量子(Time Quantum),一个位周期被分成多个时间量子,采样点就在BS1和BS2的边界上。
4.2 初始化代码实战
用CubeMX生成工程后,核心初始化代码在MX_CAN1_Init函数里:
static void MX_CAN1_Init(void) { hcan.Instance = CAN1; hcan.Init.Prescaler = 4; hcan.Init.Mode = CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_9TQ; hcan.Init.TimeSeg2 = CAN_BS2_2TQ; hcan.Init.TimeTriggeredMode = DISABLE; hcan.Init.AutoBusOff = ENABLE; hcan.Init.AutoWakeUp = DISABLE; hcan.Init.AutoRetransmission = ENABLE; hcan.Init.ReceiveFifoLocked = DISABLE; hcan.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan) != HAL_OK) { Error_Handler(); } }这些参数里,AutoRetransmission = ENABLE值得解释一下。CAN协议有硬件级的错误重发机制,当节点在发送过程中检测到错误(比如仲裁丢失、ACK错误),硬件会自动重发,不需要软件干预。这在工业现场是好东西,但在调试阶段可能会掩盖问题——如果你的总线根本没接好,硬件会一直重发,你会看到发送请求一直被占用。调试时建议把它关掉,让发送失败时立刻返回错误码,方便定位。
AutoBusOff = ENABLE意思是当错误计数超过256次,控制器自动退出bus-off状态。调试时这个也建议关掉,出错了就手动画检查,不然节点不断自动恢复,你很难捕捉到错误瞬间。
初始化之后,还需要调用HAL_CAN_Start(&hcan)来启动CAN控制器,以及HAL_CAN_ActivateNotification(&hcan, HAL_CAN_RX_FIFO0_MSG_PENDING_CB_ID)来使能接收中断回调。
4.3 过滤器配置
初始化的重头戏是过滤器配置。一个典型的配置方法:
void CAN_Filter_Config(void) { CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = 0x0000; sFilterConfig.FilterIdLow = 0x0000; sFilterConfig.FilterMaskIdHigh = 0x0000; sFilterConfig.FilterMaskIdLow = 0x0000; sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; sFilterConfig.FilterActivation = ENABLE; sFilterConfig.SlaveStartFilterBank = 14; if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK) { Error_Handler(); } }这段配置的效果是:掩码全0,即不筛选任何位,所有报文都放进FIFO0。这是最简单、最不容易出错的配置方式。等通信跑通了,再根据需要去配掩码模式或列表模式。
掩码模式的原理用一个例子说明:如果你想只接收ID高8位等于0x12的报文,忽略低3位,可以这样理解:掩码位为1的位必须匹配ID寄存器,掩码位为0的位不关心。所以掩码寄存器的1越多,筛选越严格。新手配置过滤器最容易犯的错误是搞反掩码位和ID位的对应关系,我的建议是先用全通配置跑通通信,再去研究筛选逻辑。
4.4 报文发送与接收
发送报文的代码:
CAN_TxHeaderTypeDef TxHeader; uint8_t TxData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; uint32_t TxMailbox; TxHeader.DLC = 8; TxHeader.IDE = CAN_ID_STD; TxHeader.RTR = CAN_RTR_DATA; TxHeader.TransmitGlobalTime = DISABLE; // 发送普通数据帧使用标准ID 0x123 TxHeader.StdId = 0x123; if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) != HAL_OK) { // 发送请求失败,打印错误码 printf("CAN send failed\r\n"); }注意HAL_CAN_AddTxMessage只是把报文放进了发送邮箱,并不代表已经发送成功。3个邮箱都满的时候,这个函数会返回HAL_ERROR。如果启用了自动重传,发送失败会一直占据邮箱,直到发送成功。前面说了调试阶段建议关掉自动重传,这样才能及时发现问题。
接收报文用中断方式:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; if (hcan->Instance == CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData); printf("Received ID: 0x%03lX, DLC: %d, Data: ", RxHeader.StdId, RxHeader.DLC); for (int i = 0; i < RxHeader.DLC; i++) { printf("%02X ", RxData[i]); } printf("\r\n"); } }解析一下接收流程:硬件收到一帧合法报文后,会匹配过滤器,匹配成功则存入FIFO0,然后触发中断。你在回调函数里用HAL_CAN_GetRxMessage取出报文,清FIFO,就可以处理数据了。这个流程没有软件轮询延迟,实时性很高,而且数据被硬件完整校验过了,软件层不需要再操心帧错误、CRC错误这些事。
4.5 一种更简单的验证方式:环回模式
在把两块板子用线连起来之前,F103还有一个非常有用的调试模式——SILENT_LOOPBACK(静默环回)。这个模式下,CAN控制器把发送的数据直接在内部回环给自己,不需要外部总线就能验证基础功能。初始化时把Mode改为CAN_MODE_LOOPBACK,发送报文后立刻能在接收FIFO里读到同一帧数据。如果连这个都通不过,说明代码有问题,跟接线无关。
这个模式是绝佳的“自检工具”。我每次做CAN项目,都会先在环回模式跑一遍基础收发代码,确认配置没问题,再切换到正常模式接外部总线。
5. 双机通信实测与问题排查
5.1 实测拓扑与步骤
把两块板子按前面的接线接好,配置A板为发送端、B板为接收端。A板每500ms发送一帧ID为0x123的数据,数据内容是0-7递增。B板收到后通过串口打印出来。
我在实测中遇到一个有意思的现象:第一次上电后,A板串口显示发送成功,但B板收不到。查了半天发现是A板的TJA1050模块上自带了120欧终端电阻,B板的模块也带了,这就相当于总线两端各有一个电阻没问题,但其中一块板的模块焊接有问题,导致CANH和CANL之间只有这一个电阻在工作,显性电平幅值不够,接收端识别不到有效电平。
这种问题排查有个简单的办法:用万用表测量CANH和CANL之间的电阻。正常时,在总线任意一个节点处测量,应该是60欧左右——因为总线上两个120欧电阻并联。如果量到的是120欧,说明你只接了一个终端电阻;如果量到非常小,说明总线上可能有多于两个电阻并联。这个测量方法能把大部分终端电阻相关的问题快速定位。
5.2 波特率不匹配的表现
波特率不匹配是另一个极常见的问题。两块板子的CAN配置波特率不一致,表现是发送端一直发送失败,重试一直失败,接收端完全没反应。这是因为CAN接收器的同步机制要求收发双方必须有一个相同的波特率基准,对不上就同步不了,采样点完全错位,每一帧都会被当成错误帧丢弃。
排查方法:用逻辑分析仪或者CAN分析仪抓总线波形,数一个位的实际时长,换算成波特率。没有仪器的情况下,最简单的是把波特率参数在CubeMX里重新核对一遍,保证两块板子的Prescaler、BS1、BS2完全一致。注意两块板的APB1时钟可能不一样,如果你的工程里系统时钟配置不同,即使分频参数一样,实际波特率也不一样。正确做法是统一两边的时钟树。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 收发都无反应 | CANH/CANL接反 | 交换两根线,测量静默电平差 |
| 发送成功但接收端无数据 | 终端电阻缺失 | 万用表测CANH-CANL间电阻应为60欧 |
| 偶发错误帧 | 总线过长或干扰 | 降低波特率,检查线缆双绞情况 |
| 发送函数一直返回HAL_BUSY | 自动重传开启且总线上无ACK | 关掉自动重传,检查终端电阻和节点 |
| 不带电时测量CANH-CANL短路 | 收发器损坏 | 更换TJA1050 |
| 波特率不一致 | 两块板时钟配置不同 | 统一时钟树,核对分频参数 |
5.4 CAN总线的共模干扰问题
热词里提到了“滤除CAN总线的共模干扰”,这确实是在工业现场逃不开的问题。CAN差分信号的抗共模干扰能力本身很强,但强大的前提是收发器两端的共模电压在允许范围内。TJA1050的共模电压范围是-12V到+12V,超过这个范围,接收端识别就会出错。
如果现场有变频器、大电机之类的强干扰源,总线波形会叠加共模噪声,光靠差分传输可能不够。解决方案有几个层次:
- 成本最低:用屏蔽双绞线,屏蔽层单端接地,减少共模耦合。
- 隔离方案:用带DC-DC隔离电源的CAN收发器模块,彻底断开两个节点的地,把共模干扰挡在隔离墙外。
- 抗干扰芯片:用TJA1051/1042这类增强型的收发器,或者带显性超时功能的型号,防总线死锁。
我之前在工厂调试设备时遇到过一个很典型的共模问题:控制器和驱动器之间距离大约50米,CAN通信在设备启动瞬间频繁报错,后来把收发器换成隔离型的,再把屏蔽层在控制器端接地,问题就消失了。这种问题软调是解决不了的,必须从硬件层面入手。
5.5 调试工具推荐
做CAN调试,我建议至少准备以下工具:
- 逻辑分析仪:便宜的24MHz 8通道就够了,可以直接抓CANH和CANL的波形,能看显性/隐性位、错误帧,方便分析波特率是否匹配。
- USB-CAN分析仪:比如创芯科技或者周立功的,价格一两百到上千不等。有了它可以直接看总线上的报文流、错误计数、总线负载率,调试效率翻倍。
- 串口打印:MCU端用串口打印收发状态和错误信息,是最朴素的调试方式,也最直接。
我个人的习惯是:先用环回模式验证单板功能,再用两台设备点对点通信,最后再接第三个设备模拟多节点场景。每一步观察的现象越早,排查问题越容易。
6. 实操心得与拓展建议
6.1 核心心得
这个项目做完,我的几个核心心得值得记录:
- 手册不如实测:F103的CAN外设其实比想象中简单,初始化配置好,过滤器全通,收发函数调好,通信基本就通了。太多人在配置阶段纠结于各种参数的含义,浪费了大量时间。先用最鲁棒的配置跑通,再逐步优化参数,效率高得多。
- 物理层问题远多于协议层问题:绝大多数CAN通信失败的案例,原因都在接线、终端电阻、供电这些物理层细节上。调试时先检查线缆和电阻,再排查代码配置。
- 错误处理一定要重视:F103的CAN外设有完善的错误状态机制,包括错误主动、错误被动、总线关闭三种状态。调试时把错误中断开了,就能实时看到错误种类,定位问题快很多。
- 国产替代是现实需求:不管是学习还是做产品,F103C8T6的国产替代方案已经非常成熟。硬件兼容、软件不用改,价格便宜一半以上,值得关注。
6.2 项目扩展方向
这个项目做完成,后续可以扩展的方向非常多:
- 多节点组网:3个或更多节点挂到同一条总线上,测试不同ID优先级、仲裁机制、总线负载率对通信的影响。
- CANopen协议移植:在CAN收发之上移植CANopen协议栈,实现标准化设备通信,这在工业设备里是硬需求。
- FreeRTOS + CAN:把CAN收发任务放到FreeRTOS里,用队列传递报文数据,体验一下实时操作系统下的通信架构。
- CAN与USB共存:F103的CAN和USB共用SRAM不能同时用,但如果用F105或者F407,就可以用USB虚拟串口实时监控CAN报文。
最后分享一个我在调试中的小技巧:如果CAN通信时好时坏,别急着改代码,先用串口把CAN的错误寄存器值打出来。如果看到的是位填充错误或位错误,大概率是物理层问题;如果是CRC错误或形式错误,更需要关注波特率匹配和总线上的干扰。用错误码定位问题,比自己瞎猜高效太多。这套流程走通了,你就算正式入门CAN总线了——它没有你想象的那么难,但也绝对值得认真对待。
本文还有配套的精品资源,点击获取