stm32內存架構及堆棧管理

stm32內存架構及管理

計算機的內存管理

學習stm32內存管理的時候有些雲裏霧裏,網上也看了很多博客文檔,但是大都沒有很系統的去講解stm32的內存架構。所以決定自己來做一個關於stm32內存架構的分析和自己的理解。

在討論單片機內存管理之前,我想先說一下關於計算機的內存是如何管理的。根據《C++ Primer Plus(第6版)》這本書中所講,C++(就內存管理方式而言類似C)有3種管理數據內存的方式:
①、自動存儲
1. 函數內部定義的常規常量使用自動存儲空間,該變量稱爲自動變量 (局部變量)。
2.調用函數時產生(進棧),函數結束時消亡(出棧)。後進先出(LIFO)。因此在一個函數塊中,該區域將不斷的
增大或者減小。
3.該區域我們一般稱爲棧區。
②、靜態存儲
1. 靜態存儲是一種整個程序執行期間都存在的存儲方式。
2.定義該變量的方式有兩種:在函數外面定義它/在聲明變量時使用關鍵字:static。
③、動態存儲
1. new(malloc)和delete(free)運算符(函數)提供了一種比自動變量和靜態變量更加靈活的方法。
2.它們管理了一個內存池,這在C++中被稱爲自由存儲空間或堆(heap)。
3.這種存儲方式獨立於另外兩種,可以由程序員決定變量的產生和消亡。
我們大可以把這三個區域理解成stm32的SRAM存儲區。

stm32的內存架構

stm32是32位單片機,可用尋址空間高達4GB。當然我們只用到了其中很小的一部分,以stm32f407爲例,其內部Flash(ROM)大小爲1M字節,SRAM大小爲192Kb字節。
我們平時說的內存管理管理的便是SRAM中的動態存儲區(堆)。而4GB尋址空間對應到stm32可由下圖表示(出處見水印):
在這裏插入圖片描述
由圖可由清楚的看到Cortex-M3與stm32之間的關係,雖然我們stm32f407是基於Cortex-M4架構,但也可以通過這個圖看出來4GB尋址空間和STM32之間的關係。我們的程序和常量是存儲在Flash中的,調試模式下也可以看出PC指針始終在0X0800 0000後面的Flash區域,變量則存儲在了SRAM中,SRAM的首地址爲0X2000 0000。我做了一個架構圖幫助大家更好地理解stm32的存儲架構:
在這裏插入圖片描述
其中內存地址是按照stm32f407去定義的,我們選擇主芯片的時候除了看其所帶外設ADC/TIM/DAC/FSMC等,還要看Flash和SRAM的大小夠不夠我們的項目使用。

如果我們的編譯器是Keil(當然大多人都用這個)的話,會把這些存儲區在細分爲各個段區,比如我們隨便用一個led工程編譯一下:
在這裏插入圖片描述
Flash = Code(程序代碼) + RO - data(存儲const常量和指令) + RW - data(初始值不爲0的全局/靜態變量)
SRAM = RW - data(程序運行後從Flash讀入) + ZI - data(初始值爲0或未初始化的全局/靜態變量)

Ps:SRAM在編譯時並不會把所有變量申請的內存顯示完,因爲SRAM是一個動態的存儲的過程,所以不能看到SRAM大小滿足板載SRAM大小,就認爲程序滿足硬件要求!當然涉及堆棧存儲更爲複雜,內存泄漏,內存疊加等等,這個比較重要所以我們後面單獨說一下。

SRAM的內部結構

其實就我們平時寫寫程序就只需要知道有這麼一個大小的SRAM給我們用就好了,但是如果基於操作系統OS的話,還是要更加細分SRAM區域,合理利用每一個區域去完成我們的項目,最大限度的利用單片機資源,這裏我們放一張數據手冊中的矩陣圖:在這裏插入圖片描述
我們可以看到192Kb的SRAM其實分爲3部分,SRAM1,SRAM2,和64Kb的CCMRAM。查看數據手冊的關於SRAM的介紹:
Seven slaves:
– Internal Flash memory ICode bus
– Internal Flash memory DCode bus
– Main internal SRAM1 (112 KB)
– Auxiliary internal SRAM2 (16 KB)
– AHB1 peripherals including AHB to APB bridges and APB peripherals
– AHB2 peripherals
– FSMC
我們可以看到SRAM1是主要的存儲區,主要存放一些程序運行時產生的變量,SRAM2輔助內部存儲區,主要作用於與外設數據有關的變量。而CCMRAM則比較特殊,stm32f1裏沒有這個東西,從矩陣圖中也可以看出這個區域通過D總線直接和CPU相連,這意味着,CPU能以最大的系統時鐘和最小的等待時間從CCM中讀取數據或者代碼。對於要求速度,精度高的項目我們可以嘗試將變量定義在這個區域裏面。定義的方法網上大概分爲兩種:
嫌麻煩的話就 u32 ccmTextNum attribute((at(0x10000000)))用at去定位到這個區域裏面,還有一種方法分散加載文件(.sct),更爲強大一些,感興趣的可以自己去嘗試一下。再Ps:CCMRAM沒有與一些外設如DMA相連,說明這個區域的變量不可以通過DMA傳輸!

