本站为个人非经营性网站,仅用于技术学习与分享,不提供任何商品、服务、报价或收费咨询。 合规信息

单路隔离 RS485 诊断小板:
把偶发故障变成可复现的问题

RS485 现场最让人头疼的不是“通信不通”,而是“偶尔不通,去了就好了”。 这块板子的目标只有一个:在故障发生的那一刻,把现场留下来。 这篇记录它的设计思路、几个反直觉的硬件约束,以及我认为现场该保留哪些证据。

RS485 Modbus 现场诊断 隔离 已发布
已发布 · 个人学习项目

单路隔离 RS485 诊断小板

一块挂在 RS485 总线上的被动监听节点:不发送任何数据, 只记录总线上每一个字节和它与前一个字节之间的时间间隔。 通过间隔时间就能把字节流还原成一帧一帧的报文,进而统计超时、CRC 错误、 异常码、应答延迟与重试次数,并在超出阈值的时刻把前后原始报文落盘保存。 目前已完成方案设计与固件的帧重组部分,硬件在打样验证阶段。

PLATFORM HC32F030 / Cortex-M0+
STACK 被动监听 + 字节间隔时间戳 + 环形记录
TOOLS Keil MDK · 示波器 · USB-RS485 对照
STATUS 方案与固件帧重组已完成 · 硬件打样中

一、为什么需要一块“只听话”的板子

排查 RS485 通信问题,通常的手段是用电脑加一个 USB 转 485 转换器接上去抓包。 这个办法在实验室里很好用,在现场有两个问题:

  • 电脑不方便长期挂在那里。偶发故障可能几天才出现一次, 而人不可能在配电柜旁边守三天。
  • USB 转换器的软件层会把一些细节吃掉。大多数串口助手只给你“收到了什么”, 不给你“两个字节之间隔了多久”——而后者恰恰是判断波特率不匹配、 总线冲突、发送方被中断打断这类问题的关键证据。

所以这块板子的定位很明确:一个能长期挂着、只记录不干扰、并且记录字节级时间信息的节点。 它不替代协议分析仪,它解决的是“故障发生的那一刻没人在场”这个问题。

二、三个反直觉的硬件约束

设计这块板子的时候,我踩到的坑都不是“电路画错了”,而是“想法本身有问题”。 一个诊断工具如果自己改变了被测对象,那它测出来的就不是原来的问题了。

2.1 监听节点必须“永远不会”驱动总线

RS485 是半双工,收发共用一对差分线。正常情况下固件靠一个方向控制脚 (通常标记为 DE/RE)在收和发之间切换。但在诊断板上,这个脚必须被硬件固定在接收状态, 而不是“由固件保证不去发送”。

原因很直接:如果方向脚由固件控制,那么固件跑飞、复位期间引脚处于高阻或上拉状态、 或者调试时不小心执行了一次发送,这块板子就会在总线上乱说话—— 在一个本来就有通信问题的现场,再插入一个乱说话的节点,问题会立刻变得更复杂。

/* 诊断板的方向控制:不接 MCU,硬件直接下拉到接收态 */
/*
 *  收发器 DE/RE 引脚 ——  10kΩ 下拉到 GND
 *  (低电平 = 接收使能、发送禁止)
 *
 *  绝不要把 DE/RE 接到 MCU 的 GPIO 上,
 *  哪怕固件里"保证"不会置高。
 *  这是一个硬件层面的互锁,不是软件约定。
 */

2.2 隔离要隔两样东西,不只是信号

只给 A/B 信号线加隔离是不够的。真正要断的是地环路: 如果诊断板和被测设备分别接在不同的供电回路上,两个“地”之间存在电位差, 这个电位差会通过信号地形成环流,轻则引入噪声、重则损坏接口。

所以隔离方案必须同时包含:

  • 信号隔离:数字隔离器或光耦,把 MCU 侧的串口信号与总线侧隔开;
  • 电源隔离:隔离型 DC-DC,让总线侧那一小部分电路有自己的浮地电源。

只做信号隔离、电源仍共地,等于没做——这是我在看别人的设计时发现的一个常见误解。

2.3 不要随手再加一个终端电阻

RS485 总线两端各有一个 120 Ω 终端电阻,这是常识。但“再加一个节点”的时候, 很容易顺手把诊断板也做成“带 120 Ω 终端”的设计——结果是总线上多并了一个 120 Ω, 总线负载从 60 Ω 变成 40 Ω,驱动能力不足的收发器可能因此通信质量变差。

