ARM Cortex-M3 深度研究 - 慎用 volatile 关键字修饰 double longlong 等64位长度类型的变量

ARM Cortex-M3 深度研究 - 慎用 volatile 关键字修饰 double longlong 等64位长度类型的变量

导语

大家在做嵌入式项目的时候应该都使用过volatile关键字来修饰访问比较频繁的变量。volatile关键字会告诉编译器,变量是随时可能发生变化的,每次使用它的时候必须从内存重新取出它的值。但是volatile可以无所顾虑的被使用吗?

1 ARM Cortex-M3 基础知识

1.1 字长的定义

字的大小取决去具体系统的总线宽度,如果是32位的系统,则一个字是4个字节,如果是64位,则是8个字节;由于ARM Cortex-M3是32位系统,所以在这里定义一个字为4字节(32Bit)

1.2 数据的加载与存储指令集

指令集 含义 指令集 含义 Bit
LDRB 字节数据加载指令 STRB 字节数据存储指令 8
LDRH 半字数据加载指令 STRH 半字数据存储指令 16
LDR 字数据加载指令 STR 字数据存储指令 32
LDRD 双字数据加载指令 STRD 双字数据存储指令 64

注意:LDRD和STRD Load and Store Doubleword Instructions 是ARM扩展的64位指令

1.3 常用变量类型

变量类型 名称 英文 Bit 读写指令 Volatile 修饰后
(unsigned) char 字节 Byte 8 LDRB STRB LDRB STRB
(unsigned) short 半字 Half Word 16 LDRH STRH LDRH STRH
(unsigned) int Word 32 LDR STR LDR STR
(unsigned) long Word 32 LDR STR LDR STR
float Word 32 LDR STR LDR STR
(unsigned) long long 双字 Double Word 64 LDRD STRD 2·LDR 2·STR
double 双字 Double Word 64 LDRD STRD 2·LDR 2·STR

注意:Volatile 修饰的双字变量存取指令都由单个双字指令拆分成两个单字指令

2 实验测试 - volatile 修饰双字变量的影响

2.1 实验测试场景

场景一:使用 volatile 修饰 unsinged long long (double同理)

  • 选择一款基于ARM Cortex-M3 内核的MCU,这里用的是GD32F103VET6,IDE为Keil MDK 5.06 Update7 (build 960) 编译优化O0-O3均可
  • 定义两个volatile unsigned long long 类型的全局变量A和B,A、B赋初值为0
  • 定义main函数并编写一个while死循环,循环处理函数中始终将A的值赋给B
  • 初始化并使能一个外部按键触发中断,在中断处理函数中将A赋值为0xFFFFFFFFFFFFFFFF
  • 启动汇编调试,观察赋值的汇编语句
//定义全局变量
volatile unsigned long long A=0,B=0;
//定义主函数
void main(void)
{
 EXTI_Init();//外部中断
 while(1)
 {
  /*此处使用汇编查看*/
  B=A;
  /*汇编代码如下
  LDR r0,[pc,#16]   取得变量A的内存地址并装入r0
  LDR r1,[r0,#0x00] 将变量A的低32位装入 r1       此处使用断点
  LDR r0,[r0,#0x04] 将变量A的高32位装入 r0
  LDR r2,[pc,#12]   取得变量B的内存地址并装入r2
  STR r1,[r2,#0x00] 将r1值存储到变量B的低32位
  STR r0,[r2,#0x04] 将r0值存储到变量B的高32位    此处使用断点
  汇编代码如上*/
 }
}
//外部中断处理函数
EXTI_IRQHandler()
{
 /*此处添加断点*/
 EXTI_Interrupt_Flag_Clear(EXTI);//清中断标志位
 A=0xFFFFFFFFFFFFFFFF;
}
指令 参数 含义 结果
LDR r0,[pc,#16] 从PC指针偏移16所指向的地址处读取一个字(Word/32bit)的值 装入r0中 取得变量A的内存地址并装入r0
LDR r1,[r0,#0x00] 从r0值所指向的地址处读取一个字(Word/32bit)的值 装入r1中 将变量A的低32位装入 r1
LDR r0,[r0,#0x04] 从r0值偏移4所指向的地址处读取一个字(Word/32bit)的值 装入r0中 将变量A的高32位装入 r0
LDR r2,[pc,#12] 从PC指针偏移12所指向的地址处读取一个字(Word/32bit)的值 装入r2中 取得变量B的内存地址并装入r2
STR r1,[r2,#0x00] 将r1值以字(Word/32bit)的方式存储到r2值所指向地址的内存中 将r1值存储到变量B的低32位
STR r0,[r2,#0x04] 将r0值以字(Word/32bit)的方式存储到r2值偏移4所指向地址的内存中 将r0值存储到变量B的高32位
  • 在 LDR r1,[r0,#0x00]处使用断点暂停,然后按住外部触发按键,再点击全速运行触发外部中断,操作顺序一定要对
  • 再次全速运行,跳出外部中断,在STR r0,[r2,#0x04]处使用断点暂停,然后观察r1,r0的值,可见异常
  • 再次单步运行即可完成B的赋值过程,观察B最终的值

完整调试过程如下GIF图所示:

ARM Cortex-M3 深度研究 - 慎用 volatile 关键字修饰 double longlong 等64位长度类型的变量_第1张图片
赋值结果:B=0xFFFFFFFF00000000,其值的低32位来自A更改前的值,高32位来自于中断更改后A的值!

场景二:不使用 volatile 修饰

如果不使用 volatile 修饰,则赋值汇编指令为下表所示:

指令 参数 含义 结果
LDR r0,[pc,#16] 从PC指针偏移16所指向的地址处读取一个字(Word/32bit)的值 装入r0中 读取变量A的内存地址值到r0寄存器中
LDRD r1,r0,[r0,#0] 从r0值所指向的地址处读取两个字(DWord/64bit)的值 装入r1,r0中 读取变量A的值到r1,r0寄存器中
LDR r2,[pc,#12] 从PC指针偏移12所指向的地址处读取一个字(Word/32bit)的值 装入r2中 读取变量B的内存地址值到r2寄存器中
STRD r1,r0,[r2,#0] 将r1,r0中的值 以两个字(DWord/64bit)的方式存储到r2所指向地址的内存中 将r1,r0寄存器中的值赋给变量B

由于LDRD和STRD均为单指令且一步完成了双字节赋值/读值操作,因此不会被中断程序打断!

2.2 实验测试结论

实验表明:在ARM Cortex-M3上“多线程"使用volatile 修饰double longlong 等64位长度类型的变量是非常危险的。
中断可以打断两次连续的LDR和STR指令,出现奇怪的数值,如果使用了这个异常的数值做进一步的控制处理,则有可能会出现一系列不可控的后果,大家务必注意!!!!

3 为何 volatile 修饰双字变量会改变使用的指令集宽度

暂时未知,大家可以去研究一下!!!

你可能感兴趣的:(嵌入式(MCU方向),ARM,Cortex-M3,STM32,GD32,volatile,汇编)