使用C的易失性关键字volatile

C的volatile关键字是一个限定符,在声明变量时将其应用于该变量。它告诉编译器变量的值可以随时更改-编译器在附近找到的代码不会采取任何操作。这意味着非常严重的。但是,在检查它们之前,让我们看一下语法。

C的易失关键字的语法

要声明变量volatile,请在变量定义的数据类型之前或之后包含关键字volatile。例如,这两个声明都将一个无符号的16位整数变量声明为易失性整数:

volatile uint16_t x;
uint16_t volatile y;
现在,事实证明,指向易失性变量的指针非常普遍,尤其是在内存映射的I / O寄存器中。这两个声明都将p_reg声明为一个易失的无符号8位整数的指针:

volatile uint8_t * p_reg; 
uint8_t volatile * p_reg;

非易失性数据的易失性指针非常少见(我想我曾经使用过它们),但是我最好继续讲一下语法:

uint16_t * volatile p_x;

仅出于完整性考虑,如果您确实必须具有指向volatile变量的volatile指针,则可以这样编写:

uint16_t volatile * volatile p_y;

顺便说一句,有关如何选择在何处放置volatile以及为什么应将其放置在数据类型之后的详细说明(例如,int volatile * foo),请阅读Dan Sak的“函数中的顶级cv-Qualifiers”列参数”(嵌入式系统编程,2000年2月,第63页)。

最后,如果将volatile应用于结构或联合,则该结构或联合的全部内容都是易失的。如果您不希望出现这种情况,则可以将volatile限定符应用于结构或联合的各个成员。

正确使用C的易失性关键字

只要变量的值可能意外更改,就应将其声明为volatile。实际上,只有三种类型的变量可以更改:

  • 1.内存映射外设寄存器
  • 2.由中断服务程序修改的全局变量
  • 3.由多线程应用程序中的多个任务访问的全局变量

我们将在以下各节中讨论每种情况。

外围寄存器

嵌入式系统包含真实的硬件,通常带有复杂的外围设备。这些外设包含其值可能与程序流异步更改的寄存器。举一个非常简单的例子,考虑一个8位状态寄存器,该寄存器被映射到地址0x1234。要求您轮询状态寄存器,直到状态寄存器变为非零为止。天真的和不正确的实现如下:

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

因此实现了期望的行为。

当访问具有特殊属性的寄存器而不使用易失性声明时,会出现一些微妙的错误。例如,许多外设都包含寄存器,只需通过读取即可清除它们。在这些情况下,超出预期的额外(或更少)读取可能会导致非常意外的行为。

中断服务程序

中断服务例程通常设置在主线代码中测试的变量。例如,串行端口中断可以测试每个接收到的字符,以查看它是否是ETX字符(大概表示消息的结尾)。如果字符是ETX,则ISR可能会设置一个全局标志。一个不正确的实现可能是:

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;
    } 
    ...
}

[注意:我们不提倡使用全局变量;此代码使用一个使示例简短/清晰。]

关闭编译器优化后,此程序可能会运行。但是,任何一半不错的优化器都会“破坏”程序。问题是编译器不知道可以在ISR函数中更改gb_etx_found,这似乎从未被调用过。

就编译器而言,表达式!gb_ext_found在每次循环中都将具有相同的结果,因此,您永远都不想退出while循环。因此,while循环之后的所有代码都可以被优化器简单地删除。如果幸运的话,编译器将警告您。如果您很不幸(或者您还没有学会认真对待编译器警告),您的代码将惨遭失败。自然,责任归咎于“糟糕的优化器”。

解决方案是将变量gb_etx_found声明为volatile。之后,该程序将按预期工作。

多线程应用

尽管实时操作系统中存在队列,管道和其他可感知调度程序的通信机制,但RTOS任务仍可能会通过共享内存位置(即全局存储)交换信息。当您向代码中添加抢占式调度程序时,编译器不知道什么是上下文切换或何时发生切换。因此,异步修改共享全局的任务在概念上与上面讨论的ISR场景相同。因此,所有共享的全局对象(变量,内存缓冲区,硬件寄存器等)也必须声明为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是解决问题的正确方法。

[注意:我们不提倡使用全局变量;该代码使用了全局变量,因为它正在解释易失性变量和全局变量之间的关系。]

[警告:还需要保护任务和ISR共享的全局变量不受竞争条件的影响,例如通过互斥。]

最后的想法

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

另外,当遇到意外的程序行为时,请不要试图怪罪于优化器或将其关闭。现代的C / C ++优化器是如此出色,以至于我不记得上次遇到优化错误了。相反,我经常遇到程序员使用volatile的失败。

如果为您提供了一些易懂的代码来“修复”,请对volatile执行grep。如果grep空了,这里给出的示例可能是开始查找问题的好地方。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章