DTU 透传模式下的 Modbus 实战:三个月里每天几次”通信中断”,去看又好了——透传 DTU 根本不知道帧边界在哪

2026年10月30日 0 By admin

配电房里有三台多功能电表、一台储能逆变器,通过一台 4G DTU 接到云平台。DTU 配的是透传模式(透明传输),云端跑的是 Modbus RTU over TCP——RTU 帧原样封进 TCP,CRC 由云端自己算,DTU 只管搬字节。串口 9600 8E1。

上线三个月,攒下三个症状:

  1. 每天 3~8 次”设备离线”告警,持续几十秒,自己恢复。运维跑过去看,一切正常。
  2. 偶发 CRC 校验错,重发一次就好。日志里一周几十条。
  3. 某台表的三相电压偶尔”两相数值互换”,没人报障,是我拿现场抄表本对账时才发现的。

前两条只是吵,第三条要命——它不报错,CRC 全对,长度全对,功能码全对,只是数据是上一帧的。

三个症状查到最后是同一个根:Modbus RTU 的帧边界是一段时间,而透传链路最不能保证的偏偏是时间。

读侧的对接细节在 储能 BMS 与 PCS 的 Modbus 对接实战 里写过,写侧的权限与防呆在 Modbus 写参数的权限矩阵与防呆实战 里写过——那两篇讲的都是”应用层该怎么做”。这篇讲更下面一层:字节到底是怎么从云端走到那根 485 线上的。

DTU 是个字节管道,它不知道一帧从哪开始、到哪结束;它只知道”收到字节就往串口写”。于是所有帧边界的判断,都被推给了那条链路上最不可控的两个量:网络抖动和打包延迟。

这篇文章把这条链路上的账一笔一笔算清楚。所有数值都能用一个离线脚本复算,不引用任何”经验值”。

一、帧边界是时间,不是字节

Modbus RTU 没有任何长度字段、没有起始符、没有转义。接收端判断”一帧结束”的唯一依据是静默:总线安静超过 3.5 个字符时间,就认为上一帧完了。

规范(Modbus over Serial Line V1.02)写得很明确:

  • 帧内字符间隔超过 1.5 个字符时间,判为帧损坏;
  • 帧间静默超过 3.5 个字符时间,判为帧结束;
  • 波特率高于 19200 时,这两个值一律固定为 1.750ms,不再按字符时间算。

第三条是很多人漏掉的。用脚本把各波特率的账摊开:

$ python dtu_modbus_frame_calc.py char
     波特率     字符ms   T1.5ms   T3.5ms  T3.5@8N1   1ms tick量化       误差
    1200     9.17    13.75    32.08     29.17         33ms    2.86%
    2400     4.58     6.88    16.04     14.58         17ms    5.97%
    4800     2.29     3.44     8.02      7.29          9ms   12.21%
    9600     1.15     1.72     4.01      3.65          5ms   24.68%
   19200     0.57     0.86     2.01      1.82          3ms   49.61%
   38400     0.29     1.75     1.75      0.91          2ms   14.29%
   57600     0.19     1.75     1.75      0.61          2ms   14.29%
  115200     0.10     1.75     1.75      0.30          2ms   14.29%

9600 8E1 下,一个字符 1.146ms,T3.5 = 4.01ms。注意这是 8E1(11 bit/字符);如果从站是 8N1(10 bit),同一个波特率下 T3.5 是 3.65ms,差 10%。串口校验位配错,帧间隔阈值就跟着错 10%,这类错误在”偶尔 CRC 错”的现场里非常常见——两端都能通信,只是在边界帧上开始翻车。

再看最后两列。嵌入式 RTOS 的 tick 通常是 1ms,4.01ms 要么取 4 个 tick(欠 0.3%),要么取 5 个 tick(多等 24.7%)。取整本身不致命,致命的是这个阈值只有 4ms,比一次任务切换的抖动还小。

而 115200 那一行更值得看:如果实现按 3.5 × 字符时间 硬算,得到 0.334ms——离 1.75ms 的规范要求差了 80.9%。在 1ms tick 的系统上,这个值要么被取整成 1ms(仍然欠 43%),要么在”小于一个 tick”时永远等不到,直接退化成”不等静默”。

