
工控这块做了些年头CAN 总线是我接触频率最高的现场总线之一从最早的 STM32F103 加收发器那种裸机点灯式跑法到后来上 RTOS、上 CAN FD踩过的坑能装一箩筐。这次接手的是一个基于GD32H759 RT-Thread的工控主板项目主控要同时挂多路CAN 总线对接伺服驱动器、IO 扩展模块和一块上位机网关板。选型阶段我纠结了很久最后定下 GD32H759 加国产 RT-Thread 这套组合理由和过程会在下面慢慢展开。这篇主要聊 CAN 这一块从硬件细节一路讲到 RT-Thread 里的收发线程、中断与 DMA 的取舍、负载率怎么估、错误帧怎么定位尽量把我在真实项目里验证过的写法摊开来讲。1. 先想清楚为什么是 GD32H759 RT-Thread 这套组合1.1 GD32H759 在工控场景里的实际定位先说说为什么挑这颗芯片。工控主板对主控的诉求其实很朴素主频够高、外设够全、温度范围够宽、供货稳定。GD32H759 属于 GD32H7 家族Cortex-M7 内核主频标称能跑到 600MHz片内 Flash 和 SRAM 在同类国产芯片里算第一梯队。这意味着什么意味着你可以在同一颗芯片上同时跑 CAN 通讯栈、Modbus 协议解析、以太网、还有一圈 ADC 采样滤波算法而不用像以前那样再挂一颗协处理器。我一开始其实是考虑用 H743 那类方案的后来换成 GD32H759主要看中两点。第一是这块板子上 CAN 控制器的数量足够多路 CAN 可以独立跑不同波特率一路接 500kbps 的伺服网络一路接 250kbps 的老旧 IO 模块互不干扰。第二是它带 CAN FD 能力虽然这个项目一期用不上但二期要传固件升级包和波形数据普通 CAN 那点带宽根本顶不住提前留好升级余地比事后换板子划算太多。这里要提醒一句选 M7 内核的芯片别只盯着主频看。M7 有指令和数据缓存还有 TCM 内存如果代码堆在普通 SRAM 上跑实际性能和标称主频的差距可能比你想象的大。我在早期调试时发现 CAN 中断响应抖动偏大后来把中断服务函数和相关缓冲区挪到 TCM 里抖动立刻下来了。这个细节手册上不会重点讲但工控场景对确定性要求高值得留意。1.2 为什么不用裸机非要上 RT-Thread很多人第一反应是CAN 通讯而已裸机一个大循环加中断就够了何必上 RTOS这个想法在小项目里没错但这个板子上的任务不止 CAN 一件事。它还要周期性采集模拟量、跑数字滤波、响应上位机命令、驱动几路继电器输出、记录故障日志。裸机大循环一旦某个任务耗时变长CAN 接收就会丢帧这是工控现场最忌讳的。用 RT-Thread 的好处在于CAN 接收、协议解析、业务逻辑可以拆成独立线程各归各的优先级。CAN 接收线程优先级给高一点保证及时收协议解析线程做报文拆包和状态机业务线程优先级最低负责对外输出和日志。这样即使上层业务卡了一下接收队列里的数据也不会丢因为有环形缓冲和信号量兜着。另外 RT-Thread 的设备框架是真的省事。CAN 被抽象成一个rt_device你拿到设备名就能open、read、write、control波特率、工作模式、过滤器都通过控制命令下发换芯片的时候上层业务代码基本不用动。对工控这种要出多个型号、可能换主控平台的产品线来说这个抽象层的价值很大。还有一点很少被提RT-Thread 在国内工控圈的生态这两年确实起来了遇到驱动适配问题社区里能找到类似 BSP 的参考代码比啃某个海外 RTOS 的手册快得多。国产化替代项目里这套组合的落地成本是可控的。提示上 RTOS 不是无脑上。如果项目就是单路 CAN 收发、逻辑不超过几百行裸机加中断加环形队列反而更简单可靠别为了用而用。1.3 CAN 总线在工控里到底解决什么问题说回 CAN 本身。为什么工控现场这么偏爱 CAN核心就三点差分传输抗干扰、多主结构天生适合分布式、非破坏性仲裁保证高优先级报文不被堵。差分传输这块工控现场电磁环境有多脏大家都懂变频器、伺服、接触器一动作单端信号线基本就是噪声接收器。CAN 用 CAN_H 和 CAN_L 一对差分线共模干扰被抵消掉这是我见过最皮实的现场总线之一。多主结构意味着每个节点都能主动发报文不需要像 Modbus 那样排队等主站轮询响应更快也更灵活。非破坏性仲裁是说多个节点同时发报文时不会互相破坏ID 小的自动赢输的那个退让后重发数据不丢。CAN 在工控里最常见的用法是伺服驱动器的运动控制网络和分布式 IO 采集。前者对实时性要求高报文周期固定负载率要精确算后者报文短、节点多重点在过滤和分发。这两种场景对驱动层的要求不一样后面我会分别讲。2. 动手前必须搞清楚的 CAN 硬件与位定时2.1 收发器、终端电阻和布线坑都在这软件写得再漂亮硬件不对全白搭。CAN 控制器出来的只是逻辑电平必须通过收发器转成差分信号。收发器选型上工控建议用带隔离的型号尤其是主控和现场设备不共地的时候。我见过太多通讯时好时坏最后查出来是地电位差在作怪隔离收发器能省掉一大半这类玄学问题。终端电阻是另一个高频翻车点。CAN 总线两端各要接一个 120 欧的终端电阻注意是两端不是每个节点都接。很多模块自带了终端电阻如果你一条线上串了七八个节点每个都接了 120 欧等效阻抗直接掉到十几欧总线根本驱动不起来波形塌得厉害。我的习惯是主控板这边留一个可插拔的跳线帽调试时按拓扑决定接不接。布线方面总线要尽量走手拉手的一条线避免星型拓扑。分支线长度在 500kbps 下最好控制在很短的范围太长会引起反射。屏蔽层的处理也有讲究屏蔽层一般单点接地两头都接反而容易形成地环路。这些都是现场经验手册上写得很含糊但出问题时全在细节里。注意调试 CAN 一定先拿示波器或者 CAN 分析仪看眼波形确认电平幅度和边沿是否干净。软件层再怎么排查物理层错了都是徒劳。2.2 位定时参数怎么算完整过程摊开讲位定时是 CAN 最容易被抄参数糊弄过去的地方但一旦遇到采样点不匹配导致偶发错误帧不会算就抓瞎。我把计算过程完整写一遍。CAN 的位时间被分成若干时间份额 tq由三段组成同步段 SYNC_SEG、时间段 1含传播段和相位段 1、时间段 2相位段 2。设总 tq 数为 N则位时间 N × tq采样点位置 (1 BS1) / N波特率 CAN 外设时钟 / (预分频值 × N)假设这块板子的 CAN 外设时钟是 60MHz目标波特率 500kbps我希望采样点落在 75% 左右。取 N 20那么 tq 1 / (500000 × 20) 100ns对应 10MHz预分频值 60 / 10 6。采样点 75% 意味着 1 BS1 15所以 BS1 14BS2 20 - 1 - 14 5。验证采样点 (114)/20 75%波特率 60MHz / (6 × 20) 500kbps。参数定下来就是预分频 6、BS1 14、BS2 5、SJW 取 4同步跳转宽度一般取 BS2 和 4 的较小值。再算一个 1Mbps 的。若 N 还是取 20tq 50ns对应 20MHz预分频 3BS1 14、BS2 5采样点还是 75%。如果外设时钟不变想省点计算量也可以把 N 缩到 10tq 100ns预分频 6此时 BS1 取 7、BS2 取 2采样点 8/10 80%。两种都能跑实际选哪个要看总线上其他节点的采样点取个中间值最稳。波特率外设时钟预分频总 tq 数BS1BS2采样点250kbps60MHz122014575%500kbps60MHz62014575%1Mbps60MHz32014575%1Mbps60MHz6107280%经验上采样点最好不要低于 75%也不宜太高。采样点偏后比如 87.5%在长总线上抗延迟更好但对时钟误差的容忍度下降。整条总线上的节点采样点尽量统一在 75% 到 80% 之间这是最不容易出问题的区间。2.3 CAN FD 数据段的位定时是独立的一套CAN FD 和经典 CAN 最大的区别在数据段可以提速。仲裁段用一套位定时保证和传统节点兼容数据段用另一套更快 的位定时。GD32H759 的 CAN 控制器支持这种双位定时配置。举个实际配置仲裁段 500kbps 用上面算的参数数据段目标 2Mbps。取数据段 N 10tq 1/(2M × 10) 50ns对应 20MHz预分频 3BS1 取 6、BS2 取 3采样点 7/10 70%。数据段采样点一般可以比仲裁段稍前一点因为数据段位宽更窄对相位误差更敏感很多方案把数据段采样点放在 70% 到 80%。这里有个常被忽略的点CAN FD 要在 BRS 位之后切换到数据段的位定时切换时机对节点间的时钟精度要求更高。如果总线上有节点晶振精度一般比如用内部 RC 时钟数据段提速后会频繁出错提不了速只能退回经典帧。所以如果你的现场设备晶振参差不齐CAN FD 的提速收益可能兑现不了。提示GD32H759 的 CAN 控制器寄存器模型和传统 bxCAN 差别很大位定时寄存器是分成仲裁段和数据段两组的别拿老代码直接套。3. RT-Thread 下 CAN 驱动落地从设备注册到收发线程3.1 设备框架里的驱动适配入口RT-Thread 的 CAN 设备走标准设备框架。BSP 里通常有一个drv_can.c负责把硬件抽象成rt_can设备并通过rt_hw_can_register注册进去。GD32H759 这颗芯片官方 BSP 不一定直接带 CAN 驱动很多时候要自己写或者从相近型号改。写这个驱动要打交道的东西不少CAN 控制器的初始化、位定时寄存器配置、过滤器配置、发送邮箱或发送 FIFO 管理、接收 FIFO 中断、错误状态上报。我的做法是先把控制器的寄存器模型理清楚因为它和传统 bxCAN 的差异很大——消息 RAM、发送 FIFO、接收 FIFO、过滤器组都在一片专用内存里配置起来比老式邮箱模型复杂。注册设备这一步大概长这样static struct rt_can_device can_dev; int rt_hw_can_init(void) { rt_err_t res; /* 使能 CAN 时钟、配置引脚 */ can_gpio_config(); can_clock_enable(); /* 控制器寄存器级初始化 */ mcan_hw_init(); /* 注册进 RT-Thread 设备框架 */ res rt_hw_can_register(can_dev, can1, drv_can_ops, RT_NULL); if (res ! RT_EOK) { rt_kprintf(can1 register failed\n); return -RT_ERROR; } return RT_EOK; } INIT_BOARD_EXPORT(rt_hw_can_init);drv_can_ops里最重要的三个函数是configure、control、sendmsg。configure处理波特率和工作模式control处理过滤器和其他运行时命令sendmsg负责把rt_can_msg塞进硬件发送 FIFO。这三个函数的实现质量直接决定上层用起来顺不顺尤其是发送 FIFO 满时怎么处理、接收中断怎么把数据交给上层这两块是驱动稳定性的关键。3.2 初始化与波特率配置代码设备注册好之后应用层拿到设备就能操作了。我一般会把 CAN 初始化包成一个函数放在板级初始化里调用#include rtthread.h #include rtdevice.h #define CAN_DEV_NAME can1 static rt_device_t can_dev RT_NULL; static int can_app_init(void) { rt_err_t res; rt_uint32_t baud 500000; rt_uint32_t mode RT_CAN_MODE_NORMAL; can_dev rt_device_find(CAN_DEV_NAME); if (can_dev RT_NULL) { rt_kprintf(find %s failed\n, CAN_DEV_NAME); return -RT_ERROR; } res rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX); if (res ! RT_EOK) { rt_kprintf(open %s failed\n, CAN_DEV_NAME); return -RT_ERROR; } /* 设置波特率 */ rt_device_control(can_dev, RT_CAN_CMD_SET_BAUD, (void *)baud); /* 设置工作模式 */ rt_device_control(can_dev, RT_CAN_CMD_SET_MODE, (void *)mode); return RT_EOK; } INIT_APP_EXPORT(can_app_init);不同版本的 RT-Thread波特率控制命令的名字可能有差异有的版本叫RT_CAN_CMD_SET_BAUD有的新增了RT_CAN_CMD_SET_BAUDRATE传参类型也不完全一样。我的建议是直接翻一下自己 BSP 里drv_can.c支持哪些命令别照搬网上的代码版本对不上编译都过不了。工作模式这块要说一下。调试阶段建议先用RT_CAN_MODE_LOOPBACK自测回环模式自己发自己收能确认驱动和收发逻辑通了再去接真实总线。真实总线上如果只有一个节点发出去的帧没人应答协议会自动重发最后堆到总线关闭很容易让人误以为驱动有 bug。用回环模式先把软件问题排干净是省时间的关键一步。过滤器配置也别忽略。工控节点多的时候一股脑把所有报文都收上来再在应用层筛会导致 CPU 负载飙升、接收队列爆掉。GD32H759 的 CAN 控制器过滤器资源比较充裕可以把不关心的 ID 直接拒在硬件层struct rt_can_filter_item items[1] { { 0x180, RT_CAN_STDID, 0x7F0, 0, RT_NULL } }; struct rt_can_filter_config cfg { 1, 1, items }; rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, cfg);这段的意思是只接收 ID 在 0x180 到 0x18F 这个范围内的标准帧其它全扔。掩码 0x7F0 表示高 7 位参与匹配低 4 位忽略。过滤器用好了接收中断频率能降一大截这个优化在现场节点多的项目里效果很明显。3.3 发送与接收线程的落地写法RT-Thread 里 CAN 收发的标准做法是接收用信号量配合回调发送用普通线程或者直接在业务线程里调用。接收指示回调是驱动收到帧后触发的它的作用是通知应用层有数据了但不要在回调里做耗时操作回调里只释放信号量static struct rt_semaphore rx_sem; static rt_err_t can_rx_ind(rt_device_t dev, rt_size_t size) { rt_sem_release(rx_sem); return RT_EOK; }接收线程拿信号量收到通知就去读static void can_rx_thread(void *param) { struct rt_can_msg rxmsg {0}; rt_sem_init(rx_sem, can_rx, 0, RT_IPC_FLAG_FIFO); rt_device_set_rx_indicate(can_dev, can_rx_ind); while (1) { /* 等不到信号量就一直阻塞不占 CPU */ rt_sem_take(rx_sem, RT_WAITING_FOREVER); while (rt_device_read(can_dev, 0, rxmsg, sizeof(rxmsg)) sizeof(rxmsg)) { can_frame_dispatch(rxmsg); } } }这里有个细节值得说收到信号量之后要用while循环把硬件 FIFO 里的帧读干净不能只读一帧。因为一帧到达触发一次中断如果期间又来了好几帧它们可能堆在 FIFO 里只读一帧会导致信号量少释放下次读的时候数据已经过期。这个循环读的写法是我调了半天才总结出来的一开始只读一帧现场一忙就丢数据。发送就简单多了组好rt_can_msg直接写static rt_err_t can_send_frame(rt_uint32_t id, rt_uint8_t *data, rt_uint8_t len) { struct rt_can_msg msg {0}; rt_size_t sent; msg.id id; msg.ide RT_CAN_STDID; msg.rtr RT_CAN_DTR; msg.len len; rt_memcpy(msg.data, data, len); sent rt_device_write(can_dev, 0, msg, sizeof(msg)); if (sent ! sizeof(msg)) { /* 发送 FIFO 满交给上层重试或丢弃 */ return -RT_EBUSY; } return RT_EOK; }发送 FIFO 满的情况一定要处理。工控场景下如果发送线程和接收线程抢总线FIFO 满了之后写失败如果你不管这个返回值报文就静默丢了。我的做法是关键控制帧写失败时进重试队列非关键的周期数据帧直接丢下一周期还有机会。4. 中断接收还是 DMA 接收工控场景下的取舍4.1 CAN 控制器的 FIFO 机制天生适合中断这个问题的答案其实很明确绝大多数工控 CAN 场景用中断配合硬件 FIFO 就够了不需要 DMA。原因在于 CAN 控制器本身就有接收 FIFO 和过滤器硬件层面已经帮你做了一层缓冲和筛选。GD32H759 这类带 CAN FD 的控制器内部消息 RAM 里可以配多个接收 FIFO每个 FIFO 能存若干条报文过滤器直接把不关心的 ID 拒掉进 FIFO 的都是你真正要处理的。一帧或几帧到达触发一次中断中断里不用搬数据只释放一个信号量或者置个标志真正的读数据交给线程。这种模式中断频率低、CPU 占用小是标准做法。我实测过这个方案的负载情况500kbps 总线跑 60% 负载率约 2300 帧每秒接收线程和中断加起来占 M7 的 CPU 时间不到 3%。这个开销对工控主板来说完全可接受。4.2 中断加环形队列的稳妥方案如果担心中断里直接读 FIFO 会阻塞或者想进一步降低中断处理时间可以在驱动里加一层软件环形队列。中断里只做一件事把硬件 FIFO 的数据快速搬进环形队列然后退出。线程再从容地从环形队列里取数据处理。/* 驱动内部的环形缓冲简化示意 */ #define CAN_RING_SIZE 64 static struct rt_can_msg can_ring[CAN_RING_SIZE]; static volatile rt_uint16_t ring_head 0; static volatile rt_uint16_t ring_tail 0; void can_rx_isr(void) { struct rt_can_msg msg; /* 把硬件 FIFO 里的帧全部搬空 */ while (mcan_rx_fifo_not_empty()) { mcan_read_fifo(msg); rt_base_t level rt_hw_interrupt_disable(); can_ring[ring_head] msg; ring_head (ring_head 1) % CAN_RING_SIZE; rt_hw_interrupt_enable(level); } rt_sem_release(rx_sem); }这个方案的好处是中断里没有耗时操作数据也不会因为线程调度延迟而丢。代价是多占一点内存还要注意环形队列满时的覆盖策略——我一般选择覆盖最老的帧因为工控里最新数据往往更重要。如果业务上不允许丢任何帧那就得把队列开大或者提高处理线程优先级。注意环形队列的头尾指针在中断和线程之间共享读写指针的更新一定要做临界区保护。我踩过一次坑没保护导致指针错乱程序跑几天后开始随机读到错帧查了好久才定位到。4.3 什么情况下才考虑 DMADMA 在 CAN 上的用武之地其实很有限主要集中在几个特殊场景。一是超大流量、超低 CPU 占用的场景比如网关设备要做多路 CAN 之间的转发或者 CAN 转以太网。这种场景可以用 DMA 把 FIFO 数据成批搬进内存减少中断次数。二是配合定时器做精确时间戳记录DMA 批量搬运的同时打时间戳做报文回放和总线分析。三是片内 SRAM 紧张、想用外扩内存做接收缓冲的情况。但说实话在大多数工控节点设备上DMA 带来的复杂度描述符配置、缓存一致性、和 RT-Thread 设备框架的适配往往大于收益。CAN 帧本来就短最多 64 字节DMA 的批量优势体现不出来。我的建议是除非你明确测出中断开销成了瓶颈否则老老实实用中断加 FIFO简单可靠出问题也好排查。这个问题网上一搜全是用 DMA 更高级的说法容易误导新手实际项目里没这个必要。5. 负载率计算、错误帧与总线恢复5.1 负载率到底怎么算留多少余量负载率是 CAN 网络设计的核心指标算错了要么不够用要么浪费带宽。计算方法其实不难关键是把帧长数清楚。以标准帧、8 字节数据为例一帧的组成不含位填充大致是帧起始 1 位仲裁段 12 位控制段 6 位数据段 64 位CRC 段 16 位应答段 2 位帧结束 7 位帧间隔 3 位。加起来 111 位。位填充规则是最坏情况下每 4 个连续同极性位插 1 位标准帧最坏大约增加 20 多位所以工程上把一帧按 130 位估算比较稳妥。一帧在总线上的时间 130 / 波特率。500kbps 下约 260 微秒。一秒理论上最多约 3846 帧。如果你实际每秒发 1000 帧负载率 1000 × 260us / 1s ≈ 26%。实际帧率500kbps 负载率1Mbps 负载率500 帧/s13%6.5%1000 帧/s26%13%2000 帧/s52%26%3000 帧/s78%39%余量怎么留业界经验是负载率控制在 30% 到 50% 之间比较稳妥。超过 50% 之后仲裁冲突导致的延迟明显上升高优先级帧的响应时间会变得不可预测。超过 70% 基本就是在赌运气一次突发重传就可能把总线堵死。工控里如果涉及运动控制报文延迟有硬要求负载率我更倾向压在 30% 以下。还有一种情况要单独考虑错误帧和重传会额外吃带宽。一旦总线上有节点频繁出错重传帧加上错误帧的 6 位错误标志和 8 位错误界定符实际负载会比理论值高不少。所以算负载率要留出这部分冗余。5.2 错误帧从哪来怎么读状态CAN 总线上的错误分五类位错误、填充错误、CRC 错误、格式错误、应答错误。这些错误在控制器里都有计数器发送错误计数器 TEC 和接收错误计数器 REC。理解这两个计数器是定位问题的关键。规则是这样的正常状态下节点是主动错误状态。当 TEC 或 REC 超过 127节点进入被动错误状态它会开始只在检测到错误时才发被动的错误标志而且自身错误不再计入。当 TEC 超过 255节点进入总线关闭状态此时它会彻底停止收发需要恢复流程才能重新上线。常见的错误来源和现象填充错误多半是位定时或采样点不匹配尤其是总线上节点采样点差异大的时候CRC 错误常见于电磁干扰强、线缆没屏蔽好的场景应答错误一般是总线上没有其他节点或者对端节点挂了位错误往往指向物理层问题比如总线短路、终端电阻不对。在 RT-Thread 驱动里错误状态一般通过状态回调上报static rt_err_t can_status_ind(rt_device_t dev, rt_size_t size) { rt_uint32_t status 0; rt_device_control(dev, RT_CAN_CMD_GET_STATUS, status); if (status RT_CAN_STATUS_BUSOFF) { rt_kprintf(CAN bus-off detected\n); } else if (status RT_CAN_STATUS_ERROR_PASSIVE) { rt_kprintf(CAN passive error\n); } return RT_EOK; }接上这个回调之后可以实时看到总线状态。调试阶段我建议再加一个周期性打印 TEC/REC 的线程把错误计数器变化记录下来。现场出问题时翻这个日志往往能看出是哪个节点在频繁出错。5.3 总线关闭后的自动恢复策略总线关闭是 CAN 里最头疼的状态处理不好会导致整个节点装死。协议规定的恢复流程是节点进入总线关闭后检测到 128 次连续 11 个隐性位就可以退出总线关闭但还要根据情况重新同步。软件上的恢复策略有几种。简单粗暴的是软件层检测到 bus-off 之后重新初始化 CAN 控制器。稍好一点的是让控制器自动恢复——很多控制器支持自动恢复检测到 128 个隐性位后自动重新上线。但要注意如果硬件故障持续存在比如线缆短路自动恢复会让节点在总线上反复进出不断产生错误帧反而拖垮其他节点。我的做法是加一个恢复次数统计短时间内多次 bus-off 就停止自动恢复报故障给上层等人工排查。这个策略在工控现场很实用因为很多时候 bus-off 是硬件故障引起的自动重连只能掩盖问题。static rt_uint8_t busoff_count 0; static void can_recover(rt_device_t dev) { if (busoff_count 3) { rt_kprintf(CAN bus-off too many times, stop recovery\n); /* 上报故障等待人工处理 */ return; } busoff_count; rt_device_close(dev); rt_thread_mdelay(100); rt_device_open(dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX); /* 重新配置波特率和工作模式 */ ... }提示总线关闭的计数最好带一个时间窗口比如 10 秒内超过 3 次才判定为硬件故障。否则偶尔一次干扰引起的 bus-off 也被算进去误报率太高。6. 现场踩坑记录与排查速查表6.1 几个反复出现的典型故障项目从调试到量产CAN 这块我踩的坑大概能分几类。第一类是发送正常但接收不到。这种情况八成是过滤器配错了比如掩码和期望的 ID 对不上或者过滤器模式配错。排查方法很简单先把过滤器全通过打开能收到就说明是过滤问题。第二类是偶发丢帧。现场负载不高但偶尔丢一两帧。这种情况要看是不是接收线程没及时读 FIFO或者信号量释放次数和实际帧数不匹配。我前面说的循环读 FIFO 就是为这个场景改的。第三类是运行几小时后通讯异常。这类问题最难查往往是错误计数器慢慢累积到被动错误甚至 bus-off。根源可能是某个节点晶振偏差大、位定时不匹配、或者现场干扰。这种问题不能只看现象要抓 TEC/REC 的变化曲线找到累积的起点。第四类是总线接了设备就通不了。先量终端电阻再看节点波特率是否一致最后看是不是有节点的位定时和别人对不上。CAN 总线对位定时很敏感节点间位定时偏差过大采样点就会对不上。6.2 排查速查表把常见现象和定位思路整理成表现场出问题时照着走现象优先排查项快速验证方法完全收不到数据过滤器、终端电阻、收发器供电打开全通过过滤器量电阻发送报错无应答总线上是否有第二个节点、ACK接第二个节点或进回环模式偶发丢帧接收线程未及时读 FIFO、队列溢出看队列水位、改循环读错误计数缓慢上升位定时、采样点、晶振精度各节点位定时统一到 75% 附近干扰强时出错屏蔽、接地、差分线走线换屏蔽线、单点接地长时间后 bus-off累积错误、硬件隐患抓 TEC/REC 曲线定位起点多节点都连不上波特率不一致、终端电阻重复逐个节点单独接上验证6.3 几条不写在手册里的经验最后分享几条实打实的经验都是手册上找不到的。第一CAN 总线的调试顺序应该从物理层开始再到驱动层最后到协议层。太多人跳过物理层直接调代码结果在软件里绕圈子。一块靠谱的 CAN 分析仪是必备的能看波形、能注入报文、能计算负载率投资回报率极高。第二CAN FD 的提速一定要先小范围验证。我见过直接在整条总线上开 2Mbps 数据段的结果一半节点通讯异常最后退回经典帧。数据段提速是节点间协同的事不能单方面开。第三接收线程的栈大小要留足。CAN 报文解析里如果用了递归或者大数组栈溢出会导致各种莫名奇妙的问题。我一般给 CAN 接收线程开 2K 以上宁大勿小。第四位定时参数不要各节点各种配。整个网络统一到 75% 到 80% 的采样点全用同一套参数是减少偶发错误最有效的办法。第五长时间运行的设备一定要有总线健康监控。周期性统计错误计数器、负载率、接收队列溢出次数这些数据是现场排查的金矿出了问题不用去现场就能大致判断方向。这套 GD32H759 加 RT-Thread 的 CAN 方案从硬件到软件我基本上已经跑通并稳定运行了几个月期间通过错误计数监控抓到过一个现场干扰源也优化过接收线程的调度策略。下一篇打算聊聊 CAN FD 的固件升级传输以及这套平台上的以太网和文件系统等我把那块调通再分享出来。