诊断板应该做成高输入阻抗、终端电阻可用跳线断开的形式, 并且默认不接。如果要接,也要先确认原总线的终端位置。 同样地,节点的输入电容越小越好——多一个节点就多一份负载, 而这个负载是诊断工具自己引入的。

一条总线上挂主机与若干从站,诊断板作为只监听节点并联接入,用虚线框标出隔离边界,并标注“方向脚硬件固定在接收”
图 1 · 诊断板接入位置图

三、核心:用“字节间隔”把字节流切成帧

被动监听拿到的是一个连续的字节流,没有帧的边界。而 Modbus RTU 的帧边界是 由时间定义的——标准规定帧内字节间隔不能超过 3.5 个字符时间, 超过就认为是新的一帧开始。

所以诊断板的核心机制很简单:给每一个字节打一个时间戳,记录它与前一个字节的间隔。 有了间隔,帧边界就自然出来了:

/* 字节级记录:只存"间隔"而不是"绝对时间",可以大幅压缩数据量 */
typedef struct {
    uint8_t  data;        /* 收到的字节 */
    uint16_t gap_us;      /* 与上一个字节的间隔,单位微秒(上限截断) */
} byte_record_t;

/* 帧边界的判定:间隔超过 3.5 个字符时间就切帧 */
/* 9600 bps、8N1 时一个字符 10 bit,字符时间约 1.04 ms,3.5 字符约 3.65 ms */
#define CHAR_TIME_US(baud)   ((10000000UL) / (baud))          /* 10 bit / 波特率 */
#define FRAME_GAP_US(baud)   ((CHAR_TIME_US(baud) * 7UL) / 2UL)  /* 3.5 字符 */

static uint8_t is_frame_boundary(uint16_t gap_us, uint32_t baud)
{
    return (gap_us > FRAME_GAP_US(baud)) ? 1u : 0u;
}

为什么存“间隔”而不是“绝对时间”?因为间隔的数值范围很小—— 正常帧内间隔只有几十到几百微秒,用一个 16 位字段就能覆盖(超出就截断), 而绝对时间戳需要 32 位甚至 64 位。同样的存储空间,存间隔能多记一倍以上的字节。

两帧报文的字节流,每个字节下方标出间隔时间戳,用竖线标出帧边界判定结果
图 2 · 字节间隔记录示意图

四、从字节流到“问题报告”

仅有原始字节流还不够用——现场最需要的是结论。 所以固件在切帧之后还要做一层统计与判定。我打算统计的量如下:

统计项怎么算指向什么问题
帧间隔分布 每两帧之间的时间差 轮询周期是否稳定;是否有设备长时间占着总线
请求→应答延迟 请求帧结束到应答帧开始的时间 从站响应是否超时;主站超时时间设置是否太紧
无应答次数 发出请求后超时仍无帧出现 从站掉线、地址冲突、总线被拉死
CRC 错误次数 按协议校验每一帧 干扰、终端电阻不当、线缆过长、波特率偏差
异常码分布 统计异常应答的功能码与异常码 功能码不支持、地址越界、参数被写坏
帧内最大间隔 单帧内相邻字节的最大间隔 发送方被中断打断、DMA 断流、缓冲区不足
重试次数 相同请求连续出现的次数 主站的超时与重试策略是否合理
“只有一个字节”事件 出现单字节帧且其后长时间静默 接收方串口进入异常状态,见下文

最后一项是我特意加的,因为它对应我在另一块板子上真实遇到过的一个故障: 接收方在异常之后只能收到 1 个字节,之后再无数据。 这个现象在总线上看起来就是“主站问了,从站只吐了一个字节就哑了”, 如果没有单独统计,很容易被当成随机的 CRC 错误糊过去。

五、现场该保留哪些证据

这是我做这块板子过程中最大的收获:排查偶发故障,关键不在于“看到了什么”, 而在于“当时保留了什么”。 等故障过去、人到了现场,一切已经恢复正常, 能依靠的只有当时留下的记录。

