查看原文
其他

【嵌入式】bug粉碎机之volatile的那些坑

bug菌 最后一个bug 2022-07-15

1、聊一聊

    一路上我们会认识很多人,从"相遇"、"相知"、"相惜",其实每段旅途都是宝贵的,一路上有你或许就没那么孤单了!一首感情非常饱满的歌曲!


    今天跟大家分享下volatile关键字在嵌入式中一些值得注意的知识点,并结合实例说明,相信会给不少小伙伴带来新的收获,bug粉碎机来了~



3、bug菌想说的


    经常有小伙伴问bug菌什么叫嵌入式软件,个人觉得如果你所编写的代码与硬件打交道比较密切,并且设计上会进行硬件方面的考量就属于嵌入式软件了。


    同样嵌入式C与普通的C程序员也是有着不小差异的,或许从今天的volatile关键字开始你就会有所感受。



    对于C语言关键字volatile大家应该都很熟悉了,因为现在非常多的资料一旦讲到该关键字就会把其用法说得明明白白,其中"避免被优化"、“每次变量都从内存中取值”等等之类的词条随处可见。


    可是在平时的编码过程中估计有一半的小伙伴并不会去考虑或者鉴别所定义的变量是否需要添加volatile,因此一些bug就这样悄然潜伏在你的代码中,所以bug菌安排两个实例体验一下,方便日后关联相关问题。



2、再论volatile

    

    其实对于该关键字在之前的C进阶与MCU文章中也提到过多次了,只是比较分散,估计一些小伙伴也忘记得差不多了,所以bug菌这里整理一下:


volatile


    大家都知道所编写代码一方面供我们自己阅读维护,另一方面更重要的是供编译器来解析编译,volatile原本的英文意思叫易变的,是一个变量特征修饰符。


    也就是说该关键字修饰变量就是为了告诉编译器该变量容易被改变,希望能够每次都能从其内存中取出来,不要对其进行优化,经典的一句话:"不要你认为,我要我认为"。



    说到这里其实我们应该去了解一下对应平台的编译器优化处理,其中最常见的就是编译器为了加快代码的运行速度,内存中的数据被加载到寄存器或者缓存中,以后再次访问该数据并不会再从内存中加载,而是直接从缓存或者寄存器中获取,这样会导致内存中的数据与实际运行数据不符合的情况等等。


    编译器优化这里限于篇幅暂时不进行展开,大家可以查阅一下相关资料,期待bug菌的后面文章吧。


3、volatile实例

    下面bug菌总结了两个关于volatile对程序影响的例子,并用stm32单片机实验分析了一下:


1

逻辑无效代码优化

    

    比如对变量的连续写入,编译器会认为只进行最后一次写入,中间均认为无效代码,如下代码:

主程序代码:
1#include "sys.h"
2#include "usart.h"
3#include "timer.h"
4
5uint16_t MemeryData = 0;
6
7/*******************************
8 * Fuction: 测试volatile
9 * Author : (公众号:最后一个bug)
10 ******************************/

11 int main(void)
12 {        
13    NVIC_Configuration();   
14    uart_init(9600);    
15    TIM3_Int_Init(10,7199); 
16
17    printf("welcome volatile \r\n");
18    while(1)
19    {
20        MemeryData = 1;
21        MemeryData = 2;   
22    }    
23}     
中断程序代码:
1/*******************************
2 * Fuction: 定时器中断服务程序
3 * Author : (公众号:最后一个bug)
4 ******************************/

5extern  uint16_t MemeryData;
6
7void TIM3_IRQHandler(void)  
8{
9    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) 
10    {
11        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);  
12        if(MemeryData == 1)
13        {
14            //bugPS:仅仅作为测试,尽量不要在中断中打印,
15            //而且这里会打印得比较猛!!!
16            printf("No use volatile\r\n");
17        }
18    }
19}
分析一下:

     根据我们原本设计想法 : 主函数只要有置位,那么中断函数里面应该就能够成功打印字符串,因为定时器中断比较快,总会有机会在MemeryData=1的时候触发,然而bug菌等了许久也没有看到打印。


    于是在MemeryData前面加上volatile,便成功打印中断中的字符串。



     这事还真不能这样完了,我们得看看到底编译器把程序变成啥样了.


汇编分析一波:

1

未使用volatile


分析一下:
  • 标签1 : memerydata = 1;变成把memerydata加载到r1中 ,并没有把数值1保存到对应的内存中去;

  • 标签2 : memerydata = 2;变成了两步,首先把2存到寄存器r0,然后把r0写入到标签1载入的内存中去。

  • 标签3/4 : 形成了一个循环语句,也就是说以后都不会再执行标签1了。


2

使用volatile


分析一下:
  • 使用volatile应该很清晰了,分别把1和2加载到r1和r2寄存器,然后分别在循环中进行写内存,同时也与volatile的作用相符合。


小节一下



    看到这里可能一些小伙伴会说 : 我才不会写这么糟糕的代码呢。


    然而在嵌入式领域外设寄存器大部分都是分布在内存中,基本上操作内存就是通过操作变量,所以在st库中大部分的寄存器都会使用volatile来进行修饰:(如下图所示)





2

多任务影响


   我们再一起看看多任务的情况下volatile对程序的影响:

多任务情况:
1uint16_t Multi(uint16_t *Val)
2{
3   uint16_t result = 0;
4    result = *Val;
5    result *= *Val;
6    return result;
7}
8
9uint16_t Multi(volatile uint16_t *Val)
10{
11   uint16_t result = 0;
12    result = *Val;
13    result *= *Val;
14    return result;
15}


对应汇编:




分析一下:
  • 很明显这两种情况是有差异的,无volatile的情况是直接优化为了A*A的形式,先取变量再进行寄存器的乘法。

  • 而对于有volatile的形式,把对Val的取值分成了两次分别放入r0和r1,然后再进行乘法计算。

  • 在多任务调度处理过程中存在volatile的形式就有可能在加载r0的时候被调度休眠,当内容被修改以后唤醒,从而形成了A*B的中间结果,不具备原子性了,如果我们程序中有对其进行判断便有可能出错。


4、最后小结

    夜已经深了,volatile这块基本上就这些东西了,以上这些差异和影响都有可能成为大家日后的潜伏bug,多了解一下这方面的差异有助于帮助大家更加快速的定位问题。


    好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地。

推荐好文  点击蓝色字体即可跳转

【收藏】【看门狗软件设计】"喂狗"真那么简单吗?

【经验】bug菌谈单片机编程"十层功力",你练到了第几层?

【MCU】用stm32的UID给固件加密(重点在加密)

【硬核C进阶】如何实现 万能 "两数交换" 宏 ?

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存