在討論Cortex-M的內(nèi)存之前,先來看看Cortex-M的存儲器系統(tǒng),我們知道,Cortex-M系列的處理器,大都可以對32的存儲器進行尋址,因此存儲器的尋址空間能夠達到4G,這就意味著指定和數(shù)據(jù)共用相同的地址空間,也就是將程序存儲器、數(shù)據(jù)存儲器、寄存器和輸入輸出端口被組織在同一個4GB的線性地址空間內(nèi)。數(shù)據(jù)字節(jié)以小端格式存放在存儲器中。一個字里的最低地址字節(jié)被認(rèn)為是該字的最低有效字節(jié),而最高地址字節(jié)是最高有效字節(jié)。
1 Cortex-M存儲器架構(gòu)
4G的地址空間就是地址編碼的范圍。所謂編碼就是對每一個程序存儲器、數(shù)據(jù)存儲器、寄存器和輸入輸出端口(一個字節(jié))分配一個唯一的地址號碼,這個過程又叫做“編址”或者“地址映射”。這個過程就好像在日常生活中我們給每家每戶分配一個地址門牌號。與編碼相對應(yīng)的是“尋址”過程——分配一個地址號碼給一個存儲單元的目的是為了便于找到它,完成數(shù)據(jù)的讀寫,這就是“尋址”,因此地址空間有時候又被稱作“尋址空間”。
有了4G的可尋址空間,我們就可通過尋址來操作相應(yīng)的地址對象。這就需要將程序存儲器、數(shù)據(jù)存儲器、寄存器和輸入輸出端口進行統(tǒng)一編號,也就是存儲器映射。
存儲器映射是指把芯片中或芯片外的FLASH,RAM,外設(shè),BOOTBLOCK等進行統(tǒng)一編址。即用地址來表示對象。這個地址絕大多數(shù)是由廠家規(guī)定好的,用戶只能用而不能改。用戶只能在掛外部RAM或FLASH的情況下可進行自定義。
如下圖,是Cortex-M3存儲器映射結(jié)構(gòu)圖。
Cortex-M3是32位的內(nèi)核,因此其PC指針可以指向2^32=4G的地址空間,也就是0x0000_0000——0xFFFF_FFFF這一大塊空間。根據(jù)圖中描述,Cortex-M3內(nèi)核將0x0000_0000——0xFFFF_FFFF這塊4G大小的空間分成8大塊:代碼、SRAM、外設(shè)、外部RAM、外部設(shè)備、專用外設(shè)總線-內(nèi)部、專用外設(shè)總線-外部、特定廠商等,因此使用該內(nèi)核的設(shè)計者必須按照這個進行各自芯片的存儲器結(jié)構(gòu)設(shè)計。
首先,我們對比一下Cortex-M3存儲器結(jié)構(gòu)和STM32存儲器結(jié)構(gòu):
圖中可以很清晰的看到,STM32的存儲器結(jié)構(gòu)和Cortex-M3的很相似,不同的是,STM32加入了很多實際的東西,如:Flash、SRAM等。只有加入了這些東西,才能成為一個擁有實際意義的、可以工作的處理芯片——STM32。
STM32的存儲器地址空間被劃分為大小相等的8塊區(qū)域,每塊區(qū)域大小為512MB。
地址范圍 | 描述 |
---|---|
0x0000 0000 ~0x2000 0000 | 根據(jù)啟動引腳的狀態(tài)決定哪個存儲空間被映射到此處。 片內(nèi)系統(tǒng)存儲區(qū)起始地址:0x1fff0000(2K字節(jié)的空間) |
0x2000 0000 ~0x4000 0000 | SRAM區(qū),64K,其中位帶別名區(qū)首地址為:0x2200 0000 |
0x4000 0000 ~0x6000 0000 | 用于片內(nèi)外設(shè),外設(shè)寄存器的別名區(qū)首地址:0x4200 0000 |
0x6000 0000 ~0x8000 0000 | |
0x8000 0000 ~0xa000 0000 | 片上flash存儲區(qū)512M |
0xa000 0000 ~0xc000 0000 | |
0xc000 0000 ~0xe000 0000 | |
0xe000 0000 ~0xffff ffff |
對STM32存儲器知識的掌握,實際上就是對Flash和SRAM這兩個區(qū)域知識的掌握。由STM32的系統(tǒng)結(jié)構(gòu)可以看出,F(xiàn)lash和SRAM這兩個區(qū)域分別由ICode總線和DCode總線與處理器通信,以此完成相應(yīng)的數(shù)據(jù)交換。
當(dāng)然啦,其他Cortex-M的處理和STM32的也是類似的,比如GD32、CH32等。
下面將重點描述Flash和SRAM的知識。
1.1 Cortex-M的SRAM
RAM隨機存儲器(Random Access Memory)表示既可以從中讀取數(shù)據(jù),也可以寫入數(shù)據(jù)。當(dāng)機器電源關(guān)閉時,存于其中的數(shù)據(jù)就會丟失。比如電腦的內(nèi)存條。
RAM有兩大類,一種稱為靜態(tài)RAM(Static RAM/SRAM),SRAM速度非常快,是目前讀寫最快的存儲設(shè)備了,但是它也非常昂貴,所以只在要求很苛刻的地方使用,譬如CPU的一級緩沖,二級緩沖。另一種稱為動態(tài)RAM(Dynamic RAM/DRAM),DRAM保留數(shù)據(jù)的時間很短,速度也比SRAM慢,不過它還是比任何的ROM都要快,但從價格上來說DRAM相比SRAM要便宜很多,計算機內(nèi)存就是DRAM的。
DRAM分為很多種,常見的主要有FPRAM/FastPage、EDORAM、SDRAM、DDR RAM、RDRAM、SGRAM以及WRAM等,這里介紹其中的一種DDR RAM。
DDR RAM(Date-Rate RAM)也稱作DDR SDRAM,這種改進型的RAM和SDRAM是基本一樣的,不同之處在于它可以在一個時鐘讀寫兩次數(shù)據(jù),這樣就使得數(shù)據(jù)傳輸速度加倍了。這是目前電腦中用得最多的內(nèi)存,而且它有著成本優(yōu)勢,事實上擊敗了Intel的另外一種內(nèi)存標(biāo)準(zhǔn)-Rambus DRAM。在很多高端的顯卡上,也配備了高速DDR RAM來提高帶寬,這可以大幅度提高3D加速卡的像素渲染能力。
為什么需要RAM,因為相對FlASH而言,RAM的速度快很多,所有數(shù)據(jù)在FLASH里面讀取太慢了,為了加快速度,就把一些需要和CPU交換的數(shù)據(jù)讀到RAM里來執(zhí)行。
STM32單片機內(nèi)部的 RAM 為 SRAM。不同類型的Cortex-M單片機的SRAM大小是不一樣的,但起始地址都是0x2000 0000,終止地址都是0x2000 0000+其固定的容量大小。SRAM相對容量小,速度快,掉電數(shù)據(jù)丟失,其作用是用來存取各種動態(tài)的輸入輸出數(shù)據(jù)、中間計算結(jié)果以及與外部存儲器交換的數(shù)據(jù)和暫存數(shù)據(jù)。設(shè)備斷電后,SRAM中存儲的數(shù)據(jù)就會丟失。
1.2 Cortex-M的Flash
Cortex-M的Flash,嚴(yán)格說,應(yīng)該是Flash模塊。該Flash模塊包括: Flash主存儲區(qū)(Main memory)、Flash信息區(qū)(Information block),以及Flash存儲接口寄存器區(qū)(Flash memory interface) 。三個組成部分分別在0x0000 0000——0xFFFF FFFF不同的區(qū)域。下面介紹STM32的Flash,如下表所示。
STM32的閃存模塊由:__主存儲器、信息塊和閃存儲器塊__3部分組成。
主存儲器 ,該部分用來存放代碼和數(shù)據(jù)常數(shù)(如加const類型的數(shù)據(jù))。對于大容量產(chǎn)品,其被劃分為256頁,每頁2K,注意,小容量和中容量產(chǎn)品則每頁只有1K字節(jié)。主存儲起的起始地址為0X08000000,B0、B1都接GND的時候,就從0X08000000開始運行代碼。
信息塊 ,該部分分為2個部分,其中啟動程序代碼,是用來存儲ST自帶的啟動程序,用于串口下載,當(dāng)B0接3.3V,B1接GND時,運行的就這部分代碼,用戶選擇字節(jié),則一般用于配置保護等功能。
閃存儲器塊 ,該部分用于控制閃存儲器讀取等,是整個閃存儲器的控制機構(gòu)。
對于主存儲器和信息塊的寫入有內(nèi)嵌的閃存編程管理;編程與擦除的高壓由內(nèi)部產(chǎn)生。
在執(zhí)行閃存寫操作時,任何對閃存的讀操作都會鎖定總線,在寫完成后才能正確進行,在進行讀取或擦除操作時,不能進行代碼或者數(shù)據(jù)的讀取操作。
2 C程序內(nèi)存分析
在C/C++程序中,編譯的程序占用內(nèi)存分為5個區(qū),分別為__棧區(qū)、堆區(qū)、全局/靜態(tài)存儲區(qū)、常量存儲區(qū)、代碼區(qū)__。
1.Text段(Code Segment/Text Segment,代碼段) :通常是指用來存放程序執(zhí)行代碼的一塊內(nèi)存區(qū)域,也就是存放CPU執(zhí)行的機器指令(machine instructions)。這部分區(qū)域的大小在程序運行前就已經(jīng)確定,并且內(nèi)存區(qū)域通常屬于只讀(某些架構(gòu)也允許代碼段為可寫,即允許修改程序)。在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。
2.全局初始化數(shù)據(jù)區(qū)/靜態(tài)數(shù)據(jù)區(qū)(Initialized data segment/Data segment) :該區(qū)包含了在程序中明確被初始化的全局變量、靜態(tài)變量(包括全局靜態(tài)變量和局部靜態(tài)變量)和常量數(shù)據(jù)(如字符串常量)。數(shù)據(jù)段屬于靜態(tài)內(nèi)存分配。static聲明的變量放在data段。
3.BSS段(Block Started by Symbol) :BSS段通常是指用來存放程序中未初始化的全局變量的一塊內(nèi)存區(qū)域。BSS段屬于靜態(tài)內(nèi)存分配。
4.堆(heap) :堆是用于存放程序運行中被動態(tài)分配的內(nèi)存段,它的大小并不固定,可動態(tài)擴張或縮減。也就是常說的用malloc,calloc, realloc 等函數(shù)分配的變量空間是在堆上。當(dāng)程序調(diào)用malloc等函數(shù)分配內(nèi)存時,新分配的內(nèi)存就被動態(tài)添加到堆上(堆被擴張);當(dāng)利用free等函數(shù)釋放內(nèi)存時,被釋放的內(nèi)存從堆中被剔除(堆被縮減)。
5.棧(stack) :棧又稱堆棧,是用戶存放程序臨時創(chuàng)建的局部變量,也就是說我們函數(shù)括弧“{}”中定義的變量(但不包括static聲明的變量,static意味著在數(shù)據(jù)段中存放變量)。除此以外,在函數(shù)被調(diào)用時,其參數(shù)也會被壓入發(fā)起調(diào)用的進程棧中,并且待到調(diào)用結(jié)束后,函數(shù)的返回值也會被存放回棧中。由于棧的先進先出(FIFO)特點,所以棧特別方便用來保存/恢復(fù)調(diào)用現(xiàn)場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數(shù)據(jù)的內(nèi)存區(qū)。
一個程序本質(zhì)上都是由 __bss段、data段、text段__三個組成的。
在C/C++程序編譯完成之后,已初始化的全局變量保存在data 段中,未初始化的全局變量保存在bss 段中。
text和data段都在可執(zhí)行文件中(在嵌入式系統(tǒng)里一般是固化在鏡像文件中),由系統(tǒng)從可執(zhí)行文件中加載;而bss段不在可執(zhí)行文件中,由系統(tǒng)初始化。
3 STM32程序的存儲分配
3.1 程序所占RAM和Flash大小分析
為例調(diào)試方便,這里使用一個裸機串口例子,關(guān)于串口的使用請參看筆者博文:
串口通信:https://bruceou.blog.csdn.net/article/details/79341769
使用GCC編譯代碼,編譯信息如下:
其中:
- text 代表執(zhí)行的代碼,程序中所有的函數(shù)都位于此處。當(dāng)然還包括RO-data(Read Only)代表只讀數(shù)據(jù),程序中所定義的全局常量數(shù)據(jù)和字符串都位于此處,如const型。
- __data__代表已初始化的讀寫數(shù)據(jù),程序中定義并且初始化的全局變量和靜態(tài)變量位于此處。
- bss代表未初始化的讀寫數(shù)據(jù),程序中定義了但沒有初始化的全局變量和靜態(tài)變量位于此處。GCC編譯器默認(rèn)是把你沒有初始化的變量都賦值為例0。即上述的bss段。
值得注意的是,這些參數(shù)的單位是Byte。
text和data兩個段需要燒錄到FLASH等非易失性器件中。
data段需要燒錄到FLASH中,而bss段則不用,但在運行時,它們都必須裝載到可讀可寫的RAM中。
因此我們可以計算出FLASH和RAM的大小:
Flash = test + data
RAM = data + bss
這就要涉及到程序的兩種狀態(tài):加載域和運行域。
[]()[]()Figure ? 程序的加載域和運行域
加載域 :向Flash中下載程序時,其實僅僅下載的是text+data的內(nèi)容,意思就是說,在掉電情況下,F(xiàn)lash里面的內(nèi)存僅包含text+data的內(nèi)容。
運行域 :當(dāng)上電后,程序運行時,首先程序會從特定的地址進行啟動,啟動時會將data的數(shù)據(jù)加載到SRAM中,單片機的test區(qū)域不需要加載到 SRAM,內(nèi)核直接從 FLASH 讀取指令運行。那bss的數(shù)據(jù)怎么辦呢?對于初始值為0全局變量來說,因為要在Code區(qū)要調(diào)用該全局變量,所以肯定要對其進行描述,程序運行時就知道了,原來你是初始值為0的全局變量呀,然后就在SRAM區(qū)給你分配了一段固定區(qū)域的地址;對于局部變量來說,會自動分配大小。bss有統(tǒng)計作用,并且SRAM中一段特定的區(qū)域是運行bss數(shù)據(jù),data +bss就是程序運行總共會占用SRAM的長度,生成局部變量的棧空間包含在bss區(qū)的范圍。
3.2 程序堆棧使用分析
我們知道,程序運行需要占用的大小是RAM = data + bss,而堆棧的大小是程序開始運行后才能確定的。
那么堆和棧到底能占用多大呢,堆棧的大小是在STM32F103ZETx_FLASH.ld中設(shè)置的,這里以STM32F103ZET6為例進行分析,其內(nèi)部棧的大小為1KB,堆的大小為0.5KB。
使用objdump查看elf文件:
堆棧段起始地址為0x2000 002c,大小為0x604,這0x4又是怎么來的?這里查看map文件。
堆占用了0x200字節(jié),棧占用了0x400字節(jié),而剩下的0x4字節(jié)來自于 ALIGN(0x8),即8字節(jié)對齊,因為堆棧段緊跟.bss段之后,那首地址應(yīng)該是0x2000 0070,但是規(guī)定了8字節(jié)對齊,所以最小為32,即需要補上4個字節(jié),所以堆棧段起始地址應(yīng)該是0x2000 00F4。
【注】棧:向低地址擴展,堆:向高地址擴展。如果依次定義變量,先定義的棧變量的內(nèi)存地址比后定義的棧變量的內(nèi)存地址要大,先定義的堆變量的內(nèi)存地址比后定義的堆變量的內(nèi)存地址要小。
【Tips】
1、堆棧的大小在編譯器編譯之后是不知道的,只有運行的時候才知道,所以需要注意一點,就是別造成堆棧溢出了,不然就會發(fā)生hard fault錯誤。
2、所有在處理的函數(shù),包括函數(shù)嵌套,遞歸,等等,都是從這個“棧”里面,來分配的。所以,如果棧大小為2K,一個函數(shù)的局部變量過多,比如在函數(shù)里面定義一個char buf[512],這一下就占了1/4的棧大小了,再在其他函數(shù)里面來搞兩下,程序崩潰是很容易的事情,這時候,一般你會進入到hardfault…。
3、STM32的棧,是向下生長的。事實上,一般CPU的棧增長方向,都是向下的。而堆的生長方向,都是向上的。堆和棧,只是他們各自的起始地址和增長方向不同,他們沒有一個固定的界限,所以一旦堆棧沖突,系統(tǒng)就到了崩潰的時候了。
4、程序中的常量,如果沒加const也會編譯到SRAM里,加了const會被編譯到flash中。
3.3 實例代碼分析
前面分析了那么多,下面通過一個實例來驗證前面的分析。
main.c函數(shù)代碼如下:
/* 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的全局?jǐn)?shù)組
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指向堆區(qū)分配了一個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 */
}
編譯后內(nèi)存分配如下:
運行程序,打印信息如下:
data是初始化的全局變量,在.data區(qū);buffer是未初始化的全局變量,在.bss區(qū);pHeap是通過malloc分配的空間,在堆區(qū),逐漸增加;局部變量都在棧區(qū),增加減小。
4 堆棧的使用總結(jié)
堆的使用:
1、堆的使用是要結(jié)合malloc函數(shù),即使用一次malloc所得到的內(nèi)存空間既是屬于堆的空間。
2、堆的增長方向是向上,所以malloc申請的地址也是越來越大的,前提是連續(xù)申請且在最后一次申請后再釋放內(nèi)存(free)。則第一次申請的地址永遠小于后面申請的地址。
3、堆是不連續(xù)的,由于RAM中還存在局部變量,代碼段和棧等等,所以動態(tài)分配的內(nèi)存是取暫時空閑的內(nèi)存,而不是預(yù)先劃出一塊區(qū)域,這就是動態(tài)分配內(nèi)存的好處。
4、使用堆的壞處,由于使用malloc申請內(nèi)存時,不單只申請了所需的大小空間,還要額外暫用管理這部分空間的內(nèi)存,而釋放時又只釋放申請的內(nèi)存,所以使用堆會引入內(nèi)存碎片。當(dāng)然如果不是在短時間內(nèi)頻繁的使用malloc申請和free釋放內(nèi)存,那么操作系統(tǒng)就有足夠的時間來回收碎片空間。
棧的使用:
1、由編譯器分配,目的是將RAM劃分處一塊區(qū)域供程序運行時的局部變量參數(shù)等使用;
2、棧是一塊連續(xù)的內(nèi)存空間,由上往下增長,即使用棧時地址是會越來越小的,如先聲明的局部變量比后聲明的地址要高;
3、棧是由程序(操作系統(tǒng))自動分配,不會有內(nèi)存碎片的問題;
4、棧的壞處:棧是固定且連續(xù)的一個大小,如果使用局部變量等超出了棧的大小則會造成內(nèi)存溢出,而編譯器通常是發(fā)現(xiàn)不了的,只有當(dāng)程序運行到那個函數(shù)時才會發(fā)生的。這就會引入很難查找的bug。另外如如果使用malloc申請的內(nèi)存不規(guī)范使用,當(dāng)釋放內(nèi)存后,沒將指針地址清空,仍指向那個地址剛好是棧的地址,則會造成越界訪問。
審核編輯:湯梓紅
-
存儲器
+關(guān)注
關(guān)注
38文章
7492瀏覽量
163829 -
GCC
+關(guān)注
關(guān)注
0文章
107瀏覽量
24843 -
內(nèi)存管理
+關(guān)注
關(guān)注
0文章
168瀏覽量
14138 -
Cortex-M
+關(guān)注
關(guān)注
2文章
229瀏覽量
29761
發(fā)布評論請先 登錄
相關(guān)推薦
評論