mcu开发中关于数据一致性的问题

一直在想一个问题,mcu开发过程中,不做临界区保护的情况下,什么样的代码是不会被中断或优先级高的task打断的,有些问题没搞太明白,借着项目机会,做了一些实验,验证了一些以前的问题。

在这里记录下,有类似想法的朋友欢迎讨论。

环境信息

硬件环境:s32k144(cortex M4)

编译器:s32ds for arm gcc

示例代码

union { //变量的结构
    uint8 buf[8]; 
    struct {
        uint64_t LBMS_PackVltg_V : 20;
        uint64_t LBMS_LinkVltg_V : 20;
        uint64_t LBMS_LinkVltgVld_flg : 1;
        uint64_t LBMS_PackVltgVld_flg : 1;
        uint64_t resved1 : 22;
    } signal;
} BAT_HIGH_VOLTAGE_1;
.........
extern void LBMS_LinkVltg_V_set(uint32_t data)
{
    VehCommData.BAT_HIGH_VOLTAGE_1.signal.LBMS_LinkVltg_V = data;
}
......
LBMS_LinkVltg_V_set(0x9def9);  //调用

LBMS_LinkVltg_V_set对应的汇编代码:

LBMS_LinkVltg_V_set:
00027398:   push    {r4, r7}
0002739a:   sub     sp, #8   
0002739c:   add     r7, sp, #0
0002739e:   str     r0, [r7, #4]
000273a0:   ldr     r3, [r7, #4]
000273a2:   ubfx    r2, r3, #0, #20 
000273a6:   ldr     r3, [pc, #40]   ; (0x273d0 <LBMS_LinkVltg_V_set+56>)
000273a8:   ubfx    r1, r2, #0, #12
000273ac:   lsls    r4, r1, #4
000273ae:   ldrh    r1, [r3, #26]
000273b0:   and.w   r1, r1, #15
000273b4:   mov     r0, r1
000273b6:   mov     r1, r4
000273b8:   orrs    r1, r0
000273ba:   strh    r1, [r3, #26]
000273bc:   lsrs    r2, r2, #12
000273be:   uxtb    r2, r2
000273c0:   movs    r1, #0
000273c2:   orrs    r2, r1
000273c4:   strb    r2, [r3, #28]
000273c6:   nop     
000273c8:   adds    r7, #8
000273ca:   mov     sp, r7
000273cc:   pop     {r4, r7}
000273ce:   bx      lr
000273d0:   ands    r0, r1
000273d2:   movs    r0, #0

对汇编指令不熟,附上chatgpt的汇编语法解释:

  1. push {r4, r7}: 将寄存器r4和r7的值压入栈中,保存它们的值以备后用。

  2. sub sp, #8: 从当前栈指针(sp)中减去8个字节,为后续的内存操作腾出空间。

  3. add r7, sp, #0: 这实际上是一个多余的指令,因为它只是将sp的值赋给r7,没有实际改变r7的值。

  4. str r0, [r7, #4]: 将寄存器r0的值存储到由r7和偏移量4计算得到的内存地址中。

  5. ldr r3, [r7, #4]: 从由r7和偏移量4计算得到的内存地址中加载一个值到r3寄存器中。

  6. ubfx r2, r3, #0, #20: 对r3寄存器的值进行位提取操作,提取从第0位开始的20位,并保存到r2中。

  7. ldr r3, [pc, #40]: 从当前指令的下一个地址开始(即程序计数器pc加上40个字节)加载一个值到r3寄存器中。

  8. ubfx r1, r2, #0, #12: 同样对r2的值进行位提取操作,提取从第0位开始的12位,并保存到r1中。

  9. lsls r4, r1, #4: 对r1寄存器的值进行左移操作,移动4位,结果保存在r4中。

  10. ldrh r1, [r3, #26]: 从由r3和偏移量26计算得到的内存地址中加载一个半字(halfword)到r1寄存器中。

  11. and.w r1, r1, #15: 对r1寄存器的值执行一个逻辑与操作,使用立即数15作为掩码。这通常用于清除某些位或只保留某些位。

  12. strh r1, [r3, #26]: 将r1寄存器的半字值存储到由r3和偏移量26计算得到的内存地址中。

  13. lsrs r2, r2, #12: 对r2寄存器的值进行右移操作,移动12位。

  14. uxtb r2, r2: 将r2寄存器的值无符号扩展到全字节大小。

  15. movs r1, #0: 将立即数0移动到r1寄存器中。

  16. orrs r2, r1: 对r2和r1寄存器的值进行逻辑或操作,并将结果存储在r2中。

  17. strb r2, [r3, #28]: 将r2寄存器的字节值存储到由r3和偏移量28计算得到的内存地址中。

  18. nop: 无操作指令,通常用于插入小的时间延迟或使程序流暂时停顿。

  19. adds r7, #8: 将立即数8加到r7寄存器中。

  20. mov sp, r7: 将r7寄存器的值赋给栈指针sp,这通常用于恢复栈的状态。

  21. pop {r4, r7}: 从栈中弹出之前压入的值,恢复r4和r7寄存器的值。

  22. bx lr: 通过跳转到链接寄存器lr的值来返回,通常是回到子程序调用的主程序或从异常处理返回到正常执行流程。

代码执行过程

单步执行汇编指令,通过s32ds调试工具查看寄存器、内存变化过程。

一、程序一开始,通用寄存器值

在进入函数LBMS_LinkVltg_V_set之前寄存器状态:

跳进LBMS_LinkVltg_V_set之后

汇编单步执行,其相当于ldr r3, [r7, #4],将参数变量0x9def9存放于r3中,到这里其实只是把参数变量存入r3寄存器。

当参数传递进来后,存放到了r3通用寄存器。紧接着执行汇编指令:

ubfx r2, r3, #0, #20,

  • r2:这是目标寄存器,提取的位字段将存储在这个寄存器中。
  • r3:这是源寄存器,从中提取位字段。
  • #0:这是起始位的位置(LSB,Least Significant Bit),表示从源寄存器的哪一位开始提取。
  • #20:这是位数(bit count),表示要提取的位字段的宽度。

所以,ubfx r2, r3, #0, #20 的意思是:从 r3 寄存器中,从第0位开始(即最低有效位),提取宽度为20位的位字段,并将这个位字段存储在 r2 寄存器中

紧接着下一句:

ldr r3, [pc, #40] ; (0x273d4 )

解释如下:

  • ldr:这是“Load Register”的缩写,表示加载寄存器。
  • r3:这是目标寄存器,即将从内存中加载的数据存储在这个寄存器中。
  • [pc, #40]:这表示内存地址的计算方式。pc 是程序计数器寄存器,通常指向当前指令的地址。#40 是一个立即数值,表示偏移量。所以,内存地址是 pc 寄存器的值加上 40(以字节为单位)

pc = 0x273aa 加上40 就是0x273d2

对应map文件中全局变量的信息:

 *fill*         0x20004006        0x2 
 COMMON         0x20004008       0x78     xxxxx
                0x20004008                VehCommData
 COMMON         0x20004080       0x40     xxxx
                0x20004080                xxxxxx

LBMS_LinkVltg_V_set设置的全局变量VehCommData,其地址为:0x20004008

执行结果为:

此时VehCommData的地址已经赋值给了r3,导致里还没有给VehCommData.BAT_HIGH_VOLTAGE_1.signal.LBMS_LinkVltg_V赋值

下一句为:ubfx r1, r2, #0, #12

从 r2 寄存器中,从第0位开始(即最低有效位),提取宽度为12位的位字段,并将这个位字段存储在r1 寄存器中

解释如下:

  • ubfx:这是 "Unsigned Bit Field Extract" 的缩写,表示无符号位提取。
  • r1:这是目标寄存器,提取的位字段将存储在这个寄存器中。
  • r2:这是源寄存器,从中提取位字段。
  • #0:这是起始位的位置(LSB,Least Significant Bit),表示从源寄存器的哪一位开始提取。
  • #12:这是位数(bit count),表示要提取的位字段的宽度。

直接结果:

结合示例,r2寄存器的内容0x9def9中的0到12bit,拷贝到r1中,0~12到11bit,数据为0xef9

下一句一条逻辑左移指令lsls r4, r1, #4

  • lsls:lsl 是逻辑左移(Logical Shift Left)的缩写,而 s 表示设置(Set)状态标志。当执行这个指令时,它不仅会执行左移操作,还会更新状态寄存器以标记某些条件是否成立。
  • r4:这是目标寄存器,左移操作的结果将存储在这个寄存器中。
  • r1:这是源寄存器,其内容将被左移。
  • #4:这是移动的位数,表示将 r1 寄存器的值向左移动4个位。

所以,lsls r4, r1, #4 的意思是:将 r1 寄存器的值逻辑左移4位,并将结果存储在r4 寄存器中。同时,这个操作还会更新状态寄存器以反映某些条件(如是否有进位或溢出等)。

r1= 0xef9 左移4位后,变成了r4 = 0xEF90,看卡执行结果:

r4 = 0xef90

下一句  ldrh r1, [r3, #26]

解释如下:

  • ldrh:ld 代表 "load",h 代表 "halfword",即半字。这个指令用于从内存中加载一个半字到寄存器中。
  • r1:这是目标寄存器,即将从内存中加载的半字数据存储在这个寄存器中。
  • [r3, #26]:这表示内存地址的计算方式。具体来说,它是将 r3 寄存器的值与立即数 #26 相加,得到的结果就是需要加载的半字在内存中的地址。

所以,ldrh r1, [r3, #26] 的意思是:从由 r3 寄存器值加上 26 计算得到的内存地址中,加载一个半字到 r1 寄存器中。半字通常表示 16 位(两个字节)的数据。

r3 = 0x2004008 加26 等于0x2004022,这个就是LBMS_LinkVltg_V相对于VehCommData的偏移26的地址

执行结果为:

下一句 and.w r1, r1, #15

解释如下:

  • and.w:这是按位与操作指令,. 后面的 w 表示操作数的宽度为 word,即 32 位。
  • r1:这是目标寄存器和源寄存器。该指令将执行按位与操作,并将结果存储在 r1 中。
  • #15:这是一个立即数操作数,表示按位与操作的另一个操作数。

and.w r1, r1, #15 的意思是:将 r1 寄存器中的值与立即数 15 进行按位与操作,并将结果存储回 r1 寄存器中。在二进制表示中,立即数 15 对应的二进制是 1111,所以这个指令会将 r1 中的值与一个掩码(由四个连续的 1 组成)进行按位与操作。这通常用于执行位清除操作或者提取 r1 中特定的位字段。 

执行结果为:

下一句mov r1, r4

下一句orrs r1, r0

解释如下:

  • orrs:这里的 orr 代表 "OR" 操作,即逻辑或操作。s 表示这个指令会设置状态标志(例如,条件标志)。
  • r1 和 r0:这是两个寄存器。r0 是源操作数,r1 是目标寄存器,OR 操作的结果将存储在 r1 中。

orrs r1, r0 的意思是:执行逻辑 OR 操作,将 r0 寄存器中的值 OR 到 r1 寄存器中,并更新状态标志(如果有的话)。如果需要设置某些特定的位,这个指令会非常有用。它是一种位操作指令,用于对寄存器中的位进行逻辑组合。

结果为:

逻辑或基本没有变化

下一句strh r1, [r3, #26]

解释如下:

  • strh:str 代表 "store",h 代表 "halfword",即存储一个半字到内存中。
  • r1:这是源寄存器,其值将被存储到内存中。
  • [r3, #26]:这表示内存地址的计算方式。具体来说,它是将 r3 寄存器的值与立即数 #26 相加,得到的结果就是存储操作的目标内存地址。

所以,strh r1, [r3, #26] 的意思是:将 r1 寄存器中的半字值存储到由 r3 寄存器值加上 26 计算得到的内存地址中。半字通常表示 16 位(两个字节)的数据。

执行结果为:

注意这里已经把存在全局变量修改了,但是只改了一部分。其他的在下面操作

下一句lsrs r2, r2, #12

解释如下:

  • lsrs:lsr 是逻辑右移(Logical Shift Right)的缩写,s 表示设置(Set)状态,即这个操作会更新条件标志。
  • r2(两次出现):这是源寄存器,也是目标寄存器。该指令将 r2 中的值逻辑右移指定的位数,并将结果存回 r2。
  • #12:这是移动的位数,表示将 r2 寄存器的值向右移动 12 个位。

所以,lsrs r2, r2, #12 的意思是:将 r2 寄存器中的值逻辑右移 12 位,并将结果存回 r2 寄存器中。同时,这个操作还会更新状态寄存器以反映某些条件(如是否有进位或溢出等)。

结果为:

r2从原来的0x9def9右移12bit后,0x9def9>>12 = 0x9d

下一句uxtb r2, r2

解释如下:

  • uxtb:ux 代表 "unsigned extend",tb 代表 "to byte",即无符号扩展到字节。
  • r2(两次出现):这是源寄存器和目标寄存器。

这条指令将 r2 寄存器中的内容解释为一个无符号的字节(即只取最低的 8 位),并将这个字节值扩展到整个寄存器大小(通常是 32 位)。由于是“无符号”操作,所以会保留数据的最高位作为全 1 符号扩展部分,扩展剩余的部分使用低 8 位的值。简单来说,该指令只取源寄存器最低的字节位(#0),并为其上所有的位提供全为1的掩码,其他高位的值被清零。

所以,uxtb r2, r2 的意思是:将 r2 寄存器中的内容解释为一个无符号字节,并将该字节的值填充到整个 r2 寄存器中。任何不参与运算的高位(也就是低8位以上)都设置为零。这种操作经常在低级编程中用来进行某些特殊的字节级别的数据转换和掩码操作。

执行结果:

下一句movs r1, #0

解释如下:

  • movs:mov 是“move”的缩写,表示移动或复制数据。s 表示设置(Set)状态,意味着这个指令会更新条件标志(Condition Flags)。
  • r1:这是目标寄存器,指令将把数据移动到这个寄存器中。
  • #0:这是一个立即数,值为 0。

所以,movs r1, #0 的意思是:将立即数 0 移动(或复制)到寄存器 r1 中,并更新条件标志(如果有的话)。这个指令常用于初始化寄存器或设置某个值为 0。

结果为:

下一句orrs r2, r1

r2与r1的值或运算结果为:

下一句:strb r2, [r3, #28]

解释如下:

  • strb:str 代表 "store",b 代表 "byte",表示这是一个存储字节的操作。
  • r2:这是源寄存器,它包含要存储到内存中的值或地址。
  • [r3, #28]:这表示内存地址的计算方式。具体来说,它是将 r3 寄存器的值与立即数 #28 相加,得到的结果就是存储操作的目标内存地址。

所以,strb r2, [r3, #28] 的意思是:将 r2 寄存器中的字节值存储到由 r3 寄存器值加上 28 计算得到的内存地址中。注意,这里存储的是字节,而不是半字(halfword)或字(word),所以这个操作通常涉及单个字节的写入操作。

r3= 0x2004008 + 28 为VehCommData.BAT_HIGH_VOLTAGE_1.signal.LBMS_LinkVltg_V后半段,

执行结果:

VehCommData.BAT_HIGH_VOLTAGE_1.signal.LBMS_LinkVltg_V 的值从0xef0变成了0x9def9,到这里完成对全局变量的赋值

下面几句为释放栈空间,恢复通用寄存器R4 R7,准备跳转回去,这里省略

000273cc: adds r7, #8

000273ce: mov sp, r7

000273d0: pop {r4, r7}

000273d2: bx lr

000273d4: ands r0, r1

000273d6: movs r0, #0

总结

一句简单的赋值语句,涉及全局数据的操作,有2个执指令:

strh r1, [r3, #26]

strb r2, [r3, #28]

一个是存执半字 16bit数据一个是存储8bit,目标变量共使用了20bit,所以这里一个半字+字节操作需要2个操作才能完成。

可以得出的结论是,对VehCommData.BAT_HIGH_VOLTAGE_1.signal.LBMS_LinkVltg_V的赋值不是原子的,需要2条指令才能完成

解决办法

按照搜索到的经验,可以通过int型字节进行赋值,因为一条汇编指令就可以完成,有待考证,如果有其他办法的朋友可以分享下。

附上单个整型变量赋值的c和汇编示例:

static int test_var = 0;
int test_i = 0;
void xxxxxxxx(void )
{
.......
    test_i++;
    test_var = test_i;
 .......
}
//对应汇编代码
00025ebf:   str     r2, [r3, #0]
00025ec1:   str     r2, [r3, #4]
00025ec3:   ldr     r3, [pc, #276]  ; (0x25fd8 <xxxxxx+304>)
00025ec5:   ldr     r3, [r3, #0]
00025ec7:   adds    r3, #1    //test_i自加操作,r3 = R3+1
00025ec9:   ldr     r2, [pc, #268]  ; (0x25fd8 <xxxx+304>)//0x25fd8处加载数据到r2,这里
                                    //存放的是test_i变量的地址,把test_i的地址放到r2中                                    
00025ecb:   str     r3, [r2, #0]//test_i的值放到r3中,
00025ecd:   ldr     r3, [pc, #264]  ; (0x25fd8 <xxxx+304>)
00025ecf:   ldr     r3, [r3, #0]//将test_i 的结果放到r3
00025ed1:   ldr     r2, [pc, #264]  ; (0x25fdc <xxxxx+308>)//将test_var的地址放到r2中
00025ed3:   str     r3, [r2, #0] //将r3的值赋值到r2中。这里完成赋值


......//map文件
bss.test_var  0x20000d34        0x4     xxxx
.bss.test_i   0x20000d38        0x4     xxxxx
              0x20000d38                test_i

像这句00025ed3: str r3, [r2, #0] 就是原子操作,不会被打断。

最后,文章写的代码执行过程,知识有限,有些地方理解的可能不对,如果有不对的,请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值