volatile
2026/7/6大约 3 分钟
volatile
volatile 的核心作用:
告诉编译器:这个对象的值可能被当前代码之外的因素改变,每次读写都必须保留,不能随意优化。
嵌入式中最典型的三个场景:
- 中断与主程序共享变量
- 外设寄存器
- RTOS 多任务共享变量
一、中断与主程序共享变量
volatile int flag = 0;
void IRQ_Handler(void)
{
flag = 1;
}
int main(void)
{
while (flag == 0)
{
}
// 中断发生后继续执行
}主程序不断读取:
flag而中断可能随时修改:
flag = 1;如果没有 volatile,编译器可能认为 flag 在循环中不会改变,从而优化重复读取。
加上:
volatile int flag;就是告诉编译器:
flag可能被中断修改,每次读取都必须保留。
二、外设寄存器:STM32 UART 的 DR
硬件寄存器的值可能被外设随时改变,因此寄存器必须使用 volatile。
1. 最基本的寄存器映射
volatile uint32_t *USART_DR =
(uint32_t *)0x40013804;
uint32_t data = *USART_DR;这里:
*USART_DR就是直接读取 UART 的 DR(Data Register)。
硬件可能:
UART 接收到数据
↓
硬件写入 DR
↓
CPU 读取 DR因此编译器不能把寄存器读取当作普通变量处理。
2. STM32 是怎么封装的?
STM32 的 CMSIS/设备头文件通常会把寄存器定义成结构体:
typedef struct
{
__IO uint32_t SR;
__IO uint32_t DR;
__IO uint32_t BRR;
__IO uint32_t CR1;
__IO uint32_t CR2;
__IO uint32_t CR3;
__IO uint32_t GTPR;
} USART_TypeDef;其中:
__IO本质上就是:
#define __IO volatile所以:
__IO uint32_t DR;相当于:
volatile uint32_t DR;再通过基地址映射:
#define USART1 ((USART_TypeDef *)USART1_BASE)于是:
USART1->DR本质就是访问:
volatile uint32_t DR;也就是:
通过结构体成员访问一个
volatile的硬件寄存器。
3. 为什么要 volatile?
例如:
while ((USART1->DR & 0xFF) == 0)
{
}每次循环都需要重新读取 DR。
volatile 告诉编译器:
这个寄存器可能随时被硬件改变,不能把之前的读取结果直接复用。
三、RTOS 多任务共享变量
例如两个任务共享一个状态变量:
volatile int flag = 0;
void Task1(void *arg)
{
flag = 1;
for (;;)
{
}
}
void Task2(void *arg)
{
while (flag == 0)
{
}
// Task1 已经设置 flag
}Task1 修改:
flag = 1;Task2 不断读取:
flag使用:
volatile int flag;可以防止编译器把 flag 的读取优化掉。
但是:
volatile不等于线程安全。
例如:
volatile int count;
count++;count++ 不是天然原子的。
多个任务同时修改时,仍然可能发生竞争,需要根据场景使用:
互斥锁
临界区
原子操作
信号量
任务通知四、三个例子本质相同
中断
↓
修改变量
硬件
↓
修改寄存器
其他任务
↓
修改共享变量这些变化可能不是当前代码直接完成的,编译器无法简单判断变量始终不变。
所以:
volatile就是告诉编译器:
这个对象可能被外部执行流改变,对它的每次访问都有意义。
五、最终记忆
volatile
↓
防止编译器随意优化访问
↓
保证每次读写都保留三个典型场景:
① 中断 ↔ 主程序
② CPU ↔ 外设寄存器
③ Task ↔ TaskSTM32 中:
__IO uint32_t DR;本质上就是:
volatile uint32_t DR;
volatile解决的是“编译器优化”问题,不负责保证原子性、互斥和线程安全。