38400bps: 硬算 1.003ms → 1ms tick 取 2ms(规范要求 ≥1.75ms,欠 42.7%)
57600bps: 硬算 0.668ms → 1ms tick 取 1ms(规范要求 ≥1.75ms,欠 61.8%)
115200bps: 硬算 0.334ms → 1ms tick 取 1ms(规范要求 ≥1.75ms,欠 80.9%)

这就是”提高波特率之后反而更容易粘包”的机制:数据传得快了,但判帧的窗口缩得比数据还快,最后窗口小到调度器根本分辨不出来。

二、拆帧:一帧被 TCP 切成两段

透传 DTU 的行为可以用一句话概括:TCP 收到多少字节,就往串口写多少字节,中间不插任何等待。

那么问题来了:TCP 是字节流,接收端的 recv 边界和发送端的 write 边界没有任何对应关系。 上位机明明一次性 write 了 8 个字节,这段数据在中途被拆成两个 TCP 段到达,完全符合协议——路径 MTU 变了、经过了一个重组代理、或者发送端底层本来就分两次下发,都会这样。

假设一帧 8 字节的读请求被切成两段到达。DTU 收到第一段就写串口,写完串口空着,等第二段(几十到几百毫秒后)再写第二段。

从站看到的是:半截帧、然后一段 30ms 的静默、然后剩下的字节。30ms 远大于 4.01ms 的 T3.5,从站判定”前半截是一帧”。

$ python dtu_modbus_frame_calc.py split
     网络RTT      单向间隔   9600 T3.5      是否拆帧   115200 T3.5      是否拆帧
      10ms    5.00ms      4.01ms         是        1.75ms         是
      20ms   10.00ms      4.01ms         是        1.75ms         是
      50ms   25.00ms      4.01ms         是        1.75ms         是
     100ms   50.00ms      4.01ms         是        1.75ms         是
     200ms  100.00ms      4.01ms         是        1.75ms         是
     400ms  200.00ms      4.01ms         是        1.75ms         是

只要网络单向延迟超过 3.6ms,拆帧就是必然的——4G 上是常态,连跨城光纤专线也常见。

这里有个反直觉的结论,值得单独说:在 Modbus 透传链路上关掉 Nagle,会让事情变坏。

Nagle 算法会把小包攒着等 ACK,配合 TCP 的延迟确认(典型 40ms),本该分成两段的数据往往会被合并成一个 TCP 段发出——延迟变差了 40ms,但帧是完整的。一旦你为了”降延迟”关掉 Nagle(TCP_NODELAY),两个段立刻各走各的,拆帧率从 0 跳到接近 100%。用 40ms 的延迟换一个完整的帧,在 Modbus 场景里是笔划算的买卖。

真正该改的是上位机的写法:一帧 RTU 拼成一个 buffer,一次 write 出去。这是零成本的修复,但很多串口/网络库的封装层会先写帧头再写数据,文档里不会提。

拆帧之后更麻烦的是从站的恢复行为。前半截进来,CRC 必然错,多数实现会”丢弃这一帧”——但丢弃的是帧标志,不是接收缓冲。缓冲里那半截字节还在,下一帧的帧头接上去,整帧错位一格,于是”一个请求换回一串连环错位”。

正确的从站实现只有一句:CRC 错 → 清空整个接收缓冲 → 重新等 3.5 字符静默再开始收。这一句没写,拆帧就从”丢一帧”升级成”丢一片”。

三、粘包:DTU 是字节管道,它不知道帧在哪结束

拆帧的反面是粘包。两个来源:

第一,上位机 pipeline。 云端为了提速,把多个请求连着下发(不同线程读不同从站,或者一个批处理里连发三帧)。三个帧在 DTU 的 TCP 接收缓冲里首尾相接,DTU 一次性写进串口——帧间静默 = 0ms。从站收到的是 24 个连续字节,它会把这当成一帧,CRC 错;或者更糟,把第二帧的从站地址当成第一帧的数据,长度刚好对得上,CRC 也刚好过。

第二,一问一答也不能幸免。 上一帧的尾部和下一帧的头部如果在 DTU 缓冲里撞上(上一帧还没写完,下一帧已经到了),一样是零间隔。

主站帧间隔(线上) 9600bps 的 T3.5 判定
0.0ms 4.01ms 粘包
2.0ms 4.01ms 粘包
4.0ms 4.01ms 粘包
5.0ms 4.01ms 正常分帧
20.0ms 4.01ms 正常分帧

4ms 和 5ms 之间就是生死线,而这条线在 TCP 侧根本不可控——你无法在应用层保证”两帧之间一定隔 4.01ms”,因为字节到达 DTU 的时刻由网络决定。

所以结论很硬:透传 DTU 在原理上无法保证 Modbus RTU 的帧边界。这不是配置问题,是架构问题。

出路只有一条:让 DTU 会算帧长。Modbus RTU 的帧长是可推导的:

功能码 方向 帧长(字节) 判据
FC01/02/03/04 请求 8 固定
FC01/02 响应 3 + 2N 字节计数字段 N
FC03/04 响应 3 + 2N 字节计数字段 N
FC05/06 请求/响应 8 固定
FC15/16 请求 9 + 2N 字节计数字段 N
FC15/16 响应 8 固定
任意 异常响应 5 功能码 bit7 置位
广播(地址 0) 请求 按功能码 无响应,不能等

有了这张表,DTU 就能做一个确定性的状态机:收到帧头 → 按功能码定帧长 → 收满才写串口 → 帧间强制插入 4 个字符的静默。帧边界从”猜时间”变成”数字节”。

这就是“Modbus 网关”和”透传 DTU”的唯一本质区别——不是协议转换(RTU 到 RTU 有什么可转的),是帧同步。市面上叫”Modbus 网关模式””协议感知模式””主动采集模式”的,本质都是这一层状态机。

关键结论:整帧缓冲不增加延迟。 请求帧本来就要收满 8 字节才能发;响应帧按字节计数收满才写——这一层是纯状态机,零额外等待。唯一的成本是帧间那 4 个字符:

额外延迟 = 4×1.15ms = 4.58ms
端到端   179.81 → 184.40ms(+2.5%)

4.58ms,2.5%。 用一个帧间静默换掉一整类帧边界问题,这是这条链路上性价比最高的一次改动。

四、端到端延迟预算:大头痛不在 485

很多人调这条链路的第一个动作是”把波特率提到 115200″。先看预算表。

读 10 个寄存器,请求 8 字节、响应 25 字节,9600 8E1:

场景 网络往返 DTU 收转 请求帧 从站处理 响应帧 DTU 打包 重传 合计
典型(RTT 50ms,打包 50ms) 50.00 2.00 9.17 20.00 28.65 50.00 0.00 159.81
p99(RTT 400ms,打包 50ms) 400.00 2.00 9.17 50.00 28.65 50.00 0.00 539.81
p99 + 1 次重传(RTO 200ms) 400.00 2.00 9.17 50.00 28.65 50.00 200.00 739.81
p99 + 2 次重传(200+400ms) 400.00 2.00 9.17 50.00 28.65 50.00 600.00 1139.81
打包 200ms 的默认配置 50.00 2.00 9.17 20.00 28.65 200.00 0.00 309.81

(单位 ms。dtu_modbus_frame_calc.py budget 可复算。)

看清楚这几件事:

  • 串口那两帧加起来 37.81ms,只占 159.81ms 的 23.7%。
  • DTU 打包间隔 50ms,比整条 485 上的收发时间还长。
  • 把波特率从 9600 提到 115200,端到端只从 159.81 降到 125.15ms——降 21.7%。
  • 把 DTU 打包间隔从 50ms 改到 10ms,端到端从 159.81 降到 119.81ms——降 25.0%。

改一个 DTU 的配置项,比把所有从站波特率改成 115200、重新拉一遍布线还管用。 而且后者还要承担改错校验位的风险。

那个打包间隔到底在干什么?DTU 上行(设备→云)时不是收到一个字节就发一个 TCP 包——那样一个 25 字节的响应会拆成好几个小包,流量和开销都受不了。它要攒:等到”打包间隔到”或者”打包长度满”才封包发出。

打包间隔 打包长度 25B 响应等待 触发方式
10ms 1024B 10ms 等满间隔
50ms 1024B 50ms 等满间隔
200ms 1024B 200ms 等满间隔
200ms 32B 0ms 长度先触发

Modbus 响应永远小于 256 字节,所以”打包长度”这一项在 Modbus 场景里几乎永远是”等满间隔”那一支。 出厂默认 1024B,等于这一项从来没起过作用,起作用的只有打包间隔——而它直接加在每一次事务的端到端延迟上。

顺带说一句出厂默认 200ms 的那一档:典型条件下端到端就是 309.81ms。主站超时要是按”本地串口经验”设 300ms,这个配置下永远超时。

五、超时怎么定:p99、重传、每天几次误判

超时是这条链路上最容易被拍脑袋定的参数。现场的分布是这样的:

典型=159.81ms  p99=539.81ms  p99+1重传=739.81ms  p99+2重传=1139.81ms

按 4G 的丢包率(0.5%~2%/方向)算,一个事务涉及两个方向,丢一次由 RTO 200ms 补回,丢两次补回需要 200+400=600ms:

超时设定 能覆盖到 误判 p=0.5% 误判 p=1% 误判 p=2%
300ms p99 都不够 — — —
500ms p99 都不够 — — —
600ms 无重传 172.4/天 343.9/天 684.3/天
800ms 1 次重传 0.4/天 1.7/天 6.9/天
1000ms 1 次重传 0.4/天 1.7/天 6.9/天
1200ms 2 次重传 ≈0 ≈0 ≈0

(每日事务数按 5s 一轮、单设备算,17280 次/天。)

1000ms 这个”看起来很美”的默认值,刚好卡在刀口上:能覆盖一次重传,覆盖不了两次。 丢包率 1% 时每天约 1.7 次误判,2% 时每天 6.9 次。

这就是现场那句”每天总有几次通信中断告警,去看又好了”的准确来源。不是设备坏了,是超时贴着 p99 设,长尾全被判成了故障。

500ms 以下在 4G 上直接不可用——p99 就已经 540ms。很多网关出厂默认 300ms,那是给本地串口主站设计的,照搬到 4G 上必然大面积误报。

但也别一味加大。超时越长,一次真故障吃掉的轮询预算越多:

设备数 墙钟合计 5s 周期占用 10s 周期占用 撞上一次 1s 超时后
10 1.60s 31.96% 15.98% 51.96%
20 3.20s 63.92% 31.96% 83.93%
31 4.95s 99.08% 49.54% 119.08%
100 15.98s 319.62% 159.81% 339.62%

5s 周期在 31 台设备时就是 100% 满载。 满载系统的特征是:任何一次 1s 超时都让整轮滑出周期,下一轮立刻追尾,然后全员超时——雪崩。

工程上留 40% 余量(占用 ≤60%)不是保守,是底线。按这个标准,5s 周期最多接 18 台。

推荐配置:超时 = p99 × 1.5 ≈ 800~1200ms;重试 2 次、退避 500ms;重试前必须做重同步(下一节)。 另外,超时下限还有一个硬约束:不能小于最长响应帧的传输时间。9600bps 下 256 字节的满长帧要 293.33ms,读 100 个寄存器的响应是 234.90ms——超时设 200ms,这种请求永远读不回来。

六、错位:最危险的错误是”CRC 全对”

现在说第三条症状,也就是那个”三相电压偶尔互换”。

Modbus RTU 没有事务号。两帧之间唯一的区分是长度和功能码——两次 FC03 读同样数量的寄存器,帧长完全相同,物理上没有任何办法区分。

于是这个场景成立了:主站发出请求 A,超时(1000ms),立刻发请求 B;请求 A 的响应在 1140ms 时终于到达——此时主站正等着 B 的响应,它把这帧 CRC 完全正确、长度完全正确、功能码完全正确的数据,当成了 B 的答案。

$ python dtu_modbus_frame_calc.py desync
    丢失次数   重传退避    实际到达    相对超时          落到哪一帧
       0      0.00ms   539.81ms   -460.19ms    本帧内(正常)
       1    200.00ms   739.81ms   -260.19ms    本帧内(正常)
       2    600.00ms  1139.81ms    139.81ms    下一帧 +140ms(错位)
       3   1400.00ms  1939.81ms    939.81ms    下一帧 +940ms(错位)

四种结局里有两种落在”下一帧的等待窗内”。 而且这不是小概率——丢两次包在 1% 丢包率下每天就有 1.7 次,三个月下来一百多次,完全够把”偶发”变成”对账时才发现”。

什么时候是静默错误? 看轮询序列的构成:

  • 如果序列是 Ua → Ia → P,错位后电压位置显示电流值(220 变成 5.2),一眼假,人工能看到。
  • 如果序列是 Ua → Ub → Uc 三次独立事务,错位后 Ub 位置显示 Ua 的值。三相电压差通常不到 2%——没有任何告警会响。

错位最危险的地方,恰恰是同类数据之间的错位。

修复只有两条路,都不贵:

路一:应用层带事务标记。 设备侧提供一个递增的”通信计数”寄存器(每次收到合法请求 +1),主站每次读数据块时顺带读它,校验是否连续。多花 2 个字节,能抓 100% 的错位。这个寄存器几乎所有电表都有(叫”通信次数””帧计数””运行计数”之类),只是很少有人用。

路二:超时后重同步。 丢弃接收缓冲 → 发一个固定读请求 → 连续两次返回同一值才恢复轮询。代价是一次重同步约 2 个事务时间(320ms),比一个静默错误便宜两个数量级。

还有一个更物理的坑:半双工总线冲突。485 是半双工的,DTU 自己控制 DE 引脚。主站超时重试时,从站的响应可能还在路上(DTU 正在往串口写),此时下一个请求到达,DTU 立刻切发送——两边同时驱动总线,数据全毁。透传 DTU 不知道从站什么时候发完,它只能靠串口静默判断;而网络延迟恰好让”下一个请求”在”上一个响应”中途到达。

这个现象的表征是”连续几帧全错,然后突然好了”,很容易被误判成干扰。根治办法还是同一个:让 DTU 知道帧边界,收满一帧才发下一帧。

七、心跳:连接还在,路已经没了

还有一个独立的坑,跟帧边界无关,但症状很像”设备离线”。

4G 网络里,运营商的 NAT 表项有超时——典型 60~300 秒。而 Linux 的 TCP keepalive 默认是 7200 秒。

场景 NAT 表项 TCP keepalive 建议心跳 余量
家宽 / 企业出口 300s 7200s 30s 10.0x
运营商 4G(典型) 180s 7200s 30s 6.0x
运营商 4G(保守) 60s 7200s 30s 2.0x
部分物联网专网 30s 7200s 15s 2.0x

keepalive 是 NAT 表项的 24~120 倍。 表项被回收后,两端都认为”连接已建立”:主站发数据没人应,TCP 会一直重传直到判死,这个过程可能几十分钟。期间所有请求都在往一个死连接里灌。

现场表现:“设备白天正常,晚上掉线”或者”断线间隔特别规律”——因为夜里的空闲期更长,更容易撞上 NAT 回收。

修法:应用层心跳,间隔取 NAT 超时的 1/2,最多 30s。6 字节心跳 × 2880 次/天 = 17.3KB/天,流量可以忽略。但它会把 4G 模组从空闲态唤醒,Cat.1 平均多耗约 1.3mA——市电设备无所谓,电池设备要把这笔账算进去。

“连接还在、路已经没了”这类问题不是 Modbus 独有的,MQTT over TLS 实战 里那套长连接保活的思路可以整段平移过来——协议换一个,NAT 不讲情面的程度一样。

八、吞吐:485 很空,轮询很慢

最后算一笔吞吐账,因为它决定了这套系统能接多少台设备。

$ python dtu_modbus_frame_calc.py poll
请求帧 9.17ms + 响应帧 28.65ms + 2×静默 8.02ms = 45.83ms(总线占用)
端到端墙钟 159.81ms
总线利用率 = 28.7%  ← 485 有 70% 的时间在等网络

485 上 71% 的时间是空的,但轮询就是快不起来,因为墙钟被网络和打包间隔占满了。

除了硬扛这个墙钟,还有两条路,按性价比排:

方案 单次墙钟 100 个寄存器的总耗时 相对收益
A:10 次「读 10 个」 159.81ms 1598.12ms 基准
B:1 次「读 100 个」 446.06ms 446.06ms 3.58 倍
C:网关本地轮询,云端读缓存 55ms — 2.9 倍(且不受网络抖动影响)

(B 方案响应帧 205 字节,线缆时间 234.90ms。)

合并读是最大的一笔:把 10 次小事务合成 1 次大事务,省掉的不是串口时间,是 9 次网络往返和 9 次打包等待。上限是 RTU 的 256 字节 ADU——响应 205 字节对应 100 个寄存器,想到 120 个以上就得拆两次或者用 FC23。

方案 C 是架构级解法:让 DTU 兼做 Modbus 主站,本地按固定周期轮询,云端只读网关缓存。云端延迟降到 55ms,而且总线不再受网络抖动影响——网络断了,本地采集照常,恢复后补传。代价是网关要维护一份点表和缓存,复杂度上一个台阶;缓存怎么掉电不丢、擦写寿命怎么算,是 Flash 存储与参数管理实战 里那笔账。

九、落地清单与排查表

上位机侧:

  • ☐ 一帧 RTU 拼成一个 buffer 一次 write,不要分两次下发
  • ☐ 不要为了降延迟关 Nagle——它会提高拆帧率
  • ☐ 超时 ≥ 最长响应帧传输时间(9600 下满帧 293ms),并按 p99×1.5 设
  • ☐ 重试 2 次、退避 500ms,重试前必须重同步
  • ☐ 读数据块时同时读”通信计数”寄存器,校验事务连续性
  • ☐ 应用层心跳 ≤30s,不依赖 TCP keepalive

DTU 侧:

参数 出厂常见 建议 理由
工作模式 透传 Modbus 网关(帧同步) 帧边界靠算不靠猜
打包间隔 50 / 200ms 10~20ms 直接加在每事务延迟上
打包长度 1024B 保持 ≥256B 或关闭 Modbus 帧永远小于 256B,永不触发
注册包 跟随数据 独立发送 / 仅首次 混入数据流会被当帧头
心跳 关闭 30s NAT 表项 60~300s
串口校验位 8N1 与从站严格一致 8N1/8E1 差 10% 字符时间,T3.5 跟着变

从站固件侧(如果你是做设备的那一端):

  • ☐ CRC 错 → 清空整个接收缓冲,重新等 3.5 字符静默
  • ☐ T3.5 在 >19200bps 时按 1.750ms 固定值判,不要按字符时间硬算
  • ☐ 提供递增的通信计数寄存器
  • ☐ 半双工 DE 释放留 1 个字符的余量,别在最后一个停止位就切

现场排查表:

现象 最可能根因 一条验证路径
偶发 CRC 错、重发就好 拆帧 / 粘包 抓串口原始字节,看帧间静默是否 <3.5 字符
每天几次通信中断告警 超时贴 p99 统计超时时长分布,看是否集中在 1.0s 附近
读回值在合理范围但不对 响应错位 连续读同一寄存器 10 次,看是否跳变
三相电压某一相偶尔像另一相 同类数据错位 加通信计数寄存器校验连续性
白天正常、晚上掉线 NAT 表项超时 看断线间隔是否固定,加 30s 心跳
上电后第一帧总是错 注册包混入数据流 关掉”注册包跟随数据”
读 100 个寄存器必超时 响应帧 234ms > 超时 超时 ≥ 帧长×字符时间 + 网络预算
提高波特率没变快 瓶颈不在串口 拆端到端预算,看打包间隔与 RTT
多主站时整段卡死 半双工总线冲突 DTU 是否在从站响应中途抢发
换 DTU 就好、换回来又坏 透传 vs 网关模式 看 DTU 有没有帧长解析状态机

回头看那三个月:每天几次的”离线”告警,是超时贴着 p99 设出来的;偶发的 CRC 错,是 TCP 分段让 DTU 把一帧拆成两段转发;而那个真正危险的”两相电压互换”,是超时重试把迟到响应喂给了下一帧——三个症状,一个根:透传链路没有帧边界的概念。

改完之后是这样的:DTU 换成网关模式(整帧缓冲 + 帧间 4 字符静默),打包间隔 50→20ms,上位机改成单次 write,超时 1000→900ms 并加重同步,读块时顺带校通信计数。端到端延迟从 179.81ms 变成 184.40ms,慢了 2.5%;三个症状全部消失。

这笔交易的关键在于:2.5% 的延迟,换掉了一整类”CRC 全对但数据是上一帧”的错误。在工业现场,宁可慢一点,也不能错得无声无息。

下一篇预告:帧边界这笔账算完,剩下的是设备端怎么把它落实成几行代码。USART 的 IDLE 中断到底能不能顶替 T3.5 定时器、DMA 循环缓冲的读写指针怎么防撕裂、CRC 错之后为什么必须先关 DMA 再清标志、DE 引脚在最后一个停止位上切换会切掉什么——《Modbus RTU 从站接收状态机实现:IDLE 中断省掉了一个定时器,也顺手省掉了半帧数据》,把这几行代码的时序拆开看。


本文所有数值由离线脚本 dtu_modbus_frame_calc.py 复算(29 项自检全过),场景数据来自现场复盘,时间参数按 9600 8E1 取值。


本文为个人工程实践记录,具体设计请以所用芯片手册与协议规范为准。