STM32系列的M0内核刚出来的时候,为了降本,我有很多项目都需要移植到M0平台,本以为只是把底层驱动看一下就可以,却不想总是遇到莫名其妙的死机,着实折腾过一段时间的。今天在DIY遥控器的时候,突然又发现了一个当时遇到的问题,记录下来给大家提个醒,万一遇到的死机的情况,不妨思考一下这个问题。
布置场景
因为我的遥控器是基于无线RF通信的,因此我需要定义了两个帧结构,分别用数组来存放,然后再发送和接收函数中直接处理数组。帧结构的前四个字节作为通信的地址,也就是一个过滤ID,不符合这个ID的帧直接放弃掉。为了让系统中兼容更多的设备对,我把这个通信地址定义为4个字节,也就是一个uint32_t
类型。数组及其索引值定义如下:
// 接收缓冲区长度定义
#define DW_RX_MSG_LEN 18
// 接收包中的地址索引
#define RX_ADDRESS_LL 0
#define RX_ADDRESS_LH 1
#define RX_ADDRESS_HL 2
#define RX_ADDRESS_HH 3
...
uint8_t g_dw_rx_msg[DW_RX_MSG_LEN] = {0}; //接收缓存定义
接收缓冲器如上定义完成后,我将在接收函数中处理这个接收到的帧,第一件事情肯定是比对地址是否匹配,假设我宏定义的地址为:
#define ADDRESS 0x12345678
最简单的写法肯定是这样判断:
if((g_dw_rx_msg[RX_ADDRESS_LL] == (ADDRESS >> 0) & 0xFF) &&
(g_dw_rx_msg[RX_ADDRESS_LH] == (ADDRESS >> 8) & 0xFF) &&
(g_dw_rx_msg[RX_ADDRESS_HL] == (ADDRESS >> 16) & 0xFF) &&
(g_dw_rx_msg[RX_ADDRESS_HH] == (ADDRESS >> 24) & 0xFF)
)
{
}
上面这种写法很明了,我们比对每一个字节是否相等,如果都相等,我们认为这个帧是发送给我们的,但是这样的写法比较啰嗦,字数比较多,并且并不直观呀,好好的一个32bit的数据,非要拆分成4次比较,干起来效率一点都不高。
问题是怎么发生的?
C语言中一定要用指针才显得高级,这里很明显可以使用指针来取出数组的前四个字节,然后和我们的宏定义地址相比较,这样是非常简单高效的,大牛们常常都是这么写的。
if(*(uint32_t *)g_dw_rx_msg == ADDRESS)
{
}
你看多简单,数组的名称就是数组的地址,也是第一个元素的地址,我们取出来转换成32bit整型,然后取出来直接和我们的地址比较。既能清楚的表达这是一个地址的比对,又能省不少敲字符的力气。这样也许在CortexM3上面大概率不会出现问题,但是在M0内核上大概率会出问题,会死机的。为什么总说是概率呢?我先来描述一下你可能遇到的现象。我按照指针的写法编译通过了,运行也没有问题,性能杠杠的,贼稳定。后来,突然增加了一个需求,于是我又定义了一些变量和数组,再编译,运行….
我擦,死机了!我就一直在新增的功能那里不断地查找问题,怎么也找不到那里写的有问题,只要我新定一个变量,并且在程序中使用它(防止编译器优化掉),就会死机,去掉就没事了。调试过程中,你一定遇到过这样的场景。当我们Debug一步一步的跟踪时,就会发现,程序死机之前执行了我们的判断语句。
原因是什么呢?
我们都知道,CortexM0内核是一个32位总线的,它的硬件无法处理32位未对齐的内存访问,那么他对于内存的访问地址一定是4的整数倍,否则就会出现无法访问内存的问题。这里,我们的判断语句中使用了指针的强制类型转换(uint3_t*)
,那么如果我们数组中的元素地址恰好不是4的整数倍的时候,在转化成uint3_t进行访问时,处理器就只能报错了。
为什么出错是概率性的呢?编译器会根据我们定义的全局变量和静态变量来安排变量的地址,并且数组这东西,如果定义成uint8_t类型的,他中间的某个元素地址对齐的可能性只有四分之一。也就是说,我们第一次编译成功,运行可靠的时候,恰好我们定义的全局变量合适,编译器把我们的数组起始地址正好安排在32位对齐的地址上。当我们再定义一个变量的时候,一定是定义了非32位对齐的,比如uint8_t,或者一个数组或者结构体,总之,它不是4字节的整数倍。这样,我们再编译,编译器就会重新给我们安排所有变量的地址,我们的数组的地址就恰好给挤到了一个地址不对齐的地方。我们再用32bit的指针类型去访问时,就会死机了。
如何处理呢?
有三种方法处理。第一种,要承认傻人有傻福,笨办法有笨办法的优点,有些事情投机取巧可能会赚,但大概率还是赔的。就好比我们的大A,你以为是投资,后来认为是投机,再后来,你以为是诈骗,现在你可能认为是抢劫了,其实,我们的大A本质上是捐款!跑题了,所以,我们老老实实按字节比较就可以了。
if((g_dw_rx_msg[RX_ADDRESS_LL] == (ADDRESS >> 0) & 0xFF) &&
(g_dw_rx_msg[RX_ADDRESS_LH] == (ADDRESS >> 8) & 0xFF) &&
(g_dw_rx_msg[RX_ADDRESS_HL] == (ADDRESS >> 16) & 0xFF) &&
(g_dw_rx_msg[RX_ADDRESS_HH] == (ADDRESS >> 24) & 0xFF)
)
{
}
第二种,如果你实在忍受不了这个样式,想让程序更直观的体现出来,它是一个地址的比对,那么就自己写一个函数,将四个字节取出来组成一个32bit的整型数。或者,你可以直接利用memcpy函数来实现。
uint32_t my_address = 0;
memcpy(&my_address, &((uint32_t*)g_dw_rx_msg), sizeof(uint32_t));
第三种,如果你还是纠结于优雅和效率,那我们就不撞南墙不回头,编译器自身提供了一些指令,来强制我们定义的变量地址对齐。比如:__attribute__((aligned(4)))
如下定义:
__attribute__((aligned(4))) uint8_t g_dw_rx_msg[DW_RX_MSG_LEN] = {0}; //接收缓存
按照上面的定义方法,我们的数组其实地址就会被安排在32位地址对齐的位置上了,指针就可以大显身手了,不过要注意,如果我们的地址索引在数组内部不对齐也是不行的。比如,我们的地址是从数组的第1个地址开始的。(前面有一个第0个)
// 接收缓冲区长度定义
#define DW_RX_MSG_LEN 18
// 接收包中的地址索引
#define RX_ADDRESS_LL 1 //这里地址从数组的第1个元素开始
#define RX_ADDRESS_LH 2
#define RX_ADDRESS_HL 3
#define RX_ADDRESS_HH 4
...
__attribute__((aligned(4))) uint8_t g_dw_rx_msg[DW_RX_MSG_LEN] = {0}; //接收缓存
if(*(uint32_t *)g_dw_rx_msg == ADDRESS)
{
// 我还得死啊!!!!
}