资讯动态

STM32 CAN总线开发实战:从差分信号到收发过滤器的嵌入式通信指南

发布时间:2026/10/16 10:33:55 来源:尧图企业网站定制
做嵌入式这几年CAN 总线几乎是躲不开的一关。不管是做汽车电子、工业控制还是机器人、医疗器械只要设备之间需要“抗干扰、远距离、多节点、高可靠”地传数据CAN 总线的名字就会出现。我也见过不少做单片机的朋友用 USART、I2C、SPI 跟玩似的一碰到 CAN 就开始翻手册甚至有人直接放弃用硬件 CAN改用软件模拟——结果就是波特率不准、总线冲突没人管越搞越痛苦。这篇文章我尽量用“做过的人”的口吻把 CAN 总线从原理到 STM32 的具体落地讲清楚。你会知道 CAN 为什么能在两条线上挂几十个节点数据帧里到底装了什么STM32 的 bxCAN / FDCAN 怎么初始化、收发、过滤以及我在实际调试中踩过的那些坑。适合正在学 CAN、准备用 STM32 做 CAN 通信、或者被 CAN 总线问题折磨过的工程师参考。1. 先从最朴素的问题说起CAN 到底是什么1.1 一条双绞线如何承载整套网络CAN 的全称是 Controller Area Network控制器局域网络。它最早被设计出来的目的非常明确解决汽车里电子控制单元ECU之间通信线束爆炸的问题。早期汽车每个信号点一根线一个功能一个传感器一路信号到 ECU车上动辄几百上千根线又重又难检修。CAN 的极致思路就是“所有设备共用一对双绞线谁需要传数据谁就在上面发”。这对双绞线叫 CAN_H 和 CAN_L通信时两端各接一个 120Ω 终端电阻。数据以差分电压形式在线上传输抗共模干扰能力比单端信号强得多。通信距离和波特率成反比1 Mbps 下可靠距离大概 40 米降到 10 kbps 时能跑几公里。这个特点非常契合工业和车载场景——这两个场景的共同点是环境恶劣、线缆长、节点多。从模型上看CAN 属于 ISO/OSI 里的数据链路层加物理层但在实际工程里你不需要把它想得那么学术。你只需要记住三件事物理层负责把“0”和“1”变成总线上的电平数据链路层负责把数据打包成“帧”带 ID、CRC、应答而应用层协议比如 CANopen、J1939、UDS负责定义数据字节里每个位置的含义。这也是为什么很多人学 CAN 会觉得抽象它不像串口一样发一帧就是“一包裸字节”而是有一套完整的帧格式和错误处理机制。可一旦你理解了帧结构后面用 STM32 做收发就顺理成章了。1.2 为什么不是 RS-485 或 I2CCAN 的核心优势这个话题我几乎每次讲 CAN 都会被问到。很多人一听到差分信号、多节点就想到 RS-485一听到多主机就想到 I2C。那 CAN 到底比它们强在哪最核心的一点CAN 是“多主”的而且通过硬件仲裁解决了多节点同时发送时的冲突问题。RS-485 虽然也支持多点差分通信但它本身没有仲裁机制。如果两个节点同时发送数据总线直接冲突或产生逻辑错误。所以 RS-485 在实际工程里通常靠主机轮询、约定时间片或者额外的握手协议来控制发送时机这在实时性要求高的场合里很麻烦。I2C 虽然也有多主和仲裁但它有两个明显的短板一是开漏结构总线上必须有上拉电阻速率和距离都有限抗干扰也差二是它的仲裁主要靠时钟同步和逐位比较在长距离、强干扰的汽车环境下并不好用。I2C 面向的是板内设备通信CAN 面向的是设备间通信两者使用场景不同。CAN 还有一个被很多人忽略的优势非常完善的错误处理机制。CAN 协议里有五种错误检测方式还有错误主动、错误被动、总线关闭三种状态。某个节点如果持续发错误帧不会影响整条总线而 RS-485、I2C 一旦节点异常很容易把整个通信系统搞死。所以在“不可重来”的环境里CAN 的可靠性是非常有价值的。2. 物理层和协议层搞懂数据如何上车2.1 差分信号与总线电平CAN 物理层最常采用的是 ISO 11898-2 标准的高速 CAN它的两个电平分别是显性Dominant和隐性Recessive。总线空闲时CAN_H 和 CAN_L 都约为 2.5V差分电压接近 0V这是隐性逻辑 1。发送显性时CAN_H 被拉高到 3.5V 左右CAN_L 被拉低到 1.5V 左右差分电压约 2V逻辑 0。注意 CAN 的逻辑电平定义和常见串口相反显性 0隐性 1。这直接决定了仲裁时“谁优先”——看到后面你就知道。实际使用中我们一般不在单片机引脚上直接发 CAN 信号STM32 CAN 控制器输出的是 TTL 电平的 TX/RX 信号需要一颗 CAN 收发器芯片常见型号如 TJA1050、SN65HVD230 等把逻辑信号转换成 CAN_H/CAN_L 差分信号。收发器内部还做过 ESD 保护、共模抑制、斜率控制等处理可靠性高很多。接线上有一个非常容易忽略的点终端电阻。CAN 规范要求在总线物理两端各接一个 120Ω 电阻用来匹配传输线阻抗避免信号反射。很多初学者图省事不接结果短距离测试看不出大问题一拉长线或者节点一多就莫名其妙丢帧、总线错误。注意终端电阻不是“随便接一个 120Ω 就行”它必须接在总线的末端而且只需要两端各一个不是每个节点都接。2.2 帧格式数据帧、远程帧、错误帧、过载帧CAN 协议定义了四种帧类型数据帧、远程帧、错误帧、过载帧。我们日常说的“CAN 通信”绝大多数就是数据帧。一个标准数据帧长这样SOF帧起始1 个显性位表示一帧开始。仲裁段11 位标准 IDCAN 2.0A或 29 位扩展 IDCAN 2.0B后面跟着 RTR 位。标准帧中 IDE 为显性扩展帧中 IDE 为隐性。控制段IDE、DLC 或保留位加数据长度代码。DLC 为 4 位表示后面数据的字节数范围 0-8。数据段0-8 字节是真正的业务数据。CRC 段15 位 CRC 校验加一个 CRC 分隔位。ACK 段1 位 ACK Slot 和 1 位 ACK 分隔位。发送方在 ACK Slot 输出隐性位任何正确接收到该帧的节点会在此时拉低总线回一个显性位作为应答。这也是 CAN 的一个特点发送方知道“有没有人收到”。EOF帧结束7 个连续的隐性位。IFS帧间歇至少 3 个隐性位用来分隔上一帧和下一帧。我刚开始学的时候总觉得这些字段记不住后来用生活类比就清楚了SOF 相当于“开始说话”的前缀ID 相当于“这条消息的编号或优先级”DLC 是快递盒的尺寸数据是盒子里的东西CRC 是防篡改封条ACK 是收件人签字EOF 是说完之后的收尾。远程帧用得少它是用来请求其他节点发送数据的帧里没有数据段但带有目标 ID。错误帧和过载帧由硬件自动产生正常开发不用手动构造。你需要做的是识别错误状态并在代码里处理异常。2.3 仲裁机制为什么 CAN 不需要主机这是 CAN 最精妙的部分。所有节点都可以随时往总线上发数据那要是两个节点同时发送怎么办答案是CAN 用逐位仲裁来决定谁先发而且这个仲裁是“无损”的——赢了先发的节点完全不用重新开始。原理依赖两条规则一是显性比隐性优先二是每个节点发送时都在回读总线电平。假设节点 A 发送 ID 0x100节点 B 发送 ID 0x200它们同时开始发。当某一位 ID 出现第一个不同值时ID 较小数值小二进制里显性位靠前的节点先发的是显性 0ID 较大节点发的是隐性 1。ID 较大节点回读总线发现“我想发 1总线上却是 0”就知道自己在仲裁中输了立刻退出发送转为接收状态。整个过程在硬件里完成一帧不丢。所以 CAN 总线不设置主机所有节点地位对等但每一帧又自带优先级。这个设计让系统天然支持“突发消息”——紧急报文不需要等主机轮询抢到总线就能发。工程上大量使用这个特性比如碰撞传感器信号 ID 用小数值能立即抢占总线娱乐系统的非急报用大数值平时慢慢发也不影响。需要注意仲裁是对“整个仲裁段”进行的包括 ID、RTR 位甚至扩展帧里的 SRR、IDE 位。标准帧和扩展帧混用时标准帧通常优先因为 IDE 位显性。不同优先级帧混用也会引发判优所以设计 ID 时要慎重不要拍脑袋给几个节点分配随机 ID。2.4 位时序与采样点通信稳定的隐藏决定因素说到 CAN 通信质量很多人只关心波特率对不对却忽略了位时序。CAN 协议规定一“位”时间由四个段组成同步段SYNC_SEG、传播段PROP_SEG、相位缓冲段 1PHASE_SEG1、相位缓冲段 2PHASE_SEG2。同步段固定为 1 个时间量子TQ其他段长度可配置采样点通常在相位缓冲段 1 结束时。波特率计算公式很简单总线波特率 外设时钟频率 / (预分频值 × 每个位的时间量子数)每个位的时间量子数 1同步段 BS1 BS2其中 BS1 对应传播段加相位缓冲段 1BS2 对应相位缓冲段 2。采样点是通信稳定性的关键。CAN 规范推荐采样点在 75% 左右也就是一个位时间传输了四分之三后再采样。这样留出足够时间让信号稳定也留出相位缓冲空间来吸收时钟偏差。举个例子STM32 的 CAN 外设挂在 APB1 总线上如果 APB1 时钟是 36MHz我想配置 500kbps 波特率预分频取 4那么每个位的时间量子数就是 72 TQ实际即36MHz / (4 × 18) 500kbps如果把 BS1 设为 13 TQBS2 设为 4 TQ采样点就在 (113)/18 ≈ 78%。这是个工程上很顺手的选择。但实际上不同 MCU 的 CAN 外设对齐方式和寄存器配置略有不同。用 STM32 的 CubeMX 时你可以直接填目标波特率软件会自动算参数但你一定要去“CAN Parameter Settings”里检查“Sample Point”这个值不要让它变成 50% 或 90% 这种极端值否则高速通信时很容易出现偶发丢帧。3. STM32 的 CAN 外设怎么玩从初始化到收发3.1 选择正确的 MCUbxCAN 与 FDCAN 的区别在 STM32 家族里CAN 外设有两个主要版本bxCAN 和 FDCAN。bxCAN 是老一代 CAN 控制器常见于 STM32F1、F2、F3、F4 系列。它只支持 CAN 2.0A/B每帧最多 8 字节数据最大波特率 1Mbps。对于大部分传统车载、工业现场应用bxCAN 完全够用。FDCAN 是新一代控制器出现于 STM32G0、G4、H7 系列。它兼容传统的经典 CAN 帧同时支持 CAN FDCAN with Flexible Data-rate。CAN FD 有两个优势一帧最多 64 字节数据段波特率可以从仲裁段波特率切换到一个更高值意味着单帧能装更多数据、传更快。对需要固件升级、大数据量上传的场合非常有用。选型时我的建议是新项目如果对成本不敏感、供货没问题优先选带 FDCAN 的型号。原因很简单——向下兼容经典 CAN以后要跑 CANopen、J1939 都行数据吞吐量还有升级空间。如果你已经在用 F103那就老老实实练 bxCAN原理和配置思路完全通用。3.2 用 CubeMX 快速搭出最小工程拿 STM32F103 举例用 CubeMX 配置 CAN 的步骤大致如下在 System Core - RCC 里启用外部晶振。在 Clock Configuration 里确认 APB1 外设时钟。F103 的 APB1 最高是 36MHz如果你把系统时钟拉到 72MHzAPB1 要设置分频系数为 /2保证 CAN 外设时钟不超过 36MHz。在 Connectivity - CAN1 里勾选 Activated。CAN 参数页里Mode 选 Normal实际调试时可以先选 Loopback 回环模式。设置波特率、BS1、BS2、采样点这些值按你的目标速率填。打开 CAN1 的 NVIC 中断至少打开 RX0 和 RX1 消息挂起中断如果你要处理错误也可以打开错误中断。生成工程代码。这里有一个很多人新手栽跟头的地方CubeMX 生成的 CAN 初始化代码默认只会调用 HAL_CAN_Init并不会自动启动 CAN 外设也不自动配置过滤器。你还需要在 main 函数里做三件事配置过滤器、调用 HAL_CAN_Start、调用 HAL_CAN_ActivateNotification。很多人生成完工程直接往 CAN 发数据发现没反应其实就是漏了 HAL_CAN_Start。3.3 HAL 库发送和接收代码级的细节初始化完成后发送一帧数据的 HAL 代码大概是这样CAN_TxHeaderTypeDef txHeader; uint8_t txData[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; uint32_t mailbox; txHeader.ExtId 0; txHeader.IDE CAN_ID_STD; // 标准帧 txHeader.RTR CAN_RTR_DATA; // 数据帧 txHeader.DLC 8; // 8 字节 txHeader.TransmitGlobalTime DISABLE; if (HAL_CAN_AddTxMessage(hcan1, txHeader, txData, mailbox) ! HAL_OK) { // 发送失败检查总线状态 }HAL_CAN_AddTxMessage 里的第三个参数是数据指针第四个参数返回 mailbox 编号。CAN 控制器内部有 3 个发送邮箱这个概念类似银行的 3 个服务窗口你提交数据硬件自己在总线空闲时发出去。如果 3 个邮箱都满函数会返回 HAL_TIMEOUT。对应地接收时启用中断回调void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (hcan-Instance CAN1) { if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData) HAL_OK) { // 这里拿到 rxHeader.StdId 和 rxData交给上层处理 } } }看到没有HAL 库把硬件操作封装得已经很友好了核心逻辑就是用过滤器和中断接收。但我在实际项目里不止一次提醒自己HAL_CAN_GetRxMessage 放在回调里执行如果处理耗时太长会挤占中断时间。如果 CAN 报文量特别大建议在回调里只做“搬数据、置标志位”真正业务处理放到主循环或专门任务里。3.4 过滤器配置决定哪些帧进入你的 CPUCAN 的过滤器是一个非常实用的机制。它相当于门卫只有符合规则的帧才能进入接收 FIFO不需要的帧在硬件层面直接丢弃CPU 完全不用管。但很多初学者在处理过滤器时很崩溃因为配置寄存器的字段实在不直观。STM32 的 bxCAN 有 14 个过滤组F103 是 14 个每个过滤组可配置成列表模式或掩码模式。典型的需求是我只想接收 ID 为 0x123 的帧其他全丢掉。配置如下CAN_FilterTypeDef filter; filter.FilterBank 0; filter.FilterMode CAN_FILTER_MODE_IDMASK; // 掩码模式 filter.FilterScale CAN_FILTER_SCALE_32BIT; // 32 位宽度 filter.FilterIdHigh (0x123 5); // 标准 ID 对齐到高 16 位 filter.FilterIdLow 0; filter.FilterMaskIdHigh (0x7FF 5); // 掩码全 1完全匹配 ID filter.FilterMaskIdLow 0; filter.FilterFIFOAssignment CAN_FILTER_FIFO0; filter.FilterActivation CAN_FILTER_ENABLE; HAL_CAN_ConfigFilter(hcan1, filter);掩码为 1 的位置表示“必须匹配”为 0 的位置表示“不关心”。把掩码设成全 1就是精确匹配 ID。如果你想让同一组 ID 或一类 ID 都进来可以按业务调整掩码。例如只要高 3 位固定低 8 位不关心就能做到用一个过滤器拦下一段 ID 范围。配置过滤器时还要想清楚一件事过滤器数量有限节点多了或报文集复杂了ID 规划就要为过滤器服务。我见过不少项目一开始只想着“优先级”把 ID 设计得零散无规律最后发现过滤器不够用只能频繁修改报文矩阵。因此建议在系统设计阶段把 ID 做合理分段紧急报警一段、状态上报一段、命令控制一段这样过滤规则既简洁又高效。4. 实战调试用回环模式做第一次自测4.1 环回模式实验如果只有一块开发板没有第二块板子也没有 CAN 分析仪怎么确认 CAN 外设和收发器是好的答案是使用回环模式Loopback Mode。在这个模式下CAN 控制器发送帧后内部直接把自己发送的帧回路到接收邮箱不需要总线上的其他节点参与。环回模式是单片机的“自我对话”能非常方便地验证初始化和收发流程。CubeMX 里把 CAN 的 Mode 设为 Loopback生成代码后在 main 里加一段发送代码然后调试器里观察接收回调是否被触发。我自己的习惯是先在回环模式下跑 1000 帧记录丢帧率。正常情况下应该是零丢帧。如果出现“发送函数报错”或“接收回调没反应”优先级排查顺序是CAN 外设时钟没开或 APB1 分频不对。过滤器没配置或者配置错误帧没进 FIFO。HAL_CAN_Start 没调用。中断服务函数没使能或者 NVIC 被更高优先级风暴卡死。回环测试还有一个好处它能主动暴露波特率配置是否有基本错误。如果波特率差太多发送方可能因为 ACK 错误总线处于报错状态接收方也收不到完整帧。回环模式下发送方和接收方本来就是同一个外设波特率肯定一致所以这一步主要查代码流程查不通再接双节点。4.2 双节点通信终端电阻和波特率匹配回环模式跑通之后就可以做真正的双节点测通了。接线其实很简单把两块板的 CAN_H 接 CAN_HCAN_L 接 CAN_L然后在两个物理端各接一个 120Ω 终端电阻。注意是物理端不是每块板都接。如果只有两块板那就两块板处分别接 120Ω 电阻。双节点收发之前先做三件事确认两个开发板的 CAN 波特率配置完全一致。用示波器或逻辑分析仪看 CAN_H 和 CAN_L 的电平正常显性位能看到约 2V 的差分电压。在两块板之间测一遍“A 发、B 收”然后反过来“B 发、A 收”。这时候你可以观察到 ACK 机制的微妙之处在单节点发送、没有其他节点应答时发送节点会因 ACK Slot 收到隐性位而产生错误。所以测试双节点通信的起点很重要——先启动接收节点的 CAN再启动发送节点的 CAN发送节点才能得到 ACK 确认。我实际测试中最常遇到的问题是两个节点分别用不同厂家或不同系列的开发板CubeMX 生成的采样点不一样。表面上波特率都填 500k但一个采样点在 70%一个在 75%线长一点就丢帧。特别是波特率越高这种差异越明显。解决办法是手动固定 BS1、BS2保证两边采样点都接近 75%-80%。4.3 常见问题排查速查表下面这张表是我调试 CAN 时经常会看的也算是一个快速排障清单。故障现象可能原因检查方向HAL_CAN_AddTxMessage 返回超时三个发送邮箱都满或总线关闭检查总线是否短路、是否缺少终端电阻、是否有节点不停发错误帧发送数据能发出但接收回调不触发过滤器没配置对或中断没开先关过滤器试全收再逐步启用过滤确认 ID 和掩码收发器发烫总线短路或 CAN_H/CAN_L 差分接反断电检查线序测 CAN_H/CAN_L 对地电压短距离正常长距离丢帧终端电阻缺失或位置不对确保总线两端各有一个 120Ω 电阻两个节点波特率相同但偶发错误采样点差异统一 BS1、BS2采样点固定在 75%-80%一直收到错误帧总线存在非 CAN 信号干扰或某个节点故障用分析仪看错误帧类型隔离问题节点总线在 main 启动后立刻 Bus Off可能发送节点与总线无应答者先拉高隐藏线或先启动接收节点再做发送测试这张表不能替代标准 CAN 一致性测试但足够应付绝大多数实验室环境和原型机调试。真正要上车或上产线还是建议用专业 CAN 分析仪抓波形一台小时级别就能定位问题。5. 一些真正的经验教训5.1 CAN 总线常见的坑在我这个领域摸爬滚打了这些年CAN 总线相关项目踩过的坑真不少挑几个特别有代表性的说说。第一个是电源隔离问题。CAN 收发器在工业现场一旦和外部设备直连地电位差会造成较大的共模电压轻则数据错误重则烧掉收发器甚至 MCU。用带隔离的 CAN 收发器模块或者给收发器供电做隔离是工业项目的常见做法。别以为只是在实验室里短接一下没事现场地环路问题能把整个板子打挂。第二个是线缆和端子的选择。CAN 高速通信用的线材最好使用双绞线而且要带屏蔽更好。我见过有人用普通跳线飞了几十厘米结果差分信号衰减和串扰严重。双绞线不是玄学它是为了让两条线上感应的外界噪声尽量一致再靠接收端的差分放大把噪声抵消。第三个是软件初始化和启动顺序。STM32 的 CAN 外设初始化顺序有个容易被忽略的细节一定要先配置过滤器再启动接收再启动发送。如果反过来有时候你能收到第一帧但后续帧会因为 FIFO 溢出或邮箱管理问题丢失。这个顺序不是绝对的但按这个顺序做能少很多麻烦。第四个问题是中断优先级。CAN 接收中断的优先级如果设得不够合理会出现高优先级中断占死 CPU 的情况导致 FIFO 数据无法及时搬出来发生溢出。特别是你同时挂了 USB、定时器、外部中断时一定要想清楚每个中断的实时性要求。CAN 报文来了不及时读走硬件不会等你旧帧会被新帧覆盖丢数据是无声的这是最恐怖的。5.2 后续扩展CANopen、J1939、UDS当你把 CAN 底层的收发搞定之后一个真正的坎是应用层协议。CAN 协议只规定了“如何传帧”不规定“帧里每个字节代表什么”。不同行业有不同定义CANopen 多用于工业自动化、伺服驱动、医疗器械。它定义了对象字典、PDO/SDO、网络管理等节点可以直接配置成从站或主站。J1939 主要用于商用车和工程机械适合大范围传感器和动力系统节点通信。UDSISO 14229是车载诊断标准Bootloader 刷写、故障码读取都靠它。我的建议是如果你做的是工业设备直接学 CANopen别自己发明“一套简单协议”。自己定义协议虽然短期快但后期扩展、第三方接入都会难受。CANopen 的 PDO 非常适合周期控制和实时数据交互SDO 适合配置参数这都是现成且久经考验的设计。如果你做的是乘用车相关项目UDS 和车载以太网是绕不开的但 UDS 对协议栈实现要求高一般直接购买成熟协议栈更划算。自己做一套 UDS 要花的时间至少比你以为的多五倍。最后说一个实际心得不管用什么协议CAN 报文矩阵Signal Matrix一定要在设计阶段就写清楚。每个 ID 占用哪几个 Byte、每个 Bit 是什么含义、单位多少、偏移量多少都要提前定。协议这东西最怕“先上车后补票”等硬件打样出来再到处改 ID 和 DLC你会发现自己被整个项目牵着走。开发 CAN 项目几年下来我最深的体会是不要被协议和寄存器吓住CAN 的设计其实很优雅。真正把时间吃掉的是那些“看起来小实则致命”的细节——终端电阻、采样点、过滤器、ID 规划。把这些基础功夫练扎实CAN 总线会是你项目里最可靠的一环。如果你手头正好有 STM32 开发板和一块 CAN 收发器建议今天就照文章里的步骤跑一遍回环模式再用两块板子做一次双节点互发很多疑问会在动手过程中瞬间清晰起来。

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

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

免费获取报价 →
↑