【嵌入式】趣味理解“volatile”

告诉编译器:这个变量的值,可能会在"你看不见的地方"突然变掉,所以每次都要老老实实去内存里重新读取,不要自作聪明做优化。

例如在 STM32 + 中断 的代码里特别常见。 main.c 里这些变量就很典型:

  • volatile uint8_t key6_flag
  • volatile uint8_t key6_busy
  • volatile uint32_t key_cnt
  • volatile uint8_t led10_blink_flag

一、为什么会需要 volatile

因为在单片机里,一个变量不一定只会被"当前这段代码"改。

它可能被这些地方改掉:

  • 中断函数
  • 定时器回调
  • DMA
  • 硬件寄存器
  • 另一个任务/线程(以后学 RTOS 会遇到)

比如现在这个代码里:

  • key6_flag 是在 中断回调 里置 1
  • 但在 while(1) 主循环里读取它并处理
    这就是一个典型的"两个执行环境共同访问同一个变量"的场景。

二、不加 volatile 会发生什么

编译器会想:

这个变量在这段代码里看起来没人改啊,那我没必要每次都去内存里读,直接把它缓存到寄存器里,速度更快。

对于普通变量,这种优化没问题。

但对"可能被中断改掉"的变量,这种优化就可能出事。


三、代码举例

c 复制代码
if (led10_blink_flag == 1)
{
    led10_blink_flag = 0;
    HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_9);
}

led10_blink_flag 是在 HAL_TIM_PeriodElapsedCallback() 里置 1 的。


1)有 volatile

编译器会比较老实,理解成:

  • while(1)
  • 每次都重新去内存读一次 led10_blink_flag
  • 如果定时器中断刚好把它改成 1
  • 主循环就能看到变化,LED 就会闪

这想要的行为。


2)没有 volatile

编译器可能会优化成类似这种思路:

c 复制代码
先读一次 led10_blink_flag
发现它现在是 0
那后面我就一直用这个 0 好了
没必要反复读内存

结果就是:

  • 定时器中断明明把它改成 1 了
  • 但主循环"看不见"
  • 你就会觉得程序像没反应一样

四、可以把它想成"公告栏"和"便签"

举个生活化比喻:

  • 内存里的变量,像办公室门口的公告栏
  • 寄存器里的缓存值,像你手里抄下来的一张便签

不加 volatile

编译器会说:

我都抄到便签上了,就一直看便签,不去看公告栏了。

但问题是:

  • 中断随时可能去改公告栏
  • 你便签上的内容已经过时了

加了 volatile

编译器会说:

这个公告栏可能随时被别人改,我每次都得重新去公告栏看,不能只信便签。

这就好理解多了。


五、哪些变量应该加 volatile

像这种写法里,凡是 主循环和中断/回调共同访问 的标志位,基本都应该加。

eg:

1)key6_flag

  • HAL_GPIO_EXTI_Callback() 中断回调里被写入
  • while(1) 里被读取和清零
    所以要 volatile
  • HAL_TIM_PeriodElapsedCallback() 里被写入
  • while(1) 里被读取和清零
    所以要 volatile

3)key6_busy

  • 虽然主要在主循环里用,但它参与了按键状态控制
  • 这种和异步事件强相关的状态变量,加 volatile 是稳妥的

六、有和没有它,区别到底是什么

加了 volatile

含义是:

  • 这个变量可能随时变化
  • 编译器不要乱优化
  • 每次都重新读取
  • 每次写入都真的写回去

不加 volatile

含义是:

  • 编译器可以自由优化
  • 可以把变量缓存起来
  • 可以少读几次内存
  • 速度可能更快,但对"异步变化变量"可能出错

七、volatile 不是"线程安全"也不是"万能药"

很多新手会误以为:

只要加了 volatile,并发问题就都解决了

不是的。

volatile 只能保证:

  • 编译器不要把访问优化掉

不能保证

  • 操作是原子的
  • 多步操作不会被打断
  • 中断和主循环之间不会抢数据

例子

比如:

c 复制代码
key_cnt++;

这看起来是一句,但底层可能不是"一下就完成"的,可能分成几步:

  1. 读出 key_cnt
  2. 加 1
  3. 写回去

如果中途被中断打断,就可能出问题。

所以:

  • volatile 解决的是"看得见变化"
  • 不是"不会冲突"

这是两个不同层面的事。


八、什么时候要想到 volatile

1)中断里改,主循环里读

这是你现在最典型的场景。

2)主循环里改,中断里读

也要考虑。

3)访问硬件寄存器

寄存器值可能随硬件变化,不是普通变量。

4)多任务共享变量

以后学 FreeRTOS 会常见。


九、什么时候一般不用 volatile

如果一个变量:

  • 只在当前函数内部使用
  • 不会被中断改
  • 不会被硬件改
  • 不会被别的任务改

那通常就不用

比如:

c 复制代码
int i = 0;
for (i = 0; i < 100; i++)
{
}

这里的 i 就没必要 volatile


相关推荐
袁小皮皮不皮5 分钟前
1.HCIP BFD 学习笔记(优化版)
服务器·网络·笔记·网络协议·学习·智能路由器·ip
装不满的克莱因瓶30 分钟前
【自动驾驶领域】学习 Cityscapes 数据集——城市街景语义理解的标准基准
人工智能·pytorch·python·深度学习·学习·机器学习·自动驾驶
清辞8531 小时前
产品经理需求推进流程
大数据·深度学习·学习·产品经理
华科大胡子1 小时前
在STM32上跑通TinyML
stm32·单片机·嵌入式硬件
YM52e2 小时前
鸿蒙PC ArkTS 声明合并问题深度解析与最佳实践
学习·华为·harmonyos·鸿蒙·鸿蒙系统
海兰3 小时前
【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统
人工智能·学习·算法
iCxhust3 小时前
C#进程管理程序
开发语言·汇编·stm32·单片机·c#·微机原理
ken22323 小时前
在 Libreoffice Calc中输入自定义表情字符时,需要保存之后,才能正常显示
学习
zwenqiyu3 小时前
P5283 [十二省联考 2019] 异或粽子题解
c++·学习·算法
编程圈子3 小时前
电机驱动开发学习2. 直流无刷电机工作原理
驱动开发·学习