找回密码
 立即注册
查看: 124|回复: 3

STC32G 定时器 16 位读取竞争导致高误码率的根因分析与修复

[复制链接]
  • 打卡等级:以坛为家I
  • 打卡总天数:284
  • 最近打卡:2026-07-03 07:56:32
已绑定手机

20

主题

69

回帖

581

积分

高级会员

积分
581
发表于 2026-5-28 09:05:34 | 显示全部楼层 |阅读模式




一、事情经过

    背景

设计的一个单芯电缆通讯系统,一主多从结构,在电缆上同时传输电源与数据,数据采用曼彻斯特编码。使用 STC32G 单片机作为主控,
解码端利用 INT1 双沿中断捕获 P3.3 引脚每跳变的时间戳(由 Timer1 提供),通过相邻边沿间隔还原半位序列,再合成数据字节。

    问题现象

开发过程中,采用两块电路板做收发试验,误码率一直居高不下,稳定在 17% 左右。具体表现为:

- 主机端发出正确的 `AA 55 + 8 字节` 数据帧,数据从00-FF循环
- 从机端接收、解码后,将收到的数据再原样发送给主机
- 时不时出现接收的数据有几个字节出错,例如发送的是8个C2,收到的却是5个C2加3个其它数字
- 统计误码率,达到17%左右
- 使用示波器观察 P3.3 引脚波形,信号干净规整,无明显噪声或畸变

    调试历程

初期怀疑方向包括:
- 曼彻斯特解码算法有缺陷
- 帧头识别阈值不合适  
- 边沿捕获防抖参数不当
- 中断优先级配置问题
- 信号电平反转或毛刺
逐一排查上述方向均未解决问题,误码率顽固地维持在 17%。

    突破

通过在单片机软件总增加数据探针,出现误码时,将现场数据发送到USB口显示,再用Python工具分析接收的数据。
分析原始脉宽时间戳数据包,发现异常数据中经常出现一个约 130ms的脉宽时间。而 Timer1 是 16 位定时器,最大计时值就是130ms。
进一步分析发现,如果真实的边沿间隔只有几百微秒(正常范围 200~600),但因 Timer1 高/低字节读取时的竞争条件导致读到了
畸变值(高位来自上一周期、低位来自下一周期),造成后值比前值小,那么两者相减,就会出现大于130ms的值。

   二、原因分析

    根本原因:16 位定时器跨字节读取的竞争(Race Condition)

STC32G 的 Timer1 是 16 位定时器,计数值分布在两个 8 位寄存器中:
- **TH1**(高 8 位)
- **TL1**(低 8 位)

在 C51 微控制器中,读取一个 16 位硬件定时器不是原子操作。CPU 分两次读取 TH1 和 TL1,而定时器在持续运行。
如果在读取 TH1 的瞬间,恰好 TL1 从 0xFF 翻转为 0x00、TH1 同时 +1(即进位事件),则会出现以下情况:

```
读取顺序:先读 TL1,再读 TH1

真实值:(TH1=0x19, TL1=0x00) = 6400
         ↓ TL1=0xFF→0x00, TH1=0x18→0x19

错误读取:
  第1步:读 TL1 = 0xFF  (进位前)
  第2步:读 TH1 = 0x19  (进位后)
  合成值:0x19FF = 6655

交换顺序:先读 TH1,再读 TL1

  第1步:读 TH1 = 0x18  (进位前)
  第2步:读 TL1 = 0x00  (进位后,清0了)
  合成值:0x1800 = 6144
```

两种顺序都有可能出错。错误的计数值导致时间差计算得到一个巨大或异常的差值。例如:

- 前值: 6200,当前值(真实值)6400,被错误读成了6144
- 计算差值:`(u16)(6144-6200) = (u16)(-56) = 65536-56=65480` 被误判为"130ms 超长脉冲"

   三、改进措施

    修复方案:三次读取 + 校验重试

```c
// 旧代码(错误):
tl1 = TL1;      // 先读低位
th1 = TH1;      // 再读高位(可能已进位)
return ((u16)th1 << 8) | tl1;

// 新代码(正确):
u16 decode_timer1_get_value(void)
{
    u8 th1_1, th1_2, tl1;
    do
    {
        th1_1 = TH1;            // 先读高字节
        tl1 = TL1;              // 再读低字节
        th1_2 = TH1;            // 再读高字节校验
    } while (th1_1 != th1_2);   // 两次TH1不同则重读
    return ((u16)th1_1 << 8) | tl1;
}
```

**原理**:
1. 先读 TH1(高位),再读 TL1(低位),再读 TH1 检验
2. 如果两次 TH1 相同,说明读取过程中没有发生进位,数据可靠
3. 如果两次 TH1 不同,说明进位发生在读取过程中,丢弃本次结果并重试
4. 实测重试次数 ≤1,几乎没有性能开销

   四、改进效果

                                    量化对比

|     指标        |     改进前                     |       改进后                           |
| **误码率**  |      **~17%**             |   **0%(实测零误码)**       |
| 帧头检测      | 无/固定匹配                 | 容差扫描,适应性更强           |
| 定时器读取   | TL1→TH1(有竞争)    | TH1→TL1→TH1(三次校验)|
| 解码稳定性   | 偶发 131ms 异常脉冲   |    完全消除                           |


   五、 总结

一个看似微小的"先读 TL1 还是先读 TH1"的问题,导致了长达几天的调试痛苦和 17% 的顽固误码率。
这个问题在示波器上完全看不出,因为信号本身是干净的——问题出在 MCU 内部读取定时器的那一瞬间。

经验教训:
1. 16 位硬件定时器的跨字节读取是 8 位 MCU 上经典的陷阱,必须采用"高→低→高"校验方案;
2. 即使信号波形完美,数据也可能出错——故障不一定在信号路径上;
3. 原始时间戳数据的逐包分析(Python + USB 导出)是定位此类问题的有效手段。


回复

使用道具 举报 送花

  • 打卡等级:以坛为家I
  • 打卡总天数:284
  • 最近打卡:2026-07-03 07:56:32
已绑定手机

20

主题

69

回帖

581

积分

高级会员

积分
581
发表于 2026-5-28 11:52:10 | 显示全部楼层
测试数据:
截图202605281151394797.jpg
回复

使用道具 举报 送花

  • 打卡等级:以坛为家III
  • 打卡总天数:943
  • 最近打卡:2026-07-04 07:56:28
已绑定手机

61

主题

2089

回帖

3079

积分

论坛元老

积分
3079
发表于 2026-5-28 12:45:37 | 显示全部楼层
谢谢分享
回复

使用道具 举报 送花

  • 打卡等级:以坛为家I
  • 打卡总天数:284
  • 最近打卡:2026-07-03 07:56:32
已绑定手机

20

主题

69

回帖

581

积分

高级会员

积分
581
发表于 2026-5-28 21:19:57 | 显示全部楼层
我先把坑踩了,让别人无坑可踩!
回复

使用道具 举报 送花

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

本版积分规则

QQ|手机版|深圳国芯人工智能有限公司 ( 粤ICP备2022108929号-2 )

GMT+8, 2026-7-5 07:48 , Processed in 0.070348 second(s), 60 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表