嵌入式开发中volatile的应用

1、volatile简介

        在嵌入式开发中关键字volatile经常被提及,其本质是说:被volatile修饰的变量每次都要到内存中获取后再使用,而不是直接使用寄存器中临时数据;

        在单片机内核中存在多个寄存器,以Cortex-M3内核单片机为例,该类型单片机有R0~R15共16个寄存器,R0~R12为通用寄存器,R13为栈指针寄存器、R14为链接寄存器LR、R15为程序计数器PC。

        在编译器编译代码时候,有时候为了优化代码空间或代码运行时间,在读取变量时直接使用寄存器中的变量,而不是从RAM中读取数据,代码举例:

uint8_t test = 1;

void main(void)
{
    ……
    while(test < 1000);
    ……
}

// 中断代码
void xxx_Handle(void)
{
    ……
    test++;
    ……
}

        由于变量test没有volatile修饰,如果开启了优化,main函数中可能会直接使用寄存器中存储的临时test值,而不再从变量地址处读取变量;这就导致,即使产生了相应中断并修改了test值,可main函数不知道,使得main函数陷入该死循环,进而死机;从下面是在MDK中设置优化等级为3时编译后的汇编代码:(本文使用的MDK版本为:5.36)

        

        其中1000=0x3E8 ,

        0x08000982:将test变量地址加载到寄存器R0中;

        0x08000988:将test变量值加载到寄存器R0中;

        0x0800098A:比较r0、r1,也就是比较寄存器中的test和1000,比较失败继续比较;显然,寄存器中存的test一直都是1,代码自此陷入死循环。

        当然,如果没有开启优化,编译器编译出来的代码都会从RAM中读取变量值来进行比较,下面是同样的代码,只是将编译器优化等级改为了0,即不优化后生成的汇编代码:

        0x08000C4E:将test变量地址加载到寄存器R0中;

        0x08000C50:将test变量值加载到寄存器R0中;

         0x08000C52:比较r0、r1,也就是比较寄存器中的test和1000,比较失败代码跳转到0x08000C4E,重新读取test变量值到r中,然后再次比较。

2、volatile常见用法

2.1、硬件接口中的应用

        我们在使用STM32的库时,经常会见到类似下面的结构体:

typedef struct
{
  __IOM uint32_t CTRL;                   /*!< Offset: 0x000 (R/W)  SysTick Control and Status Register */
  __IOM uint32_t LOAD;                   /*!< Offset: 0x004 (R/W)  SysTick Reload Value Register */
  __IOM uint32_t VAL;                    /*!< Offset: 0x008 (R/W)  SysTick Current Value Register */
  __IM  uint32_t CALIB;                  /*!< Offset: 0x00C (R/ )  SysTick Calibration Register */
} SysTick_Type;

        这些结构体的成员变量用_I、_IO、_IM、_IOM这样的宏定义修饰符修饰,将该宏定义展开后的代码为:

#ifdef __cplusplus
  #define   __I     volatile             /*!< Defines 'read only' permissions */
#else
  #define   __I     volatile const       /*!< Defines 'read only' permissions */
#endif
#define     __O     volatile             /*!< Defines 'write only' permissions */
#define     __IO    volatile             /*!< Defines 'read / write' permissions */

/* following defines should be used for structure members */
#define     __IM     volatile const      /*! Defines 'read only' structure member permissions */
#define     __OM     volatile            /*! Defines 'write only' structure member permissions */
#define     __IOM    volatile            /*! Defines 'read / write' structure member permissions */

        使用volatile const修饰的寄存器为只读寄存器,仅用volatile修饰的寄存器可读、可写。如下表为systick寄存器列表,其中CALIB就为只读,所以ST在库中使用_IM关键字修饰该寄存器。

         之所以要用volatile修饰,就是保证不论是否使用了优化,都能够从寄存器地址处读取;而使用volatile const修饰,只是表明该寄存器为只读,但不代表该寄存器值不会变,只是这种变是由芯片自身将其改变的。

2.2、软件延时函数中的应用

……
while(delay-- > 0);
……

        该延时函数在MDK中优化等级为0时编译的汇编代码:

        下面是延时函数在MDK中优化等级为1或2时编译的汇编代码:(先将R0结果装载到delay变量地址处,然后再比较)

        下面延时函数在MDK中优化等级为3时编译的汇编代码:(只对R0做比较,在R0比较成功后才将R0结果装载到delay变量地址处)

        在变量被volatile修饰后,延时函数在MDK中优化等级为0时编译的汇编代码:(与没有被volatile修饰时的差别不是很大)

         在变量被volatile修饰后,延时函数在MDK中优化等级为3时编译的汇编代码:(每次比较前都会重新获取delay变量地址处的值)

         总结:在MDK中的软件延时函数中,不管有没有加volatile,优化后的延时时间都会变短;但加volatile修饰后,该变化相对较小一些。

        当然,不同的编译器对代码的优化方式不一样,曾经见过一个资料:有的编译器会将类似while(delay-- >0); 这样的指令,如果没有加volatile修饰,会将其优化成一条指令,导致实际只延时了一条指令的时间;但我还没用到过这样的编译器,就不多介绍。

2.3、多任务/中断中的应用

        中断完全可以看成是一种优先级比较高的任务,对比一下下面几个任务:

void TaskA(void const * argument)
{
  while(test < 10000);
  for(;;)
  {
      osDelay(1000);
      SEGGER_RTT_printf(0, "test:%d\r\n", test);
  }
}

void TaskB(void const * argument)
{
  for(;;)
  {
      while(test < 10000);

      osDelay(1000);
      SEGGER_RTT_printf(0, "test:%d\r\n", test);
  }
}

void funC(void)
{
      while(test < 10000);
      osDelay(1000);
      SEGGER_RTT_printf(0, "test:%d\r\n", test);
}

void TaskC(void const * argument)
{
  for(;;)
  {
      funC();
  }
}

        变量test在其他任务或中断中不断执行test++;如果test没有被volatile修饰,MDK中的优化等级设置为3,TaskA、TaskC都将会死机,而TaskB不会。直接放在for循环中,由于每个循环都会读取变量在RAM中的值,故而不会死机;但在TaskA、TaskC这两种情况就不会这么幸运了,他们都只会使用寄存器中的拷贝来做相关事情。

        但是下面的代码将会被正确执行:

void funD(void)
{
      if(test < 2000)
      {
          return;
      }
      osDelay(1000);
      SEGGER_RTT_printf(0, "test:%d\r\n", test);
}

void TaskD(void const * argument)
{
      for(;;)
      {
          funD();
      }
}

        主要原因是:在每次进入函数funD后,都会重新从test变量地址处获取当前值,如果这里不是像TaskC中的funC一样多次读取test,就不存在被优化的说法。

         图中0x08001694/0x08001696就是每次进入funD后重新读取test变量地址处的值。

        总结:在MDK编译器中,只有在连续多次读取某个变量时才存在没有加volatile修饰而被优化的说法。至于其它类型编译器的编译效果是个什么样子,就靠各位看官自己整理了。

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值