STM32F103C8T6 CAN总线通信实战:原理、配置与双机收发

发布时间:2026/9/1 9:56:22
STM32F103C8T6 CAN总线通信实战:原理、配置与双机收发 简介这是围绕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之间的电压差决定了总线电平状态显性位DominantCANH拉到3.5VCANL拉到1.5V差分电压约2V逻辑上对应0。隐性位RecessiveCANH和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越小优先级越高。接收FIFO2个每个深度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的TXDCAN_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、时间段1Time Segment 1、时间段2Time Segment 2它们共同决定了一个位时间分成多少份。波特率计算公式CAN波特率 APB1时钟频率 / (Prescaler x (1 BS1 BS2))比如APB1时钟36MHzPrescaler4BS19BS22那波特率就是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总线了——它没有你想象的那么难但也绝对值得认真对待。本文还有配套的精品资源点击获取