聊聊記憶體那些事(基於微控制器系統)

東小東發表於2020-10-27

微控制器的RAM和ROM

微控制器的ROM,叫只讀程式儲存器,是FLASH儲存器構成的,如U盤就是FLASH儲存器。所以,FLASH和ROM是同義的。微控制器的程式,就是寫到FLASH中了。

而RAM是隨機讀/寫儲存器,用作資料儲存器,是在執行程式時,存放資料的。

記憶體區

記憶體主要分為:程式碼區、常量區、靜態區(全域性區)、堆區、棧區這幾個區域。

程式碼區:存放程式的程式碼,即CPU執行的機器指令,並且是隻讀的。

常量區:存放常量(程式在執行的期間不能夠被改變的量,例如: 25,字串常量”dongxiaodong”, 陣列的名字等)

靜態區(全域性區)靜態變數和全域性變數的儲存區域是一起的,一旦靜態區的記憶體被分配, 靜態區的記憶體直到程式全部結束之後才會被釋放

堆區:由程式設計師呼叫malloc()函式來主動申請的,需使用free()函式來釋放記憶體,若申請了堆區記憶體,之後忘記釋放記憶體,很容易造成記憶體洩漏

棧區:存放函式內的區域性變數,形參和函式返回值。棧區之中的資料的作用範圍過了之後,系統就會回收自動管理棧區的記憶體(分配記憶體 , 回收記憶體),不需要開發人員來手動管理。棧區就像是一家客棧,裡面有很多房間,客人來了之後自動分配房間,房間裡的客人可以變動,是一種動態的資料變動。

STM32F103C8T6中

ROM起始地址為:0x8000000, 大小為:0x10000 (64K)

只讀的,存放著程式碼區和常量區

RAM起始地址為:0x20000000,大小為:0x5000  (20K)

可讀可寫的,存放著靜態區、棧區和堆區

STM32各區詳細介紹:

程式碼區:

l  程式碼區存放著程式編譯後的CPU指令

l  函式名稱是一個指標,可以通過查詢函式名稱所處的記憶體地址,查詢函式存放的區域

 1 //函式宣告
 2 void dong();
 3 //主函式定義
 4 int main(void)
 5 { 
 6   //串列埠初始化
 7     Uart1_Init(115200);
 8     //函式呼叫
 9   dong();
10     //輸出test函式地址
11     printf("dong() addrs : 0x%p\n",dong);
12     
13     while(1);
14 }
15 void dong(){
16      //輸出main函式地址
17      printf("mian() addrs : 0x%p\n",main);
18 }

輸出:

可見0x08000c2d和0x08000be9都在ROM裡的程式碼區

常量區

指標可以指向常量也可以指向變數的區域,通過指標(char *p)來測試一下常量與變數去的地址變化。

 1 void dongxiaodong_fun(){
 2    char *p=NULL;//定義一個指標變數
 3      //常量
 4      p="2020dongxiaodong";//指標指向一個常量
 5    printf("addr1:0x%p\r\n",p);//輸出常量地址
 6      //變數
 7      char data[]={"dong1234"};
 8      p=data;//指標指向一個變數
 9    printf("addr2:0x%p\r\n",p);//輸出變數地址
10 }

輸出:

可見常量的地址在ROM裡的常量區,區域性變數在RAM的棧空間下

靜態區

靜態區包括靜態變數和全域性變數,靜態變數通過static修飾,一旦初始化則一直佔用RAM空間

 1 int global_a;//全域性變數,預設值為0
 2 static int global_b;//靜態全域性變數,預設值為0
 3 void fun(){
 4   static int c;//靜態變數,預設值為0
 5     printf("static int c add:0x%p , val:%d \r\n",&c,c);
 6     c++;
 7 }
 8 void dongxiaodong_fun(){
 9   //輸出全域性變數
10     printf("       int a add:0x%p , val:%d \r\n",&global_a,global_a);
11     printf("static int b add:0x%p , val:%d \r\n",&global_b,global_b);
12     //呼叫函式檢視靜態變數
13     for(int i=0;i<3;i++){
14          fun();
15     }
16 }

輸出:

其中global_a為全域性變數、global_b為全域性靜態變數、c為區域性靜態變數,他們如果沒有賦初值都會被系統自動賦值為0,靜態變數初始化則一直有效,並不會因為多次呼叫了初始化語句而出現多次初始化的問題。程式碼中雖然看似初始化了c變數三次,其實實際只有第一次有效。

