單片機(MCU)如何才能不死機之對齊訪問(Aligned Access)

topsemic 發佈 2020-01-02T01:45:10+00:00

Var_DW這個成員,如果按照在結構體中的順序,應該緊隨 Var_W1 之後,分配在 0x20000012,但是這個地址是不能被 4 整除的,所以編譯器在填充了 2 個字節 0 之後,把 Var_DW 的起始地址分配在了 0x20000014 。



從一個結構體說起。如下,在 STM32F0 的程序中,我們定義了一個結構體My_Struct ,那麼這個結構體占用多少內存呢?


struct Struct_Def {


uint8_t Var_B;

uint16_t Var_W0;

uint16_t Var_W1;

uint32_t Var_DW;


};


struct Struct_Def My_Struct;


int main(void)

{

My_Struct.Var_B = 0x01;

My_Struct.Var_W0 = 0x0203;

My_Struct.Var_W1 = 0x0405;

My_Struct.Var_DW = 0x06070809;


while(1);

}


我們粗略一算,1 + 2 + 2 + 4 = 9 Bytes 。

下載到晶片,觀察一下變量,似乎沒錯。

如果有更進一步的好奇心,我們來到內存中實際看一下,可能會有出乎意料的發現:

編譯器在 Var_B 之後插入了一個字節,在 Var_W1 之後插入了兩個字節。這個結構體在內存中實際占用了 1 + 1 + 2 + 2 + 2 + 4 = 12 Bytes 。


為什麼會這樣呢?這是 ARM Cortex M0 體系決定的,它只支持對齊訪問 ( Aligned Access )。比如我們訪問一個 4 字節 (Double Word) 型的變量時,如果這個變量的起始地址是能被 4 整除的話,我們說這種訪問是雙字節對齊的。如果訪問一個 2 字節 ( Word ) 變量,當起始地址能被 2 整除時是對齊的。訪問字節 ( Byte ) 型變量,總是對齊的。


那麼如果進行了非對齊訪問呢?那就會產生一個嚴重錯誤 ( HardFault ) !!!


大家看一下例子中的這一個賦值語句:


My_Struct.Var_DW = 0x06070809;


它是一個 4 字節 ( Double Word ) 型的變量賦值。Var_DW 這個成員,如果按照在結構體中的順序,應該緊隨 Var_W1 之後,分配在 0x20000012,但是這個地址是不能被 4 整除的,所以編譯器在填充了 2 個字節 0 之後,把 Var_DW 的起始地址分配在了 0x20000014 。



到這裡大家肯定會有一個疑問,這樣豈不是很浪費 RAM 嗎?RAM 又是相對來說價格比較高的。特別是在結構體比較多的情況下,大量的 RAM 白白浪費了!


還好,在這裡我們可以用到偽指令 #pragma pack 了。


如下例所示,#pragma pack(1) 將會使結構體中的變量一個字節緊挨著一個字節在內存中分配,而不再考慮是否對齊的問題。可以看到結構體占用從 0x2000000C 到 0x20000014 的9個字節 RAM空間。


#pragma pack(1)

struct Struct_Def {


uint8_t Var_B;

uint16_t Var_W0;

uint16_t Var_W1;

uint32_t Var_DW;


};


struct Struct_Def My_Struct;

#pragma pack()


那麼問題來了,當我們讀寫地址非對齊的變量時,不就會產生 HardFault 嗎?

在這裡,編譯器採取了曲線救國的方針。大家看下面賦值語句對應的彙編部分就會看到,它用 4 個STRB 指令(單字節操作,無論任何地址都是對齊操作), 代替了 1 個 STR 指令 ( 4 字節操作 )。如此,犧牲了一些效率,但是節省了內存空間。


這種用法節省了 RAM,但是帶來了一種比較隱蔽的錯誤。 尤其是當我們用指針方式訪問這些變量時,編譯器無法發現錯誤,而且只有當語句實際執行時才會引起問題。所以在使用指針式要特別注意,指針所指向的地址,是否和指針類型所需要的地址對齊方式吻合。


以上面的 RAM 分配方式為例,非對齊訪問時會導致 MCU 進入 HardFault 。


volatile uint32_t Test_Var;


Test_Var = *(uint8_t *)(&My_Struct.Var_B); // 這句是可以正確執行的

Test_Var = *(uint16_t *)(&My_Struct.Var_W0); // 非對齊訪問,進入 HardFault

Test_Var = *(uint32_t *)(&My_Struct.Var_DW); // 非對齊訪問,進入 HardFault


對於變量的定義,我們還可以用下面的偽指令把變量以 n 字節對齊:

__align(n)


大家在實際工程中可以根據實際情況靈活的選擇和使用這些偽指令。


TopSemic 讓晶片使用更簡單!

關鍵字: