在讨論Cortex-M的記憶體之前,先來看看Cortex-M的存儲器系統,我們知道,Cortex-M系列的處理器,大都可以對32的存儲器進行尋址,是以存儲器的尋址空間能夠達到4G,這就意味着指定和資料共用相同的位址空間,也就是将程式存儲器、資料存儲器、寄存器和輸入輸出端口被組織在同一個4GB的線性位址空間内。資料位元組以小端格式存放在存儲器中。一個字裡的最低位址位元組被認為是該字的最低有效位元組,而最高位址位元組是最高有效位元組。
1 Cortex-M存儲器架構
4G的位址空間就是位址編碼的範圍。所謂編碼就是對每一個程式存儲器、資料存儲器、寄存器和輸入輸出端口(一個位元組)配置設定一個唯一的位址号碼,這個過程又叫做“編址”或者“位址映射”。這個過程就好像在日常生活中我們給每家每戶配置設定一個位址門牌号。與編碼相對應的是“尋址”過程——配置設定一個位址号碼給一個存儲單元的目的是為了便于找到它,完成資料的讀寫,這就是“尋址”,是以位址空間有時候又被稱作“尋址空間”。
有了4G的可尋址空間,我們就可通過尋址來操作相應的位址對象。這就需要将程式存儲器、資料存儲器、寄存器和輸入輸出端口進行統一編号,也就是存儲器映射。
存儲器映射是指把晶片中或晶片外的FLASH,RAM,外設,BOOTBLOCK等進行統一編址。即用位址來表示對象。這個位址絕大多數是由廠家規定好的,使用者隻能用而不能改。使用者隻能在挂外部RAM或FLASH的情況下可進行自定義。
如下圖,是Cortex-M3存儲器映射結構圖。
Cortex-M3是32位的核心,是以其PC指針可以指向2^32=4G的位址空間,也就是0x0000_0000——0xFFFF_FFFF這一大塊空間。根據圖中描述,Cortex-M3核心将0x0000_0000——0xFFFF_FFFF這塊4G大小的空間分成8大塊:代碼、SRAM、外設、外部RAM、外部裝置、專用外設總線-内部、專用外設總線-外部、特定廠商等,是以使用該核心的設計者必須按照這個進行各自晶片的存儲器結構設計。
首先,我們對比一下Cortex-M3存儲器結構和STM32存儲器結構:
圖中可以很清晰的看到,STM32的存儲器結構和Cortex-M3的很相似,不同的是,STM32加入了很多實際的東西,如:Flash、SRAM等。隻有加入了這些東西,才能成為一個擁有實際意義的、可以工作的處理晶片——STM32。
STM32的存儲器位址空間被劃分為大小相等的8塊區域,每塊區域大小為512MB。
位址範圍 | 描述 |
0x0000 0000 ~ 0x2000 0000 | 根據啟動引腳的狀态決定哪個存儲空間被映射到此處。 片内系統存儲區起始位址:0x1fff 0000(2K位元組的空間) |
0x2000 0000 ~ 0x4000 0000 | SRAM區,64K,其中位帶别名區首位址為:0x2200 0000 |
0x4000 0000 ~ 0x6000 0000 | 用于片内外設,外設寄存器的别名區首位址:0x4200 0000 |
0x6000 0000 ~ 0x8000 0000 | |
0x8000 0000 ~ 0xa000 0000 | 片上flash存儲區512M |
0xa000 0000 ~ 0xc000 0000 | |
0xc000 0000 ~ 0xe000 0000 | |
0xe000 0000 ~ 0xffff ffff |
對STM32存儲器知識的掌握,實際上就是對Flash和SRAM這兩個區域知識的掌握。由STM32的系統結構可以看出,Flash和SRAM這兩個區域分别由ICode總線和DCode總線與處理器通信,以此完成相應的資料交換。
當然啦,其他Cortex-M的處理和STM32的也是類似的,比如GD32、CH32等。
下面将重點描述Flash和SRAM的知識。
1.1 Cortex-M的SRAM
RAM随機存儲器(Random Access Memory)表示既可以從中讀取資料,也可以寫入資料。當機器電源關閉時,存于其中的資料就會丢失。比如電腦的記憶體條。
RAM有兩大類,一種稱為靜态RAM(Static RAM/SRAM),SRAM速度非常快,是目前讀寫最快的儲存設備了,但是它也非常昂貴,是以隻在要求很苛刻的地方使用,譬如CPU的一級緩沖,二級緩沖。另一種稱為動态RAM(Dynamic RAM/DRAM),DRAM保留資料的時間很短,速度也比SRAM慢,不過它還是比任何的ROM都要快,但從價格上來說DRAM相比SRAM要便宜很多,計算機記憶體就是DRAM的。
DRAM分為很多種,常見的主要有FPRAM/FastPage、EDORAM、SDRAM、DDR RAM、RDRAM、SGRAM以及WRAM等,這裡介紹其中的一種DDR RAM。
DDR RAM(Date-Rate RAM)也稱作DDR SDRAM,這種改進型的RAM和SDRAM是基本一樣的,不同之處在于它可以在一個時鐘讀寫兩次資料,這樣就使得資料傳輸速度加倍了。這是目前電腦中用得最多的記憶體,而且它有着成本優勢,事實上擊敗了Intel的另外一種記憶體标準-Rambus DRAM。在很多高端的顯示卡上,也配備了高速DDR RAM來提高帶寬,這可以大幅度提高3D加速卡的像素渲染能力。
為什麼需要RAM,因為相對FlASH而言,RAM的速度快很多,所有資料在FLASH裡面讀取太慢了,為了加快速度,就把一些需要和CPU交換的資料讀到RAM裡來執行。
STM32單片機内部的 RAM 為 SRAM。不同類型的Cortex-M單片機的SRAM大小是不一樣的,但起始位址都是0x2000 0000,終止位址都是0x2000 0000+其固定的容量大小。SRAM相對容量小,速度快,掉電資料丢失,其作用是用來存取各種動态的輸入輸出資料、中間計算結果以及與外部存儲器交換的資料和暫存資料。裝置斷電後,SRAM中存儲的資料就會丢失。
1.2 Cortex-M的Flash
Cortex-M的Flash,嚴格說,應該是Flash子產品。該Flash子產品包括:Flash主存儲區(Main memory)、Flash資訊區(Information block),以及Flash存儲接口寄存器區(Flash memory interface)。三個組成部分分别在0x0000 0000——0xFFFF FFFF不同的區域。下面介紹STM32的Flash,如下表所示。
STM32的閃存子產品由:主存儲器、資訊塊和閃存儲器塊3部分組成。
主存儲器,該部分用來存放代碼和資料常數(如加const類型的資料)。對于大容量産品,其被劃分為256頁,每頁2K,注意,小容量和中容量産品則每頁隻有1K位元組。主存儲起的起始位址為0X08000000,B0、B1都接GND的時候,就從0X08000000開始運作代碼。
資訊塊,該部分分為2個部分,其中啟動程式代碼,是用來存儲ST自帶的啟動程式,用于序列槽下載下傳,當B0接3.3V,B1接GND時,運作的就這部分代碼,使用者選擇位元組,則一般用于配置保護等功能。
閃存儲器塊,該部分用于控制閃存儲器讀取等,是整個閃存儲器的控制機構。
對于主存儲器和資訊塊的寫入有内嵌的閃存程式設計管理;程式設計與擦除的高壓由内部産生。
在執行閃存寫操作時,任何對閃存的讀操作都會鎖定總線,在寫完成後才能正确進行,在進行讀取或擦除操作時,不能進行代碼或者資料的讀取操作。
2 C程式記憶體分析
在C/C++程式中,編譯的程式占用記憶體分為5個區,分别為棧區、堆區、全局/靜态存儲區、常量存儲區、代碼區。
1.Text段(Code Segment/Text Segment,代碼段):通常是指用來存放程式執行代碼的一塊記憶體區域,也就是存放CPU執行的機器指令(machine instructions)。這部分區域的大小在程式運作前就已經确定,并且記憶體區域通常屬于隻讀(某些架構也允許代碼段為可寫,即允許修改程式)。在代碼段中,也有可能包含一些隻讀的常數變量,例如字元串常量等。
2.全局初始化資料區/靜态資料區(Initialized data segment/Data segment):該區包含了在程式中明确被初始化的全局變量、靜态變量(包括全局靜态變量和局部靜态變量)和常量資料(如字元串常量)。資料段屬于靜态記憶體配置設定。static聲明的變量放在data段。
3.BSS段(Block Started by Symbol):BSS段通常是指用來存放程式中未初始化的全局變量的一塊記憶體區域。BSS段屬于靜态記憶體配置設定。
4.堆(heap):堆是用于存放程式運作中被動态配置設定的記憶體段,它的大小并不固定,可動态擴張或縮減。也就是常說的用malloc,calloc, realloc 等函數配置設定的變量空間是在堆上。當程式調用malloc等函數配置設定記憶體時,新配置設定的記憶體就被動态添加到堆上(堆被擴張);當利用free等函數釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)。
5.棧(stack):棧又稱堆棧,是使用者存放程式臨時建立的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味着在資料段中存放變量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的程序棧中,并且待到調用結束後,函數的傳回值也會被存放回棧中。由于棧的先進先出(FIFO)特點,是以棧特别友善用來儲存/恢複調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時資料的記憶體區。
一個程式本質上都是由 bss段、data段、text段三個組成的。
在C/C++程式編譯完成之後,已初始化的全局變量儲存在data 段中,未初始化的全局變量儲存在bss 段中。
text和data段都在可執行檔案中(在嵌入式系統裡一般是固化在鏡像檔案中),由系統從可執行檔案中加載;而bss段不在可執行檔案中,由系統初始化。
3 STM32程式的存儲配置設定
3.1 程式所占RAM和Flash大小分析
為例調試友善,這裡使用一個裸機序列槽例子,關于序列槽的使用請參看筆者博文:
序列槽通信:https://bruceou.blog.csdn.net/article/details/79341769
使用Keil編譯代碼,編譯資訊如下:
其中:
- Code 代表執行的代碼,程式中所有的函數都位于此處。即上述的text段。
- RO-data(Read Only)代表隻讀資料,程式中所定義的全局常量資料和字元串都位于此處,如const型。
- RW-data(Read Write) 代表已初始化的讀寫資料,程式中定義并且初始化的全局變量和靜态變量位于此處。
- ZI-data(Zero Initialize) 代表未初始化的讀寫資料,程式中定義了但沒有初始化的全局變量和靜态變量位于此處。Keil編譯器預設是把你沒有初始化的變量都指派為例0。即上述的bss段。
值得注意的是,這些參數的機關是Byte。
Code和RO-Data兩個段統稱為RO段,它們和RW段,需要燒錄到FLASH等非易失性器件中。
RW段需要燒錄到FLASH中,而ZI段則不用,但在運作時,它們都必須裝載到可讀可寫的RAM中。
是以我們可以計算出FLASH和RAM的大小:
Flash = Code + RO Data + RW Data
RAM = RW-data + ZI-data
這就要涉及到程式的兩種狀态:加載域和運作域。
加載域:向Flash中下載下傳程式時,其實僅僅下載下傳的是CODE+RO-data+RW-data的内容,意思就是說,在掉電情況下,Flash裡面的記憶體僅包含CODE+RO-data+RW-data這三塊。
運作域:當上電後,程式運作時,首先程式會從特定的位址進行啟動,啟動時會将RW-data的資料加載到SRAM中,單片機的 RO區域不需要加載到 SRAM,核心直接從 FLASH 讀取指令運作。那ZI-data的資料怎麼辦呢?對于初始值為0全局變量來說,因為要在Code區要調用該全局變量,是以肯定要對其進行描述,程式運作時就知道了,原來你是初始值為0的全局變量呀,然後就在SRAM區給你配置設定了一段固定區域的位址;對于局部變量來說,會自動配置設定大小。ZI-data有統計作用,并且SRAM中一段特定的區域是運作ZI-data資料,RW-DATA+ZI-DATA就是程式運作總共會占用SRAM的長度,生成局部變量的棧空間包含在ZI-data區的範圍。
3.2 MAP 檔案剖析
程式後成功編譯後,通過編譯資訊可以檢視程式空間配置設定情況,而map檔案更加詳細的描述了程式編譯資訊。
map檔案是程式的全局符号、源檔案和代碼行号資訊的唯一的文本表示方法,它可以在任何地方、任何時候使用,不需要有額外的程式進行支援。
在MDK5中,在項目中輕按兩下Target就能自動打開.map檔案。
3.2.1 Section Cross References
該部分主要是不同檔案中函數的調用關系。
這個以main.c中的main()函數為例,調用了stm32f1_bsp_led.c中的BSP_LED_Init()函數,其他函數也都列出來了。
3.2.2 Removing Unused input sections
MDK優化會删除的備援的函數。
以stm32f10x_gpio.c檔案為例,很多函數沒有用到,是以這裡就會時删除備援的函數,減少代碼空間。
當然啦,這部分和編譯器相關,不一定會删除,這裡想要最大可能删除備援函數,需要都選相應的選項,如下圖所示。
在 Removing Unused input sections from the image 的最後會列出删除的備援函數的大小,如果在MDK上改變上圖所示的配置,下圖中的删除總代碼會有變化。
3.2.3 Image Symbol Table
Symbol Table會有兩個部分:Local Symbols和Global Symbols。
Local Symbols
該部分是Static聲明的全局變量以及C檔案中函數的位址和static聲明的函數。
Global Symbols
該部分是全局變量以及C檔案中函數。
3.2.4 Memory Map of the image
映像檔案可以分為加載域(Load Region)和運作域(Execution Region):加載域反映了ARM可執行映像檔案各個段存放在存儲器中時的位置關系。關于加載域和運作域前文已經介紹過了。
這部分為兩塊,一部分是Flash的,另外一部分是RAM的。
Flash中存放的是text段,代碼段不用加載到RAM中執行,程式在運作時MCU會直接從Flash中讀取指令。
第842行:Flash加載域的基本資訊,這裡size表示Code + RO Data + RW Data的大小,也就是檔案後面ROM Size。
第844行:Flash運作域的基本資訊,這裡size表示Code + RO Data的大小,也就是檔案後面RO Size。運作域的大小比加載域少了RW Data部分,這部分會在運作時候加載到RAM中。
另外還可以看到Flash的大小時512 Kb。
RAM中存放的是data段和bss段,該部分是需要從Flash中加載進來。
這裡隻有運作域,這裡的size包含RW Data + ZI Data,也就是RW Size,RAM的大小是64 Kb。該部分的最後兩行就是堆棧大小。
3.2.5 Image component sizes
這部分就是各個檔案中各個資料段的大小。
在xxx.map檔案的最後也會有不同資料段的資訊統計。
3.3 程式堆棧使用分析
我們知道,程式運作需要占用的大小是RAM = RW-data + ZI-data,而堆棧的大小是程式開始運作後才能确定的,堆棧的記憶體占用就是在上面RAM配置設定給RW-data + ZI-data之後的位址開始配置設定的。
那麼堆和棧到底能占用多大呢,堆棧的大小是在startup_stm32fxxx.s中設定的,這裡以STM32F103ZET6為例進行分析,其内部棧的大小為1KB,堆的大小為0.5KB。
startup_stm32fxxx.s檔案是系統的啟動檔案,主要包括堆和棧的初始化配置、中斷向量表的配置以及将程式引導到main( )函數等。
startup_stm32fxxx.s主要完成三個工作:棧和堆的初始化、定位中斷向量表、調用Reset Handler。
避免産生這類錯誤的産生,程式設計時就應該考慮變量大小和堆棧大小是否合适。一個是減少過大的臨時變量和動态申請記憶體,另一個是在SRAM空間允許的情況下增大堆棧大小,如上圖中棧大小是1024位元組,堆大小是512位元組。
我們知道,堆棧的設定是在startup_stm32fxxx.s中設定的,但是startup_stm32fxxx.s檔案是隻讀的,無法修改,隻需要設定一下該檔案的屬性,把隻讀取消即可修改。
另外,FLASH和SRAM起始位址在Options中可以檢視:
還是在xxx.mp中,我們可以看到SRAM的配置設定,如下圖所示。
從上圖中可以看出SRAM空間用來存放:
1.各個檔案中聲明和定義的全局變量、靜态資料和常量;
2.未初始化的全局變量;
3.HEAP區;
4.STACK區。
堆在使用時會從低位址往上加,而棧是從__initial_sp開始往下減。以上圖中的堆棧位址為例,malloc會從0x200000C8開始往上加,局部變量的配置設定會從0x200002C8開始往下減。如果入棧元素過大,使得入棧元素的位址通路到了0x2000068C之後的内容,就發生了棧溢出,首先會改變堆中的元素值,如果入棧元素夠大,可能會直接改變HEAP後面的全局變量。同理,當動态申請的記憶體過大時,堆中變量越界到棧中,此時就發送堆溢出。
【注】棧:向低位址擴充,堆:向高位址擴充。如果依次定義變量,先定義的棧變量的記憶體位址比後定義的棧變量的記憶體位址要大,先定義的堆變量的記憶體位址比後定義的堆變量的記憶體位址要小。
當然啦,如果使用J-link調試程式,也能檢視堆棧大小,棧頂指針就是使用SRAM的大小。
【Tips】
1、堆棧的大小在編譯器編譯之後是不知道的,隻有運作的時候才知道,是以需要注意一點,就是别造成堆棧溢出了,不然就會發生hardfault錯誤。
2、所有在處理的函數,包括函數嵌套,遞歸,等等,都是從這個“棧”裡面,來配置設定的。是以,如果棧大小為2K,一個函數的局部變量過多,比如在函數裡面定義一個char buf[512],這一下就占了1/4的棧大小了,再在其他函數裡面來搞兩下,程式崩潰是很容易的事情,這時候,一般你會進入到hardfault…。
3、STM32的棧,是向下生長的。事實上,一般CPU的棧增長方向,都是向下的。而堆的生長方向,都是向上的。堆和棧,隻是他們各自的起始位址和增長方向不同,他們沒有一個固定的界限,是以一旦堆棧沖突,系統就到了崩潰的時候了。
4、程式中的常量,如果沒加const也會編譯到SRAM裡,加了const會被編譯到flash中。
3.4 執行個體代碼分析
前面分析了那麼多,下面通過一個執行個體來驗證前面的分析。
main.c函數代碼如下:
/* Includes ------------------------------------------------------------------*/
#include "stm32f1_bsp_usart.h"
#include "stm32f1_bsp_led.h"
#include "stm32f1_bsp_systick.h"
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
uint8_t buffer[10];//聲明了一個初始化為0的全局數組
uint8_t data = 1;//初始化的全局變量
/* Private function prototypes -----------------------------------------------*/
/* Private functions ---------------------------------------------------------*/
/**
* @brief mian
* @param None
* @retval int
*/
int main(void)
{
ST_BSP_LED_Dev BSP_LED_Dev0 = LED_DEV0_CONFIG;
ST_BSP_LED_Dev BSP_LED_Dev1 = LED_DEV1_CONFIG;
ST_BSP_LED_Dev BSP_LED_Dev2 = LED_DEV2_CONFIG;
ST_BSP_USART_Dev BSP_USART_Dev0 = USART_DEV0_CONFIG;
uint8_t stack_i; //未初始化的局部變量,
uint8_t stack_j = 1; //初始化的局部變量
uint8_t *pHeap1 = (uint8_t *)malloc(10);//指針pHeap指向堆區配置設定了一個uint8_t類型10大小的空間
uint8_t *pHeap2 = (uint8_t *)malloc(10);
/* Configure the NVIC Preemption Priority Bits */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/*Systick init*/
SysTick_Init();
/*LED init*/
BSP_LED_Init(&BSP_LED_Dev0);
BSP_LED_Init(&BSP_LED_Dev1);
BSP_LED_Init(&BSP_LED_Dev2);
/* USART1 配置模式為 115200 8-N-1,中斷接收 */
BSP_USART_Init(&BSP_USART_Dev0, 115200, 0, 1);
printf("未初始化的全局變量 buffer 的首位址:0x%p\r\n", buffer);
printf("初始化的全局變量 data 的位址:0x%p\r\n", &data);
printf("未初始化的局部變量 stack_i 的位址:0x%p\r\n", &stack_i);
printf("初始化的局部變量 stack_j 的位址:0x%p\r\n", &stack_j);
printf("pHeap1 在堆區首位址:0x%p\r\n", pHeap1);
printf("pHeap2 在堆區首位址:0x%p\r\n", pHeap2);
free(pHeap1);
free(pHeap2);
while(1)
{
BSP_LED_Toggle(&BSP_LED_Dev0);
Delay_ms(500);
BSP_LED_Toggle(&BSP_LED_Dev1);
Delay_ms(500);
BSP_LED_Toggle(&BSP_LED_Dev2);
Delay_ms(500);
}
}
編譯後map檔案中記憶體配置設定如下:
運作程式,列印資訊如下:
data是初始化的全局變量,在.data區;buffer是未初始化的全局變量,在.bss區;pHeap是通過malloc配置設定的空間,在堆區,逐漸增加;局部變量都在棧區,增加減小。