嵌入式C中的volatile关键字

volatile,译为"易失的",也就说该关键字用来申明一个易变的变量,该变量是会在程序运行时会被不经意地改变,因此该关键字的做用就是告诉编译器每次都必须从新当心的读取该变量的值,而不是简单地拷贝该变量保存在副本里面的值,同时也不容许编译器对该变量进行优化。安全

咱们先来看看常见的volatile申明的变量有哪些?多线程

一、定义易失型变量异步

//定义整型变量 
volatile int Data;
int volatile Data; //两种申明等价

二、定义指针函数

//定义指针
volatile int* pData;  //指针是整型的,可是指针指向的值是易失的 
int volatile* pData;  //指针是volatile型的,可是指向的值是非易失的

//正确的用法 
volatile int Value; 
volatile int *pData = &Value;

//错误的用法
volatile int Value; 
int* pData = &Value;

//定义易失指针而且指向的值也是易失的
//此时pData自己和指向的值都是不会被优化的 
volatile intvolatile pData;

三、定义结构体性能

//定义结构体用法1 
typedef struct {
  int Data;
  int Next;
}List;

List volatile Node; //此时Node中的结构体成员Data、Next是易失的,而List原有的结构体成员变量是非易失的

//定义结构体用法2 
typedef volatile struct {
  int Data;
  int Next;
}List;

List Node;  //此时List定义的全部结构体成员变量是易失型的

//定义结构体用法3
typedef volatile struct {
  int volatile Data;
  int Next;
}List;

List Node;  //此时List定义的结构体成员变量Data都是易失型的

//定义结构体用法4等价于用法5,不等同于用法6 
typedef struct {
  int  Data;
  int* Next;
}List;

volatile List Node;

//定义结构体用法5 
typedef struct {
  int volatile  Data;
  intvolatile Next;
}List;

List Node;

//定义结构体用法6
typedef struct {
  volatile int   Data;
  volatile int*  Next;
}List;

List Node;

再来看看volatile申明的变量的用途!优化

在如下状况中,可能就须要volatile关键字来申明变量了ui

一、程序运行良好,可是当提升编译器的优化级别时,其行为将发生变化,而且没法按需运行;spa

二、一切都很好,可是一旦启用中断,代码行为就会发生变化,而且没法按预期运行;线程

三、不稳定的硬件驱动程序;指针

四、孤立执行的任务正常,但启用另外一个任务后崩溃。

下面来看几个代码块,看看定义易失型变量的好处,你们能够看一下代码块1和代码块2的区别,区别是对于FLAG_REG的定义,一个是无符号char变量,一个是无符号易失型变量,那你们想一想哪个代码块会运行正常呢?答案是代码块2,在编译器优化时,为了得到更好的性能,会将FLAG_REG值加载到寄存器中,尽管FLAG_REG的值由硬件更改,但不会再次读取。代码块1中定义的FLAG_REG变量永远只会被取到它的副本值,不会从新从寄存器中读该变量的值,若是该变量的值被改变为了0x01的话,在代码块1中读回的值仍是0x00,那么代码块1将不会按照预期的意图去运行了,而代码块2则不一样。

//Code Block 1
unsigned char FLAG_REG;   // Hardware flag register

void func (void) {
  while (FLAG_REG & 0x01)   // Repeat while bit 0 is set
    {
      //do something 
    }
}

//Code Block 2
unsigned volatile char FLAG_REG;   //qualify from the volatile

void func (void) {
  while (FLAG_REG & 0x01)   // Repeat while bit 0 is set
    {
      //do something 
    }
}

关键字volatile主要使用的场合有哪些?

一、访问内存映射的外设寄存器。

二、在多个线程之间共享全局变量或缓冲区。

三、在中断例程或信号处理程序中访问全局变量。

在这些场合下,都须要将变量设置为易失型的,才可能保证代码的正常运行。

外设寄存器

在嵌入式系统中,全部外围设备均位于特定的存储器地址。外设具备寄存器,这些寄存器的值能够异步地更改成代码流。在程序中,为了方便地访问外设寄存器,咱们必须将外设寄存器映射为c变量,而后使用指针访问此变量。

注意:在映射中,不只要关心寄存器的大小和地址,还须要关心其在内存中的对齐方式。

来上一个例子,这里是一个位于地址0x40000000的32位标志状态寄存器,咱们必须监视它的第一位并在循环中等待,直到其第一位为1。

#define  STATUS_REG    (unsigned volatile int*)(0x40000000)