5.1 我会保留的七类证据

  1. 原始报文(含字节间隔),故障前后各若干帧,不是只留出错那一帧。 因为原因往往在故障之前就埋下了。
  2. 时间锚点:设备开机时长 + 一个粗粒度的绝对时间(来自 RTC)。 只有这样,“昨天下午三点左右出的问题”才能对上号。
  3. 设备侧的状态:如果条件允许,从站自己记录复位原因、故障码、 参数校验结果。总线上看不到的东西,只能靠设备自己说。
  4. 电气层面的观察:用示波器看差分电平幅度、上升沿、 空闲时的共模电压。通信问题时,示波器往往比串口助手有用得多。
  5. 环境与工况:出故障时是不是有大功率设备在启停、 是不是在某个温度点、是不是刚有人动过线。这类信息只能靠现场记录。
  6. 总线拓扑:节点数、终端电阻位置、线缆长度与走向、屏蔽层怎么接的。 很多“偶发”问题其实是拓扑问题,一直在那里,只是偶尔才表现出来。
  7. 对照实验的结论:换线之后好了没有、降波特率之后好了没有、 减少节点之后好了没有。这些结论比任何单次记录都有价值。

六、记录怎么存:环形缓冲 + 事件快照

长期挂机记录会产生大量数据,不可能都存下来,所以需要两级策略。

6.1 第一级:常驻的统计量

上面那张表里的统计项都是计数器,每个只占几个字节, 掉电前写一次存储即可。它们告诉你“有没有问题”,但不告诉你“问题长什么样”。

6.2 第二级:事件触发的原始记录快照

在 SPI Flash 里开一个环形区域,平时只记录最近一段时间的原始字节流, 写满就覆盖最旧的数据。当统计量越过阈值(例如 CRC 错误连续出现、 或出现“只有一个字节”事件)时,锁定当前这段记录并单独保存一份, 这样故障前后各一段报文都被留下来。

/* 两级记录:环形区一直滚,命中事件时把当前窗口另存为快照 */
typedef struct {
    uint32_t total_frames;      /* 累计帧数 */
    uint32_t crc_errors;        /* CRC 错误累计 */
    uint32_t timeouts;          /* 无应答累计 */
    uint32_t short_events;      /* "只有一个字节"事件累计 */
    uint32_t max_gap_us;        /* 观察到的帧内最大字节间隔 */
} diag_stats_t;

/* 命中任一条件即触发快照(阈值由上位机配置,不写死在固件里) */
static uint8_t trigger_hit(const diag_stats_t *s, const diag_stats_t *prev)
{
    if ((s->crc_errors - prev->crc_errors) >= crc_err_burst_th)   return 1;
    if ((s->timeouts   - prev->timeouts)   >= timeout_burst_th)   return 1;
    if ((s->short_events > prev->short_events))                   return 1;
    if (s->max_gap_us >= gap_th_us)                               return 1;
    return 0;
}

阈值刻意做成由上位机配置而不是固件里的常量。 原因是不同现场的总线负载差别很大,同一个阈值在这个现场天天触发、 在另一个现场永远不触发。做成可配置之后,一个固件就能适配不同场合。

七、问题与解决

下面几条是设计过程中遇到并解决的,属于“现象 → 原因 → 办法”的记录。

八、局限与还没做完的部分

  • 硬件还在打样验证阶段。目前完成的是方案设计与固件中的 帧重组、统计与记录格式部分,隔离与总线负载这些硬件特性还需要实测确认。
  • 只支持单路。现在的设计是一条 RS485 总线, 如果现场有多条总线同时需要记录,得插多块板子,时间基准无法统一。
  • 时间戳的精度还没实测。用 MCU 定时器打时间戳, 理论分辨率够用,但中断响应抖动会引入误差,需要用示波器标定一下实际误差范围。
  • 没有做总线的电气量采集。差分电平、共模电压这些只能靠示波器, 板子上没有加这方面的前端。将来如果做第二版,会考虑加一个简单的差分电压监测。
  • 上位机还没有。现在只能把原始记录导出来手工分析, 没有自动生成“问题报告”的工具。这部分工作量其实不小。
  • 没有校准过的波特率自适应。目前需要手动配置波特率, 如果配错了就只能看到一堆垃圾数据,不能自动识别。

最后一条其实是个挺实际的问题:现场排查时,第一件要做的事往往就是“确认波特率到底是多少”。 如果这块板子能自动测出总线的波特率,那它单独就已经很有用了。

参考资料与说明

  • Modbus 应用协议规范中关于 RTU 传输模式、帧间隔(3.5 字符时间)与异常码的定义。
  • TIA/EIA-485 串行通信接口标准中关于差分电平、终端匹配与总线负载的通用要求。
  • 所用 MCU 参考手册中关于定时器输入捕获与时基精度的章节。
  • 本文涉及的平台型号、设计参数与进度描述均为个人学习项目中的实际状态; 文中示例代码为按理解重写的片段,不代表任何产品或交付代码。
  • 文中芯片与器件型号仅用于说明技术方案,与相关厂商无隶属或授权关系。