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 常常被设计为读取后自动清零。如果编译器优化掉了这个读取操作,那么状态寄存器的值就无法被清零,这可能会导致一些问题。