unsigned volatile int* puiData = STATUS_REG;
while((*puiData) & 0x01// Wait until first bit is set
{ 
  //do something
}

当咱们使用volatile关键字对状态寄存器地址进行限定时,编译器就会知道该地址的值能够随时更改,而无需任何先验信息。所以,编译器不会对此变量执行任何优化,并始终从地址中从新读取该值。

中断服务函数

对于中断服务程序,ISR()和main()函数之间一般共享一个全局变量。在这种状况下,设置全局变量的值或经过ISR或main函数进行检查,为更好地理解,下面来举个例子。

int Timer_Flag = 0;

Timer_ISR(void) 
{ 
    Timer_Flag = 1; 
}

int main(void)  {
    while(!Timer_Flag)
    {
        //do some work
    }

    return 0;
}

在上面的代码中,全局标志是main函数中的监视器,而且main函数执行其余一些任务,直到全局标志值为零为止。当咱们打开编译器的优化级别时,此代码可能工做正常,由于编译器没有意识到ISR中全局变量的值更改,所以它假定while循环始终为真,而且永远不会退出循环 。

解决此问题的方法是使全局标志易变,此技术可防止编译器对全局标志进行任何优化,并告诉编译器该标志的值能够随时由外部事件更改,而无需代码采起任何措施。

volatile int Timer_Flag = 0;

Timer_ISR(void) 
{ 
    Timer_Flag = 1; 
}

int main(void)  {
    while (!Timer_Flag)
    {
        //do some work
    }

    return 0;
}

多线程应用

在多线程应用程序中,两个线程使用管道或消息队列相互通讯,除了此种方式实现通讯外,还有另外一种技术可使线程彼此通讯,该技术是共享地址(共享缓冲区或全局变量)。

一般,线程以异步方式执行。若是咱们不使用volatile声明这些共享位置,而是提升编译器的优化级别,则编译器会将这些值存储在线程上下文的局部变量中,并始终从这些局部变量中读取值。所以,对于所需的操做,咱们必须将共享缓冲区或全局变量声明为volatile,能够防止当提升编译器的优化级别时线程运行失败。

volatile int gValue;

void Task_1(void)  {
    gValue = 0;

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

void Task_2(void)  {
    ...
    gValue++; 
    sleep(10); 
    ...
}

若是关键字const和volatile同时申明一个变量,那该变量是作什么用途的呢?

一块儿使用volatile和const关键字很是有趣,由于volatile(“随时间变化的”)和const(“只读”)的质量彷佛相反,可是有时同时用这两个关键字定义变量会有意想不到的好处。

GPIO寄存器的常量地址

限定符const和volatile在访问GPIO寄存器或任何硬件寄存器的场景中常用,要访问这些寄存器,咱们须要声明一个指向volatile寄存器的const指针。

unsigned int volatile * const pLcdReg = (unsigned int volatile *) 0x00020000;

*pLcdReg = WRITE_DATA; // to write data on LCD
READ_DATA = *pLcdReg; //to read data from the LCD

理解上述复杂声明的正确方法是:pLcdReg是指向易失性无符号整数的常量指针。在此声明中,惟一的指针是常量(即固定地址0x00020000),这意味着在整个程序中,其值不会改变。关键字volatile仅修改无符号整数的行为。这是访问GPIO或硬件寄存器的指针的最有用且最安全的声明。

只读共享内存位置

当const和volatile一块儿使用时,这种状况也很重要。若是两个处理器使用共享位置相互通讯,而且处理器仅将该位置用于读取,那么咱们必须使用const关键字将这些位置设置为只读类型,咱们能够经过如下方式声明共享内存的位置。

unsigned int const volatile gSharedFlag;

unsigned char const volatile acSharedBuffer[BUFFER_SIZE];

只读状态寄存器

如下硬件寄存器表明了硬件不一样阶段的状态,该寄存器是只读类型,要访问这些寄存器,咱们须要声明一个指向常量易失性寄存器的常量指针。

理解此复杂声明的正确方法是pStatusFlagReg是指向易失性常量无符号整数的常量指针。

unsigned int const volatile * const pStatusFlagReg = (uint8_t *) 0x20000000;

READ_DATA = * pStatusFlagReg; //to read status from the status register

* pStatusFlagReg = WRITE_DATA // Not possible because address qualify by const keyword

由上面的三个应用场景,关键字const和volatile同时申明一个变量是很是有用的!

参考:

https://aticleworld.com/understanding-volatile-qualifier-in-c/

相关文章
相关标签/搜索