程序驗證堆棧區地址及內存覆蓋

說了這麼多,現在大家對stm32內存架構可能會有更清晰的瞭解,我們現在動手驗證一下我們所說的是否準確,首先我們先測試堆和棧的地址範圍及容量大小(程序基於正點原子探索者):

僅驗證堆棧首地址及其大小

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "stdlib.h"

//爲了方便起見,STACK區(棧)我們用SRAM1來表示,HEAP區(堆)我們用SRAM2來表示,靜態常量區我們用SRAM3來表示。

u8 SRAM3_Buffer[100]={0};//聲明瞭一個初始化爲0的全局數組,在靜態常量區,0x2000 0000開頭

int main(void)
{ 
	u8 SRAM1_i,SRAM1_j = 1,m;//i爲普通的未初始化局部變量,j作爲初始化的局部變量,測試可得均存在於棧區
	u8* pSRAM2 = (u8*)malloc(200);//指針pSRAM2指向堆區分配了一個u8類型200大小的數組的首地址,測試可得存在於堆區
	
	u8 SRAM1_Buffer[100] = {0};//聲明瞭一個局部數組,存在於靜態常量區。
	
	delay_init(168);		  //初始化延時函數
	LED_Init();		        //初始化LED端口
	uart_init(115200);
  
	for(m = 0;m < 100;m++)
	{
		SRAM3_Buffer[m] = m;
	}
	
	printf("未初始化局部變量SRAM1_i在棧中的地址爲:0x%x\r\n",&SRAM1_i);
	printf("初始化的局部變量SRAM1_j在棧中的地址爲:0x%x\r\n",&SRAM1_j);
	printf("初始化局部數組SRAM1_Buffer在棧中的地址爲:0x%x\r\n",&SRAM1_Buffer);
	printf("調用malloc聲明瞭一個在堆區的數組首地址爲:0x%x\r\n",pSRAM2);
	//printf("靜態常量區的數組的地址爲:0x%x\r\n",SRAM3_Buffer);
	
//	printf("靜態常量區的數組值爲:\r\n");
//	for(m = 0;m < 100;m++)
//	{
//		printf("%d ",SRAM3_Buffer[m]);
//	}
	

	while(1)
	{
		delay_ms(500);
		
		LED1 = ~LED1;
	}
}

通過串口調試助手我們可以看到我們聲明的變量的具體地址:
在這裏插入圖片描述
可以看出前三個局部變量是在同一個區域,後一個動態變量在另一個區域,而這兩個區域,就是我們平時說的堆棧區。堆棧區的大小我們可以從啓動文件裏得到:
在這裏插入圖片描述
在這裏插入圖片描述
從啓動文件我們可以得到棧區大小爲0x0000 0400(1Kb),堆區的大小爲:0x0000 0200(512字節)。這裏我們再放一張圖,可以清楚的看到堆棧區與靜態常量區的關係(出處見水印(我也看不清。。)):
在這裏插入圖片描述
這裏大家可能會發現我們申請的位於棧區的局部變量首地址,是位於棧頂的0x2000 06f0開始的,而位於堆區的動態變量首地址是從堆底0x200000f0開始的。這就涉及堆棧對於數據處理方式的不同,前面的文章也講了,棧是後進先出,所以棧變量(局部變量)是從棧頂向下分配地址,而堆則相反,是先進後出,所以從堆底向上分配地址。那麼有沒有一種可能,棧變量我們申請的比較大,那麼它就會一直向下申請內存,直到污染了HEAP和靜態存儲區,這就是比較可怕的內存疊加,我們在軟件裏模擬一下這個事件:

加上全局數組後的程序

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "stdlib.h"

//爲了方便起見,STACK區(棧)我們用SRAM1來表示,HEAP區(堆)我們用SRAM2來表示,靜態常量區我們用SRAM3來表示。

u8 SRAM3_Buffer[100]={0};//聲明瞭一個初始化爲0的全局數組,在靜態常量區,0x2000 0000開頭

