【译】volatile C语言关键字,如何使用?

本文探讨了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-bit无符号指针,指针指向的内容为volatile。

volatile uint8_t * pReg;
uint8_t volatile * pReg;

volatile的指针指向非volatile的变量很少见(我只使用过一次),但我还是给出相应的语法。

int * volatile p;

顺便提一下,关于为什么要在数据类型前使用volatile关键字,请参考Dan Sak的文章Top-Level cv-Qualifiers in Function Parameters

最后,如果你再struct或者union前使用volatile关键字,表明struct或者union的所有内容都是volatile。如果这不是你的本意,可以在struct或者union成员上使用volatile关键字。

正确使用C语言关键字volatile

只要变量可能被意外的修改,就需要把该变量声明为volatile。实际应用中,只有三种类型数据可能被修改。

  1. 外设寄存器地址映射
  2. 在中断服务程序中修改全局变量
  3. 在多线程、多任务应用中,全局变量被多个任务读写

我们将分别讨论上述三种情况。

外设寄存器

嵌入式系统包含真正的硬件,通常会有复杂的外设。这些外设寄存器的值可能被异步的修改。举个简单的例子,我们要把一个8-bit状态寄存器的地址映射到0x1234.在程序中循环查看该状态寄存器的值是否变为非0. 下面是最容易想到,但错误的实现方法

uint8_t * pReg = (uint8_t *)0x1234;

//wait for register to become non-zero
while(*pReg == 0){} //do something else

当你打开编译器优化时,程序总是执行失败。因为编译器会生成下面的汇编代码:

mov ptr, #0x1234
mov a, @ptr

loop:
bz loop

程序被优化的原因很简单,既然已经把变量的值读入累加器,就没有必要重新一遍,编译器认为值是不会变化的。就这样,在第三行,程序进入了无限死循环。为了告诉编译器我们的真正意图,我们需要修改函数的声明:

uinit8_t volatile * pReg = (uint8_t volatile *)0x1234;

编译器生成的汇编代码:

mov ptr, #0x1234

loop:
mov a, @ptr
bz loop

像这样,我们得到了正确的动作。

中断服务程序

在中断服务程序中,经常会修改一些全局变量值,来作为主程序中的判断条件。例如,在串口中断服务程序中,可能会检测是否接收到了ETX(假如是消息的结束标识符)字符。如果接收到了ETX,ISR设置一个全局标志位。
错误的做法:

int etx_rcvd = FALSE;
void main()
{
    ...
    while(!ext_rcvd)
    {
        //wait
    }
    ...
}

interrupt void rx_isr(void)
{
    ...
    if(ETX == rx_char)
    {
        etx_rcvd = TRUE;
    }
    ...
}

在关闭编译器优化的情况下,程序可能执行正常。然而,任何像样点而优化都会“break”这段程序。问题是编译器并不知道etx_rcvd可能被ISR中被修改。编译器只知道,表达式!ext_rcvd始终为真,你讲用于无法退出循环。结果,循环后面的代码可能被编译器优化掉。幸运的话,你的编译器可能会发出警告;不幸的话,(或者你不认真的查看编译器警告),你的程序无法正常执行。当然,你可以责怪编译器执行了“糟糕的优化”。

解决方式是,将变量etx_rcvd声明为volatile,所有问题(当然,也可能是部分)就消失了。

多线程应用

在实时系统中,尽管有想queues,pipes等这些同步机制,使用全局变量实现两个任务共享信息的做法依然很常见。即使在你的程序中加入了抢占式调度器,你的编译器依然无法知道什么是上下文切换,或何时发生上下文切换。因此,从概念上讲,多任务修改全局变量的的做法与中断服务程序中修改全局变量的做法是相同的。因此,所有这类全局变量都应该声明为volatile。例如,下面的程序

int cntr;

void task1(void)
{
    cntr = 0;

    while(cntr == 0)
    {
        sleep(1);
    }
    ...
}

void task2(void)
{
    ...
    cntr++
    sleep(10);
    ...
}

当打开编译器优化时,这段程序可能执行失败。解决方法是将cntr声明为volatile。

最后的思考

一些编译器允许你把所有的变量隐式的声明为volatile。请抵制这种诱惑,因为它会令你不再思考,当然,也会导致生成低效的代码。

另外,也不要责怪优化器或直接把它关掉。现代的优化器已经足够优秀,我已经记不清上次遇到优化bug是什么时候了。相反,我常常看到程序员们错误的使用volatile。

如果你被要求去修改一个很古怪的代码,请在程序中查找一下volatile关键字,如果你什么也没有找到,本文中讨论的例子可以向你提供一些解决问题的思路。

关于作者

Nigel Jones,著名嵌入式专家,有很多经典文章,如《A ‘C’ Test: The 0x10 Best Questions for Would-be Embedded Programmers》,中文《想成为嵌入式程序员应知道的0x10个基本问题》;《Efficient C Code for 8-bit Microcontrollers》。

Nigel Jones 博客地址

参考

  1. 如何使用C语言中的volatile关键字
  2. How to Use C’s volatile keyword
  3. Volatile陷阱
  4. 详解C中volatile关键字
  5. volatile关键字
  6. C语言中volatile关键字的作用
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值