STM32F103 CAN通信实战:从位定时计算到滤波器配置的避坑指南

📅 发布时间:2026/10/16 19:16:26
STM32F103 CAN通信实战:从位定时计算到滤波器配置的避坑指南
简介这份资源面向嵌入式开发初学者与STM32工程师聚焦STM32F103系列微控制器的CAN总线通信实现适用于汽车电子、工业自动化及物联网等需要多节点可靠通信的场景。压缩包共206个文件约4.73MB以C源码、头文件、编译中间文件及Keil工程配置为主涵盖CAN外设初始化、滤波器设置、收发函数与错误管理等核心模块并附带LCD、定时器、Flash、RCC等外设驱动便于在完整工程中理解CAN通信的集成方式。资源围绕CAN控制器与收发器的硬件连接、标准ID与扩展ID帧格式、波特率配置、中断收发及错误状态处理展开读者可据此掌握HAL_CAN_Transmit与HAL_CAN_GetRxMessage等关键接口的使用并参考工程目录结构快速搭建可运行的CAN通信实验。目前已有1909人学习下载适合需要从零实现STM32F103 CAN通信、排查总线异常或完善嵌入式通信方案的开发者参考。1. stm32f103can通信从点灯到总线一个让新手翻车的分水岭很多人第一次接触 stm32f103can通信都是被“CAN 比串口高级”这句话带进来的。我当年也是串口收发跑通了觉得总线不过如此结果接上收发器、写了两行发送代码示波器上连个毛刺都没有。问题不在代码在于 CAN 不是“你发我就收”的透明管道它是一套带仲裁、带位定时、带错误帧的差分总线协议。STM32F103 的 bxCAN 外设把协议层几乎全包了但前提是你得把 GPIO 重映射、波特率分频、滤波器、终端电阻这几件事同时做对。这篇笔记面向已经能跑通 GPIO 和串口、准备把两块 F103 板子用 CAN 连起来的工程师我会把标准库和 HAL 两条路都拆开讲参数怎么算、坑在哪、怎么用回环模式先自检全部落到可复现的命令和代码上。2. 先把物理层和位定时算清楚CAN 不通八成卡在这2.1 差分线、120 欧终端电阻和共地一个都不能少CAN 的物理层是差分信号CAN_H 和 CAN_L 之间的电压差决定显性位和隐性位。显性位逻辑 0压差约 2V隐性位逻辑 1压差约 0V。这意味着两件事第一总线上任意一个节点发显性位就能覆盖隐性位这是仲裁的物理基础第二如果 CAN_H 和 CAN_L 接反或者终端电阻缺失差分电压根本建立不起来收发器不会输出有效电平。我一般会先拿万用表量三样东西CAN_H 对 CAN_L 的静态电阻两个 120 欧并联后应该是 60 欧左右CAN_H 对地、CAN_L 对地的静态电压收发器正常工作时都在 2.5V 附近两块板子的 GND 必须连在一起否则共模电压漂到收发器承受范围之外通信时好时坏这种玄学问题最耗时间。终端电阻只在总线两端各放一个中间节点不要放。很多开发板已经把 120 欧焊在板上了如果你再外接一个总线负载变成 40 欧波形幅度被拉低短距离可能还能跑长距离直接丢帧。常见做法是两块板点对点通信两端各一个 120 欧三块以上只有物理最远的两端放。2.2 位定时参数怎么算以 500kbps 为例手算一遍STM32F103 的 bxCAN 挂在 APB1 上默认 36MHz。位定时由三段组成同步段SYNC_SEG固定 1Tq、时间段 1BS1、时间段 2BS2。采样点落在 BS1 结束处。波特率 1 / (Tq × (1 BS1 BS2))其中 Tq (BRP) / 36MHz。目标 500kbps周期 2us。取 BRP4则 Tq 4/36MHz ≈ 111.1ns。需要总 Tq 数 2us / 111.1ns ≈ 18。于是 1 BS1 BS2 18取 BS115BS22采样点 (115)/18 ≈ 88.9%这是 CAN 推荐的采样点位置75%~87.5% 之间偏上。对应到代码里标准库的配置如下// 标准库CAN 位定时配置APB136MHz目标 500kbps CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_TTCM DISABLE; // 关闭时间触发 CAN_InitStructure.CAN_ABOM ENABLE; // 自动离线恢复调试期建议开 CAN_InitStructure.CAN_AWUM DISABLE; // 自动唤醒关闭 CAN_InitStructure.CAN_NART DISABLE; // 禁止自动重传关闭失败会重发 CAN_InitStructure.CAN_RFLM DISABLE; // FIFO 不锁定溢出覆盖 CAN_InitStructure.CAN_TXFP DISABLE; // 发送优先级由标识符决定 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; // 同步跳转宽度 1Tq CAN_InitStructure.CAN_BS1 CAN_BS1_15tq; // 时间段1 15Tq CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; // 时间段2 2Tq CAN_InitStructure.CAN_Prescaler 4; // 分频系数 4 CAN_Init(CAN1, CAN_InitStructure);逻辑说明SJW 取 1Tq 是为了在时钟偏差不大时保持稳定如果两个节点晶振精度差可以加到 2Tq 或 3Tq。BS1 和 BS2 的比例决定了采样点BS1 越大采样点越靠后对长线缆的延迟容忍更好但太小会压缩相位缓冲。Prescaler 就是 BRP改这个值直接改波特率但改完必须重新算 BS1/BS2不能只动一个。HAL 库的写法更直观用CAN_Init里的Prescaler、TimeSeg1、TimeSeg2、SyncJumpWidth四个字段含义完全一样。注意 HAL 里TimeSeg1对应的是 BS1不包含同步段别多算一个 Tq。2.3 回环模式先自检别一上来就接总线新画的板子或者新写的驱动我强烈建议先跑回环模式Loopback。bxCAN 支持静默回环和正常回环回环模式下发送的帧不经过收发器直接在内部被接收用来验证滤波器、FIFO、中断配置是否正确。如果回环都收不到问题一定在软件回环能收接上总线收不到再去查物理层。// 回环模式初始化用于软件自检 CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack; CAN_Init(CAN1, CAN_InitStructure); // 发送一帧标准数据帧标识符 0x123数据长度 8 CanTxMsg TxMessage; TxMessage.StdId 0x123; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC 8; for (uint8_t i 0; i 8; i) { TxMessage.Data[i] i; // 填充 0~7 便于比对 } uint8_t mailbox CAN_Transmit(CAN1, TxMessage); // 等待发送完成超时退出避免死等 uint32_t timeout 0xFFFFF; while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok timeout--);这段代码里CAN_Transmit返回的是邮箱编号0~2不是成功失败。必须用CAN_TransmitStatus查状态或者等CAN_IT_TME中断。timeout递减是防止硬件异常时卡死实际项目里我会用定时器做超时。回环模式下如果接收中断能进且数据对得上说明 bxCAN 的收发路径是通的。3. 滤波器、FIFO 和中断让接收端只拿你想要的那几帧3.1 滤波器组怎么配掩码模式和列表模式的选择STM32F103 有 14 个滤波器组互联型是 28 个每个组可以配成 32 位或 16 位。32 位掩码模式最常用一个寄存器放标识符一个放掩码掩码位为 1 表示该位必须匹配为 0 表示不关心。比如我只想收标准帧 ID 0x123掩码全 1就是精确匹配如果想收 0x120~0x12F掩码低 4 位设 0其余设 1。// 滤波器 032 位掩码模式只接收标准帧 ID 0x123 CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x123 5; // 标准帧 ID 左移 5 位 CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0xFFFF; // 高 16 位全匹配 CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; // 低 16 位不关心 CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);关键点标准帧的 11 位 ID 在过滤器里是左对齐的CAN_FilterIdHigh的高 11 位才是 ID所以必须左移 5 位。这一点手册里写得清楚但很多人第一次配完发现收不到就是忘了移位。掩码的对应关系是FilterIdHigh和FilterMaskIdHigh逐位对应掩码为 1 的位参与比较。列表模式适合需要精确接收多个不连续 ID 的场景但会消耗更多滤波器组。如果 ID 数量少且固定列表模式更省 CPU如果是一段连续 ID掩码模式更划算。3.2 FIFO0 和 FIFO1 的分流策略bxCAN 有两个接收 FIFO每个 3 级深度。滤波器可以指定把匹配的帧丢进 FIFO0 还是 FIFO1。我一般把高优先级、需要快速响应的 ID 放 FIFO0用更高抢占优先级的中断处理低优先级、可以批量处理的放 FIFO1。这样避免低优先级帧把 FIFO 占满导致高优先级帧丢失。FIFO 溢出是个隐蔽的坑如果中断没及时取走第 4 帧进来时 FIFO 溢出CAN_RF0R的FOVR0位会置 1但默认不报错只是新帧被丢弃。调试期我会在中断里读一下溢出标志确认没有静默丢帧。// FIFO0 接收中断处理读取一帧并打印 void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 检查是否溢出溢出说明中断响应太慢 if (CAN_GetFlagStatus(CAN1, CAN_FLAG_FOV0) ! RESET) { CAN_ClearFlag(CAN1, CAN_FLAG_FOV0); } // RxMessage.StdId / Data[0..DLC-1] 可用 } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); }中断向量名取决于芯片型号和启动文件F103 通常是USB_LP_CAN1_RX0_IRQHandler。如果编译报未定义去启动文件里搜CAN1_RX0确认。CAN_Receive会同时清掉 FMP0 标志但保险起见再清一次中断挂起位。3.3 发送优先级和邮箱占用三个发送邮箱优先级由标识符数值决定数值越小优先级越高。如果三个邮箱都满CAN_Transmit会返回CAN_TxStatus_NoMailBox。我一般会在发送前检查返回值如果没邮箱就等一个发送完成中断再重试而不是死循环。// 带邮箱检查的发送函数 uint8_t CAN_SendStd(uint16_t std_id, uint8_t *data, uint8_t len) { CanTxMsg tx; tx.StdId std_id; tx.IDE CAN_Id_Standard; tx.RTR CAN_RTR_Data; tx.DLC len; for (uint8_t i 0; i len; i) tx.Data[i] data[i]; uint8_t mb CAN_Transmit(CAN1, tx); if (mb CAN_TxStatus_NoMailBox) { return 1; // 无空闲邮箱调用者稍后重试 } return 0; }参数说明len最大 8超过 8 会被截断或触发错误。标准帧 ID 范围 0x000~0x7FF扩展帧 0x00000000~0x1FFFFFFF。如果传了超出范围的值bxCAN 不会自动截断可能发出意外 ID所以调用前自己 clamp 一下。4. 避坑与排查CAN 不通时我按这个顺序查4.1 现象发送成功但接收端毫无反应原因最常见的是终端电阻缺失或 CAN_H/CAN_L 接反。其次是波特率不匹配两个节点一个 500k 一个 250k仲裁阶段就出错。还有一种情况是滤波器把所有帧都过滤掉了接收端 FIFO 始终为空。解决先用示波器看 CAN_H 和 CAN_L 的差分波形正常发送时应该有明显的显性位脉冲。如果没有波形查收发器供电和使能引脚有些收发器有 STB 或 EN 脚低电平才工作。如果有波形但接收不到把滤波器临时改成全通过掩码全 0确认是不是滤波问题。波特率用回环模式自测回环能通说明位定时算对了。4.2 现象通信一段时间后突然全部报错总线瘫痪原因某个节点进入错误被动或总线关闭状态持续发错误帧。常见诱因是某个节点晶振偏差太大或者总线某段接线松动导致反射严重。CAN_ABOM没开的话节点进入离线后不会自动恢复。解决初始化时把CAN_ABOM置 ENABLE让硬件在检测到 128 次连续 11 个隐性位后自动恢复。同时检查各节点晶振CAN 对时钟精度要求比串口高陶瓷谐振器往往不够要用晶振。总线分支尽量短星形拓扑在 CAN 里是大忌。4.3 现象回环模式正常一接总线就进不了接收中断原因GPIO 重映射没做或者重映射后没使能 AFIO 时钟。F103 的 CAN 默认引脚是 PA11/PA12重映射到 PB8/PB9 需要开 AFIO 时钟并配置GPIO_PinRemapConfig。另一个可能是收发器的 TX/RX 和 MCU 的 CAN_TX/CAN_RX 交叉接错TX 必须接 TXRX 接 RX不能像串口那样交叉。解决确认RCC_APB2Periph_AFIO时钟已开重映射配置在 GPIO 初始化之前。用万用表通断档查 MCU 的 CAN_TX 到收发器 TXD 是否直连。如果用的是带隔离的收发器模块还要确认隔离侧供电是否正常。4.4 现象中断里读到的数据长度对但内容错位原因CAN_Receive之后没有及时取走数据FIFO 里下一帧覆盖了当前帧的缓冲区。或者发送端 DLC 设了 8但实际只填了 4 个字节接收端按 8 字节读后 4 字节是上一次的残留。解决接收中断里先把数据拷到自己的缓冲区再清中断。发送端严格按实际数据长度设 DLC不要图省事固定 8。如果数据本身有结构体注意字节对齐和大小端CAN 传输的是原始字节流没有类型信息。4.5 现象两个节点单独通信正常三个以上就随机丢帧原因总线负载过高或者某个节点发送过于频繁仲裁失败后不断重试把总线占满。CAN 没有重传次数上限NART关闭时失败就无限重发低优先级帧可能永远发不出去。解决算一下总线负载率500kbps 下每帧约 108 位标准帧含填充如果每秒发 1000 帧负载约 21.6%还算安全超过 50% 就要考虑降频或分批。调试期可以临时开NART禁止自动重传让发送失败快速返回暴露问题而不是掩盖问题。5. 进阶用 CAN 做固件升级和双机冗余的实操要点CAN 除了传传感器数据还能做固件升级IAP和双机冗余。IAP 的思路是Bootloader 常驻通过 CAN 接收固件包写入 Flash 的应用区校验通过后跳转。关键点是 Flash 分页要和 F103 的页大小对齐中容量 1KB/页大容量 2KB/页擦除前先解锁写入时按半字16 位编程。// CAN IAP 写入一帧固件数据到 Flash // 假设应用区起始 0x08004000数据已按 2 字节对齐 uint8_t flash_write(uint32_t addr, uint8_t *data, uint16_t len) { FLASH_Unlock(); for (uint16_t i 0; i len; i 2) { uint16_t half data[i] | (data[i1] 8); if (FLASH_ProgramHalfWord(addr i, half) ! FLASH_COMPLETE) { FLASH_Lock(); return 1; // 写入失败 } } FLASH_Lock(); return 0; }参数说明addr必须是 2 的倍数且落在应用区范围内。写入前该页必须已擦除F103 的 Flash 只能把 1 写成 0不能把 0 写回 1。固件包建议加 CRC32 校验收完整个包再跳转避免半包变砖。跳转前关掉所有中断和外设设置 MSP 和复位向量。双机冗余则是两个节点互发心跳帧标识符固定周期比如 100ms。如果连续 3 个周期收不到对方心跳判定对方离线接管关键输出。这里要注意心跳帧的优先级要设高避免被大数据帧阻塞。我一般把心跳 ID 设成 0x000数值最小仲裁必赢。最后说一个我踩过的坑调试 CAN 时不要开着NART又开着回环模式跑压力测试发送失败不重传回环 FIFO 又满最后现象是“发送成功但接收不到”查了半天才发现是邮箱和 FIFO 同时溢出。现在的习惯是任何 CAN 工程先跑通回环再跑通点对点最后上多节点每一步都用示波器和溢出标志确认不跳步。希望帮到你。本文还有配套的精品资源点击获取