volatile 关键字:为什么寄存器读取不能被优化掉

Smart AI Code2026-10-01阅读约 6 分钟

本文你能获得什么

  • 理解为什么寄存器读取必须使用 volatile。
  • 掌握 SysTick->VAL 的硬件行为与正确读取方式。
  • 学会排查和减轻因编译器优化导致的 1 ms 计时误差。
  • 了解验证寄存器读取是否真实的调试手段。

1. 编译器为什么会“删掉”你的读取操作

在开启优化(如 -O2)后,编译器会分析代码的“可见结果”。如果一段读写操作对最终输出没有影响,编译器可能直接将其移除。例如:

c uint32_t i; for (i = 0; i < 72000U; i++) { }

这段循环不改变任何外部状态,编译器会认为它毫无作用,直接删除,导致延时失效。

2. volatile 的作用

volatile 告诉编译器:该变量的读写必须真实执行,不得缓存到寄存器或优化掉。其行为在 C 标准中定义为“允许访问硬件寄存器或中断安全地修改的变量”。

来源:cnblogs.com 文章指出,volatile uint32_t i; 会让 i++ 每次都真正读写内存,循环不会被当作无关代码删除。

3. SysTick->VAL 的硬件本质

3.1 它是一个硬件寄存器

SysTick->VAL 位于 ARM Cortex‑M 的 SysTick 定时器中,地址为 0xE000E018(基地址 0xE000E010 偏移 0x08)。该寄存器由硬件计数器在每个内核时钟周期自动递减,无需任何 C 代码对其赋值。

来源:dzone.com 与 community.st.com 均给出该地址定义;cnblogs.com 描述了硬件递减行为。

3.2 结构体中的 volatile 修饰

在 CMSIS 标准外设库中,__IO 被定义为 volatile,因此 SysTick_Type 结构体中的所有字段(包括 VAL)都具备 volatile 语义。这意味着每次读取 SysTick->VAL 都会直接访问硬件当前计数值,而不是读取缓存的软件变量。同组的 CTRL、LOAD、CALIB 寄存器同样受此修饰。

来源:cnblogs.com 明确说明 __IO 等同于 volatile。

4. 正确读取 SysTick->VAL 的方法

4.1 基本用法

c uint32_t start = SysTick->VAL; // 读取当前计数值 /* 执行需要计时的操作 */ uint32_t end = SysTick->VAL; // 再次读取 uint32_t elapsed = start - end; // 递减计数器,差值即经过的周期数(需考虑回滚)

由于 SysTick->VAL 是 24 位递减计数器,从 LOAD 寄存器的重装值开始递减到零后自动重载,因此通常 start 大于或等于 end。如果发生回滚(end > start),需要特殊处理。

4.2 检测回滚

当计数器从 0 重载时,end 可能大于 start。一种经验性修正方法是检查 SysTick->VAL 是否大于阈值 0x0000BB1C,若大于则认为发生了回滚并加上一个周期。该方法并不完美,有时会多算一个 tick。

来源:community.st.com 用户描述了这一方案及其局限性。

5. 中断与主循环之间的竞态问题

当 SysTick 中断(SysTick_Handler)修改全局变量 uwTick,而主循环同时读取该变量时,可能产生竞态条件。实测约 0.5% 的概率会出现 1 ms 的计时误差。

来源:community.st.com 帖子中用户报告了约 0.5% 的时间 uwTick 偏差 1 个计数(相当于 1msec 误差)。

5.1 已尝试的解决方案

方案描述效果
软件 dirty‑bit进入 ISR 时置位,主循环读取后检查是否被修改未消除误差
投票采样多次快速读取并取多数结果仍有边界情况漏网
全部内联将函数内联,减少调用开销显著减少滞后读取,但未根治

来源:community.st.com 帖子详细记录了这三种尝试。

5.2 为什么直接读取 SysTick->VAL 更可靠

由于 SysTick->VAL 是硬件寄存器,不受 ISR 修改软件变量的影响。结合回滚检测,可以在一定程度上获得更精确的时间戳,但仍需注意其不完美性。

6. 验证寄存器读取是否真实

6.1 使用 GPIO 翻转 + 示波器

在读取 SysTick->VAL 的前后分别翻转一个 GPIO 引脚,用示波器观察两次翻转之间的时间间隔,可以直观判断读取操作是否被优化掉。例如,community.st.com 的用户通过 GPIO2_HIGH 和 GPIO2_LOW 配合示波器测量了 HAL_Precision_GetTick 函数的执行时间。

来源:community.st.com 用户实际使用了 GPIO2_HIGH/LOW 配合示波器进行测量。

6.2 检查汇编代码

在优化等级下编译后,查看生成的汇编是否包含实际的 LDR 指令访问 0xE000E018,而非从寄存器或栈中读取缓存值。

7. 注意事项与局限

  • volatile 只保证读写不被优化,不提供原子性或内存屏障。在多线程或中断环境中,仍需配合关中断或临界区。
  • SysTick->VAL 的回滚检测阈值 0x0000BB1C 是经验数值,不同时钟频率下可能需要调整。
  • 其他外设寄存器(如 TIM2->CNT、GPIOB->IDR)同样会由硬件自主改变,也应声明为 volatile。

来源:cnblogs.com 列举了 TIM2->CNT 与 GPIOB->IDR 的例子。

8. 下一步

  • 阅读 ARM Cortex‑M 参考手册中关于 SysTick 的章节。
  • 在自己的工程中实践 volatile 修饰符,并使用示波器或 GPIO 翻转验证读取行为。
  • 若需更高精度的时间戳,考虑使用硬件定时器的捕获/比较功能或专用的时间戳单元。