int main(void)
{ 
	u8 SRAM1_i,SRAM1_j = 1,m;//i爲普通的未初始化局部變量,j作爲初始化的局部變量,測試可得均存在於棧區
	u8* pSRAM2 = (u8*)malloc(200);//指針pSRAM2指向堆區分配了一個u8類型200大小的數組的首地址,測試可得存在於堆區
	
	u8 SRAM1_Buffer[100] = {0};//聲明瞭一個局部數組,存在於靜態常量區。
	
	delay_init(168);		  //初始化延時函數
	LED_Init();		        //初始化LED端口
	uart_init(115200);
  
	for(m = 0;m < 100;m++)
	{
		SRAM3_Buffer[m] = m;
	}
	
	printf("未初始化局部變量SRAM1_i在棧中的地址爲:0x%x\r\n",&SRAM1_i);
	printf("初始化的局部變量SRAM1_j在棧中的地址爲:0x%x\r\n",&SRAM1_j);
	printf("初始化局部數組SRAM1_Buffer在棧中的地址爲:0x%x\r\n",&SRAM1_Buffer);
	printf("調用malloc聲明瞭一個在堆區的數組首地址爲:0x%x\r\n",pSRAM2);
	printf("靜態常量區的數組的地址爲:0x%x\r\n",SRAM3_Buffer);
	
	printf("靜態常量區的數組值爲:\r\n");
	for(m = 0;m < 100;m++)
	{
		printf("%d ",SRAM3_Buffer[m]);
	}
	

	while(1)
	{
		delay_ms(500);
		
		LED1 = ~LED1;
	}
}

在這裏插入圖片描述
這個首先我們在main函數添加了一個100位局部數組並且把0-99賦值進去,我們可以看到正常數組是位於靜態存儲區。這時我們聲明一個位於棧區的數組,這個數組很大,大到足以污染靜態存儲區。

涉及內存覆蓋的程序

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "stdlib.h"

//爲了方便起見,STACK區(棧)我們用SRAM1來表示,HEAP區(堆)我們用SRAM2來表示,靜態常量區我們用SRAM3來表示。

u8 SRAM3_Buffer[100]={0};//聲明瞭一個初始化爲0的全局數組,在靜態常量區,0x2000 0000開頭

int main(void)
{ 
	u8 SRAM1_i,SRAM1_j = 1,m;//i爲普通的未初始化局部變量,j作爲初始化的局部變量,測試可得均存在於棧區
	u8* pSRAM2 = (u8*)malloc(200);//指針pSRAM2指向堆區分配了一個u8類型200大小的數組的首地址,測試可得存在於堆區
	
	u8 SRAM1_Buffer[100] = {0};//聲明瞭一個局部數組,存在於靜態常量區。
	
	u8 SRAM2_Buffer[1500] = {0};
	
	delay_init(168);		  //初始化延時函數
	LED_Init();		        //初始化LED端口
	uart_init(115200);
  
	for(m = 0;m < 100;m++)
	{
		SRAM3_Buffer[m] = m;
	}
	
	printf("未初始化局部變量SRAM1_i在棧中的地址爲:0x%x\r\n",&SRAM1_i);
	printf("初始化的局部變量SRAM1_j在棧中的地址爲:0x%x\r\n",&SRAM1_j);
	printf("初始化局部數組SRAM1_Buffer在棧中的地址爲:0x%x\r\n",&SRAM1_Buffer);
	printf("調用malloc聲明瞭一個在堆區的數組首地址爲:0x%x\r\n",pSRAM2);
	printf("靜態常量區的數組的地址爲:0x%x\r\n",SRAM3_Buffer);
	
	printf("靜態常量區的數組值爲:\r\n");
	for(m = 0;m < 100;m++)
	{
		printf("%d ",SRAM3_Buffer[m]);
	}
	

	while(1)
	{
		delay_ms(500);
		
		LED1 = ~LED1;
	}
}

這時我們看到位於靜態存儲區的數組SRAM1_Buffer裏的值已經發生了變化,這是非常可怕的,棧區變量太大導致別的區域如堆區/靜態存儲區的變量值被影響!
在這裏插入圖片描述
當然這是指經過編程運行後把結果顯示在串口調試助手上,如果大家閒麻煩,也可以直接進入調試模式通過Memory 1顯示程序運行中的數值變化:
在這裏插入圖片描述
綜上所述,我想大家已經大致理解stm32的內存架構了,有一點要說的就是堆是向上延伸,而棧是向下延伸,而且棧區的大小編譯時看不出來,所以我們對於堆棧的操作一定要小心,申請的內存都要delete/free掉,不然會造成內存泄漏,養成良好的編程習慣!還有就是局部變量申請過大,導致內存覆蓋的問題,也很容易造成程序的崩潰,關於stm32的內存架構就介紹到這裏把。

接下來我會向大家介紹我們是如何通過程序來管理我們的內存的,這是我的第一篇博客,歡迎關注,轉載標明出處即可,2020 元宵節快樂!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章