【嵌入式】趣味理解“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


相关推荐
happymaker06262 小时前
web前端学习日记——DAY02(CSS样式表的使用)
前端·css·学习
田梓燊2 小时前
算法题学习题单
学习·算法
Sunsets_Red2 小时前
乘法逆元的 exgcd 求法
c++·学习·数学·算法·c#·密码学·信息学竞赛
张张123y2 小时前
AI应用开发工程师面试题:深度技术拷打
人工智能·学习·面试
qcwl662 小时前
深入理解Linux进程与内存 学习笔记#1
笔记·学习
xuhaoyu_cpp_java2 小时前
Servlet学习
java·笔记·学习
謓泽2 小时前
【51单片机】DS18B20温度传感器(1-Wire)
单片机·嵌入式硬件·51单片机
F137298015572 小时前
220V降5V,100MA非隔离离线恒压稳压器WD5202A
stm32·单片机·嵌入式硬件·51单片机
ADHD多动联盟2 小时前
提升学生注意力涣散问题的情绪管理与学习能力策略
学习·学习方法·玩游戏