堆區

堆區是呼叫malloc函式來申請的記憶體空間,這部分空間使用完後要呼叫free()函式來釋放申請的空間。Void * malloc(size_t);函式的引數是需要分配的空間位元組大小,返回是一個void*型別的指標,該指標指向分配空間的首地址,void*型別指標可以轉換為任意的其它型別指標。

l  堆是向上增長,即首地址遞增的方向增長

l  通過malloc()申請的空間必須通過free()進行釋放,如果申請的記憶體未釋放則可能造成記憶體洩露

l  malloc()記憶體申請失敗將返回NULL

l  malloc分配的記憶體空間在邏輯上是連續的,而在物理上可以不連續。

l  釋放只能釋放一次,如果釋放兩次及兩次以上會出現錯誤(但是釋放空指標例外,釋放空指標其實也等於什麼都沒有做,所以,釋放多少次都是可以的),free()釋放空間後可以將指標指向“NULL”確保指標不會成為野指標。

STM32C8T6:

標準庫中定義了預設堆的大小為0x200=512位元組,其可以認為程式同一時間的malloc分配大小不可大於512位元組資料。

堆空間預設不常駐RAM空間,但當程式碼出現malloc關鍵字後,堆空間將分配設定的整體大小(512位元組)佔用RAM空間。

void dongxiaodong_fun(){
  //申請
    printf("-----malloc-----\r\n");
    char *p1=malloc(100);
    if(p1==NULL) printf("p1 malloc fail \r\n");
    char *p2=malloc(1024);
    if(p2==NULL) printf("p2 malloc fail \r\n");
    
    //賦值  
    memcpy(p1,"dongxiaodong123456",strlen("dongxiaodong123456"));
    
    printf("p1 addr:%p  ,val:%s \r\n",p1,p1);
    printf("p2 addr:%p\r\n",p2);
    
    
    //釋放
    printf("-----free-----\r\n");
    free(p1);
    free(p2);
    
    printf("p1 addr:%p  ,val:%s \r\n",p1,p1);

    
    p1=NULL;
    printf("p1 addr:%p \r\n",p1);
    
}

輸出:

可見堆空間分配記憶體失敗則會返回NULL,並且地址指向0x00,釋放時只是通過free(),僅是把指向的內容變成了空值,但地址還是存在的,所以標準的做法是賦上“NULL”值。記憶體釋放後(使用free函式之後指標變數p本身儲存的地址並沒有改變),需要將p的值賦值為NULL(拴住野指標)。

分配空間不能達到所規定的最大值:

void dongxiaodong_fun(){
       char *d=malloc(512);
       //char *d=malloc(500); //可行
       if(d==NULL) printf("512 malloc fail\r\n");
}

輸出:

檢視解釋:

如果用malloc(n)來分配堆記憶體,那麼分配的記憶體比n大,為什麼呢?

0.malloc分配的記憶體不一定連續,所以需要header指標來連結各部分

1.實際分配的堆記憶體是Header + n結構。返回給使用者的是n部分的首地址  所以他還有一部分記憶體是用來存header的,所以比原始的大

2.由於記憶體對齊值8,記憶體對其機制,實際分配的堆記憶體大於等於sizeof(Header) + n

棧區

棧區由編譯器自動分配和是釋放,存放函式中定義的引數值、返回值和區域性變數,在程式執行過程實時分配和釋放,棧區由作業系統自動管理,無須手動管理。棧區是先進後出原則。

l  棧是向下增長,即首地址遞減的方向增長

l  編譯器不會給未初始化的區域性變數賦初始值0,所以未初始化的區域性變數通常是一個混亂值,所以定義區域性變數時賦初值是最穩妥的。

STM32C8T6:

標準庫中定義了預設棧的大小為0x400=1024位元組,其可以認為程式同一時間的區域性變數不可大於1024位元組資料。

棧空間的位元組數是常駐空間,一經初始化將分配設定的整體大小(1024位元組)佔用RAM空間。

 1 //主函式定義
 2 int main(void)
 3 { 
 4     //串列埠初始化
 5     Uart1_Init(115200);
 6     printf("start SYS 1\r\n");
 7     char data1[1024]={0};   //1024位元組
 8     printf("start SYS 2\r\n");
 9     char data2[100]={0};    //100位元組
10     printf("start SYS 3\r\n");
11     char data3[100]={0};     //100位元組,10位元組可以正常執行
12     printf("start SYS 4\r\n");
13     while(1);
14 }

實測發現棧空間的大小到1024+100+10位元組都是可以正常執行的,這個難道是STM32做了棧空間的預留嗎?1024並不是做了完全的強制限制。

地址測試

void dongxiaodong_fun(){
int a=100;
    int b;
    printf("a addr:0x%p val:%d\r\n",&a,a);
    printf("b addr:0x%p val:%d\r\n",&b,b);
}

輸出:

可見b的地址小於a的地址,其是向首地址遞減的方向增長(向下增長),b的值沒有賦初值,其值是混亂的,建議賦初值使用。

注意:

const修飾的資料

l  const修飾的是變數名,之所以叫const常量,意思是不可以更改,許可權為只讀,但是它的本質是變數,只不過是不可修改的變數

l  const修飾區域性變數則存放在棧區,如果修飾全域性變數就存放在靜態區(全域性區)

資料儲存(大小端模式)

資料在記憶體中存放,分為大端模式和小端模式

大端模式:低位位元組存在高地址上,高位位元組存在低地址上。

小端模式:低位位元組存在低地址上,高位位元組存在高地址上。

網路位元組序:TCP/IP各層協議將位元組序列定義為大端模式,因此在TCP/IP協議中使用的大端模式通常稱為網路位元組序。

void dongxiaodong_fun(){
    int data=0x12345678;
    char *p=(char*)&data;
    printf("p+0:0x%p-->0x%02X\r\n",p,*(p+0));
    printf("p+1:0x%p-->0x%02X\r\n",p,*(p+1));
    printf("p+2:0x%p-->0x%02X\r\n",p,*(p+2));
    printf("p+3:0x%p-->0x%02X\r\n",p,*(p+3));
}

輸出:

可見其值的高位儲存在地址的低位上,所以STM32的變數儲存是小端模式

動態記憶體申請的碎片化問題

標準的記憶體動態分配是動態連結串列進行管理。由於malloc返回的是一個指標再加上微控制器沒有MMU,使得分配的指標就像一個個釘子一樣在記憶體中了,直到被釋放。這就會導致記憶體管理非常困難,從而導致記憶體碎片化。

這是一個理想的極端例子

微控制器的堆空間分配有1KB的空間,其為1024位元組,為了說明和計算方便我們忽略掉連結串列佔用的空間,只計算實際儲存空間大小。

第一步:申請64塊記憶體空間,每塊16位元組,那麼就會分配完1K位元組的空間。

char *p[64]={NULL};
for(int i=0; i<64; i++){
    ptr[i] = malloc(16);
}

第二步:釋放掉偶數塊記憶體空間

for(int i=0; i<64; i+=2){
    free(ptr[i] );
    ptr[i]=NULL;
}

第三步:

我們釋放掉的空間達到了堆的一半大小,512位元組了,但都是不連續的。32塊16位元組的非連續空間,所以要分配出大於16位元組的記憶體塊是分配不出來的。有512位元組的空間但只能分配小於16位元組的連續空間,在某些場合原本微控制器RAM的堆空間資源就很緊張,再加上這種不充分的使用使得程式穩定性大打折扣。

STM32C8T6真實案例:

記憶體碎片化可以通過如下列子進行驗證:

 1 void dongxiaodong_fun(){
 2     char *p[8]={NULL};
 3     //512位元組的堆空間,似乎只能分配8*50=400位元組
 4     for(int i=0;i<8;i++){
 5         p[i]=malloc(50);
 6         if(p[i]==NULL) printf("p[%d] malloc fail\r\n",i);
 7     }
 8    //輸出其中一個數的地址
 9     printf("%p\r\n",p[2]);
10     printf("%p\r\n",p[3]);
11     //釋放偶數下標空間
12     for(int i=0;i<8;i+=2){
13         free(p[i]);
14         p[i]=NULL;
15     }
16     //分配失敗,記憶體碎片化
17     char *d1=malloc(100); //可行
18     if(d1==NULL) printf("d1 100 malloc fail\r\n");
19     
20     //釋放一個奇數位空間
21     free(p[3]);
22     //分配成功,分配的空間在p[2]和p[3]的空間上,和多了10個位元組的額外空間
23     char *d2=malloc(160);
24     if(d2==NULL) printf("d2 100 malloc fail\r\n");
25     printf("%p\r\n",d2);
26 }

