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;
}
}
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图所示:
赋值结果: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 修饰双字变量会改变使用的指令集宽度
暂时未知,大家可以去研究一下!!!
|