volatile
修饰
volatile
(易变的)是 C/C++ 在嵌入式开发中常用的关键字,它的作用是告诉编译器,这个变量可能会在编译器不知道的情况下被改变,因此编译器不要对这个变量进行优化,而是完全遵循代码(按原样)进行读取、写入。
在嵌入式开发中,DMA 涉及的内容、中断回调中涉及的变量、读写寄存器等往往需要 volatile
修饰,因为它们的值可能会在正常的 C/C++ 代码流程之外被改变。
用法
中断变量
在中断回调函数中,如果要使用全局变量,那么这个全局变量往往需要被 volatile
修饰,否则编译器可能会对这个变量进行常量折叠、寄存器缓存等错误优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
如果不使用 volatile
修饰 flag
:
- 在
run()
函数中,编译器按照正常流程分析,无法探查到callback
被调用(中断是一种打破正常流程方式的强制“跳转”) - 编译器认为
flag
的值一直为 0。 - 根据编译参数和编译器版本的不同,可能会把
while
循环条件优化成0 == 0
,导致while
循环永远无法退出。
volatile
修饰后,编译器按照代码原样忠实地进行每一次内存读写,因此 while
循环可以正常退出。
DMA
DMA 也是一个典型的应当使用 volatile
的场景,因为 DMA 是硬件直接操作内存,因此编译器在进行流程分析时,无法探测到 DMA 的操作,导致在优化时对相关变量做出错误推断,因此需要使用 volatile
修饰 DMA 相关的变量。
注意:关于 Cache Line
对于带有 Cache Line 的 MCU,不仅需要使用 volatile
修饰变量,还可能需要手动刷新内核的 L1/L2 Cache。
寄存器
对于映射到内存的寄存器(Memory-mapped registers,如各种外设的寄存器,不包括内核内部的 R0
R1
等寄存器),也应当使用 volatile
修饰。这有多种原因:
- 寄存器的值可能会在正常的代码流程之外被改变,如被相关外设直接改变。
- 寄存器的值必须被直接传递给外设,而不是存在于编译器为优化而创建的某种中间变量上,否则外设无法正常工作。
- 对于寄存器的读写,常常具有额外的副作用。
Example 1
1 2 3 4 5 6 7 8 9 10 11 |
|
在正常 C/C++ 代码流程下 *reg_tx = 0xFE
不会导致 reg_flag
改变,如果不使用 volatile
修饰 reg_flag
,那么编译器可能优化为 while (0 == 0)
,因此 while
循环永远无法退出。
volatile
修饰后,编译器按照代码原样忠实地进行每一次内存读写,因此 while
循环可以正常退出。
Example 2
1 2 3 4 5 6 |
|
如果不使用 volatile
修饰 reg
,那么编译器可能优化成:
1 2 3 4 5 |
|
这对于普通内存来说,其效果是一样的。但是对于寄存器来说,往往还具有额外的副作用。假如这是某个通信相关的寄存器,且被设计为每次写入都会触发一次通信,那么第一段代码相当于发送了 0x1
和 0x2
数据,而第二段代码只发送了 0x2
数据,这显然是不一样的。
Example 3
1 2 3 4 5 |
|
我们读取了寄存器的值,但并没有使用这个值。对于普通内存而言,这是一个无用的操作,编译器可能会优化掉,而使用 volatile
修饰可以保证这一操作得到保留。
对于寄存器来说,如果读取寄存器的值,可能会触发一些副作用。如状态寄存器 SR
常常被设计为读取后自动清零。如果编译器优化掉了这个读取操作,那么状态寄存器的值就无法被清零,这可能会导致一些问题。