尧图精选

GD32H759+RT-Thread CAN总线:位定时、负载率与错误帧排查

🕒 发布时间:2026/9/18 12:10:13 📁 来源:尧图网络
工控现场的总线通信从来不是插上线就能跑的事。几年前接手一个多轴运动控制柜改造项目主控板换成了 GD32H759跑的是 RT-Thread十几台伺服、IO 模块、编码器采集盒挂在同一条 CAN 总线上250kbps 和 500kbps 混着用第一次上电就遇到了丢帧、总线关闭、节点之间互相打架的问题。折腾了小半个月把 GD32H759 的 CAN-FD 控制器、RT-Thread 的设备驱动框架、收发器电路和终端电阻这几块彻底梳理了一遍才算把这条总线跑稳。这篇就把整个 CAN 总线落地过程拆开讲清楚GD32H759 上的 CAN 外设怎么用、RT-Thread 里设备框架怎么接、位定时和采样点怎么算、中断收还是 DMA 收、滤波器怎么配、负载率怎么估、错误帧怎么定位。内容偏向工程落地适合正在做工业控制器、车载节点、CANopen 从站或者自研总线协议的同行参考新手可以照着步骤走有经验的可以直接跳到负载率计算和错误帧排查那两节。1. GD32H759 RT-Thread 做 CAN 节点的整体设计思路1.1 先看清楚这颗芯片的 CAN 家底GD32H759 属于 GD32H7 系列内核是 Cortex-M7主频可以跑到 600MHz带指令和数据 CacheSRAM 分成好几块TCM、AXI SRAM、AHB SRAM 各自独立Flash 容量按具体型号标号有差别选型时对着数据手册确认就行。对 CAN 总线这件事来说真正要关注的是三件事CAN 控制器数量、是否支持 CAN FD、时钟从哪来。GD32H7 系列一般集成多路 CAN-FD 控制器常见是 CAN0/CAN1/CAN2 三路具体以你手上的型号数据手册为准支持 ISO 11898-1:2015 定义的经典 CAN 和 CAN FD 两种帧格式。这一点在工控项目里其实挺关键老设备大概率只认经典 CAN新板子如果只支持经典 CAN后面想升级到 5Mbps 数据段的 CAN FD 就得换主控反过来控制器支持 CAN FD 但配置成经典模式向下兼容是没问题的只是不能和纯经典 CAN 的控制器组网时开启 FD 帧。我一般建议新手项目先按经典 CAN 跑通把位定时、滤波器、错误处理摸熟再打开 FD 的数据段加速这样出问题时排查路径短很多。时钟来源要先理顺。CAN 控制器的时钟通常挂在 APB1 上也有的配置可以从其他时钟源取。很多人算位定时算不对问题往往不在公式而在于他假设的 CAN 时钟频率和实际寄存器里配的不是一个数。我吃过这个亏代码里按 60MHz 算的分频实际时钟树因为某个分频系数写成了 120MHz结果波特率整整差了一倍示波器上看波形才发现位宽不对。所以动手之前先在时钟配置那一段把 CAN 的输入时钟锁死最好打印出来或者用调试器看一眼寄存器再往下算。1.2 为什么用 RT-Thread 而不是裸机大循环裸机做 CAN 通信当然能做早期项目我就是这么干的主循环里轮询接收 FIFO定时器里发心跳帧中断里把数据塞进环形缓冲区。问题出在节点数量一多、协议一复杂主循环就被拆得七零八落——CAN 报文解析、上位机串口协议、IO 采集、看门狗喂狗全挤在一个 while 里任何一个环节卡住CAN 的实时性就崩了。换成 RT-Thread 之后最大的变化是解耦。CAN 收发是一个线程协议解析是一个线程业务逻辑是一个线程各自有自己的优先级和栈空间用消息队列、邮箱或者信号量串起来。CAN 中断里只做最轻的活——把收到的报文丢进 ringbuffer 或者信号量唤醒线程绝不在这里做解析、不做内存分配、不打印日志。这种结构在 500kbps、总线负载 50% 左右的场景下实测下来抖动非常小。另一个好处是 RT-Thread 自带 CAN 设备驱动框架。它把 CAN 控制器抽象成一个 rt_device对上提供标准的 open/close/read/write/control 接口对下由 BSP 里的 drv_can.c 去对接寄存器或者厂商固件库。你可以用同一套应用代码在 GD32、STM32、国产其他 M7 芯片之间换着跑只需要换 BSP。当然框架不是万能的比如滤波器的高级配置、FD 的位定时切换框架层可能只暴露了一部分能力剩下的还得自己改 drv_can.c。我个人的做法是能在框架里做的就在框架里做框架覆盖不到的直接在 BSP 里补一个私有 control 命令不去污染应用层。1.3 分层设计从收发器到业务逻辑把整个链路画清楚后面调哪儿心里都有数。自下而上大概是这么几层层级内容关注点物理层收发器、隔离器、终端电阻、线缆共模电压、反射、地环路控制器层GD32H759 的 CAN-FD 控制器位定时、采样点、滤波器驱动层BSP 里的 drv_can.c中断/DMA、FIFO、错误上报框架层RT-Thread CAN 设备接口设备注册、消息结构体应用层协议栈、业务线程报文分发、超时、心跳这个分层的意义在于定位问题时能快速收敛。比如总线上一报错先看是不是物理层用万用表量终端电阻、用示波器看差分幅度再看控制器层错误计数器有没有涨最后才怀疑应用层解析逻辑。我见过不少项目一上手就怀疑代码结果查了半天是收发器供电电压不够导致显性电平幅度不达标。2. CAN 总线协议里工控现场最容易翻车的几个点2.1 帧格式与 ID 规划要提前定死CAN 标准帧用 11 位 ID扩展帧用 29 位 ID两个格式可以在同一条总线上共存但不能有两个节点用相同的 ID 发数据仲裁会乱。工控项目里我一般建议这么规划ID 的高位段表示功能域中间段表示节点地址低位段表示报文类型。比如用扩展帧 29 位高 8 位是系统号接着 8 位是节点号再 8 位是命令码剩下 5 位留给子索引或者序号。这样规划的好处是滤波器可以用掩码模式一次性过滤一整类报文不用为每个 ID 单独建表。数据长度方面经典 CAN 一帧最多 8 字节CAN FD 可以到 64 字节。工控里如果报文本身很短比如一个位置值加一个状态字6 字节那就老老实实用经典帧别为了省事全上 FD——FD 帧的 CRC 更长、位填充规则也更复杂出错概率不低而且对老设备不友好。只有当确实需要传固件分片、图像小块、大块配置参数时才值得开 FD。还有一点容易忽略RTR 远程帧。现在很多新项目已经不用远程帧了因为远程帧在总线负载高的时候会带来额外的仲裁开销而且数据段为空接收方还得再发一帧数据回来。我建议新项目直接用数据帧周期性广播别用远程帧做请求响应。2.2 位定时与采样点决定通信稳不稳这是整个 CAN 配置里最容易出错的地方。一个位时间bit time被切分成若干时间份额 tq结构是位时间 同步段(SyncSeg) 传播段(PropSeg) 相位缓冲段1(PhaseSeg1) 相位缓冲段2(PhaseSeg2)采样点落在 PhaseSeg1 结束的位置。采样点比例 (SyncSeg PropSeg PhaseSeg1) / 总 tq 数。业界经验值经典 CAN 500kbps 时采样点落在 75% 到 87.5% 之间比较稳CAN FD 数据段 2Mbps 以上时采样点通常要求 75% 到 80%而且要求发送节点和接收节点的采样点偏差不要太大。同一个网络里所有节点的采样点必须一样否则长线缆上信号传播延迟加上相位误差就会在采样边界上抖动表现为偶发错误帧。我在一个 500kbps、总线长度 60 米的现场遇到过这种问题三个节点的采样点分别是 75%、80%、87.5%短线上跑得好好的线一拉长就开始偶发错误。把三个节点统一到 87.5% 之后问题消失。所以位定时参数不是单机说了算是整个网络统一约定的事。2.3 错误帧、错误计数器与总线关闭CAN 协议本身有很强的自诊断能力只是很多人不看这些计数器。每个 CAN 控制器内部有两个错误计数器发送错误计数器 TEC 和接收错误计数器 REC。发送出错时 TEC 加 8如果是应答错误或位填充错误这类接收出错时 REC 加 1特殊情况下加 8成功发送一帧TEC 减 1成功接收一帧REC 减 1减到 0 为止。当 TEC 或 REC 超过 127节点进入错误被动状态此时它发的是被动错误帧不再强行拉低总线干扰别人。当 TEC 超过 255节点进入总线关闭状态自动从总线上脱开不再收发。总线关闭之后必须由软件介入恢复一般是检测到错误状态后延时一段时间重新初始化控制器。总线关闭在实际项目里的典型诱因就几个终端电阻没接或者只接了一头、波特率不匹配、线缆断了或者短路、某个节点供电不稳导致收发器输出异常。我建议在 BSP 里把错误状态变化做成回调或者消息上报到应用层应用层记录日志并触发恢复流程。GD32H7 的 CAN 控制器一般支持自动恢复或者手动恢复两种模式配成手动恢复更可控恢复前先检查一下物理层。2.4 负载率到底怎么算多少算安全负载率这个词很多人挂在嘴边但真让他算又说不清。公式不复杂总线负载率 单位时间内所有节点发送的位总数 / 波特率麻烦的是位总数怎么算。一帧标准数据帧假设 8 字节数据的位数不含位填充时大概是 111 位左右具体构成是帧起始 1 位、仲裁段 11 位标准 ID、RTR 1 位、IDE 1 位、保留位 1 位、DLC 4 位、数据 64 位、CRC 15 位、CRC 界定符 1 位、ACK 槽 1 位、ACK 界定符 1 位、帧结束 7 位、帧间隔 3 位。加起来约 111 位。但 CAN 有位填充机制连续 5 个相同极性的位之后要插入 1 个相反极性的位最坏情况下填充位最多能占到原始位数的 20% 左右。所以工程上估最坏情况标准帧 8 字节按 130 到 135 位算比较稳妥扩展帧 29 位 ID 的比标准帧多 20 位按 150 到 160 位估。数据长度少的话按比例往下调。举个实际例子500kbps 总线10 个节点每个节点每 20ms 发一帧标准帧 8 字节数据最坏按 135 位估。每个节点每秒发 50 帧10 个节点每秒 500 帧500 × 135 67500 位/秒负载率就是 67500 / 500000 13.5%。这个水平很舒服。如果把周期改成 5ms负载率就跳到 54%这时候就要小心了——任何一次突发重传都会让负载继续往上堆一旦超过 80%报文延迟就会显著增加实时性没法保证。工程上的经验阈值设计阶段把稳态负载率控制在 40% 以内留出余量给突发和重传超过 60% 就要考虑拆总线或者提高波特率80% 以上基本就是在赌运气了。3. 硬件电路收发器、终端电阻与地回路3.1 收发器选型与隔离MCU 的 CAN 控制器输出的是逻辑电平的 TX/RX 信号真正上总线需要收发器。选型时看几个参数支持的最高速率经典 CAN 用 1Mbps 的够上 FD 要选支持 5Mbps 以上的、共模电压范围、是否带待机/静默模式、供电电压。3.3V 供电的收发器和 GD32H759 的 IO 电平匹配最省事不用加电平转换。工控柜里的电磁环境比实验室恶劣得多变频器、伺服驱动器、继电器动作都会往总线上灌噪声。这种场合我强烈建议用带隔离的收发器方案数字隔离器隔离 TX/RX隔离电源给总线侧供电收发器再挂到总线上。隔离之后地电位差带来的共模电流不会串到主控板上实测抗干扰能力提升非常明显。成本会高一些一个隔离收发器模块比裸收发器贵好几倍但在停机一次的代价面前这点钱不算什么。有一个细节值得提隔离方案下隔离电源的爬电距离和 PCB 上的分割间距要留够别为了省面积把两侧的地铺得很近那样隔离等于白做。3.2 终端电阻与拓扑别在这上面偷懒CAN 总线是差分总线两端各需要一个 120 欧姆的终端电阻用来吸收信号反射。整条总线用万用表量 CANH 和 CANL 之间的电阻两端都接好的话应该接近 60 欧姆两个 120 欧并联。我见过太多现场问题出在这里有的节点自带 120 欧终端电阻结果一条总线上插了四五个这样带电阻的节点等效阻抗掉到二三十欧姆收发器驱动能力不够波形幅度直接塌下去还有的是两端都没接电阻短线缆上勉强能跑线一长全是反射错误帧哗哗地涨。正确的做法是终端电阻只放在物理总线的两个最远端而且最好是可跳线或者焊接选择的形式方便调试时确认。中间节点的终端电阻一律不焊。拓扑方面主干线尽量拉直分支线越短越好。经验值是分支线长度不要超过主干线长度的十分之一如果波特率是 500kbps单个分支线最好控制在 30 厘米以内。有些柜内布线为了走线好看绕来绕去分支线拉了一米多这种布局在低速下勉强能跑速率一上去就废。线缆选择上用特性阻抗 120 欧姆的双绞屏蔽线屏蔽层单点接地一般接在控制柜的地排或者主控端不要两端都接地否则屏蔽层上形成地环流反而变成天线。4. 驱动落地时钟、位定时与滤波器配置4.1 时钟和引脚配置先把 CAN 控制器的时钟打开。GD32H7 的外设时钟使能一般在 RCU 相关寄存器或者厂商固件库的 rcu_periph_clock_enable 里做。CAN 通常挂在 APB1 上配置的时候顺手把 CAN 时钟源选好很多型号支持从 APB1 或者外部晶振分频取源选哪个取决于你的时钟树规划。引脚方面CAN_TX 和 CAN_RX 要配成复用功能模式注意有的封装上多路 CAN 共用同一组引脚的不同复用编号别选错。配上拉输入或者复用推挽输出的时候TX 一般是复用推挽RX 是上拉输入具体看手册的 GPIO 复用表。配置完之后我习惯先不急着写业务代码而是先让 CAN 控制器进入正常工作模式读一下状态寄存器确认它没有卡在初始化或者总线关闭状态再去配位定时。4.2 位定时参数的计算过程假设我们把 CAN 的输入时钟配到 60MHz目标是 500kbps 经典 CAN。计算步骤是这样第一步算位时间。500kbps 对应的位时间是 1 / 500000 2 微秒。第二步选 tq 数。位时间要切成整数个 tq总 tq 数越多采样点调节粒度越细。我一般取 16 到 20 个 tq。这里取 20。第三步算 tq 长度。2 微秒 / 20 0.1 微秒也就是 100 纳秒。第四步算预分频。tq 长度 预分频值 / CAN 时钟频率。0.1 微秒 prescaler / 60MHz得出 prescaler 6。第五步分配各段。20 个 tq 分成同步段 1 个 tq传播段 相位缓冲段1 合计 13 个 tq相位缓冲段2 取 6 个 tq。1 13 6 20。采样点 (1 13) / 20 70%。70% 在短线上没问题如果线缆较长或者网络里有采样点偏高的节点可以把相位缓冲段2 减到 5相位缓冲段1 加 1变成 1 14 5采样点 75%。再保守一点1 15 4采样点 80%。这三个组合都合法就看整个网络的约定。对应的配置结构大概是这个形式伪代码字段名以厂商固件库为准can_parameter_struct can_init; can_struct_para_init(can_init); can_init.working_mode CAN_NORMAL_MODE; can_init.resync_jump_width 4; /* SJW单位 tq */ can_init.time_segment_1 13; /* 传播段 PhaseSeg1 */ can_init.time_segment_2 6; /* PhaseSeg2 */ can_init.time_triggered DISABLE; can_init.auto_bus_off_recovery DISABLE; can_init.auto_wake_up DISABLE; can_init.auto_retrans ENABLE; /* 自动重传工控里一般开 */ can_init.prescaler 6; can_init.filter_bits 0; can_init.transmit_fifo_priority DISABLE; can_init(can_init);要注意 resync_jump_width 也就是 SJW它决定重同步时能调整多少个 tq。SJW 太大重同步幅度大抗抖动强但可能引入较大相位跳变SJW 太小又容易失步。一般取 PhaseSeg2 的一半左右2 到 4 之间比较常见。如果是 CAN FD还要单独配数据段的位定时。假设数据段跑 2Mbps数据段位时间 500 纳秒如果还是 60MHz 时钟tq 长度取 25 纳秒的话预分频是 1.5不是整数那就得换 tq 数或者换时钟。比如取数据段 15 个 tqtq 长度 33.3 纳秒预分频就是 225 纳秒或者 350 纳秒都不太对。这种情况下要么把 CAN 时钟调成 40MHz 或 80MHz 这种能被整除的值要么调整 tq 数。我一般倾向于把 CAN 时钟调到 80MHz2Mbps 时取 20 个 tq预分频 2采样点 1 12 7 2080%干净利落。4.3 中断接收还是 DMA 接收这个问题在搜索热词里排得很靠前说明大家确实纠结。我的结论是经典 CAN 场景下用中断加 FIFO 就够了CAN FD 传大报文时再考虑 DMA。理由很直白。经典 CAN 一帧最多 8 字节数据加上 ID、DLC 这些字段一帧报文的总长度不过十几个字节。在 500kbps 下一帧传输时间约 220 微秒1Mbps 下约 110 微秒。就算总线跑到 60% 负载中断频率也就是每秒几千次对 600MHz 的 M7 来说完全不是负担。DMA 在这里省下的那点 CPU 搬运开销远远抵不上它带来的复杂度和风险。CAN FD 就不一样了64 字节数据段在 5Mbps 下传输时间很短突发流量下中断频率会到每秒上万次这时候用 DMA 把 FIFO 里的数据打包搬到内存能明显降低 CPU 占用。但要用 DMA 就得处理 Cache 一致性——GD32H759 是 M7带 D-CacheDMA 写内存之后 CPU 读到的可能是 Cache 里的旧数据。收方向要在 DMA 完成中断里调用 Cache 无效化操作发方向要在启动 DMA 之前做 Cache 清零操作地址和长度都要按 Cache line 对齐。这块如果处理不好会出现数据偶尔是旧的这种极其难查的问题。所以我的实操建议是第一阶段先用中断接收把协议跑通、把业务逻辑调稳等确定要用 CAN FD 高带宽传输、并且实测 CPU 占用确实偏高时再引入 DMA同时把 Cache 维护加进去加完之后用带随机数据的大报文做压力测试确认没有脏数据。中断优先级方面CAN 接收中断的优先级要设得比普通业务线程对应的中断高一些但别设成最高免得把系统滴答或者其他关键中断压住。RT-Thread 里用 rt_hw_interrupt_install 注册中断服务函数时注意中断上下文里只能调用带 _isr 后缀的 RT-Thread API比如 rt_sem_release、rt_mq_send不能用会引起阻塞的接口。4.4 滤波器配置掩码模式和列表模式怎么选CAN 控制器收到一帧报文会先过验收滤波器只有匹配的才放进接收 FIFO。GD32H7 的滤波器一般支持两种模式掩码模式mask mode给一个 ID 和一个掩码掩码位为 1 的位置要求 ID 完全匹配为 0 的位置不关心。适合过滤一整类报文。列表模式list mode直接列出若干个必须精确匹配的 ID。适合只关心少数几个固定 ID 的场景。工控项目里我一般混合用对周期性的状态广播用掩码模式一次性过滤一个功能域对关键的少数控制命令用列表模式精确收。滤波器数量有限别浪费在无关报文上——总线负载高的时候让不需要的报文在硬件层就被丢掉比收进来再在软件里判断要高效得多。在 RT-Thread 里滤波器通常通过 rt_device_control 下发配置结构类似这样struct rt_can_filter_item items[2] { /* 掩码模式接收 0x1800 段的标准帧 */ RT_CAN_FILTER_ITEM_INIT(0x1800, 0, 0, 0, 0x7F00, RT_NULL, RT_NULL), /* 列表模式精确接收 0x123 */ RT_CAN_FILTER_ITEM_INIT(0x123, 0, 0, 1, 0x7FF, RT_NULL, RT_NULL), }; struct rt_can_filter_config cfg { 2, 1, items }; rt_device_control(can_dev, RT_DEVICE_CTRL_CONFIG, cfg);不同 RT-Thread 版本里这个宏的参数顺序和个数会有差别有的版本多一个 hdr_bank 参数。写之前对着 rtdevice.h 里的定义看一眼别照抄别人的代码版本对不上编译能过但行为是错的这种坑最难查。5. RT-Thread 应用层线程模型与收发实现5.1 设备注册与应用初始化RT-Thread 的 CAN 设备在 BSP 里通过 rt_hw_can_register 注册到系统应用层用 rt_device_find 按名字找。名字一般是 can0、can1具体看 BSP 里的定义。初始化流程大致是找设备、打开设备带 INT_RX 标志表示中断接收、配置工作模式、配置滤波器、创建接收线程。#define CAN_DEV_NAME can0 static rt_device_t can_dev RT_NULL; static int can_app_init(void) { rt_err_t ret; can_dev rt_device_find(CAN_DEV_NAME); if (can_dev RT_NULL) { rt_kprintf(can device %s not found\n, CAN_DEV_NAME); return -RT_ERROR; } /* 中断接收 中断发送 */ ret rt_device_open(can_dev, RT_DEVICE_FLAG_INT_RX | RT_DEVICE_FLAG_INT_TX); if (ret ! RT_EOK) { rt_kprintf(open can dev failed: %d\n, ret); return ret; } /* 工作模式正常模式 */ rt_device_control(can_dev, RT_DEVICE_CTRL_CONFIG, (void *)RT_CAN_MODE_NORMAL); return RT_EOK; }打开设备之后建议先确认一下设备状态有的 BSP 在 open 的时候会去初始化控制器如果时钟没配好或者引脚复用没写对open 会返回错误。这一步的错误信息一定要打印出来不要吞掉。5.2 接收线程与消息分发接收线程的写法有两种。一种是阻塞读rt_device_read 在没有数据的时候会阻塞在当前线程收到数据自动唤醒CPU 占用低代码简单。另一种是中断里发消息队列或者信号量线程里主动取控制更灵活。我一般用第一种简单可靠。static void can_rx_thread_entry(void *parameter) { struct rt_can_msg rx_msg {0}; rt_size_t len; while (1) { len rt_device_read(can_dev, 0, rx_msg, sizeof(rx_msg)); if (len 0) { continue; } /* 这里只做分发不做好时操作 */ switch (rx_msg.id 8) { case 0x18: dispatch_node_status(rx_msg); break; case 0x10: dispatch_control_cmd(rx_msg); break; default: /* 未定义 ID计数并丢弃 */ g_unknown_frame_cnt; break; } } }RT-Thread 的 rt_can_msg 结构里一般包含 id、ide标准/扩展标志、rtr、len、data 数组、priv 等字段。data 数组在经典 CAN 下用前 8 字节FD 模式下有的实现会扩展成 64 字节。取数据的时候一定按 len 去取不要无脑读 8 字节否则解析短报文时会读进脏数据。分发逻辑要注意不要在接收线程里做耗时操作。解析、查表、更新状态都是毫秒级的可以接受但如果要写 Flash、要发网络包、要等锁就继续往下丢给业务线程处理。接收线程的栈也别开太小协议解析里如果用了浮点或者 printf栈需求会上去512 字节肯定不够我一般给 2048 到 4096 字节。5.3 周期发送与负载控制发送侧我一般单独开一个线程用一个软件定时或者 tick 计数来驱动周期帧。RT-Thread 里可以用 rt_timer 做软定时也可以用线程里 rt_thread_delay_until 做精确周期。后者在多周期报文共存的时候更好用。static void can_tx_thread_entry(void *parameter) { struct rt_can_msg tx_msg {0}; rt_tick_t next_tick rt_tick_get(); tx_msg.ide RT_CAN_STDID; tx_msg.rtr RT_CAN_DTR; tx_msg.len 8; while (1) { /* 组装心跳报文 */ tx_msg.id 0x180; tx_msg.data[0] g_node_state; tx_msg.data[1] g_error_flag; /* ... 其余字节填充 */ rt_device_write(can_dev, 0, tx_msg, sizeof(tx_msg)); /* 固定 20ms 周期 */ next_tick rt_tick_from_millisecond(20); rt_thread_delay_until(next_tick, 20); } }这里有个细节值得说用 rt_thread_delay_until 比用 rt_thread_mdelay 的周期精度高因为它会补偿每次循环的执行时间长期跑下来不会累积漂移。对于周期性 CAN 报文尤其是做同步用的累积漂移会让不同节点的节拍越差越远。发送侧还要做流控。如果业务上某一时刻突然要发一大批报文直接往控制器里灌发送邮箱满了之后 rt_device_write 会返回失败或者阻塞。我的做法是在发送线程里加一个软件队列队列满了就丢最旧的或者直接丢弃并计数同时监控发送失败次数。如果发送失败次数持续增长说明总线负载太高或者有节点不响应这时候要么降频要么报警。6. 调试排查与常见问题实录6.1 常见问题速查表现象大概率原因排查动作完全收不到帧收发器没供电、TX/RX 接反、终端电阻缺失量收发器供电示波器看 TX 引脚有没有波形短线上能通长线就丢终端电阻只接了一头、采样点不一致万用表量总线阻抗统一各节点位定时错误帧频繁TEC 持续上涨波特率不匹配、反射严重、共模干扰用分析仪看错误类型检查线缆和屏蔽节点间歇性总线关闭供电不稳、地电位差大、某节点异常发送查该节点供电测共模电压看是否需隔离偶发收到错误数据解析时没按 DLC 取长度、字节序搞错打印原始报文对比确认大小端上电后必须重启才能通信初始化顺序问题、控制器卡在总线关闭检查初始化流程确认总线关闭恢复逻辑6.2 抓不到帧的时候按这个顺序查遇到完全收不到数据我一般按从物理到软件的次序走别跳步。第一步断电量总线阻抗。CANH 和 CANL 之间应该在 60 欧姆左右。如果量出来 120 欧说明只有一端接了终端电阻量出来 40 欧说明有三四个终端电阻并联量出来无穷大说明一个都没接。第二步上电量差分电平。显性位的时候 CANH 比 CANL 高 2V 左右隐性位两者都在 2.5V 附近差压接近 0。如果差分幅度明显不足比如只有 0.5V要么是收发器驱动能力不够要么是总线上挂了太多终端电阻。第三步看 MCU 的 TX 引脚有没有波形。用示波器或者逻辑分析仪抓 TX如果发数据的时候 TX 上有波形但总线上没有问题在收发器或者收发器到总线的连接如果 TX 上根本没波形问题在 MCU 侧检查引脚复用、时钟、控制器状态。第四步看控制器状态寄存器。确认它是不是卡在初始化模式、是不是处于总线关闭、错误计数器是不是已经爆了。这一步很多人不做其实最省时间。第五步才去看软件。检查滤波器配置是不是把所有报文都滤掉了这个坑我踩过掩码配反了本来是只收 0x180结果变成了屏蔽 0x180其他全收看起来像是收不到目标报文其实是收到了别的。6.3 几个踩过的坑和经验第一个坑是关于自动重传的。GD32 的 CAN 控制器有个 auto_retrans 配置开启之后发送失败的报文会自动重传直到成功或者进入总线关闭。工控里通常要开保证可靠性。但在某些场景下比如节点正在做固件升级、总线上暂时没人应答自动重传会让这个节点一直占用总线重试把整个网络的负载推高形成雪崩。后来我在固件升级这类场景里改成关闭自动重传由应用层控制重发次数和间隔问题就没了。第二个坑是 Cache。前面提过一次这里再强调只要用了 DMA 搬 CAN 数据就必须处理 Cache 一致性。我遇到过一次非常诡异的现象——CAN FD 大报文偶尔会丢几个字节用调试器单步就正常全速跑就出错。查了两天才定位到是 DMA 写入的内存区域在 D-Cache 里被缓存了CPU 读到的是旧内容。处理方式是把 DMA 缓冲区放到非缓存区域或者在 DMA 完成中断里做无效化。具体用哪种看你的链接脚本和 MPU 配置方便程度。第三个坑是打印。调试阶段用 rt_kprintf 打印接收到的每一帧报文很方便但一旦总线负载上到 30% 以上串口输出就成了瓶颈接收线程被打印拖住FIFO 溢出丢帧。后来我改成用一个环形缓冲区记录最近的 N 帧出问题的时候用命令一次性 dump 出来平时不打印。这个改动之后同样的负载下丢帧率直接降到零。第四个经验是关于 CANopen 和自定义协议的取舍。如果项目里的从站设备都是第三方成熟产品直接上标准 CANopenPDO、SDO、心跳、NMT 都是现成的省事。如果是自研设备节点数量又不多自定义一个简单的协议反而更轻量——省掉对象字典那一整套报文语义直接对着业务写调试也直观。我做过一个六轴控制器的项目自定义协议从开发到现场稳定只用了两周比套 CANopen 快不少。最后一个小技巧在总线上长期挂一个监听节点只收不发把报文按 ID 分类统计频率和周期抖动一旦某个 ID 的周期突然变化或者消失立刻能发现是哪个节点出了问题。这个监听节点用一块带 CAN 的小板子加个简单的上位机就够了比事后拿分析仪去现场抓要主动得多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →