在討論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 | 根據啟動引腳的狀態決定哪個存儲空間被映射到此處。 片內系統存儲區起始地址:0x1fff0000(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
使用GCC編譯代碼,編譯信息如下:
其中:
- text 代表執行的代碼,程序中所有的函數都位于此處。當然還包括RO-data(Read Only)代表只讀數據,程序中所定義的全局常量數據和字符串都位于此處,如const型。
- __data__代表已初始化的讀寫數據,程序中定義并且初始化的全局變量和靜態變量位于此處。
- bss代表未初始化的讀寫數據,程序中定義了但沒有初始化的全局變量和靜態變量位于此處。GCC編譯器默認是把你沒有初始化的變量都賦值為例0。即上述的bss段。
值得注意的是,這些參數的單位是Byte。
text和data兩個段需要燒錄到FLASH等非易失性器件中。
data段需要燒錄到FLASH中,而bss段則不用,但在運行時,它們都必須裝載到可讀可寫的RAM中。
因此我們可以計算出FLASH和RAM的大小:
Flash = test + data
RAM = data + bss
這就要涉及到程序的兩種狀態:加載域和運行域。
[]()[]()Figure ? 程序的加載域和運行域
加載域 :向Flash中下載程序時,其實僅僅下載的是text+data的內容,意思就是說,在掉電情況下,Flash里面的內存僅包含text+data的內容。
運行域 :當上電后,程序運行時,首先程序會從特定的地址進行啟動,啟動時會將data的數據加載到SRAM中,單片機的test區域不需要加載到 SRAM,內核直接從 FLASH 讀取指令運行。那bss的數據怎么辦呢?對于初始值為0全局變量來說,因為要在Code區要調用該全局變量,所以肯定要對其進行描述,程序運行時就知道了,原來你是初始值為0的全局變量呀,然后就在SRAM區給你分配了一段固定區域的地址;對于局部變量來說,會自動分配大小。bss有統計作用,并且SRAM中一段特定的區域是運行bss數據,data +bss就是程序運行總共會占用SRAM的長度,生成局部變量的棧空間包含在bss區的范圍。
3.2 程序堆棧使用分析
我們知道,程序運行需要占用的大小是RAM = data + bss,而堆棧的大小是程序開始運行后才能確定的。
那么堆和棧到底能占用多大呢,堆棧的大小是在STM32F103ZETx_FLASH.ld中設置的,這里以STM32F103ZET6為例進行分析,其內部棧的大小為1KB,堆的大小為0.5KB。
使用objdump查看elf文件:
堆棧段起始地址為0x2000 002c,大小為0x604,這0x4又是怎么來的?這里查看map文件。
堆占用了0x200字節,棧占用了0x400字節,而剩下的0x4字節來自于 ALIGN(0x8),即8字節對齊,因為堆棧段緊跟.bss段之后,那首地址應該是0x2000 0070,但是規定了8字節對齊,所以最小為32,即需要補上4個字節,所以堆棧段起始地址應該是0x2000 00F4。
【注】棧:向低地址擴展,堆:向高地址擴展。如果依次定義變量,先定義的棧變量的內存地址比后定義的棧變量的內存地址要大,先定義的堆變量的內存地址比后定義的堆變量的內存地址要小。
【Tips】
1、堆棧的大小在編譯器編譯之后是不知道的,只有運行的時候才知道,所以需要注意一點,就是別造成堆棧溢出了,不然就會發生hard fault錯誤。
2、所有在處理的函數,包括函數嵌套,遞歸,等等,都是從這個“棧”里面,來分配的。所以,如果棧大小為2K,一個函數的局部變量過多,比如在函數里面定義一個char buf[512],這一下就占了1/4的棧大小了,再在其他函數里面來搞兩下,程序崩潰是很容易的事情,這時候,一般你會進入到hardfault…。
3、STM32的棧,是向下生長的。事實上,一般CPU的棧增長方向,都是向下的。而堆的生長方向,都是向上的。堆和棧,只是他們各自的起始地址和增長方向不同,他們沒有一個固定的界限,所以一旦堆棧沖突,系統就到了崩潰的時候了。
4、程序中的常量,如果沒加const也會編譯到SRAM里,加了const會被編譯到flash中。
3.3 實例代碼分析
前面分析了那么多,下面通過一個實例來驗證前面的分析。
main.c函數代碼如下:
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include
#include
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart1;
/* USER CODE BEGIN PV */
uint8_t buffer[10];//聲明了一個初始化為0的全局數組
uint8_t data = 1;//初始化的全局變量
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
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);
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
printf("First address of uninitialized global variable buffer: %p\\r\\n", buffer);
printf("Address of initialized global variable data: %p\\r\\n", &data);
printf("Address of uninitialized local variable stack_i: %p\\r\\n", &stack_i);
printf("Address of uninitialized local variable stack_j: %p\\r\\n", &stack_j);
printf("The first address of pHeap1 in the heap: %p\\r\\n", pHeap1);
printf("The first address of pHeap2 in the heap: %p\\r\\n", pHeap2);
free(pHeap1);
free(pHeap2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
HAL_Delay(500);
}
/* USER CODE END 3 */
}
編譯后內存分配如下:
運行程序,打印信息如下:
data是初始化的全局變量,在.data區;buffer是未初始化的全局變量,在.bss區;pHeap是通過malloc分配的空間,在堆區,逐漸增加;局部變量都在棧區,增加減小。
4 堆棧的使用總結
堆的使用:
1、堆的使用是要結合malloc函數,即使用一次malloc所得到的內存空間既是屬于堆的空間。
2、堆的增長方向是向上,所以malloc申請的地址也是越來越大的,前提是連續申請且在最后一次申請后再釋放內存(free)。則第一次申請的地址永遠小于后面申請的地址。
3、堆是不連續的,由于RAM中還存在局部變量,代碼段和棧等等,所以動態分配的內存是取暫時空閑的內存,而不是預先劃出一塊區域,這就是動態分配內存的好處。
4、使用堆的壞處,由于使用malloc申請內存時,不單只申請了所需的大小空間,還要額外暫用管理這部分空間的內存,而釋放時又只釋放申請的內存,所以使用堆會引入內存碎片。當然如果不是在短時間內頻繁的使用malloc申請和free釋放內存,那么操作系統就有足夠的時間來回收碎片空間。
棧的使用:
1、由編譯器分配,目的是將RAM劃分處一塊區域供程序運行時的局部變量參數等使用;
2、棧是一塊連續的內存空間,由上往下增長,即使用棧時地址是會越來越小的,如先聲明的局部變量比后聲明的地址要高;
3、棧是由程序(操作系統)自動分配,不會有內存碎片的問題;
4、棧的壞處:棧是固定且連續的一個大小,如果使用局部變量等超出了棧的大小則會造成內存溢出,而編譯器通常是發現不了的,只有當程序運行到那個函數時才會發生的。這就會引入很難查找的bug。另外如如果使用malloc申請的內存不規范使用,當釋放內存后,沒將指針地址清空,仍指向那個地址剛好是棧的地址,則會造成越界訪問。
審核編輯:湯梓紅
-
存儲器
+關注
關注
38文章
7528瀏覽量
164340 -
GCC
+關注
關注
0文章
108瀏覽量
24886 -
內存管理
+關注
關注
0文章
168瀏覽量
14188 -
Cortex-M
+關注
關注
2文章
229瀏覽量
29841
發布評論請先 登錄
相關推薦
評論