- 打卡等级:以坛为家I
- 打卡总天数:284
- 最近打卡:2026-07-03 07:56:32
已绑定手机
高级会员
- 积分
- 581
|
一、事情经过
背景
设计的一个单芯电缆通讯系统,一主多从结构,在电缆上同时传输电源与数据,数据采用曼彻斯特编码。使用 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 导出)是定位此类问题的有效手段。
|
|