虫虫首页| 资源下载| 资源专辑| 精品软件
登录| 注册

您现在的位置是:首页 > 技术阅读 >  如何使用C的volatile关键字

如何使用C的volatile关键字

时间:2024-05-31

首先声明本文译自国外网站的一篇文章,原文链接如下:

https://barrgroup.com/embedded-systems/how-to/c-volatile-keyword

建议有条件的直接阅读英文原版。可能读了这篇文章后,你会有所怀疑,因为你平时可能遇到过下面出现的情况,但是你并没有添加volatile关键字,程序任然正常的运行,个人觉得可能有以下的原因:

1.其实BUG出现了,但是难以复现,所以被你忽略了

2.现在的优化器足够智能,即使打开了优化,也能避免这些BUG的出现

正文开始

许多程序员对C的volatile关键字了解得很少。这并不奇怪,因为大多数C文章对它都是一两句话避而不谈。这篇文章将教你正确的使用它。

在你的C/C++嵌入式代码中你是否经历过以下情况?

代码正常运行--直到你使能了编译器优化

代码正常运行--直到你使能了中断

奇怪的硬件驱动程序

RTOS任务工作正常--直到你添加了其它任务

如果你对上面任意一个的回答为"是",那么意味着你没有使用volatile键字。许多程序员和你一样对volatile关键字了解得很少。遗憾的是,大多数关于C编程语言的书只用一两句话就避开它。

【正确的使用volatile是消除BUG的一部分  Embedded C Coding Standard】

C关键字volatile是一个限定符,在变量声明的时候应用它。它告诉编译器这个变量的值可能随时被改变--编译器不要去优化它。影响是非常严重的。但是,在剖析它之前,我们先看一下它的语法。

C关键字volatile的语法

声明一个变量为volatile,就在这个变量声明的数据类型前面或者后面加一个volatile关键字。下面两个实例都声明一个无符号16位整型变量为volatile整型:


volatile uint16_t x; 
uint16_t volatile y;


现在,实际证明指向volatile变量的指针也是非常普遍的,特别是内存映射I/O寄存器。下面两个声明都将p_reg声明为一个volatile的无符号8位整型指针:


volatile uint8_t * p_reg; 
uint8_t volatile * p_reg;


指向非volatile数据的volatile指针是非常少见的,但是我最好还是讲一下语法:


uint16_t * volatile p_x;


并且,为了完整性,如果你真的需要一个指向volatile数据的volatile指针,你可以这样写:


uint16_t volatile * volatile p_y;


顺便提一句,如果你想得到一个更好的解释对于如何选择在哪儿放置volatile以及为什么要放在数据类型的后面(例如,int volatile * foo),可以阅读Dan Sak's的栏目,"Top-Level cv-Qualifiers in Function Parameters" (Embedded Systems Programming, February 2000, p. 63)。

最后,如果你将volatile应用于结构体或者共用体,那么整个结构体或者共用体就都是volatile的。如果你并不是想这样,你可以对结构体或者共用体中需要的成员单独的添加volatile限定符。

正确的使用C的volatile关键字

如果一个变量的值会被意想不到的修改那它应该被volatile修饰,实际上,只有三种类型的变量可以被修改:

1.内存映射外设寄存器

2.被中断服务程序修改的全局变量

3.多线程内部的多任务访问的全局变量

我们将在下面的章节讨论每一种情况。

外设寄存器

嵌入式系统包含真正的硬件,通常带有复杂的外设。这些外设包含可能被程序流异步更改的寄存器。在一个非常简单的程序中,包含一个8位的状态寄存器,它的内存地址被映射到0x1234。需要你轮询这个状态寄存器直到它的值变为非0。不正确的实现如下:


uint8_t * p_reg = (uint8_t *) 0x1234;

// Wait for register to read non-zero 
do { ... } while (0 == *p_reg)


一旦你打开编译器优化,这段代码几乎肯定会失败。这是因为编译器将生成如下的汇编语言(这里以16位x86机器为例):


mov p_reg, #0x1234
  mov a, @p_reg
