深入理解C语言关键字volatile:如何正确使用它解决嵌入式代码问题
许多程序员对 C 语言关键字 volatile 难以正确理解,这是很正常的。因为多数 C 语言书籍往往只是简单提及一两句就略过了。而本文会告诉你怎样正确使用它。

在C/C++嵌入式代码中,你是否经历过以下情况:
代码执行正常–直到你打开了编译器优化
代码执行正常–直到打开了中断
古怪的硬件驱动
RTOS的任务独立运行正常–直到生成了其他任务
如果你的回答是“yes”,那么很可能你没有使用 C 语言的关键字 volatile。你并非是个例,很多程序员都不能正确地使用 volatile。不幸的是,大多数 C 语言书籍对 volatile 存在轻视,只是简单地将其一带而过。
volatile 是用于声明变量的限定符。它的作用是告知编译器该变量的值可能随时发生改变,并且这种改变并非由代码所导致。向编译器给出这个暗示是很重要的。在开始之前,我们先来了解一下 volatile 的语法。
C语言关键字volatile语法
声明一个变量为 volatile 时,可以在数据类型的前面或者后面加上关键字 volatile 。例如,以下语句将 foo 声明为一个 volatile 的整型。
volatile int foo; int volatile foo;
将指针所指向的变量声明为 volatile 这种情况较为常见,特别是在 I/O 寄存器的地址映射方面。以下语句可将 pReg 声明为一个指向 8 位无符号类型的指针,且该指针所指向的内容是 volatile 的。
volatile uint8_t * pReg; uint8_t volatile * pReg;
指针的 volatile 属性指向非 volatile 的变量这种情况很少见,我仅使用过一次。不过,我还是会给出与之对应的语法。
int * volatile p;
最后,若在 struct 或者 union 前使用 volatile 关键字,那就意味着 struct 或者 union 的所有内容都是 volatile 的。倘若这并非你的本意,那么可以在 struct 或者 union 的成员上使用 volatile 关键字。
正确使用C语言关键字volatile
如果变量有可能被意外地修改,那么就需要将该变量声明为 volatile。在实际的应用当中,只有以下三种类型的数据可能会被修改:
外设寄存器地址映射
在中断服务程序中修改全局变量
在多线程、多任务应用中,全局变量被多个任务读写
接下来,我们将分别讨论上述三种情况。
外设寄存器
嵌入式系统包含真实的硬件,通常会具备复杂的外设。这些外设的寄存器的值有可能会被异步地进行修改。例如,我们需要将一个 8 位状态寄存器的地址映射到 0x1234 。在程序中,会进行循环,以查看该状态寄存器的值是否变为非 0 。
下面是最容易想到,但错误的实现方法:
你打开编译器优化后,程序总是执行失败。原因是编译器会生成如下的汇编代码:
程序优化的原因较为简单。因为已经将变量的值读入累加器,所以没必要再重新读取一遍。编译器认为该值不会发生变化。基于此,在第三行,程序进入了无限死循环。为了向编译器表明我们的真实意图,我们需要对函数的声明进行修改。
编译器生成的汇编代码:
像这样,我们得到了正确的动作。
中断服务程序
在中断服务程序里,时常会对一些全局变量的值进行修改,以此作为主程序中的判断条件。比如,在串口中断服务程序中,有可能会去检测是否接收到了 ETX(假设其为消息的结束标识符)这个字符。要是接收到了 ETX,ISR 就会设置一个全局标志位。
错误的做法:
在关闭编译器优化时,程序或许能正常执行。不过,只要进行任何像样的优化,就会使这段程序“出错”。问题在于编译器并不知晓 etx_rcvd 有可能在 ISR 中被修改。编译器仅仅知道,表达式!ext_rcvd 始终是真的,这会导致你无法退出循环。其结果是,循环后面的代码有可能被编译器优化掉。如果幸运,你的编译器或许会发出警告;要是不幸,(或者你没有认真查看编译器警告),你的程序就无法正常运行。当然,你可以将程序无法正常运行归咎于编译器执行了“糟糕的优化”。
解决方式是,把变量 etx_rcvd 声明为 volatile 。这样一来,所有问题(或许只是部分问题)就消失了。
多线程应用
在实时系统里,有 queues、pipes 等这样的同步机制。使用全局变量来让两个任务共享信息这种做法很常见。即便在你的程序中加入了抢占式调度器,你的编译器也不知道什么是上下文切换,也不知道何时会发生上下文切换。所以从概念角度来看,多任务修改全局变量的做法和中断服务程序中修改全局变量的做法是一样的。
因此,所有这类全局变量都应该声明为 volatile。
最后的思考
一些编译器允许将所有变量隐式地声明为 volatile。不要受这种诱惑,因为它会使人不再思考,并且会导致生成效率低下的代码。
另外,不要去责怪优化器,也不要直接将其关闭。如今的优化器已经相当优秀了,我已经记不起上次碰到优化 bug 是在何时了。相反,我时常能看到程序员们错误地使用 volatile 。如果你被要求修改一个很怪异的代码,那么就在程序中找找 volatile 关键字;要是你没找到任何东西,上面所讨论的例子能够给你提供一些解决问题的思路。