【燒腦技術貼】無法回避的字節對齊問題,從八個方向深入探討(變量對齊,棧對齊,DMA對齊,結構體成對齊,Cache, RTOS雙堆棧等)


【本文為安富萊電子原創】

本期的知識點要稍微燒點腦細胞,因為字節對齊問題涉及到的地方太多,且無法規避,必須硬着頭皮上。

下面要說的每個技術點,其實都可以專門開一個帖子說,所以我們這里的討論,爭取言簡意賅,並配上官方文檔和實驗數據,力求有理有據。如果講解有誤的地方,歡迎大家指正,我們主要討論M0,M0+,  M3,M4和M7內核。



一、引出問題:

字節對齊的含義:4字節對齊的含義就是變量地址對4求余數為0; 8字節對齊就是地址對8求余等於0,依次類推:

比如
uint32_t *p;
p=(uint32_t *)0x20000004; 這個地址是4字節對齊。

如果讓p去訪問0x20000001, 0x20000002,0x20000003這都是不對齊訪問。



二、背景知識:

對於M3和M4而言,可以直接訪問非對齊地址(注意芯片要在這個地址有對應的內存空間),  因為M3和M4是支持的,而M0/M0+/M1是不支持的,不支持內核芯片,只要非對齊訪問就會觸發硬件異常。

M7內核也支持非對齊訪問,在M7的TRM中描述如下:




三、全局變量對齊問題:

基本上用戶定義的變量是幾個字節就是幾字節對齊,這個比較好理解。

uint8_t定義變量地址要1字節對齊。
uint16_t定義變量地址要2字節對齊。
uint32_t定義變量地址要4字節對齊。
uint64_t定義變量地址要8字節對齊。

指針變量是4字節對齊。




四、結構體成員對齊問題:

首先明白一點,結構體里面的變量是什么類型,此變量的位置就是至少要幾字節對齊,所以就存在結構體實際占用大小不是這些變量之和。

typedef struct
{
        uint8_t a;
        uint16_t b;
        uint32_t c;
        uint64_t d;        
}info;

這種定義,info占用了16字節,a單字節對齊,b是兩字節對齊,而c要是4字節對齊,從出現b定義完畢后空出來1個字節未被使用。d是8字節對齊,這樣就是16字節。而我們切換下變量定義順序:
typedef struct
{
        uint16_t b;
        uint32_t c;
        uint64_t d;        
        uint8_t  a;
}info;

這種定義就要占用24字節,b占用2字節對齊,c需要4字節對齊,這樣就空出來2兩個字節未使用,d占用8字節,最后一個a占用了8字節。

如果想定義幾個變量就幾個字節,變量前面加前綴__packed即可。

不管是上面那種定義方式,都是占用15個字節。

__packed typedef struct
{
        uint8_t a;   1個
        uint16_t b; 2個
        uint32_t c; 4個
        uint64_t d; 8個        
}info;



五、局部變量對齊問題:

局部變量使用的是棧空間(除了靜態局部變量和編譯器優化不使用棧,直接用寄存器做變量空間),也就是大家使用在xxxx.S啟動文件開辟的stack空間。

在M內核里面,局部變量的對齊問題如果研究起來是最燒腦的,這個涉及到AAPCS規約(Procedure Call Standard for the Arm Architecture,  Arm架構的程序調用標准)。

 

 

上面這個貼圖最重要,僅需理解上面這兩條就可以,意思是說,棧地址是全程至少保持4字節對齊的,因為M內核的硬件長做了處理,SP最低兩個bit,bit0和bit1直接固定為0了。

但是在程序調用入口處必須滿足8字節對齊,對於C語言,不需要用戶去管,編譯器都幫我們處理好了,先來個簡單的示例壓壓驚:

 

而匯編文件是需要用戶去處理的。以xxx.S啟動文件為例,通過偽指令PRESERVE8來保證

 

那么問題來了,我們搞個4對齊是不是會出問題,一般情況下也沒問題的,但特殊情況下不行,特別調用C庫的sprintf和printf函數,直接給你輸出個不知所以然的結果來。比如我在H7上做如下測試:

 

輸出結果:




六、中斷服務程序的棧對齊問題:

