[工业自动化] GD32 USART 的 IDLE 中断把 ModBus 帧拆成了两半

[复制链接]
10|0
小海师 发表于 2026-9-2 14:01 | 显示全部楼层 |阅读模式
> 一句话结论:GD32  的 IDLE 中断在线路空闲 ≥1 个字符时间(约 87µs@115200)就触发,
> 比 ModBus  协议允许的帧内最大字节间隔(1.5 字符时间,约 130µs)更灵敏。
> 任何字节间隙落在 87~130µs 之间,ModBus 规则认为"帧还没完",GD32 硬件却认为
> "线路空了,帧结束了"——帧被拦腰截断 ,CRC 必挂,设备***不回包。
> 解法:关掉 IDLE 中断,改用软件字符超时判帧。

---

## 一、背景

产品原方案用 TI TM4C123,正在国产化移植到 GD32F427。上位机是我们的 Qt  程序
(串口轮询),通过 RS-232(MAX3232,USART0,115200-8-N-1)走 ModBus RTU 协议
访问设备。

移植完固件后,上位机始终"连不上设备":发 `01 A2 00 00 00 00 F8 13`(0xA2 读实时数据),
设备不回包,上位机 1 秒超时报"未收到设备响应"。

## 二、排查:把两层剥开,一次只测一层

问题可能出在 **USART 层**(物理收发)或 **ModBus 层**(解析/CRC/判帧)。
一开始两层一起查,定位非常模糊。改成严格分层:先证明 USART 层干净,再查 ModBus 层。

### 第一步:回显测试,证明物理层干净

在接收回调里加一个编译开关,收到什么原样回显:

#define USART_ECHO_TEST 1   // 置1=回显模式(只测USART), 置0=正常ModBus路径

static void modbus_rx_byte(uint8_t data)
{
#if USART_ECHO_TEST
    HWL_USART_SendByte(USART0, data);   // 裸回显, 完全绕过解析/CRC
#endif
    ModBus_RDataTmp[ModBus_RAmountTmp] = data;
    ModBus_RAmountTmp++;
}




```

发 `01 A2 00 00 00 00 F8 13` 八字节,设备原样返回八字节,**一字不差**。
物理层(USB-232 线 → MAX3232 → USART 引脚 → RBNE 中断 → 回调)彻底排除。

### 第二步:走 debug 抓现场,不要猜

在 `ModBusHaveNewMessage()` 打断点,Watch 窗口看变量:

```
ModBus_RAmount = 2          ← 只收到 2 个字节?!
ModBus_RData   = 01 A2      ← 半截帧
```

收到的不是完整 8 字节帧,而是被**切断**的 2 字节。帧被截断 → CRC 校验必失败 →
按协议正确"静默拒绝" → 上位机***等不到响应。**设备没死,是根本没收到完整帧。**

### 第三步:先验 CRC,别急着怀疑表

ModBus 不通先怀疑 CRC 是老习惯。这次先把固件的 crc.c 查表实现用 Python 1:1 复现,
对整帧计算:
# 输出 0x0 —— 整帧(含CRC)校验和为 0, 完全符合 ModBus Spec 附录

def crc16(data):
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return crc

print(hex(crc16(bytes.fromhex('01 A2 00 00 00 00 F8 13'))))






**CRC 实现正确**,问题回到"帧为什么被切断"。

## 三、根因:IDLE 中断的灵敏度比 ModBus 允许的帧内间隔更严

ModBus RTU 对时序有两条硬规定:
- 帧内相邻字节最大间隔:**1.5 字符时间**(@115200 ≈ 130µs),超过则本帧不完整
- 帧间最小间隔:3.5 字符时间(≈ 304µs)

GD32 USART 的 **IDLE 中断**:收到第一个字节后,只要线路空闲满 **1 个字符时间**
(@115200 ≈ 87µs)就触发。

```
1 字符时间      = 87µs   (GD32 IDLE 中断触发阈值)
1.5 字符时间    = 130µs  (ModBus 允许的帧内最大字节间隔)
                   ↑
             87 < 130 —— 致命不等式
```

任何一个字节间隙落在 87~130µs 之间,ModBus 规则说"这帧还没完",GD32 硬件却说
"线路空了,一帧结束"。IDLE 一触发,帧就被拆。

而 USB-232 适配器恰恰是产生这种间隙的元凶:USB 虚拟串口走的是**批量传输**,
字节不是匀速流出的,由主机 USB 调度决定每批到达时刻,字节间隙天然带抖动,
实测经常 >87µs → IDLE 在帧中间触发 → 半截帧。

## 四、为什么 TI 版没这个问题?(移植时最容易被忽略的差异)

TI TM4C123 的 UART 用的是**接收超时中断 RT(Receive Timeout )**——硬件检测到
连续 **32 个 bit 时间**无新字节才触发。@115200:

```
32 bit 时间 = 32 × 8.68µs ≈ 277µs
```

对比两个数字就明白了一切:

| | 触发阈值 | vs ModBus 帧内上限(130µs) | 结果 |
|---|---|---|---|
| TI TM4C123 RT | 32 bit ≈ **277µs** | ✅ 大于 | 从不拆帧 |
| GD32 IDLE | 1 字符 ≈ **87µs** | ❌ 小于 | 帧中被触发 |

TI 的默认硬件阈值**恰好落在 ModBus 允许的时序窗口里**(大于 130µs、小于 304µs),
所以 TI 版从没暴露过这个问题。移植到 GD32,`IDLE` 和 `RT` 都是"判帧尾",但阈值
天差地别——**同名中断,语义不同**,这就是"TI 能跑、GD32 不能跑"的全部秘密。

## 五、解决:扔掉 IDLE,用软件字符超时判帧

GD32 没有 TI 那种可配阈值的 RT 中断。方案:**关掉 IDLE 中断,只留逐字节的 RBNE
中断,帧尾交给软件计时**——每收到一字节重置一个 1ms 计数器,连续 2ms 没有新字节,
判为一帧结束。

为什么选 2ms:
- 是 ModBus 帧内允许间隔(130µs)的十几倍,任何合法字节间隙都不会误判
- 又能容忍 USB-232 适配器的字节间隙抖动(几百 µs ~ 1ms)
- 响应延迟无感:ModBus 请求-响应模式下,上位机本来就要等 ≥3.5 字符(304µs)
  才认为收到完整响应,2ms 完全在协议语义内

### 代码实现

接收回调里,收到字节就重置计时器:

#define MODBUS_RX_TIMEOUT_MS  2    // 连续 2ms 无新字节 = 帧尾

static void modbus_rx_byte(uint8_t data)
{
    ModBus_RDataTmp[ModBus_RAmountTmp] = data;   // 逐字节入缓冲
    ModBus_RAmountTmp++;
    ModBus_RxTicks = 0;                          // ★ 收到新字节, 重置超时计时
}



SysTick(1ms)里判帧尾:


void ModBus_RxTick(void)      // SysTick_Handler 每 1ms 调一次
{
    ModBus_RxTicks++;
    if (ModBus_RxTicks >= MODBUS_RX_TIMEOUT_MS && ModBus_RAmountTmp > 0) {
        __disable_irq();                        // 临界区保护搬移(见下)
        ModBus_RAmount    = ModBus_RAmountTmp;
        ModBus_RAmountTmp = 0;
        memcpy(ModBus_RData, ModBus_RDataTmp, ModBus_RAmount);
        ModBus_HaveNewData = 1;                 // 置标志, 通知主循环
        __enable_irq();
    }
}



中断配置只使能 RBNE,不再使能 IDLE:

usart_interrupt_enable(USART0, USART_INT_RBNE);   // 只留逐字节中断
// usart_interrupt_enable(USART0, USART_INT_IDLE); // IDLE 弃用, 帧尾交给软件判


### 一个必须处理的细节:中断优先级抢占

SysTick 的优先级(0,0)高于 USART0(1,0),会**抢占** RBNE 中断。如果帧尾搬移期间
被打断,新到的字节会写进刚清空的临时缓冲,破坏下一帧。所以搬移动作用
`__disable_irq() / __enable_irq()` 包成临界区(40 字节 memcpy 约 1µs@120MHz,
开销可忽略)。

## 六、验证

改完烧录,还是同一个 USB-232 适配器,发 `01 A2 00 00 00 00 F8 13`:

```
收 ← 01 A2 00 00 00 11 7E 00 78 00 D8 02 00 ... 00 3B 71   (25 字节)
```

- 帧头/功能码/数量完整:01 A2 / 0000 / 0011(17 字节)
- 17 字节数据区解析正常(4 通道检测值 + 4 通道 RAW + 1 报警状态)
- 整帧 CRC 用 Python 验证 = 0x713B,与收到值完全一致
- Qt 上位机恢复连接

## 七、经验总结

1. **移植到另一家芯片,外设"同名中断"不是同一个语义。** TI 的 RT 和 GD32 的 IDLE
   都用来"判帧尾",但触发阈值天差地别。看手册不能只看"有这个中断",要把它触发
   的时序参数算出来,和你的协议允许的时序窗口比一比,看它宽不宽于你需要的窗口。
2. **两层耦合时,一次只测一层。** 一个回显测试就把 USART 层单独剥出来,立刻排除
   物理层,后面才敢把矛头指向判帧逻辑,不靠猜。
3. **别急着怀疑 CRC。** 用标准定义(Python 1:1 复现固件实现)一分钟验证,
   把它从嫌疑人名单里干净划掉,把精力留给真正的根因。
4. **USB-232 适配器的字节间隙是现实。** 任何依赖"字节匀速到达"的硬件判帧手段
   (IDLE、短超时)在 USB 虚拟串口上都可能踩雷,软件超时判帧是最稳的兜底。
————————————————
版权声明:本文为CSDN博主「高铃」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/april_lie_/article/details/163943364

您需要登录后才可以回帖 登录 | 注册

本版积分规则

253

主题

635

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部
0