资讯详情

STM32F10x CAN通信全链路:硬件、位时序、过滤器与调试

📅 2026/10/31 12:49:11 | 华诺云谱 👁 阅读
STM32F10x CAN通信全链路:硬件、位时序、过滤器与调试
CAN 这个总线很多做嵌入式的人都是先听说、后接触真到自己上手写 STM32F10x 的驱动时往往卡在几个很实在的地方波特率算出来对不上、示波器上波形有了但就是收不到数据、接上第二个节点总线直接瘫痪。我前后在三个不同形态的项目里用过 STM32F103 的片上 CAN从最早的照抄例程到后来能自己写一套稳定的收发封装中间踩的坑比想象中多。这篇就把 STM32F10x 上实现 CAN 通信的完整链路拆开讲硬件怎么接、位时序怎么算、bxCAN 外设怎么配、过滤器怎么写、收发怎么封装、现场出问题怎么查。适合刚接手 CAN 相关模块、手里有块 F103 开发板和两个收发器想跑通的开发者也适合已经能发数据但总线时不时报错、想弄明白背后原因的人。1. 先把问题定准CAN 在 F10x 项目里到底扮演什么角色1.1 为什么很多项目最终都会落到 CAN 上做过多节点设备的人应该都有体会UART 点对点、I2C 板内短距、SPI 高速板内这三种在“多个板子相距几米、需要互相知道对方在不在线”的场景下都不太顺手。UART 要自己定帧头帧尾和校验I2C 一出板就容易被干扰SPI 更是没法拉长。CAN 恰好补上了这块差分传输抗共模干扰、硬件自动仲裁、硬件自动重发、每个节点都能听到总线上的所有帧还带错误计数和自动离线机制。这几点落到实际项目里价值非常直接。硬件仲裁意味着你不用在软件里排队抢总线ID 小的自然优先实时性有保障硬件重发意味着偶发的位错误不需要应用层干预错误计数和离线机制意味着某个节点线路出问题时会自己退出总线不会把整条总线拖死。所以像车载电子、工业控制、电机驱动、电池管理这类“多节点 强实时 环境恶劣”的场合CAN 几乎是默认选项。STM32F10x 系列的一个便利之处是中容量以上的型号基本都带了 bxCAN 外设不需要外挂控制器只要加一颗收发器就能上总线。对成本敏感的产品来说这是很实在的优势。1.2 bxCAN 外设的能力边界先摸清楚再动手动手写代码之前先明确这颗外设给了什么、没给什么能省掉很多“以为是软件问题其实是硬件限制”的排查时间。F10x 上的 bxCAN 支持 CAN 2.0A 和 2.0B 协议标准帧 11 位 ID、扩展帧 29 位 ID 都能收发数据场最长 8 字节。收发邮箱是硬件管理的3 个发送邮箱、2 个接收 FIFO每个 3 级深度。过滤器组在 F103 上一般是 14 组互联型更多可以配成屏蔽位模式或标识符列表模式位宽 32 位或 16 位。这里有几个容易忽略的边界。第一接收 FIFO 只有 3 级如果中断响应不及时或者主循环卡顿第 4 帧进来就会溢出溢出标志会置位那一帧直接丢掉——所以要么提高中断优先级要么在中断里只做搬运、把帧塞进软件队列。第二发送邮箱只有 3 个连续快发时会返回“无空邮箱”必须处理这个返回值不能假设每次都能塞进去。第三过滤器组的数量有限如果项目里要收的 ID 很多得规划好哪些用列表模式精确匹配、哪些用掩码模式批量放行。还有一点bxCAN 不支持 CAN FD这点在做新项目评估时要提前确认别做到一半发现对端用的是 FD 帧。协议版本这种硬边界越早确认越省事。2. 硬件这一关躲不掉收发器、终端电阻和地2.1 收发器怎么挑三个参数就够筛片上 CAN 外设输出的是逻辑电平的 TX/RX 信号真正上总线需要一颗收发器做电平转换和差分驱动。市面上常见的 8 脚收发器型号很多选型时我一般就看三个参数。第一个是供电电压和 IO 电平。有些经典款是 5V 供电RXD 输出电平跟着 VCC 走也有 3.3V 供电的型号。STM32F103 的 PA11/PA12 是 5V 容忍引脚理论上直连 5V 输出的 RXD 不会烧但这属于“能用但不推荐”的做法长期可靠性没有保障。更稳妥的是选 3.3V 供电的收发器或者用 5V 供电但带独立 IO 电平引脚的型号把 IO 侧接到 3.3V。第二个是速率等级。普通款标称 1Mbps高速款能到 5Mbps 甚至更高。项目里跑 500k 以下的话普通款完全够用没必要为用不上的带宽多花钱。第三个是有没有待机/静默引脚。带待机脚的型号可以在低功耗场景下把收发器切到睡眠靠总线活动唤醒带静默脚的可以只收不发做监听节点时很有用。如果产品有低功耗要求这个引脚值得单独引出来接到 MCU 上。顺带说一个实操细节收发器芯片的散热和布局别太随意。它驱动差分线时瞬态电流不小电源脚旁边一定要有 0.1uF 的去耦电容地线走宽一点。我见过因为去耦电容省了导致高负载下误码率飙升的情况查了半天软件才发现是电源纹波。2.2 120Ω 终端电阻只有两端多一个都不行这条几乎是 CAN 的铁律但每年还是有人栽在上面。CAN 总线是差分线特征阻抗 120Ω需要在总线的物理两端各接一个 120Ω 电阻做阻抗匹配中间节点一个都不加。两个 120Ω 并联后用万用表在断电状态下量 CANH 和 CANL 之间应该读到 60Ω 左右。这个 60Ω 是排查硬件最快的判据上电前先量一下能省掉后面一大截时间。加多了会怎样每多一个 120Ω并联阻抗就往下掉驱动器负载加重信号幅度变小边沿变缓短距离可能还能凑合线一长就开始出错误帧。加少了或者两端都没加会怎样信号在末端反射波形上出现振铃和过冲采样点落在振铃区就会误判位值表现为随机错误帧、错误计数器缓慢上涨。还有一个经常被忽略的问题支线长度。从主干线分出来接到节点的那一小段叫支线stub在 500kbps 下支线建议控制在 0.3m 以内250kbps 下可以放宽到 1m 左右。很多人把节点用很长的排线单独引出去主干是双绞线、支线是杜邦线结果就是通信时好时坏。调试阶段用杜邦线搭 CAN 是最容易出问题的做法能焊就焊能压端子就压端子。2.3 引脚、重映射与电平兼容STM32F103 上 CAN1 的默认引脚是 PA11CAN_RX和 PA12CAN_TX。这里有个容易搞反的点很多资料写的是“PA11 TX、PA12 RX”实际上是反过来的RX 在前。接线时 MCU 的 CAN_TX 接收发器的 TXDMCU 的 CAN_RX 接收发器的 RXD交叉接不要同名对接。如果 PA11/PA12 被 USB 占用了这在 F103 上非常常见USB DM/DP 就是这两根就得做引脚重映射。把 CAN1 重映射到 PB8RX/PB9TX需要先使能 AFIO 时钟再调用重映射配置。注意不同封装和型号的重映射分组可能不一样动手前翻一下对应型号的参考手册引脚定义表别照抄别的型号的代码。GPIO 的配置模式也有讲究。CAN_TX 是复用推挽输出配成GPIO_Mode_AF_PPCAN_RX 是输入配成GPIO_Mode_IPU上拉输入。RX 配上拉是有意义的总线空闲或收发器未上电时RX 线不会被拉低成持续的显性电平避免误判。注意如果板上同时有 USB 和 CAN规划引脚时优先把 USB 留给 PA11/PA12CAN 走重映射。反过来操作会导致 USB 无法使用而且这个错误在硬件打样后才发现的话改板成本很高。3. 位时序与波特率算错一步全线不通3.1 一个位到底由几段拼起来CAN 的每一个位时间被切成若干个时间份额tq再分成四段同步段SYNC_SEG固定 1tq、传播段PROP_SEG、相位缓冲段 1BS1包含 PROP_SEG、相位缓冲段 2BS2。在 STM32 的寄存器模型里PROP_SEG 和 Phase_Seg1 被合并成一个字段 BS1所以配置时只需要填三个东西预分频Prescaler、BS1、BS2。波特率的计算公式是波特率 APB1时钟 / (Prescaler × (1 BS1 BS2))这里的 1 就是固定的 SYNC_SEG。注意分母里的 tq 总数等于 1 BS1 BS2而不是 BS1 BS2。APB1 时钟是关键前提。F103 在 72MHz 主频下APB1 默认是 36MHz二分频。如果有人把系统时钟改成 56MHz 或者做了别的分频CAN 的输入时钟跟着变波特率就全错了。所以算波特率的第一步永远是确认当前工程的 APB1 实际频率不是想当然地按 36MHz 算。举个具体例子目标 500kbps36MHz / 500kbps 72所以 Prescaler × (1 BS1 BS2) 72取 Prescaler 4则 1 BS1 BS2 18即 BS1 BS2 17想要 75% 到 80% 的采样点采样点位置 (1 BS1) / 18取 BS1 13则 14/18 ≈ 77.8%BS2 4最终Prescaler4BS113tqBS24tqSJW 取 1tq在标准外设库里这些值用宏表示CAN_SJW_1tq、CAN_BS1_13tq、CAN_BS2_4tq宏内部已经处理了“寄存器值 实际 tq 数 - 1”的换算直接填宏名就行不要自己减一。3.2 常用波特率参数速查把常用档位一次性算好列出来实际项目里直接查表能省不少事。下表按 APB1 36MHz 计算波特率PrescalerBS1BS2总tq采样点SJW1Mbps462977.8%1tq800kbps562977.8%1tq500kbps41341877.8%1tq500kbps952875.0%1tq250kbps1852875.0%1tq125kbps3652875.0%1tq100kbps4552875.0%1tq50kbps451141675.0%1tq20kbps602273076.7%1tq10kbps1002873680.6%1tq800kbps 那一行验证一下36M / 5 / 9 800k对的。500kbps 给两行是因为这两种组合都常用前者 tq 多、分辨率细后者预分频大、对时钟抖动不那么敏感长线缆场景我更倾向用 18tq 那组。如果 APB1 不是 36MHz比如做了 48MHz 的配置那就得重算。快速心算法先算APB1 / 目标波特率得到总除数 N然后在 N 的因数里挑一个当 Prescaler剩下的当 tq 总数再按采样点比例拆成 BS1 和 BS2。3.3 采样点、SJW 与长线缆的取舍采样点位置这件事很多入门资料一句“80% 左右”就带过了但它在实际工程里是会真实影响通信质量的。信号从发送端出发经过收发器延时、线缆传播延时、接收器延时才到达采样比较器。线缆越长传播延时越大采样点就必须往后放否则采到的是上一个位还没稳定的电平。经验值是短总线几米以内75% 到 80% 都无所谓几十米的中长线建议往 80% 到 87.5% 靠更长的线缆则要按传播延时反算必要时把波特率降下来。反过来采样点也不能太靠后太靠后留给 BS2 的时间就短了对时钟偏差的容忍度下降。SJW重同步跳变宽度的作用是允许节点在检测到边沿时微调位时间补偿各节点晶振之间的频率偏差。一般取 1tq 就够晶振精度差或者总线上节点多的时候可以取到 2tq 或 3tq但 SJW 不能超过 BS2 的长度。这里有个隐性成本节点用的晶振精度。用内部 RC 振荡器HSI做系统时钟再分频给 CAN温漂和批次差异可能到百分之几跑 500k 时很容易出问题。我一般要求 CAN 项目必须用外部晶振哪怕是无源晶振成本也就几毛钱比后期查通信不稳定省事太多。4. 代码落地初始化、过滤器、收发闭环4.1 时钟与 GPIO初始化顺序上有个硬性要求必须先开时钟再配外设。CAN1 挂在 APB1 上用RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE)GPIO 挂 APB2用RCC_APB2PeriphClockCmd。如果做了重映射AFIO 时钟也要开。static void CAN_GPIO_Config(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); /* CAN_RX: PA11, 上拉输入 */ gpio.GPIO_Pin GPIO_Pin_11; gpio.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, gpio); /* CAN_TX: PA12, 复用推挽 */ gpio.GPIO_Pin GPIO_Pin_12; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); }这段看着简单但两个细节值得说。一是 GPIO_Speed 别配太低CAN_TX 在 1Mbps 下的边沿变化不慢2MHz 档位虽然能用但裕量小直接给 50MHz 最省心。二是 RX 配成 IPU 而不是 IN_FLOATING原因前面提过收发器没上电或者线断了的时候浮空输入会读到随机电平可能让 CAN 外设一直认为总线在活动进而影响进入初始化模式。4.2 CAN 初始化逐字段拆CAN 外设的初始化结构体字段不算多但每个都对应一个实际行为糊里糊涂全填 Disable 也能跑但出了问题就不知道该调哪个。static void CAN_Mode_Config(void) { CAN_InitTypeDef init; CAN_DeInit(CAN1); CAN_StructInit(init); init.CAN_TTCM DISABLE; /* 时间触发通信模式, 一般不用 */ init.CAN_ABOM ENABLE; /* 自动离线管理, 强烈建议开 */ init.CAN_AWUM DISABLE; /* 自动唤醒, 低功耗场景再开 */ init.CAN_NART DISABLE; /* 关闭自动重传 不重传, 这里保持重传 */ init.CAN_RFLM DISABLE; /* 接收FIFO不锁定, 溢出时覆盖最旧帧 */ init.CAN_TXFP DISABLE; /* 发送按ID优先级, 置1则按请求顺序 */ init.CAN_Mode CAN_Mode_Normal; init.CAN_SJW CAN_SJW_1tq; init.CAN_BS1 CAN_BS1_13tq; init.CAN_BS2 CAN_BS2_4tq; init.CAN_Prescaler 4; /* 36M / 4 / 18 500kbps */ CAN_Init(CAN1, init); }几个字段单独解释。CAN_ABOM置 ENABLE 之后节点进入离线状态后只要检测到总线上连续 128 次 11 个隐性位硬件会自动恢复不需要软件干预。这个功能对于现场设备非常重要某次线束松动导致离线松动能自己恢复的话设备就自愈了不用派人现场断电重启。CAN_NART是“禁止自动重传”名字有点绕。置 DISABLE 表示允许自动重传这是默认行为也是绝大多数项目的正确选择。只有做单向广播、丢一帧无所谓、又极度在意实时性的场景才会置 ENABLE 关掉重传。CAN_RFLM是接收 FIFO 锁定模式。置 DISABLE 时 FIFO 满了新帧会覆盖最旧的置 ENABLE 时满了就丢新帧。我一般保持 DISABLE因为对控制类应用来说最新的数据往往比最旧的数据有价值。CAN_TXFP决定发送顺序。置 DISABLE 时三个邮箱里的报文按 ID 优先级发送置 ENABLE 时按请求的先后顺序发。如果你的协议里有“先发配置再发指令”这种顺序依赖就得置 ENABLE否则 ID 小的配置帧会插到前面去执行。还有一个必须知道的点CAN_Init函数内部会先请求进入初始化模式等硬件置位 INAK配置完再退出并等 INAK 清零。这个过程里如果 CAN 时钟没使能函数会卡在等待循环里出不来表现为程序死在初始化。所以排查“上电就卡住”的问题时第一个要看的就是 CAN 时钟到底有没有开。4.3 过滤器把总线噪音挡在外面过滤器是 bxCAN 里最容易被跳过、也最容易配错的部分。很多人图省事把过滤器配成全部放行结果总线上所有帧都进 FIFO中断频率高得离谱主循环被拖慢。正确做法是按需要精确放行。过滤器有两种模式屏蔽位模式IdMask和标识符列表模式IdList。屏蔽位模式是“ID 掩码”掩码位为 1 表示该位必须匹配为 0 表示不关心适合按段过滤列表模式是把两个32 位或四个16 位ID 列进去只有完全相等才放行适合处理少量固定 ID。位宽也有两种。32 位模式一个过滤器组管一个 ID16 位模式一个组能管两个 ID但只能处理标准帧。ID 少的时候用 16 位模式更省资源。static void CAN_Filter_Config(void) { CAN_FilterInitTypeDef f; /* 只接收标准帧 ID 0x120 ~ 0x12F 这一段, 进 FIFO0 */ f.CAN_FilterNumber 0; f.CAN_FilterMode CAN_FilterMode_IdMask; f.CAN_FilterScale CAN_FilterScale_32bit; /* 标准ID在寄存器里左移5位对齐到 [31:21] */ f.CAN_FilterIdHigh (0x120 5) 0xFFFF; f.CAN_FilterIdLow 0x0000; /* 掩码: 低4位不关心, 其余匹配; IDE位和RTR位也要固定 */ f.CAN_FilterMaskIdHigh ((0x7F0 5) | 0x0010) 0xFFFF; f.CAN_FilterMaskIdLow 0x0000; f.CAN_FilterFIFOAssignment CAN_FilterFIFO0; f.CAN_FilterActivation ENABLE; CAN_FilterInit(f); }这段代码里有两个容易出错的点值得展开说。第一个是 ID 对齐。bxCAN 的过滤器寄存器是 32 位标准帧的 11 位 ID 存在 bit21 到 bit31所以写 ID 的时候要左移 5 位。扩展帧的 29 位 ID 则存在 bit3 到 bit31左移 3 位。这一步漏了的话过滤器匹配的 ID 和你以为的完全对不上现象是“发是发出去了但对面就是收不到”。第二个是掩码里要带上 IDE 位和 RTR 位。上面代码里掩码的 0x0010 对应的是 IDE 位bit4置 1 表示必须匹配“标准帧”。如果不加这一位扩展帧也会被放进来收帧解析的时候就得多做一层判断。同理 RTR 位bit1也可以固定成数据帧。调试阶段有个很实用的技巧先用一个全放行的过滤器确认底层通了再逐步收紧。全放行的配置是 ID 全 0、掩码全 0这样任何帧都能进。等收发链路验证完毕再换成精确过滤这样能把“过滤问题”和“物理层问题”分开排查不会一上来就被两个问题同时干扰。4.4 发送邮箱、超时与重传发送接口本身不复杂难的是把返回值都处理完整。CAN_Transmit会返回使用的邮箱号0、1、2如果没有空邮箱则返回CAN_NO_MB。塞进去之后还要用CAN_TransmitStatus确认最终结果这个状态有三种成功、失败、仍在等待。/* 返回 0 成功, 非 0 为各类错误码 */ uint8_t CAN_SendStdFrame(uint16_t stdId, const uint8_t *data, uint8_t len) { CanTxMsg tx; uint8_t mb; uint32_t t0; if (stdId 0x7FF || len 8) return 1; tx.StdId stdId; tx.ExtId 0; 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]; mb CAN_Transmit(CAN1, tx); if (mb CAN_NO_MB) return 2; /* 三个邮箱全满 */ t0 GetTickMs(); while (CAN_TransmitStatus(CAN1, mb) CAN_TxStatus_Pending) { if (GetTickMs() - t0 5) /* 5ms 还没发出去 */ { CAN_CancelTransmit(CAN1, mb); return 3; } } return (CAN_TransmitStatus(CAN1, mb) CAN_TxStatus_Ok) ? 0 : 4; }这里有三处是实际调试中加进去的不是一开始就有。第一处是len 8的判断。经典 CAN 数据场就是 8 字节上限多写了会被硬件截断宁可返回错误让上层知道自己越界了。第二处是 5ms 超时。正常情况下 500kbps 发一帧 8 字节数据总共不到 300us等 5ms 还没发出去基本可以确定是总线被持续占用、或者是仲裁一直失败、或者是物理层根本没有节点应答某些配置下收不到 ACK 时会一直重发。没有超时的版本在总线故障时会把整个任务卡死这是很典型的现场故障形态。第三处是超时后取消发送。CAN_CancelTransmit会把邮箱里的报文撤回来避免故障恢复后突然把积压的旧报文发出去造成业务层的时序错乱。关于“邮箱满了怎么办”我的处理是返回错误码由调用者决定。有人喜欢在驱动里加软件队列自动排队看起来更优雅但实际项目里往往掩盖了业务层发送过快的问题。如果你的发送周期是 10ms 一帧 8 字节在 500kbps 下完全不会有邮箱满的问题如果频繁满那说明波特率定低或者发送逻辑本身有问题加队列只是把问题藏起来。4.5 接收中断与环形缓冲接收这块的架构选择直接决定了系统的稳定性。最省事的写法是在中断里直接解析业务逻辑但这样中断占用时间长总线负载一高就容易丢帧。我采用的方案是中断里只做两件事把帧从硬件 FIFO 搬到软件环形缓冲然后立刻退出。#define CAN_RX_BUF_SIZE 16 /* 必须是 2 的幂 */ typedef struct { uint16_t id; uint8_t dlc; uint8_t data[8]; } CanRxFrame; static CanRxFrame s_rxBuf[CAN_RX_BUF_SIZE]; static volatile uint8_t s_rxHead 0; static volatile uint8_t s_rxTail 0; void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg rx; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, rx); uint8_t next (s_rxHead 1) (CAN_RX_BUF_SIZE - 1); if (next ! s_rxTail) /* 队列未满才入队 */ { s_rxBuf[s_rxHead].id rx.StdId; s_rxBuf[s_rxHead].dlc rx.DLC; for (uint8_t i 0; i rx.DLC; i) s_rxBuf[s_rxHead].data[i] rx.Data[i]; s_rxHead next; } /* 队列满则丢弃新帧, 由上层统计丢帧数 */ } } uint8_t CAN_PopFrame(CanRxFrame *out) { if (s_rxTail s_rxHead) return 0; *out s_rxBuf[s_rxTail]; s_rxTail (s_rxTail 1) (CAN_RX_BUF_SIZE - 1); return 1; }几个设计选择说明一下。缓冲区大小取 16 是经验值F103 的主循环一般在几毫秒量级500kbps 下 8 字节帧加上帧间隔大约 260us16 级能撑住大约 4ms 的主循环阻塞对大多数裸机程序够用。缓冲区大小必须是 2 的幂这样可以用位与代替取模中断里的开销更小。CAN_Receive内部会清掉 RFOM 标志把 FIFO 里的帧释放掉所以不需要额外清中断标志。这一点不看库源码很容易重复写一遍清除操作虽然无害但容易和别的标志混淆。中断向量的名字要注意。F103 上 CAN1 的接收 FIFO0 中断是和 USB 低优先级共用一个向量的所以函数名是USB_LP_CAN1_RX0_IRQHandler而不是看起来更合理的CAN1_RX0_IRQHandler。写成后者编译器不报错只是个普通函数但中断永远不会进去现象是“发数据完全正常就是收不到”这种问题靠读代码很难发现只能对着启动文件里的向量表核对。4.6 封装成可复用的一层把上面的东西组织成一个文件对外只暴露几个接口后面换项目直接拷过去就能用。我习惯的接口大致是这么几个CAN_Init_All()、CAN_SendStdFrame()、CAN_PopFrame()、CAN_GetErrorCounter()、CAN_IsBusOff()。错误计数器的读取很值得单独封装因为它是判断总线健康度的最直接指标void CAN_GetErrorCounter(uint8_t *tec, uint8_t *rec) { uint32_t esr CAN1-ESR; *tec (uint8_t)((esr 16) 0xFF); /* 发送错误计数器 */ *rec (uint8_t)((esr 24) 0xFF); /* 接收错误计数器 */ }这两个计数器的意义正常情况下它们应该稳定在 0 附近偶尔跳动一下也正常。如果持续上涨到几十说明有偶发干扰或者采样点不对涨到 96 以上进入“错误被动”状态节点开始自觉少发涨过 255 就进入离线Bus-Off。我在产品里会把这两个值通过诊断帧周期性上报运维的时候看一眼就知道总线质量比现场拿示波器方便得多。5. 联调排错从回环自测到总线恢复5.1 第一步永远是回环模式新板子第一次上电我不会直接接总线而是先把 CAN 配成环回模式CAN_Mode_LoopBack自测。这个模式下发送的帧不会真正发到总线上而是在内部直接被自己接收到不依赖收发器、不依赖终端电阻、不依赖对端节点。具体做法配成环回模式每 100ms 发一帧已知 ID 和数据的标准帧在接收中断里把收到的内容打印出来。能稳定收到且内容和发送一致说明三件事都对了位时序配置正确内部也要走完整的位时间逻辑、过滤器和中断通道打通、收发数据结构没问题。这一步通过之后再切回正常模式接总线。如果切过去就不通了问题范围立刻就能锁定在物理层或者对端配置上不用再怀疑软件。这个方法的效率比“直接上总线出问题全链路排查”高一个数量级我现在的习惯是把它写进项目自检流程里。还有一种情况是只有一块板子、手头没有第二个节点。这时候单节点正常模式发帧会因为收不到 ACK 而不断重发表现为发送一直失败、错误计数器快速上涨。这是正常现象不是代码问题。要单板验证物理层可以用“单节点 另一端接 120Ω 电阻”的方式但依然收不到 ACK。所以单板调试基本就靠环回模式别在正常模式上浪费时间。5.2 故障速查表现场问题见得多了大部分都能归到有限的几类。整理成表排查时按顺序过一遍能覆盖八成以上的情况。现象最可能的原因排查手段发送一直失败, TEC 快速上涨总线上没有其他节点应答, 或终端电阻缺失断电量 CANH-CANL 阻抗, 应约 60Ω能发不能收过滤器 ID 未左移对齐, 或中断向量名写错先改全放行过滤器, 再核对向量表偶发错误帧, 频率随线长增加采样点偏前, 或支线过长, 或无屏蔽线采样点后移到 80% 以上, 缩短支线上电程序卡死CAN 时钟未使能, 卡在等 INAK检查 APB1 时钟使能顺序通信一段时间后失效, 重启恢复进入 Bus-Off 且未开自动恢复打开 ABOM, 或软件检测 BOFF 重新初始化波特率明显不对APB1 频率与计算时假设不符打印 RCC 时钟配置, 确认 APB1 实际值数据错位或数值异常多字节字段大小端未统一协议层约定字节序, 收发两端核对板间接地不良时通信异常地电位差导致共模超范围加共模电感, 检查地线连接质量最后一行值得多提一句。CAN 虽然是差分传输抗共模干扰能力强但这个“强”是有范围的收发器的共模输入范围一般也就 -2V 到 7V 左右。两块板子各自用独立的电源适配器供电、地之间又没有可靠连接时地电位差可能超出这个范围表现就是通信时断时续而且把示波器地夹上去的时候现象还会变因为示波器地线又引入了一条通路。遇到这种“接上示波器就好了、拿掉就坏了”的诡异现象基本可以确定是接地问题。5.3 错误计数器与离线恢复Bus-Off 是 CAN 里比较特别的一个机制值得单独说清楚。节点发送错误计数器超过 255 时硬件会自动把它从总线上摘下来不再收发这就是离线状态。设计这个机制的初衷是一个坏节点不应该拖垮整条总线。问题在于很多项目没有处理离线恢复节点一旦离线就永远不通信了现场表现为“设备还亮着灯但数据没了”。处理方式有两种。第一种是打开CAN_ABOM让硬件自动恢复。优点是零代码缺点是恢复时机不可控而且如果物理故障一直存在它会反复尝试进入总线、反复离线错误计数器在 0 到 255 之间来回跳。对于偶发干扰导致的离线这种方式很合适。第二种是软件检测。周期性检查CAN_GetFlagStatus(CAN1, CAN_FLAG_BOFF)发现置位就重新走一遍完整的初始化流程同时上报一条故障记录。这种方式可控能记录离线次数用于分析缺点是代码稍多。产品级项目我一般用第二种同时把 ABOM 也打开做兜底。还有一种中间状态叫“错误被动”错误计数器在 128 到 255 之间。这个状态下节点还能正常收发但发错误帧的时候会比较保守。如果诊断数据里发现某台设备长期处于错误被动说明它的线路或者收发器已经有隐患了这时候换根线、紧一下端子通常就能避免一次现场故障。5.4 几个让我印象深刻的坑第一个坑是“同名对接”。早期接线时把 MCU 的 CAN_TX 接到了收发器的 TXD看起来同名很合理实际上是错的。收发器的 TXD 是“输入”MCU 的 TX 也是“输出”两个输出怼在一起当然不通。正确接法是 MCU_TX → 收发器 TXDMCU_RX → 收发器 RXD名字上看着是“交叉”的逻辑上是“信号方向对接”。这个错误在原理图上很显眼在实物上却很难看出来。第二个坑是杜邦线。调试时为了省事用 20cm 杜邦线把开发板接到总线端子上结果 500kbps 下误码率居高不下。换成焊接的短双绞线之后立刻稳定。CAN 的差分信号对线缆的对称性和阻抗一致性有要求杜邦线两根线的间距和长度都不一致等于人为制造了一个不平衡传输线。凡是超过 100kbps 的场合我都不建议用杜邦线。第三个坑是采样点的“够用就行”。有个项目 250kbps、总线长度约 30 米一开始用 75% 采样点白天运行正常晚上偶尔报错。后来查出来是环境温度变化导致晶振频偏加大加上线缆延时75% 的裕量不够了。把采样点改到 85%、SJW 加到 2tq 之后问题消失。这件事让我养成了一个习惯只要线长超过 10 米采样点一律按 80% 起步不再图省事用默认值。第四个坑是过滤器数量规划。有个项目前期用全放行过滤器跑得很顺后期要过滤 20 多个 ID 的时候才发现14 个过滤器组按 32 位模式根本不够用改成 16 位模式也只能覆盖一部分。最后是靠掩码模式把 ID 按高字节分组才勉强够。如果一开始就按“ID 分段 掩码”的思路规划根本不会有这个问题。所以项目启动阶段把报文 ID 分配表定下来这件事比写代码更值得花时间。我个人在这些项目里最深的一点体会是CAN 这类现场总线的调试软件和硬件是没法分开看的。你在代码里看到的每一个错误计数器数值背后都是线缆、终端电阻、收发器、地线这些东西的综合结果。与其在代码里加各种重试和容错去掩盖问题不如先花十分钟量一下那 60Ω核对一下 APB1 的实际频率把物理层和时钟这两个地基打牢。地基稳了软件层其实简单得超乎想象核心逻辑加起来不过几百行。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