輸出:

這個例子大體上還是體現出了記憶體碎片化的問題所在,因為總共有8個空間快,申請後釋放奇數塊理論上有50*4=200位元組,但分配100位元組卻行不通,重要原因在於釋放的偶數塊每塊大小為50,並且其地址是不連續的。當釋放其中一個奇數塊後,記憶體就可以達到需要分配的連續塊大小了,所以分配的空間使用了p[2]、 p[3]、p[4]的空間。

 

存在幾個問題:

Malloc分配的空間總共可以有512,但分1包也只能是500左右的有效空間,分8包是400左右的有效空間,利用率為什麼這麼低?

碎片化測試時,p[2]、p[3]、p[4]的大小應該是3*50=150,結果最大可以是160左右。

 

檢視解釋:

如果用malloc(n)來分配堆記憶體,那麼分配的記憶體比n大,為什麼呢?

0.malloc分配的記憶體不一定連續,所以需要header指標來連結各部分

1.實際分配的堆記憶體是Header + n結構。返回給使用者的是n部分的首地址  所以他還有一部分記憶體是用來存header的,所以比原始的大

2.由於記憶體對齊值8,記憶體對其機制,實際分配的堆記憶體大於等於sizeof(Header) + n

 

記憶體碎片化的主要解決方法:

將間隔的小記憶體移動到一起並排,釋放連續空間

現在普遍採用的段頁式記憶體分配方式就是將程式的記憶體區域分為不同的段,然後將每一段由多個固定大小的頁組成。通過頁表機制,使段內的頁可以不必連續處於同一記憶體區域,從而減少了外部碎片,然而同一頁內仍然可能存在少量的內部碎片,只是一頁的記憶體空間本就較小,從而使可能存在的內部碎片也較少。

 

正點原子的mymalloc() 函式

問題1:Malloc函式標準庫有為什麼又出現這個?

問題2:記憶體碎片化處理?

總結:

l  可以進行多種RAM的記憶體管理,比如外部的SRAM,方便管理多個RAM空間

l  可以檢視到記憶體的使用率

l  沒有進行記憶體碎片化處理

STM32檢視FLASH空間和RAM空間使用量

開啟顯示:

 編譯後輸出:

 

Program Size: Code=38356 RO-data=6676 RW-data=400 ZI-data=47544

 

Code:程式碼佔用的空間

RO-data:其中RO表示Read Only ,只讀常亮的大小

RW-data:其中RW表示Read Write,可讀可寫的變數大小,初始化已經付了初值

ZI-data:其中ZI表示Zero Initialize,可讀可寫的變數大小,沒有賦初值而被系統賦值為0的位元組數

 

RAM的大小:

RAM=【RW-data】+【ZI-data】

 

ROM的大小:

ROM =【Code】+【RO-data】+【RW-data】,ROM的大小即為程式所下載到ROM Flash中的大小。為什麼Rom中還要有RW,因為掉電後RAM中所有的資料都丟失了,每次上電RAM中的資料是被程式賦值的,每次這些固定的值就是儲存在ROM中的,為什麼不包含ZI段呢,是因為ZI資料都是0,沒必要包含,只要查詢執行之前將ZI資料所在的區域一律清零即可。包含進去反而浪費儲存空間。

 

程式執行:

燒錄到ROM中的image檔案與實際執行時的ARM程式之間並不是完全一樣的。MCU執行過程是先將RW從ROM中搬到RAM中,因為RW是變數,變數不能存在ROM中。然後將ZI所在的RAM區域全部清零,因為ZI區域並不在Image中,所以需要程式根據編譯器給出的ZI地址及大小來將相應的RAM區域清零。ZI中也是變數,同理:變數不能儲存在ROM中,在程式執行的最初階段,RO中的指令完成了這兩項工作後C程式才能正常訪問變數。否則只能執行不含變數的程式碼。

 

參考:

記憶體碎片化:

https://blog.csdn.net/chenyefei/article/details/82534058

STM32的編譯記憶體資訊:

https://blog.csdn.net/qq_37858386/article/details/79541451

程式分割槽:

https://blog.csdn.net/u014470361/article/details/79297601

正點原子malloc:

http://www.openedv.com/forum.php?mod=viewthread&tid=954&extra=&highlight=malloc&page=1

相關文章