loop:
  ...
  bz loop


优化器的理由很简单:它已经把变量的值读取到了累加器中(对应汇编代码第二行),后面就不需要再重复读取了,这样的话这个值总是相同的。因此,从汇编代码的第三行开始就进入了一个死循环。要强制编译器如我们想的那样做,我们需要修改声明如下:


uint8_t volatile * p_reg = (uint8_t volatile *) 0x1234;


汇编代码现在看起来就像这样:


mov p_reg, #0x1234
loop:
  ...
  mov a, @p_reg
  bz loop


因此实现了我们想要的行为。

当具有特殊属性的寄存器操作没有volatile声明时就会产生一些微妙的BUG。例如,许多外设具有通过简单的读取就能清除它们的寄存器。在这种情况下,额外的读取可能会导致超出预期的行为。

中断服务程序

中断服务程序通常设置在主线代码中被测试的变量。例如一个串口中断程序也许测试每个收到的字符是否是一个ETX字符(用以表示消息的结尾),如果这个字符是ETX,中断服务程序也许设置一个全局标志。一个不正确的实现可能如下:


bool gb_etx_found = false;

void main() 
{
    ... 
    while (!gb_etx_found) 
    {
        // Wait
    } 
    ...
}

interrupt void rx_isr(void) 
{
    ... 
    if (ETX == rx_char) 
    {
        gb_etx_found = true;
    } 
    ...
}


【注意:我们不提倡使用全局变量;这段代码使用仅为了让例程简短/清晰。】

在编译器优化关闭的情况下,这段程序也许正常的工作。然而,任何一半像样的优化器都会"破坏"这段程序。问题是编译器不知道这个变量gb_etx_found可以在中断服务程序中被更改,这似乎从来没有被调用过。

就编译器而言,表达式!gb_ext_found在循环中每次都是一样的结果,因此,你不要想那能够退出循环。因而,所有在while循环之后的代码都可能被优化器简单的移除。如果你够幸运,编译器将警告你。如果你不够幸运(或者你还没有学会认真对待编译器警告),你的代码将不幸地失败。自然,这责任将归咎于"糟糕的优化器"。

解决方案是使用volatile声明变量gb_etx_found。这样,程序就会按照你的预期正常工作。

多线程应用

在实时操作系统中尽管存在队列,管道,及其它调度感知的通信机制,但RTOS任务任然可能通过一段共享内存来交换信息。当你添加一个抢占式调度器到你的代码中时,你的编译器并不知道什么是上下文切换或者它何时发生,因此,一个任务异步修改一个共享的全局内容就和中断服务程序讨论的情况差不多。因此所有全局对象(变量,内存缓冲区,硬件寄存器等等)都必须声明为volatile以防止编译器优化而引入的不可预料的行为。例如,下面的代码询问问题:


uint8_t gn_bluetask_runs = 0;

void red_task (void) 
{   
    while (4 < gn_bluetask_runs) 
    {
        ...
    } 
    // Exit after 4 iterations of blue_task.
}

void blue_task (void) 
{
    for (;;)
    {
        ...
        gn_bluetask_runs++;
        ...
    }
}


这段代码将失败一旦编译器优化被使能。使用volatile声明gn_bluetask_runs是解决这个问题的正确方式。

【注意:我们不提倡使用全局变量,这段代码使用全局变量仅仅是因为它正在说明volatile和全局变量的关系。】

【警告:被任务及中断共享的全局变量还应该被保护以防止竞争,比如通过互斥量。】

最后的想法

一些编译器允许你隐式的声明所有变量为volatile,抵制这种诱惑,因为它本质上是思想的替代品。这也潜在的导致代码效率降低。

另外,当你的程序出现非预期的行为时,不要责备优化器或者关掉它。现代C/C++优化器是如此出色,以至于我不记得上次遇到了优化BUG。相反,我经常遇到程序员使用volatile失败。如果给你一份行为怪异的代码去"修复",请对volatile执行grep。如果grep为空,这里给出的示例可能是开始查找问题的好地方。



下面是我的公众号二维码,欢迎关注。