先來看兩個圖:

 

 

通過這兩個圖我們了解到:M0/M0+/M7的棧地址是固定8字節對齊,M3/M4的棧地址是對齊是可以通過SCB->CCR寄存器編程的為4字節或者8字節對齊。

比如我們設置的8字節對齊,那么中斷發生的時候,如果SP指針位置在4字節對齊,那么硬件自動插入4字節來保證8字節對齊,之后就是硬件自動入棧的寄存器開始存入棧中。

另外就是不同的M內核硬件版本,這個地方略有不同,這個大家作為了解即可,早期的內核硬件版本應該沒什么人用來做芯片了。

 

七、硬件浮點對齊問題

如果使用的是帶FPU硬件浮點單元的M內核芯片就要注意對齊訪問了,訪問單精度浮點數訪問一定要4字節對齊,雙精度要8字節對齊。

比如我們使用支持單精度浮點的M4內核芯片,測試代碼如下:

 

MDK直接給你來個不對齊硬件異常:





八、RTOS的任務棧:

RTOS的任務棧涉及到雙棧指針問題,SP(R13寄存器)有兩個棧指針,MSP主棧指針和PSP進程棧指針。簡單的說,我們在中斷服務程序里面都是用的MSP,而任務里面用的PSP。

優勢是方便任務和中斷棧空間分別管理,了解了這點知識就夠了。

RTOS任務棧的關鍵依然是8字節對齊問題,如果僅僅是滿足4字節對齊,就會出現我們前面printf和sprintf浮點數或者64bit數據的錯誤問題,早年各種RTOS移植案例還不是那么發達的時候(現在問題依舊),經常在這個地方入坑,加上硬件浮點寄存器入棧出棧后更是玩不轉了。

比如大家搜索關鍵詞 uCOS printf 或者uCOS 浮點數,一堆的問題,平時不用浮點不知道,一用浮點,各種問題就來了,特別是多任務都使用浮點計算,更是懵。

根本原因是底層移植文件的堆棧8字對齊有問題,很多人都是采用的指令__align(8)來設置堆棧對齊問題,其實修改底層port文件才是解決問題的根本。

為什么會造成這個問題,根本原因依然是前面AAPCS規約的要求,RTOS的移植都有個匯編的port文件,這個port文件的關鍵是實現任務切換,任務切換的關鍵就是進入任務前保證PSP是8字節對齊。



九、DMA對齊問題:

DMA對齊指的是源數據地址和目的數據對齊問題。這個問題最容易出錯的地方就是網上倒騰SD卡移植FatFS的SDIO DMA方式。

大家網上搜關鍵詞FatFS SDIO DMA,也是一瓢的問題,特別是BMP等格式圖片顯示的時候,這種問題就來了,因為很難保證每次的讀取都是4字節對齊的。

以STM32F4的DMA為例,我們的底層移植無需再單獨開一個緩沖做4字節對齊,本質是F4 DMA支持了源地址和目的地址的數據寬度可以不同,但是數據地址必須要跟其數據類型對齊。

比如使用SDIO DMA從SD卡讀取數據,我們就可以設置源地址依然是4字節對齊(外設訪問要4字節對齊),而目的地址設置為字節對齊,就可以方便的解決4字節對齊問題。

 

其實不僅是通用的DMA,像圖形加速DMA2D,SDMMC自帶的IDMA等都有這種問題。



十、配置MPU造成的對齊問題:

這個問題主要是對於M7內核芯片來說,以STM32H7 TCM以外空間為例:AXI RAM(0x2400 0000),
SRAM1(0x3000 0000),
SRAM2(0x3002 0000),
SRAM3(0x3004 0000),
SRAM4(0x3800 0000),
SDRAM等做非對齊訪問都會有硬件異常,而開啟Cache就不會有問題。

這個問題的關鍵就是M7的TRM中這句話:

意思是,如果用戶使用MPU將H7的AXI總線下的內存空間配置為Device 或者 Strongly-ordered模式,用戶采用非對齊方式訪問,將會觸發UsageFault

 

 

實際測序下,果然會觸發這個異常

 

配置內存空間的MPU屬性為Device 和 Strongly-ordered以外的屬性就可以解決此問題了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM