
简介本资源是一套面向STM32初学者与嵌入式开发者的CAN总线通信实战工程聚焦两个STM32F1系列开发板如MINI板间的可靠双机通信实现适用于汽车电子、工业控制等需高抗干扰通信的场景。压缩包含109个文件涵盖31个头文件.h、29个源码文件.c含stm32f10x_can.c等核心驱动、13个编译中间文件.o/.d、Keil工程配置.uvprojx/.uvoptx、可执行镜像.axf/.hex及调试辅助文件.map/.lst整体883KB结构完整开箱即用。已有1259人学习下载项目基于HAL库构建包含CAN初始化、双滤波器配置、标准帧收发、中断响应与错误状态处理等关键模块代码注释清晰配合实际硬件可快速验证通信时序与数据一致性是掌握STM32 CAN底层机制与工程落地的典型参考范例。 之前群里有人问两片STM32用CAN总线做数据交换到底怎么搞我把手头的F103板子翻出来搭了个最简单的“一收一发”实验顺带把调试过程中踩的坑都记了下来。这篇内容从CAN协议基础、硬件接法、CubeMX配置、HAL库收发代码一直写到实际调试时的波形分析和问题排查适合刚接触CAN通信、想用两块STM32板快速跑通总线通信的人。先说结论两片STM32之间做CAN通讯核心就三件事——物理层接线不要接反、波特率两边必须一致、过滤器别把消息全挡掉。绝大多数调不通的案例最后都能归到这三类问题上。1. 项目概述与整体设计思路1.1 为什么用CAN而不是串口或I2C很多人在两板通信时第一反应是USART简单直接。但如果你需要在一条总线上挂多个节点、节点之间对等地互相发数据、而且现场电磁环境还比较恶劣比如电机旁边、工业控制柜里串口就不太合适了。串口是一对一的想多节点就得自己搞分时复用或者额外加控制逻辑I2C虽然能挂多设备但抗干扰和传输距离又比较弱。CAN总线当初就是为汽车环境设计的差分信号、双绞线传输、多主仲裁、错误检测与重发机制这些特性决定了它在工业控制和车载场景里几乎是默认选择。用两片STM32做CAN通讯本质上是在验证三个能力第一CAN控制器本身怎么配置和驱动第二收发器Transceiver和物理层接线怎么处理第三应用层怎么处理帧的发送、接收和错误状态。这三个能力打通之后往上面加协议、加节点、加CAN FD都是顺水推舟的事。1.2 方案选型F103 TJA1050 STM32CubeMX硬件上的选型我建议用最常见的组合STM32F103C8T6或者F103RCT6加一颗TJA1050收发器。F103系列自带bxCAN外设支持CAN协议2.0A和2.0B标准帧扩展帧都能处理。TJA1050是目前最常用的CAN收发器芯片之一3.3V或者5V供电都能配合使用把STM32 CAN控制器的TTL电平转成总线上的差分电平。软件方面我强烈建议用STM32CubeMX生成初始化代码配上HAL库用C语言写业务逻辑。原因很简单CubeMX里CAN的波特率、工作模式、过滤器这些参数都有图形化界面配置完直接生成代码不太容易因为寄存器操作写错位导致低级错误。当然如果习惯寄存器操作直接操作bxCAN寄存器也完全可以但调试成本会高一些。至于标题里提到的C说实话在STM32裸机环境下用纯C更省心但如果你打算上RTOS比如FreeRTOS或者写一套可复用的CAN驱动层用C做封装是可行的后面我会单独说。1.3 通信模型的确定一收一发带反馈我给这次实验定的通信模型很简单节点A作为发送端按键按下后向总线发送一帧数据节点B作为接收端收到合法帧后翻转板载LED并立刻向节点A回发一帧“确认帧”。节点A收到确认帧后翻转自己的LED。这样一来发送、接收、应答三个环节全部验证到位而且可以直观看到双向通信是否正常。为什么非要做成“带回执”而不是单纯单向发因为在调试CAN时有一个非常典型的坑是“发送端自以为发出去了但接收端根本没收到”。如果只有单向通信你很难判断是发送端没发出去还是接收端没收到还是总线上压根没有信号。加了回执之后只要两个板子的LED都在动说明整条链路是通的哪个灯不动问题就缩小到哪一侧。2. CAN硬件连线的关键细节2.1 CAN物理层差分信号与总线电平CAN总线的物理层使用两根线CAN_H和CAN_L。它不像串口那样用高低电平表示0和1而是用两根线之间的电压差来表示。显性电平逻辑0时CAN_H被拉到高、CAN_L被拉到低差分电压大约2V左右隐性电平逻辑1时CAN_H和CAN_L都被偏置到2.5V左右差分电压接近0V。这种差分结构的好处很明显外部干扰如果同时作用在两根线上差分电压基本不受影响抗干扰能力自然强。这个特性和调试有什么关系最大关系就是CAN_H和CAN_L接反通信一定失败。因为控制器发出的显性位被反相成了总线上的错误信号接收端根本解析不出正确数据。很多新手第一次接线两根线颜色又不区分插上去发现通信超时第一反应是查程序查了半天才发现是杜邦线颜色不一致。这个事情我干过一次之后再也不敢乱用线色了。2.2 收发器的作用与供电STM32的CAN控制器内部是没有差分驱动能力的它输出的只是普通的TTL电平。要让信号跑到总线上并传100米、抗住共模干扰就必须外接一颗CAN收发器比如TJA1050。收发器负责把控制器的“发送数据”引脚CAN_TX信号转换成CAN_H/CAN_L上的差分电平同时把总线上的差分电平转换成控制器能读的“接收数据”引脚CAN_RX信号。TJA1050的VCC一般接5VCAN_TX和CAN_RX直接连到STM32对应引脚。需要注意TJA1050是5V逻辑供电的芯片但它和3.3V的STM32之间通过TX/RX引脚连接通常是兼容的因为TJA1050的输出在STM32高电平阈值以内实际使用中问题不大。如果用的是3.3V版本的收发器比如SN65HVD230接法类似但供电电压要注意。2.3 终端电阻两个120欧一个都不能少CAN总线规范要求在总线两端各接一个120欧姆终端电阻作用是消除信号反射。很多人做实验的时候只在一端接或者干脆不接短距离低速比如桌面上一共二十厘米的线时可能也能跑通但在线长、节点多或者波特率较高的情况下表现就会很不稳定偶发错误帧、节点莫名其妙进bus-off、CRC错误计数增长。我做这个实验的习惯是每个板的CAN_H和CAN_L之间都放一个120欧电阻有的开发板已经集成然后用导线把两个板串联起来这样总线自然就是两端各有一个120欧。注意电阻一定要接在总线的物理两端不是接在每个节点旁边就完事了。如果只有两个节点那两个板子本身就构成总线两端。2.4 接线表照着接就不会错节点A发送端节点B接收端说明STM32 PA11 (CAN_RX)——板上连接TJA1050 RXDSTM32 PA12 (CAN_TX)——板上连接TJA1050 TXDCAN_HCAN_H两根总线高压线CAN_LCAN_L两根总线低压线GNDGND两个板子必须共地120欧在A端120欧在B端位于总线物理两端这里最容易被忽略的是“共地”。CAN虽然靠差分信号传输但收发器的共模范围是有限度的。如果两个板子各自供电、地线又没有连在一起总线上的共模电压可能会漂移轻则通信不稳定重则根本收不到帧。我做实验时两个板子都用USB线接着电脑以为共地了结果其中一根USB线只给了电没给地折腾了半小时。后来老老实实从其中一个板的GND引脚拉一根线到另一个板的GND问题立刻消失。3. 软件实现基于HAL库的CAN收发3.1 CubeMX配置与滤波器参数说明在STM32CubeMX里选择CAN1外设之后只需要填几个参数波特率相关的预分频器Prescaler、时间段1BS1、时间段2BS2以及工作模式Loopback或Normal。Loopback模式是内部回环主要用于自测两个板子之间通信必须选Normal模式。F103的CAN1挂载在APB1总线上APB1时钟典型值是36MHz。目标波特率设为500kbps采样点尽量落在75%到80%左右因为采样点靠后一些信号在一位的后半段已经足够稳定抗干扰更好。计算方法如下CAN波特率 APB1时钟 / (Prescaler × (1 BS1 BS2))以APB1 36MHz为例要得到500kbps位时间 1 / 500k 2微秒选择Prescaler 6则(1 BS1 BS2) 36MHz × 2us / 6 12取BS1 8BS2 3则采样点 (1 BS1) / (1 BS1 BS2) 9 / 12 75%符合推荐范围。所以CubeMX里填Prescaler 6BS1 8BS2 3波特率会自动显示500kbit/s。如果你用的开发板APB1时钟是别的值按这个公式自己算一遍别直接抄网上的参数。我以前就用过一个外部晶振配错导致APB1变成54MHz的工程波特率参数照搬例程结果发一帧错误一帧查了一下午。Filter过滤器配置方面F103的CAN1有14个过滤器组每个组可以工作在列表模式或掩码模式。两个板子通信最省事的方式是发送端用标准帧ID接收端把过滤器配成掩码模式让它可以接收某个范围的ID。比如发送端发ID为0x180的标准帧接收端滤波器组0配成过滤器模式Mask mode过滤器ID0x180掩码0x7F8这样凡是ID高11位和0x180匹配的帧都能通过如果你后续想发0x181、0x182这些扩展ID也不需要改过滤器。注意标准帧ID只有11位掩码计算时只关心ID区域其他位置零即可。3.2 发送端代码按键触发发送这是发送端的主要逻辑。初始化流程里先启动CAN外设再启动发送邮箱按下按键后调用发送函数CAN_TxHeaderTypeDef tx_header; uint8_t tx_data[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; uint32_t tx_mailbox; void Send_Can_Frame(void) { tx_header.ExtId 0; tx_header.IDE CAN_ID_STD; tx_header.RTR CAN_RTR_DATA; tx_header.DLC 8; tx_header.StdId 0x180; tx_header.TransmitGlobalTime DISABLE; if (HAL_CAN_AddTxMessage(hcan1, tx_header, tx_data, tx_mailbox) ! HAL_OK) { // 发送请求失败检查CAN外设状态 Error_Handler(); } }这里要注意一点HAL_CAN_AddTxMessage只是把消息提交到发送邮箱并不等于已经发出去。你真正确认发送成功要么用发送完成中断回调HAL_CAN_TxMailbox0CompleteCallback要么在短时间内查询邮箱状态。很多新手在这个函数返回HAL_OK后以为数据已经上总线了结果调试发现对方根本没收到实际上邮箱一直发送失败比如ACK错误。所以我在按键任务里加了一个简单状态机按键置位发送标志主循环里调用发送函数然后通过变量记录返回值并打印。实际贴代码时发送端主循环里可以这样组织while (1) { if (button_pressed) { Send_Can_Frame(); button_pressed 0; } }3.3 接收端代码中断回调处理接收端我的建议是用中断唤醒方式而不是轮询查FIFO。理由很简单CAN帧到达的时间不确定轮询会占用主循环大量时间而且万一在处理其他任务时错过了FIFO溢出窗口帧就丢了。中断回调的方式不会漏帧代码也更清晰。初始化时激活FIFO0消息挂起中断HAL_CAN_ActivateNotification(hcan1, HAL_CAN_RX_FIFO0_MSG_PENDING);然后在回调函数里接收数据void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8]; if (hcan-Instance CAN1) { HAL_CAN_GetRxMessage(hcan, HAL_CAN_RX_FIFO0, rx_header, rx_data); if (rx_header.StdId 0x180) { // 收到发送端的帧翻转LED并回发确认帧 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); Send_Ack_Frame(); } } }注意一点回调函数的执行上下文是中断。在中断里不要做大量数据处理尽量只做标志位设置把真正的处理放到主循环里。为什么CAN波特率500kbps时一位2微秒一帧标准帧大约120位左右也就是一帧耗时约240微秒。如果中断里耗时太长下一帧到达时中断还挂着轻则影响实时性重则FIFO溢出丢帧。我见过有人在CAN回调里直接做浮点运算和字符串格式化结果系统卡得不像样。把接收标志置上主循环再处理业务逻辑才是稳妥做法。发送端如果想接收确认帧同样要配置好过滤器比如接收0x280并启动接收中断通知。这样两个板子就形成了一来一回的通路。3.4 关于C在CAN驱动中的应用虽然这个例子完全是C语言写的但标题里带了C我就多说几句。如果你是在FreeRTOS或者RT-Thread这类支持C的环境上做开发完全可以用类来封装CAN设备。比如定义CanDevice类成员方法包括Init()、Send()、RegisterRxCallback()内部封装HAL句柄和过滤器配置。业务层只需要维护好设备对象和回调函数指针代码复用性会好些。但有一点要提醒在STM32裸机工程里开C需要注意启动文件支持、全局构造函数调用这些细节工具链处理不好会踩很多坑。我的建议是如果系统复杂度不高一门C语言足够如果要做协议栈、多节点管理这类相对复杂的软件架构才考虑引入C。两者没有绝对的好坏只看团队维护能力和项目阶段。4. 实验调试过程与波形验证4.1 调试硬件准备调试CAN通信我建议至少准备这几样东西两个STM32开发板、两个TJA1050模块或者双CAN口开发板、一个USB转CAN分析仪比如兼容周立功的CANalyst-II或者国产的PCAN再加一个简单的数字示波器或者逻辑分析仪。没有CAN分析仪也能调通但效率会低很多。分析仪挂在总线上能实时看到总线上每一个帧的ID、数据内容、错误帧计数能帮你一眼判断出“是协议问题还是硬件问题”。示波器则用来观察CAN_H和CAN_L的差分波形可以直观判断收发器有没有工作、总线电平正不正常。如果实在没有分析仪把电脑串口和STM32 USB转串口接起来用串口打印错误标志也能凑合排查。4.2 测试步骤从单板自测到双板联调我调试时的顺序是先单板自测再双板联调不要一上来就把两个板子都烧录好然后望着空气发呆。单板自测的方法很简单CubeMX里把CAN工作模式选成Loopback发送一帧在接收回调里看能不能收到自己发的帧。Loopback模式下CAN1发出的帧会直接被自己接收过滤器收到不需要其他节点。如果Loopback能通说明CAN外设的发送路径和接收路径基本没问题接下来排查重点就只剩物理层和过滤器了。双板联调时我先把发送端程序只发送不接收接收端程序只接收不发用分析仪或者串口打印确认发送端帧“上总线”成功接收端能解析出正确的ID和数据。这一步通过后再启用回执帧功能。回执帧如果收不到问题往往出在回执帧的过滤器和发送优先级上优先级考虑一下ID设置回执帧的ID可以设得比数据帧高一点CAN仲裁ID越小优先级越高避免两个板子同时发数据时低优先级的帧持续被抢占。4.3 参数计算的验证假设你要在CAN分析仪上看到500kbps的波特率和正确的帧格式需要确认几个数值标准帧ID 0x180二进制是001 1000 0000DLC8对应数据段01 02 03 04 05 06 07 08如果你配置了DBE调试或者CAN错误主动帧尾还会带CRC、ACK slot等这些是协议自动完成的用示波器抓CAN_H对GND的波形会看到一串显性/隐性电平以2微秒为基本位时间。如果一位的宽度明显偏离2微秒比如变成了4微秒多半是预分频或BS1/BS2配置不对。我习惯直接把示波器时间轴调到10us/div看一个完整帧大约占据多少格标准帧8字节约为120位 × 2us 240us也就是24格。如果波形显示一帧很短很长基本可以断定波特率参数没生效。4.4 常见问题速查表现象可能原因排查方向HAL_CAN_AddTxMessage返回HAL_ERROR发送邮箱被占满或CAN总线关闭检查接收端是否正常接入波特率是否一致发送端一直报ACK错误总线上只有自己没有其他节点应答用分析仪看ACK slot确认物理层接线接收端收不到但发送端说成功过滤器ID/掩码配置错误把过滤器改成接收全部掩码全0测试有错误帧干扰终端电阻缺失、线太长、共地不良两端接120欧确认电源地连接通信偶尔中断恢复后正常总线干扰或位时序容限不足优化采样点位置改用屏蔽双绞线两个板子都发数据时相互冲突仲裁机制正常但优先级不满足业务给小ID数据帧更高优先权或分时发送4.5 一个典型的排查案例我在实验过程中遇到过这样一个问题发送端一直返回HAL_OK但接收端一个字节都收不到。用CAN分析仪挂上去发现总线上确实没有帧。这就奇怪了发送邮箱都空了为什么总线上没有波形后来查HAL库的代码发现HAL_CAN_AddTxMessage返回HAL_OK并不是指“发送成功”而是指“成功加入发送邮箱”什么时候真正发出、有没有发成功需要看邮箱状态是否转成CAN_TX_MAILBOX_EMPTY。我加了发送完成中断回调打印发送完成标志发现根本没进回调。这时候再查物理层发现TJA1050模块的CAN_TX和CAN_RX两个引脚从STM32侧被板载跳线帽短接成了回环状态等于控制器发给自己的回环数据根本没有驱动总线。把跳线帽拔掉正常接到收发器上问题解决。这个案例说明一个道理调试CAN这类带物理层协议栈的通信分层排查很重要。先确认控制器侧发送邮箱正常再确认收发器是否有差分输出最后确认接收端有没有正确解析逐层往下推进比盲目改代码高效得多。5. 避坑经验与工程化建议5.1 一定不要忘记的物理层检查项把CAN通信从“实验室能跑通”升级到“现场不容易出问题”我认为至少要检查下面几项第一总线两端必须各有一个120欧终端电阻。电阻放中间会导致反射信号在采样点附近来回震荡位错误率显著升高。第二CAN_H和CAN_L必须用双绞线不要用两根平行杜邦线凑合。双绞线能有效抑制共模干扰现场布线时哪怕多花几分钟也不要省这一步。第三所有节点的GND必须连在一起。CAN规范里虽然要求隔离但大多数低成本实验板没有隔离电路GND不共地总线共模电压会漂移严重时收发器会损坏。我自己用过最长的一根CAN线是三十多米的屏蔽双绞线500kbps下跑得很稳但前提是终端电阻良好、屏蔽层单端接地、波特率采样点设置在80%。如果你的应用会走长线建议实际测试一下误码率不要想当然。5.2 软件架构上的一点建议在两个板子的实验阶段代码怎么写都无所谓但一旦准备上多节点工程我建议在C语言层面做一层薄薄的驱动封装。比如typedef struct { uint8_t node_id; uint32_t base_id; uint16_t filter_mask; void (*rx_callback)(uint32_t id, uint8_t *data, uint8_t len); } CanNode_TypeDef;然后写一套CanNode_Init、CanNode_Send、CanNode_Process接口。这样每个节点只需要维护自己的CanNode_TypeDef实例应用层不需要关心底层是CAN1还是CAN2后续换芯片平台时只需要重写底层上层协议和应用逻辑不用动。如果你非要上C这套结构可以平滑改成纯虚接口加派生类但骨架思路是一致的。5.3 扩展路径从双板到多节点和CAN FD双板CAN通信跑通后值得做的扩展方向有三个。最简单的是增加节点数量三个四个板子并联在总线上每个板子分配一组ID范围测试仲裁机制是否按预期工作。再进一步是自定义应用层协议比如仿照J1939那样的“源地址PGN”模式或者参考CANopen的PDO/SDO模型把数据帧和命令帧区分开。如果板子支持CAN FD比如F103不行但G0/G4系列支持也可以体验一下64字节数据和更高波特率带来的吞吐量提升。不过我要泼一盆冷水CAN FD虽然好但需要总线上所有节点都支持FD格式。混合组网时FD帧和普通CAN帧的兼容处理是个大话题别一上来就全换。先把基础标准帧玩明白再谈演进。5.4 给新手的一句话总结我在这个双板CAN通讯项目里最大的体会就是CAN并不复杂真正容易出问题的地方往往在软件之外。协议本身是可靠的你只要把波特率算对、过滤器配对、物理层接对它就像一个很听话的管道。反过来如果这三个环节有一个出错现象就会表现为各种“玄学问题”有时能发有时不能发、距离一远就不行、干扰一来就断。别慌按物理层、数据链路层、应用层逐层拆问题一定能定位到。最后再分享一个实用小技巧在你第一次调CAN代码的时候别急着写业务逻辑。先用CubeMX生成一个空工程打开Loopback模式跑一个1秒周期发送、接收回调里翻转LED的测试程序。如果LED闪起来说明CAN外设这一条链路是通的。这个测试程序不要删以后怀疑问题出在CAN时先烧回去验证硬件能帮你快速区分是硬件还是业务代码的问题。本文还有配套的精品资源点击获取