这是个十分奇葩的问题,碰巧被我遇到了,我承认是我代码写的不够规范,但正是这个不规范的代码,才得以发现这个奇葩的事件。实在忍不住用了两个奇葩来形容。把过程简化一下,如下所述:
假如你的工程至少有两个.c文件,其中一个为timer.c,里面有个定时器中断程序,每10ms中断一次,定义一个变量来统计定时器中断次数:
- unsigned int unIdleCount;
unsigned int unIdleCount;
还有一个timer.h文件,里面是一些timer.c模块的封装,其中变量unIdleCount就被封装在里面:
- extern unsigned int unIdleCount;
extern unsigned int unIdleCount;
在main.c函数中,包含timer.h文件,并利用定时器变量 unIdleCount来精确延时2秒,代码如下:
- unIdleCount=0;
- while(unIdleCount!=200); //延时2S钟
unIdleCount=0;
while(unIdleCount!=200); //延时2S钟
keil MDK V5.54下编译,默认优化级别,编译后下载到硬件平台。你会发现,代码在
- while(unIdleCount!=200);
while(unIdleCount!=200);
处陷入了死循环。反汇编,代码如下:
- 122: unIdleCount=0;
- 123:
- 0x00002E10 E59F11D4 LDR R1,[PC,#0x01D4]
- 0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
- 0x00002E18 E1A00005 MOV R0,R5
- 0x00002E1C E5815000 STR R5,[R1]
- 124: while(unIdleCount!=200); //延时2S钟
- 125:
- 0x00002E20 E35000C8 CMP R0,#0x000000C8
- 0x00002E24 1AFFFFFD BNE 0x00002E20
122: unIdleCount=0;
123:
0x00002E10 E59F11D4 LDR R1,[PC,#0x01D4]
0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
0x00002E18 E1A00005 MOV R0,R5
0x00002E1C E5815000 STR R5,[R1]
124: while(unIdleCount!=200); //延时2S钟
125:
0x00002E20 E35000C8 CMP R0,#0x000000C8
0x00002E24 1AFFFFFD BNE 0x00002E20
重点看最后两句汇编代码,寄存器R0是当前变量unIdleCount的值,汇编指令CMP为比较指令,如果R0中的内容与0xC8不等,则循环。但是这里并没有更新寄存器R0的代码,也就是说变量
unIdleCount的值虽然在变化,但跟0xC8
一直
比较的却是内容不变的R0。因为之前变量unIdleCount被清零,所以R0的内容也是0,永远不等于0xC8,永远不会跳出循环。
看到这里,也许你已经笑翻了:你这个小白,这很明显是没用volatile修饰变量unIdleCount造成的!!!不错,比起从RAM中读写数据,ARM或其它硬件从寄存器读取数据要快的多的多的多...因此编译器会“自作主张”的将某些变量读到寄存器中,再次运算时也优先从寄存器中读取,上面的例子就是这样。解决这样的方法是用关键字volatile修饰你不想让编译器优化的变量,明白的告诉编译器:你不准优化我,每次使用我你都要本本分分的从RAM中读取或写入RAM。
所以先不要笑,我是不会犯这种错误的,之所以从这里说起,是为了照顾下还不知道volatile关键字的。。。
其实在timer.c中我是这样定义统计定时器中断次数变量的:
- unsigned int volatile unIdleCount;
unsigned int volatile unIdleCount;
但是,在timer.h中,我确偷了个懒,声明这个变量的代码如下:
- extern unsigned int unIdleCount;
extern unsigned int unIdleCount;
没有使用关键字volatile,在
keil MDK V5.54下编译,默认优化级别,然后查看代码的反汇编,如下所示:
- 122: unIdleCount=0;
- 123:
- 0x00002E10 E59F11D4 LDR R1,[PC,#0x01D4]
- 0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
- 0x00002E18 E1A00005 MOV R0,R5
- 0x00002E1C E5815000 STR R5,[R1]
- 124: while(unIdleCount!=200); //延时2S钟
- 125:
- 0x00002E20 E35000C8 CMP R0,#0x000000C8
- 0x00002E24 1AFFFFFD BNE 0x00002E20
122: unIdleCount=0;
123:
0x00002E10 E59F11D4 LDR R1,[PC,#0x01D4]
0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
0x00002E18 E1A00005 MOV R0,R5
0x00002E1C E5815000 STR R5,[R1]
124: while(unIdleCount!=200); //延时2S钟
125:
0x00002E20 E35000C8 CMP R0,#0x000000C8
0x00002E24 1AFFFFFD BNE 0x00002E20
可以看出,这个反汇编代码居然和没加volatile关键字的时候一模一样!!代码还是会在while出陷入死循环。
现在,应该知道我要表达的意思了吧,如果引用的变量声明中没有使用volatile关键字修饰,即便定义这个变量的时候使用了volatile关键字修饰,MDK编译器照样优化掉它!
将timer.h中的声明更改为:
- extern unsigned int volatile unIdleCount;
extern unsigned int volatile unIdleCount;
同样环境下编译,查看反汇编代码,如下所示:
- 122: unIdleCount=0;
- 123:
- 0x00002E10 E59F01D4 LDR R0,[PC,#0x01D4]
- 0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
- 0x00002E18 E5805000 STR R5,[R0]
- 124: while(unIdleCount!=200); //延时2S钟
- 125:
- 0x00002E1C E5901000 LDR R1,[R0]
- 0x00002E20 E35100C8 CMP R1,#0x000000C8
- 0x00002E24 1AFFFFFC BNE 0x00002E1C
122: unIdleCount=0;
123:
0x00002E10 E59F01D4 LDR R0,[PC,#0x01D4]
0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
0x00002E18 E5805000 STR R5,[R0]
124: while(unIdleCount!=200); //延时2S钟
125:
0x00002E1C E5901000 LDR R1,[R0]
0x00002E20 E35100C8 CMP R1,#0x000000C8
0x00002E24 1AFFFFFC BNE 0x00002E1C
看最后三句汇编代码,发现多了一个载入汇编指令LDR,这个指令在每次循环中都将变量unIdleCount从RAM中读出到寄存器R1中,然后R1的值再和0xC8比较。这才是符合逻辑的需要的代码。
其实如果好好看看编译原理的书,是不会犯这么低级的错误的,编译器是分文件编译,然后链接,文件A使用了文件B中定义的变量,在编译的时候,文件A是完全不知道文件B里面有什么东西的,只能通过文件B的接口文件(.h文件)来获得使用变量的属性.
以这个为例子,着重说明下关键字volatile,同时也要掌握编译原理的知识,用好手中的工具.