volatile 关键字
作用
告诉编译器:该变量可能在程序控制之外被修改,每次访问都必须从内存读取,禁止编译器对其进行缓存优化(寄存器缓存、指令重排)。
RTOS中典型应用:ISR和任务共享变量(中断中修改flag,任务中轮询读取)、多任务共享全局变量、外设寄存器访问。
典型使用场景
| 场景 | 说明 |
|---|---|
| 硬件寄存器 | 外设寄存器的值随时可能被硬件改变 |
| 中断服务程序(ISR)共享变量 | 主循环读取、ISR 写入的标志位 |
| 多线程共享变量(有限) | 防止编译器优化,但不保证原子性,不能替代锁/atomic |
| DMA缓冲区 | DMA控制器直接修改内存,需要将指针本身声明为 volatile |
| MMIO 操作 | Memory-mapped I/O 必须加 volatile |
对于指针:
volatile int *p:p 指向的内容是 volatile 的,每次通过 p 读取值都从内存重读。p 本身不是 volatile。
int * volatile p:p 本身是 volatile 的,意味着指针的值(即地址)可能被外部改变,每次使用 p 都重新读取指针值。
volatile int * volatile p:指针和指向的内容都是 volatile 的。
寄存器映射(外设寄存器)
外设寄存器挂在总线地址上,值会被硬件随时改变(如状态寄存器的「发送完成」位由外设硬件置位),所以用指针访问必须加 volatile。
// 等发送完成:不加 volatile 会死循环
volatile uint32_t *status_reg = (volatile uint32_t *)0x40013800;
while ((*status_reg & 0x01) == 0); // 等待就绪位不加 volatile 为什么翻车:编译器发现循环体里没改 *status_reg,就认为它每次读都一样,把读取提到循环外只读一次,于是 while 永远卡在第一次读到的值(死循环 / 永不等待)。
加了 volatile,编译器强制每次循环真正去读硬件寄存器,才能看到硬件改的新值。
铁律:硬件寄存器地址必须用
volatile修饰。 内核侧的特殊寄存器(MRS/MSR 访问、无地址)不需要 volatile,见 特殊寄存器。
函数int square(volatile int *ptr)能实现预期目标吗?为什么?
函数 int square(volatile int *ptr) { return *ptr * *ptr; } 不能实现预期目标。
原因:因为 *ptr 是 volatile 的,编译器会两次从内存读取 *ptr 的值。如果在两次读取之间该值被中断或其他线程修改了,两次读到的值可能不同,结果就不是一个数的平方。
正确写法:
int square(volatile int *ptr)
{ int val = *ptr; // 只读一次,存到局部变量
return val * val;
}