STM32系列的M0內(nèi)核剛出來的時(shí)候,為了降本,我有很多項(xiàng)目都需要移植到M0平臺(tái),本以為只是把底層驅(qū)動(dòng)看一下就可以,卻不想總是遇到莫名其妙的死機(jī),著實(shí)折騰過一段時(shí)間的。今天在DIY遙控器的時(shí)候,突然又發(fā)現(xiàn)了一個(gè)當(dāng)時(shí)遇到的問題,記錄下來給大家提個(gè)醒,萬一遇到的死機(jī)的情況,不妨思考一下這個(gè)問題。
布置場景
因?yàn)槲业倪b控器是基于無線RF通信的,因此我需要定義了兩個(gè)幀結(jié)構(gòu),分別用數(shù)組來存放,然后再發(fā)送和接收函數(shù)中直接處理數(shù)組。幀結(jié)構(gòu)的前四個(gè)字節(jié)作為通信的地址,也就是一個(gè)過濾ID,不符合這個(gè)ID的幀直接放棄掉。為了讓系統(tǒng)中兼容更多的設(shè)備對,我把這個(gè)通信地址定義為4個(gè)字節(jié),也就是一個(gè)uint32_t
類型。數(shù)組及其索引值定義如下:
// 接收緩沖區(qū)長度定義
#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}; //接收緩存定義
接收緩沖器如上定義完成后,我將在接收函數(shù)中處理這個(gè)接收到的幀,第一件事情肯定是比對地址是否匹配,假設(shè)我宏定義的地址為:
#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)
)
{
}
上面這種寫法很明了,我們比對每一個(gè)字節(jié)是否相等,如果都相等,我們認(rèn)為這個(gè)幀是發(fā)送給我們的,但是這樣的寫法比較啰嗦,字?jǐn)?shù)比較多,并且并不直觀呀,好好的一個(gè)32bit的數(shù)據(jù),非要拆分成4次比較,干起來效率一點(diǎn)都不高。
問題是怎么發(fā)生的?
C語言中一定要用指針才顯得高級,這里很明顯可以使用指針來取出數(shù)組的前四個(gè)字節(jié),然后和我們的宏定義地址相比較,這樣是非常簡單高效的,大牛們常常都是這么寫的。
if(*(uint32_t *)g_dw_rx_msg == ADDRESS)
{
}
你看多簡單,數(shù)組的名稱就是數(shù)組的地址,也是第一個(gè)元素的地址,我們?nèi)〕鰜磙D(zhuǎn)換成32bit整型,然后取出來直接和我們的地址比較。既能清楚的表達(dá)這是一個(gè)地址的比對,又能省不少敲字符的力氣。這樣也許在CortexM3上面大概率不會(huì)出現(xiàn)問題,但是在M0內(nèi)核上大概率會(huì)出問題,會(huì)死機(jī)的。為什么總說是概率呢?我先來描述一下你可能遇到的現(xiàn)象。我按照指針的寫法編譯通過了,運(yùn)行也沒有問題,性能杠杠的,賊穩(wěn)定。后來,突然增加了一個(gè)需求,于是我又定義了一些變量和數(shù)組,再編譯,運(yùn)行….
我擦,死機(jī)了!我就一直在新增的功能那里不斷地查找問題,怎么也找不到那里寫的有問題,只要我新定一個(gè)變量,并且在程序中使用它(防止編譯器優(yōu)化掉),就會(huì)死機(jī),去掉就沒事了。調(diào)試過程中,你一定遇到過這樣的場景。當(dāng)我們Debug一步一步的跟蹤時(shí),就會(huì)發(fā)現(xiàn),程序死機(jī)之前執(zhí)行了我們的判斷語句。
原因是什么呢?
我們都知道,CortexM0內(nèi)核是一個(gè)32位總線的,它的硬件無法處理32位未對齊的內(nèi)存訪問,那么他對于內(nèi)存的訪問地址一定是4的整數(shù)倍,否則就會(huì)出現(xiàn)無法訪問內(nèi)存的問題。這里,我們的判斷語句中使用了指針的強(qiáng)制類型轉(zhuǎn)換(uint3_t*)
,那么如果我們數(shù)組中的元素地址恰好不是4的整數(shù)倍的時(shí)候,在轉(zhuǎn)化成uint3_t進(jìn)行訪問時(shí),處理器就只能報(bào)錯(cuò)了。
為什么出錯(cuò)是概率性的呢?編譯器會(huì)根據(jù)我們定義的全局變量和靜態(tài)變量來安排變量的地址,并且數(shù)組這東西,如果定義成uint8_t類型的,他中間的某個(gè)元素地址對齊的可能性只有四分之一。也就是說,我們第一次編譯成功,運(yùn)行可靠的時(shí)候,恰好我們定義的全局變量合適,編譯器把我們的數(shù)組起始地址正好安排在32位對齊的地址上。當(dāng)我們再定義一個(gè)變量的時(shí)候,一定是定義了非32位對齊的,比如uint8_t,或者一個(gè)數(shù)組或者結(jié)構(gòu)體,總之,它不是4字節(jié)的整數(shù)倍。這樣,我們再編譯,編譯器就會(huì)重新給我們安排所有變量的地址,我們的數(shù)組的地址就恰好給擠到了一個(gè)地址不對齊的地方。我們再用32bit的指針類型去訪問時(shí),就會(huì)死機(jī)了。
如何處理呢?
有三種方法處理。第一種,要承認(rèn)傻人有傻福,笨辦法有笨辦法的優(yōu)點(diǎn),有些事情投機(jī)取巧可能會(huì)賺,但大概率還是賠的。就好比我們的大A,你以為是投資,后來認(rèn)為是投機(jī),再后來,你以為是詐騙,現(xiàn)在你可能認(rèn)為是搶劫了,其實(shí),我們的大A本質(zhì)上是捐款!跑題了,所以,我們老老實(shí)實(shí)按字節(jié)比較就可以了。
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)
)
{
}
第二種,如果你實(shí)在忍受不了這個(gè)樣式,想讓程序更直觀的體現(xiàn)出來,它是一個(gè)地址的比對,那么就自己寫一個(gè)函數(shù),將四個(gè)字節(jié)取出來組成一個(gè)32bit的整型數(shù)。或者,你可以直接利用memcpy函數(shù)來實(shí)現(xiàn)。
uint32_t my_address = 0;
memcpy(&my_address, &((uint32_t*)g_dw_rx_msg), sizeof(uint32_t));
第三種,如果你還是糾結(jié)于優(yōu)雅和效率,那我們就不撞南墻不回頭,編譯器自身提供了一些指令,來強(qiáng)制我們定義的變量地址對齊。比如:__attribute__((aligned(4)))
?如下定義:
__attribute__((aligned(4))) uint8_t g_dw_rx_msg[DW_RX_MSG_LEN] = {0}; //接收緩存
按照上面的定義方法,我們的數(shù)組其實(shí)地址就會(huì)被安排在32位地址對齊的位置上了,指針就可以大顯身手了,不過要注意,如果我們的地址索引在數(shù)組內(nèi)部不對齊也是不行的。比如,我們的地址是從數(shù)組的第1個(gè)地址開始的。(前面有一個(gè)第0個(gè))
// 接收緩沖區(qū)長度定義
#define DW_RX_MSG_LEN 18
// 接收包中的地址索引
#define RX_ADDRESS_LL 1 //這里地址從數(shù)組的第1個(gè)元素開始
#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)
{
// 我還得死?。。。?!
}