资讯动态

STM32F103 CAN实例代码:从回环到真实总线调试指南

发布时间:2026/10/14 18:31:22 来源:尧图企业网站定制
简介这份资源是面向嵌入式开发初学者与工程师的STM32F103 CAN通信实例代码包针对工业控制、汽车电子及物联网场景中多主总线通信的落地需求帮助读者从寄存器配置到库函数调用完整理解CAN外设的使用方式。压缩包共81个文件约280KB以30个h头文件与28个c源文件为核心配合12个s启动文件、uv2与opt等工程配置及dep依赖文件构成可直接编译的完整工程另附txt说明与doc文档辅助阅读。内容覆盖CAN控制器初始化、波特率与滤波器配置、发送接收邮箱操作、中断回调及错误处理等关键环节代码结构清晰便于对照HAL或标准外设库逐层调试。目前已有2872人学习下载适合作为CAN通信入门实践与项目移植的参考起点。1. 从零跑通 STM32F103 CAN 实例代码为什么你的回环测试通了接上收发器就哑了很多人第一次接触 STM32F103 CAN 实例代码都是从一个回环模式Loopback的例程开始的。代码烧进去发送计数正常接收中断也进了于是觉得“CAN 已经通了”。结果把两块板子接上 TJA1050 之类的收发器终端电阻也焊了代码却一声不吭——发送失败、总线错误、或者干脆卡在等待发送完成的循环里。这个现象太常见了常见到几乎每个做 STM32F103 CAN 的工程师都踩过。这篇笔记就围绕 STM32F103 的 CAN 外设把实例代码从最小可运行版本讲到能上真实总线的版本。我会先讲清楚 bxCAN 外设的过滤器、位时序、工作模式这几个关键概念再给出可以直接抄的初始化代码和收发代码然后重点讲回环能通、真实总线不通的排查路径。适合已经会点灯、会串口、想用 CAN 做多机通信的嵌入式开发者也适合手上有现成代码但调不通、想搞清楚每个参数含义的人。整篇代码基于标准外设库SPL风格寄存器操作和库函数调用都会标注清楚HAL 库用户也能对应迁移。2. bxCAN 外设的过滤器、位时序与工作模式三个决定成败的配置STM32F103 的 CAN 外设叫 bxCAN它不是一个简单的“发送寄存器接收寄存器”结构而是一个带过滤器组、发送邮箱、接收 FIFO 和位时序控制器的完整控制器。实例代码能不能跑起来八成取决于这三个东西配得对不对。2.1 过滤器组为什么你的接收中断永远不进bxCAN 有 14 个过滤器组互联型是 28 个F103 一般是 14 个每个组可以配置为屏蔽位模式或标识符列表模式可以关联到 FIFO0 或 FIFO1。很多例程为了省事直接把过滤器配置成“接收所有报文”但配置方式写错了结果就是接收中断死活不进。常见做法是过滤器组 0 配置为屏蔽位模式屏蔽寄存器全 0表示“所有位都不关心”也就是接收全部报文。代码如下// 过滤器初始化组0屏蔽位模式接收所有ID关联FIFO0 CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; // 屏蔽位模式 CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; // 32位宽 CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; // ID高16位 CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; // ID低16位 CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; // 屏蔽高16位全0表示不关心 CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; // 屏蔽低16位 CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);这里的关键是CAN_FilterMaskIdHigh和CAN_FilterMaskIdLow都设为 0。屏蔽位为 0 表示该位不参与匹配所以任何 ID 都能通过。如果你把屏蔽位设成 0xFFFF那就变成精确匹配只有 ID 完全等于CAN_FilterIdHigh/Low的报文才能进来。很多人抄代码时只改了 ID 没改屏蔽结果过滤器变成了“只收某个 ID”其他报文全被丢弃自然觉得“接收中断不进”。还有一个坑过滤器组 0 默认关联 FIFO0但如果你在别处又把 FIFO0 的溢出处理关掉了或者中断优先级配置冲突也会导致接收异常。建议实例代码里先把过滤器配成接收全部确认收发链路通了再逐步收紧过滤规则。2.2 位时序波特率不对什么都白搭CAN 是异步总线没有时钟线靠位时序来同步。STM32F103 的 CAN 位时序由CAN_BTR寄存器控制主要参数是BRP波特率预分频、TS1时间段1、TS2时间段2和SJW同步跳转宽度。波特率计算公式是BaudRate APB1_Clock / (BRP * (1 TS1 TS2))假设 APB1 时钟是 36MHz要配 500kbps常见的一组参数是BRP4TS115TS22SJW1。代入公式36M / (4 * (1152)) 36M / 72 500k。这组参数在采样点位置约 87.5%上也比较合理。代码里这样写CAN_InitTypeDef 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; // 接收FIFO不锁定 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_15tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 4; CAN_Init(CAN1, CAN_InitStructure);注意CAN_BS1_15tq和CAN_BS2_2tq是库里的宏分别对应 15 和 2 个时间单元。如果你用寄存器直接写要查手册确认编码值。位时序配错最直接的现象就是发送时进入错误被动或总线关闭状态CAN_GetLastErrorCode会返回位填充错误或格式错误。2.3 工作模式回环、静默、正常到底怎么选bxCAN 支持多种测试模式实例代码里最常用的是回环模式Loopback和正常模式Normal。回环模式下发送的报文不经过外部引脚直接在内部回送到接收 FIFO用来验证软件收发逻辑。静默模式Silent只接收不发送用来监听总线。正常模式才是真正接收发器上总线的模式。很多例程默认用回环模式因为不需要外部硬件就能跑通。但你要上真实总线必须改成CAN_Mode_Normal。改完之后如果收发器没接、终端电阻没焊、或者总线两端电平不对就会出问题。建议的调试顺序是先回环模式验证代码逻辑再静默模式确认能收到总线上的报文最后正常模式收发。3. 最小可运行实例初始化、发送、接收中断的完整代码这一章给出一个可以直接编译下载的实例代码框架基于标准外设库包含 GPIO、CAN 初始化和一个简单的收发测试。代码里会标注每个参数的作用方便你按自己的板子和总线速率修改。3.1 硬件连接与 GPIO 配置STM32F103 的 CAN 引脚是 PA11CAN_RX和 PA12CAN_TX需要配置成复用推挽输出和上拉输入。如果你用的是重映射引脚 PB8/PB9需要先开 AFIO 时钟并调用GPIO_PinRemapConfig。// 使能GPIOA和CAN1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; // CAN_RX: PA11 上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); // CAN_TX: PA12 复用推挽 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);这里 RX 配成上拉输入是因为 CAN 收发器的 RX 引脚通常是开漏输出需要上拉才能有确定电平。TX 配成复用推挽因为 CAN 控制器直接驱动这个引脚。如果 TX 配成普通推挽CAN 控制器无法控制它发送就废了。3.2 CAN 初始化与过滤器配置把第 2 章讲的位时序和过滤器配置整合起来形成一个完整的初始化函数。注意CAN_DeInit和CAN_StructInit可以先复位到默认值避免残留配置干扰。void CAN_Config(void) { CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_DeInit(CAN1); CAN_StructInit(CAN_InitStructure); // 位时序36MHz APB1500kbps 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; CAN_InitStructure.CAN_Mode CAN_Mode_Normal; // 上真实总线用Normal CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_15tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 4; CAN_Init(CAN1, CAN_InitStructure); // 过滤器接收所有报文 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_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure); // 使能FIFO0接收中断 CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); }CAN_ABOM设为 ENABLE 是自动离线管理当总线错误计数超过阈值进入离线状态后检测到 128 次 11 个隐性位就自动恢复。CAN_AWUM是自动唤醒配合睡眠模式用。CAN_NART关掉表示自动重传发送失败会一直重试直到成功或进入错误状态。这些参数在调试阶段建议先按上面配置等通了再按需调整。3.3 发送一帧标准数据帧发送函数需要先找一个空的发送邮箱然后填充标识符、数据长度和數據最后请求发送。下面这个函数发送一帧标准帧ID 和数据通过参数传入。uint8_t CAN_SendStdMsg(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; // 找一个空的发送邮箱 mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox CAN_TxStatus_NoMailBox) { return 1; // 没有空邮箱 } TxMessage.StdId id 0x7FF; // 标准帧ID是11位 TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len 0x0F; // 数据长度0~8 for (uint8_t i 0; i TxMessage.DLC; i) { TxMessage.Data[i] data[i]; } mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox CAN_TxStatus_NoMailBox) { return 1; } return 0; }注意这里调用了两次CAN_Transmit第一次其实只是为了检查邮箱但标准库的CAN_Transmit会直接占用邮箱并开始发送所以正确做法是先填充结构体再调用一次。上面代码里第一次调用是多余的实际应该直接填充后调用。修正后的写法是uint8_t CAN_SendStdMsg(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; TxMessage.StdId id 0x7FF; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len 0x0F; for (uint8_t i 0; i TxMessage.DLC; i) { TxMessage.Data[i] data[i]; } if (CAN_Transmit(CAN1, TxMessage) CAN_TxStatus_NoMailBox) { return 1; } return 0; }CAN_Transmit返回邮箱编号0~2表示发送请求已写入返回CAN_TxStatus_NoMailBox表示三个邮箱都满了。发送成功不代表总线上其他节点收到了只代表本节点把报文发出去了。要确认发送完成可以轮询CAN_TransmitStatus或者用发送中断。3.4 接收中断处理与数据读取接收中断里要把 FIFO 里的报文读出来释放 FIFO否则下一次中断可能因为 FIFO 满而丢失。标准库提供了CAN_Receive来读取。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, RxMessage.Data, RxMessage.DLC CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }中断向量名USB_LP_CAN1_RX0_IRQHandler是 F103 上 CAN1 的 RX0 中断入口不同型号可能略有差异查启动文件确认。CAN_Receive会从 FIFO0 取出最早的一帧并释放对应的邮箱。如果 FIFO 溢出CAN_IT_FMP0可能不会再次触发需要检查CAN_GetFlagStatus(CAN1, CAN_FLAG_FOV0)。4. 回环能通、真实总线不通五条血泪排查记录这一章专门讲实例代码从回环模式切到正常模式后接上收发器却不通的常见原因。每条都按“现象 → 原因 → 解决”写都是实际调试中反复遇到的。4.1 现象发送计数正常但总线上其他节点收不到原因CAN_TX 和 CAN_RX 引脚接反或者收发器的 TXD/RXD 与 MCU 的 PA12/PA11 交叉接错。很多收发器模块丝印标的是 TXD 和 RXD对应的是收发器侧连接到 MCU 时要 TXD 接 PA12CAN_TXRXD 接 PA11CAN_RX。如果接成 TXD 接 PA11RXD 接 PA12回环模式能通因为内部回送正常模式就废了。解决用万用表确认 PA12 到收发器 TXD 的连通性PA11 到 RXD 的连通性。如果不确定先只接一块板子用示波器看 PA12 在发送时有没有波形。没有波形说明 GPIO 配置或 CAN 模式不对。4.2 现象总线错误计数飙升进入错误被动或总线关闭原因终端电阻没接或阻值不对。CAN 总线两端各需要一个 120 欧姆终端电阻总阻值约 60 欧姆。如果两个节点都接了 120 欧姆并联后是 60 欧姆正常。如果一个都没接或者只接了一个信号反射会导致位错误。解决断电测量 CANH 和 CANL 之间的电阻正常应该是 60 欧姆左右两个 120 欧姆并联。如果测到 120 欧姆说明只接了一个终端电阻如果测到无穷大说明一个都没接。补上终端电阻再试。4.3 现象回环模式收发正常正常模式发送失败原因波特率配置不一致。回环模式下不经过真实总线位时序错误不会暴露。正常模式下如果两个节点的波特率差太多采样点对不上就会出错。常见的是 APB1 时钟不是 36MHz比如用了外部晶振但系统时钟没配到 72MHzAPB1 就不是 36MHz按 36MHz 算的 BRP 就错了。解决用示波器测 PA12 上发送一位的时间或者用 CAN 分析仪看实际波特率。确认RCC_GetClocksFreq得到的 PCLK1 频率重新计算 BRP。如果 PCLK1 是 36MHz500kbps 用 BRP4如果是 24MHzBRP 要改成 3 左右24M/(3*18)444k接近但不精确需要调整 TS1/TS2。4.4 现象接收中断偶尔进偶尔不进数据丢失原因FIFO 溢出。接收中断处理太慢或者中断优先级被其他高优先级中断打断导致 FIFO0 的三个邮箱满了之后新报文被丢弃。CAN_RFLM如果设为 ENABLEFIFO 锁定新报文不会覆盖旧报文但会丢失如果设为 DISABLE新报文覆盖旧报文也会丢数据。解决提高 CAN 接收中断优先级缩短中断处理时间把数据处理放到主循环或队列里。如果数据量大考虑用 FIFO1 分担或者把CAN_RFLM设为 DISABLE 并接受覆盖策略。另外检查CAN_ITConfig是否只开了 FMP0没开 FOV0溢出中断导致溢出时没有提示。4.5 现象两块板子单独测试都正常接在一起就互相干扰原因共地问题。CAN 收发器需要共地如果两块板子用不同的电源供电地电位差可能导致收发器损坏或通信异常。另外如果总线走线太长、分支太多信号完整性也会变差。解决确保所有节点共地用双绞线连接 CANH 和 CANL总线两端接终端电阻分支线尽量短小于 0.3 米。如果距离超过 40 米降低波特率到 125k 或更低。用示波器看 CANH 和 CANL 的差分波形正常应该有明显的显性和隐性电平切换。5. 进阶技巧用 CAN 分析仪验证时序与过滤器以及一个发送超时的后悔药实例代码跑通之后下一步是让它稳定可靠。这一章讲两个具体技巧用 CAN 分析仪验证位时序和过滤器配置以及给发送函数加超时重试避免卡死在等待发送完成的循环里。5.1 用 CAN 分析仪看位时序和过滤器是否生效CAN 分析仪比如常见的 USB-CAN 盒子能直接看到总线上的报文、波特率和错误帧。把分析仪接到总线上设置成和你的节点一样的波特率然后让节点发送一帧已知 ID 和数据。如果分析仪能收到说明位时序和硬件连接没问题。如果收不到分析仪会显示错误帧或者干脆没数据。更细一点可以看分析仪上的采样点位置。大多数分析仪会显示波特率、采样点百分比。500kbps 下采样点建议在 75%~87.5% 之间。如果你的采样点偏离太多长距离通信时容易出错。调整CAN_BS1和CAN_BS2可以改变采样点位置公式是采样点 (1 BS1) / (1 BS1 BS2)比如 BS115BS22采样点 16/18 ≈ 88.9%。如果改成 BS113BS24采样点 14/18 ≈ 77.8%。根据总线长度和节点数选一个合适的值。过滤器是否生效可以通过分析仪发送不同 ID 的报文看你的节点是否只接收预期的 ID。如果过滤器配成接收所有那所有 ID 都应该进中断如果配成只收某个 ID其他 ID 应该被丢弃。用分析仪发几个不同 ID观察接收中断是否触发就能确认过滤器配置对不对。5.2 给发送函数加超时避免卡死在等待循环很多实例代码在发送后这样等CAN_Transmit(CAN1, TxMessage); while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok);如果总线故障、没有其他节点应答、或者终端电阻不对这个循环会一直等下去程序卡死。血泪经验是任何等待硬件的循环都要加超时。改成这样uint8_t CAN_SendStdMsgWithTimeout(uint32_t id, uint8_t *data, uint8_t len, uint32_t timeout) { CanTxMsg TxMessage; uint8_t mailbox; TxMessage.StdId id 0x7FF; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len 0x0F; for (uint8_t i 0; i TxMessage.DLC; i) { TxMessage.Data[i] data[i]; } mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox CAN_TxStatus_NoMailBox) { return 1; } while (timeout--) { if (CAN_TransmitStatus(CAN1, mailbox) CAN_TxStatus_Ok) { return 0; } } CAN_CancelTransmit(CAN1, mailbox); // 超时取消发送 return 2; }timeout可以用简单的循环计数也可以用 SysTick 计时。超时后调用CAN_CancelTransmit取消邮箱里的发送请求避免邮箱一直被占用。这个后悔药在调试阶段特别有用能防止一块板子故障导致整个程序卡死。5.3 中断优先级与 FIFO 溢出的取舍CAN 接收中断的优先级要设得比普通串口、定时器中断高但不要设成最高以免影响系统其他关键中断。在 NVIC 里配置NVIC_InitTypeDef NVIC_InitStructure; 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);抢占优先级 1 表示可以被优先级 0 的中断打断但比优先级 2、3 的高。如果系统里有多个 CAN 节点或者高频率报文建议把接收中断优先级再提高一点同时把数据处理逻辑尽量简化只做“读 FIFO、存队列、清中断”三件事复杂处理放到主循环。FIFO 溢出是实际项目里最容易翻车的地方之一。如果总线负载率高接收中断又不够快FIFO0 三个邮箱很快满。除了提高优先级还可以把过滤器分配到 FIFO0 和 FIFO1 两个 FIFO分担压力。另外CAN_RFLM设为 DISABLE 时新报文会覆盖旧报文虽然丢数据但不会阻塞设为 ENABLE 时FIFO 满后新报文直接丢弃但旧数据保留。根据业务需求选。我自己的习惯是调试阶段先把CAN_RFLM设为 DISABLE配合溢出中断CAN_IT_FOV0一旦溢出就记录错误计数方便定位。等稳定了再根据实际负载决定是否锁定 FIFO。这个习惯帮我省了很多“数据莫名其妙丢了”的排查时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