I²C 主机读到的 U16 偶尔跳变?先修复多字节快照发布
一句话: 多字节监测值被后台任务逐字节更新时,I²C 中断可能夹在中间读取,得到"新高字节 + 旧低字节";先组装完整快照,再用短临界区整体发布。
适合谁读:设备内部持续刷新温度、电压、功率或计数值,同时允许主机通过 I²C 读取的嵌入式开发者。
先检查读写是否共享同一份数组
排查动态数据时,把写入和读取的时间线画出来:
text
后台任务:写高字节 → 写低字节
I²C 中断: 读高字节 → 读低字节
只要两条线可以交错,就存在撕裂窗口。读出的数值未必是随机数,它可能是一个格式完全合法、但从未真正采样过的组合,所以更难发现。
volatile 解决不了整块一致性
volatile 主要告诉编译器:每次访问都要按内存中的最新值处理。它不能让两个独立字节的更新变成一个不可分割的事务,也不能阻止中断在两个写操作之间插入。
下面的写法即使变量声明为 volatile,仍可能被读到混合值:
c
volatile uint8_t monitor_bytes[2];
void update_value(uint16_t value)
{
monitor_bytes[0] = (uint8_t)(value >> 8);
monitor_bytes[1] = (uint8_t)value;
}
快照发布的三步
更稳妥的方式是把"准备数据"和"对外可见"分成两步:
c
static uint8_t work_snapshot[MONITOR_SIZE];
static uint8_t published_snapshot[MONITOR_SIZE];
void update_monitor(const source_t *source)
{
build_snapshot(work_snapshot, source);
disable_irq();
memcpy(published_snapshot, work_snapshot, MONITOR_SIZE);
enable_irq();
}
关键点不是一定要 memcpy,而是:
- 在临时区域组装完整数据;
- 让临界区只覆盖最终提交;
- 读取端只访问已发布区域。
临界区越短越好。若数据块很大,可以使用指针交换或版本号方案,但必须确保读取端不会拿到正在被重用的缓冲区。
什么时候需要整块发布
| 数据类型 | 推荐策略 |
|---|---|
| 单字节状态位 | 单次写入通常足够,但仍要明确清除语义 |
| U16/U32 数值 | 快照、双读校验或临界区整体发布 |
| 多字段相关数据 | 整组发布,避免字段之间来自不同采样周期 |
| 可独立变化的字段 | 可以分开发布,但要在协议中说明一致性边界 |
不要只看 CPU 是否"能一次写完一个字节"。协议读者关心的是一组字段是否来自同一个采样时刻。
验证顺序
text
静态检查:所有多字节字段都有一致性边界
构建检查:临界区和缓冲区大小正确
压力测试:后台高频更新时连续读取,不出现混合值
总线实测:抓取主机读取时序,确认长时间运行稳定
如果只有源码路径和静态门禁证据,应把结论写成"已消除代码层撕裂窗口,实际总线压力测试待完成"。
总结
- 合法格式不代表数据来自同一次采样。
volatile不是多字节原子性工具。- 先组装快照,再在短临界区整体发布。
- 数据一致性边界应与协议字段的业务关联保持一致。
有用的话点个收藏,下次监测值偶尔跳变时,先查它是不是由两个不同采样周期拼出来的。