告诉编译器:这个变量的值,可能会在"你看不见的地方"突然变掉,所以每次都要老老实实去内存里重新读取,不要自作聪明做优化。
例如在 STM32 + 中断 的代码里特别常见。 main.c 里这些变量就很典型:
volatile uint8_t key6_flagvolatile uint8_t key6_busyvolatile uint32_t key_cntvolatile 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。
2)led10_blink_flag
- 在
HAL_TIM_PeriodElapsedCallback()里被写入 - 在
while(1)里被读取和清零
所以要volatile。
3)key6_busy
- 虽然主要在主循环里用,但它参与了按键状态控制
- 这种和异步事件强相关的状态变量,加
volatile是稳妥的
六、有和没有它,区别到底是什么
加了 volatile
含义是:
- 这个变量可能随时变化
- 编译器不要乱优化
- 每次都重新读取
- 每次写入都真的写回去
不加 volatile
含义是:
- 编译器可以自由优化
- 可以把变量缓存起来
- 可以少读几次内存
- 速度可能更快,但对"异步变化变量"可能出错
七、volatile 不是"线程安全"也不是"万能药"
很多新手会误以为:
只要加了
volatile,并发问题就都解决了
不是的。
volatile 只能保证:
- 编译器不要把访问优化掉
它不能保证:
- 操作是原子的
- 多步操作不会被打断
- 中断和主循环之间不会抢数据
例子
比如:
c
key_cnt++;
这看起来是一句,但底层可能不是"一下就完成"的,可能分成几步:
- 读出
key_cnt - 加 1
- 写回去
如果中途被中断打断,就可能出问题。
所以:
volatile解决的是"看得见变化"- 不是"不会冲突"
这是两个不同层面的事。
八、什么时候要想到 volatile
1)中断里改,主循环里读
这是你现在最典型的场景。
2)主循环里改,中断里读
也要考虑。
3)访问硬件寄存器
寄存器值可能随硬件变化,不是普通变量。
4)多任务共享变量
以后学 FreeRTOS 会常见。
九、什么时候一般不用 volatile
如果一个变量:
- 只在当前函数内部使用
- 不会被中断改
- 不会被硬件改
- 不会被别的任务改
那通常就不用。
比如:
c
int i = 0;
for (i = 0; i < 100; i++)
{
}
这里的 i 就没必要 volatile。