單片機的內存無非就兩種,內部FLASH和SRAM,最多再加上一個外部的FLASH拓展。在這里我以STM32F103C8T6為例子講解FLASH和SRAM。
STM32F103C8T6具有64KB的閃存和20KB的SRAM。
一. Flash
1.1 定義
非易失性存儲器,即使在斷電后,其所存儲的數據也不會丟失。它可以進行多次擦除和寫入操作,但擦除和寫入的速度相對較慢。
對于flash而言,其內的數據只能讀,不能寫。
1.2 flash存儲的數據
對于flash而言,其內存儲的數據有以下幾種:
1.用戶代碼
2.中斷向量表
3.全局變量:
已被初始化和未被初始化的全局變量。
4.常量:
這里的常量是只可以讀,不可以被修改的常量,被const修飾的變量。
1.3 STM32的flash地址
stm32f103c8t6的flash地址為:0x08000000 ~ 0x0801 FFFF
二. SRAM
2.1 定義
是一種易失性存儲器,只有在通電的情況下才能保持數據,斷電數據丟失。它的讀寫速度非常快,能夠快速地響應 CPU 的訪問請求。
對于SRAM而言,其內的數據可讀可寫。
2.2?SRAM存儲的數據
1.局部變量:
2.靜態變量:
被關鍵字static修飾的變量/常量。
在這里需要注意一點如果 static變量被初始化為常量(如?static int x = 5;
),初始化值會被編譯到 Flash 中,但變量本身(運行時的存儲空間)仍在 SRAM 中。
運行時:程序啟動時,初始化值會從 Flash 復制到 SRAM 的靜態數據區,之后變量的修改都發生在 SRAM 中。
3.堆Heap:動態分配的內存。
注意:堆的內存分配有用戶自己實現。主要通過下面幾個函數進行開辟和釋放
?malloc
/free
、new
/delete
?
在學習freertos的時候都應該知道freertos有5個heap管理的算法,有興趣的可以查看freertos的源碼查看一下其不同內存管理算法的區別。
4.棧Stack:
存放局部變量、函數參數、返回地址等,由編譯器自動管理,遵循 “后進先出” 原則。
棧的空間在調用期間自動創建和釋放。
我們可以打開stm32的啟動文件來查看初始系統堆和棧的大小。這個大小可以自己修改,但是一定不能超出SRAM的大小。
5.寄存器reg
2.3?STM32的SRAM地址
stm32f103c8t6的SRAM地址為:0x20000000 ~ 0x20004FFF
三.存儲器映像圖分析
?初始看見這個圖,大家可能會很懵,但是一點點拆開來看,就很清晰了。
3.1 Flash memory
在表上可以明顯看出其地址范圍是0x0800 0000 ~?0x0801 FFFF
3.2 SRAM
在圖中可以看出SRAM的起始地址是0x2000 0000,但是圖中并未標明結束地址,這是因為這個圖是F103系列的存儲器映像圖,所以不同的型號其SRAM的結束地址不同
這個地址大家可以自己計算,計算公式為:起始地址 + 容量字節數 - 1
?STM32F103C8T6 的結束地址:
0x20000000+(20×1024)?1=0x20004FFF
3.3?Peripherals(外設)
在圖中可以看到,外設的地址是從 0x4000 0000開始的,TIM,SPI,I2C等外設均在此地址范圍內。
在此大家可能會有疑問,外設地址是不是就是寄存器地址呢?
其實不然,外設地址是寄存器的基地址。寄存器的地址等于外設基地址+偏移量。
打開stm32的xb.h文件,里面存放了關于每個外設的地址信息,我們可以很明顯的看出,每一個外設的地址都是外設基地址+偏移量。
在查看STM32芯片手冊的時候,也可以看到在配置每一個外設寄存器的時候,都會顯示一個地址偏移量
?3.4?Cortex - M3 Internal Peripherals
?Cortex - M3 內核內部外設地址:0xE000 0000 ~ 0xE010 0000
包含內核調試組件、中斷控制器(NVIC)等。
3.5?APB memory space(APB 總線外設內存空間)
??APB 總線上的外設,涵蓋 CRC、Flash Interface、RCC、DMA 等。
3.6 特殊區域
Option Bytes:位于?0x1FFFF 800
?附近,用于配置芯片的一些特殊功能,如寫保護、讀保護、BOR級別等。
System memory:在?0x1FFFF 000
?附近,用于系統自舉(Bootloader)等功能。芯片啟動時可從這里加載代碼執行,常用于實現 ISP功能。
四.代碼段分析
?在STM32中,代碼被存在flash和sram中,但其具體的存儲位置還不明了。對于flash和sram,其中對不同的數據都有不同的存儲位置,我上面給出了一個大概的邏輯圖,接下來就對這些代碼段進行具體分析。
在Cortex-M3權威指南中也有定義說明
4.1 .text段
也叫代碼段,存儲可執行的程序代碼,包含函數體中的指令 。
比如定義的void main(void)
函數以及其他我們自己定義的函數代碼都存放在這里。
中斷向量表一般也存放在此段,位于 Flash 起始地址附近,用于中斷發生時引導 CPU 找到對應的中斷處理函數 。
在STM32的啟動文件中,也能看到部分代碼段
?AREA
定義了名為.text
的區域,屬性為代碼段(CODE
)、只讀(READONLY
) 。Reset_Handler
是復位處理函數,先調用SystemInit
進行系統初始化(如時鐘配置等 ),再跳轉到__main
(C 庫函數,最終會調用main
函數 ) 。
在Cortex-M3權威指南中,也可以看到對.text段的定義和解釋。
4.2?.rodata 段
只讀數據段 ,存放只讀的常量數據,以及一些字面量?。
在上文中,我們提到過,被const修飾的常量就是存放在此段的。
4.3?.bss 段?
未初始化數據段。用于存儲未初始化的全局變量和靜態變量。
比如int globalVar;
?(未初始化的全局變量)、static int staticVar;
?(未初始化的靜態變量)?
在程序啟動時,該段內存會被自動清零 ,這部分變量在運行時可被程序讀寫 。
4.2 .data段
已初始化數據段,存放已初始化的全局變量和靜態變量。
例如int initializedGlobal = 5;
?(已初始化的全局變量)、static int initializedStatic = 3;
?(已初始化的靜態變量)
在上面將SRAM中我們提到過,這些變量的初始值在編譯時確定,程序啟動時,其初始值從 Flash 復制到 SRAM 中,在程序運行過程中可被讀寫。
4.4?.Stack段?
棧段,用于存儲函數調用時的局部變量、函數參數、返回地址等
每當函數被調用,其局部變量等信息被壓入棧中,函數執行結束后彈出 。
比如在一個函數中定義的int localVar = 1,
該變量就存儲在棧中 。
若函數調用層級過深或局部變量占用空間過大,可能導致棧溢出 。此時需要在啟動文件處修改棧的大小或者修改代碼來避免溢出。
4.5?.heap段
堆段。用于動態內存分配的區域。
當程序需要創建動態大小的數據結構(如鏈表節點、動態數組)或處理不確定大小的數據(如網絡接收的可變長數據)時,可從堆中分配內存 。使用完后需手動釋放,否則會造成內存泄漏 。
4.6?.reg
寄存器段。主要用于上電初始化配置外設模塊的信息,此時需要操作寄存器配置一些參數。
五. .map文件分析(十分重要)
在編譯代碼結束的時候,我們都能看見這樣一段
單片機是怎么知道我們的內存占用情況呢?我們應該如何分析每一個函數的內存占用空間呢?這個時候.map文件的作用就至關重要了。
5.1 什么是.map文件
.map文件是由連接器生成的列表文件,里詳細的展示了每一段代碼的占用情況。對分析和管理內存十分重要。
5.1.1符號與地址映射
記錄程序中函數、全局變量等全局符號對應的起始地址 。通過它能知曉每個函數和變量在內存中的位置,便于調試和分析程序,比如可查找崩潰地址并定位到出錯代碼行 。
5.1.2內存使用情況
呈現程序各部分(如代碼段、數據段等)在內存中的分布及占用空間大小 可查看 Flash 和 RAM 的占用情況 。
5.1.3段映射信息
明確程序中各個段(如.text、.data、.bss 等)實際映射的起始地址與長度 。與鏈接腳本(如.ld 文件 )中的段配置相關聯,展示鏈接過程中段的具體布局 。
如果沒有.map文件,點擊下面的配置,再重現編譯代碼,就能看見了。
下面這個也建議勾選上,它可以刪除冗余代碼。?
5.2 Component頭部信息
這個時候我們打開.map文件分析,大家可以打開自己工程的.map文件查看。這里我以我的工程為例子講解。
我工程里的.map文件頭部信息如下:
顯示了編譯器的版本和編譯工具。
.map 文件的組成部分因編譯器和開發環境不同而略有差異。
有的頭文件可能會出現如硬件架構(如 Cortex-M3、ARMv7 等),工程名稱、輸出文件路徑等基本信息。
5.3 Section Cross References交叉引用表
可以很明顯的看出,交叉引用表的格式是十分統一的:
每一行的格式為:源目標文件(源段) refers to 目標目標文件(目標段) for 符號名
?
通俗一點將就是在某某函數李調用了某某函數。
以紫色部分為例:在main函數里調用了HAL_Init();SystemClock_Config();MX_GPIO_Init()等函數。
打開我們的工程main.c函數可以看見,確實如此。
?藍色框里面的就是啟動信息了,這個在啟動文件里可以看見,就不過多贅述了。
(.o文件是鏈接器生成的目標文件。(i.main)代表的是main函數的基地址)
然后我們接著往下看。
5.4?Removing Unused input sections from the image
從生成的鏡像文件中移除未使用的輸入段。
簡單來講就是刪除沒用到的代碼,節省空間。
來看紅色框部分,移除了.rrx_text代碼段的gpio.o文件,大小為6個字節。
在該段的最后會有這樣一段
?這句話的意思是:從鏡像文件中總共移除了 677 個未使用段,總計 66617 字節。
5.5?Image Symbol Table 鏡像符號表
Local Symbols:局部符號。
Symbol Name:符號名稱,這里顯示的是源文件路徑,對應目標文件中定義的符號來源 。
Value:符號對應的值,此處都是0x00000000
?,在不同場景下可能表示符號的地址等信息 。
Ov Type?:符號的類型,這里均為Number
?,表示這些符號是數值類型相關 。
Size:符號的大小,此處都是 0 。
Object(Section)?:符號所在的目標文件及段,ABSOLUTE
表示符號具有絕對地址 。
那么再看紫色橫線的部分,意思就清晰可見了。這里就不過多贅述了。
再看下面這個圖
?Global Symbols:全局符號
5.5.1 局部符號和全局符號的區別
他們的區別主要有以下幾點
1.作用域
- 局部符號:作用域局限于定義它的特定模塊、函數或代碼塊內部 。比如 C 語言中函數內部定義的局部變量、局部靜態變量 ,以及函數內部聲明的局部函數(如使用
static
修飾的內部函數 ),僅在該函數內可見和可訪問 。像在一個main
函數中定義的int localVar = 10;
?,在main
函數外部無法訪問localVar
?。 - 全局符號:作用域為整個程序 。在程序的任何位置,只要滿足訪問權限等要求,都能對其進行訪問 。例如 C 語言中在所有函數外部定義的全局變量、全局函數 ,如
int globalVar = 5;
?,在其他源文件中通過extern
聲明后也可訪問 。
2.可見性
- 局部符號:僅在定義它的代碼區域內可見 。當程序執行流程離開該區域(如函數執行完畢返回 ),局部符號就不再可見 。比如函數中定義的臨時變量,函數執行結束后其生命周期結束,無法再被訪問 。
- 全局符號:在整個程序范圍內可見 。只要程序在運行,全局符號始終存在且可被訪問(需符合訪問控制規則 ) 。
3.存儲位置
- 局部符號:一般存儲在棧(stack)中,像函數的局部變量,隨著函數調用入棧,函數結束出棧 。局部靜態變量存儲在靜態存儲區 。
- 全局符號:全局變量通常存儲在靜態存儲區 。未初始化的全局變量存放在
.bss
段,已初始化的全局變量存放在.data
段 。全局函數的代碼存放在代碼段(.text
) 。
4.生命周期
- 局部符號:自動局部變量生命周期隨函數調用開始,函數返回結束 。局部靜態變量在程序啟動時初始化,程序結束時銷毀 。
- 全局符號:全局變量和函數在程序啟動時創建并初始化,程序運行期間一直存在,直到程序結束才銷毀 。
那么它有什么作用呢?
5.5.2?鏡像符號表的作用
5.5.2.1 調試定位
查找符號地址:在調試程序時,如果遇到程序崩潰或異常,通過錯誤提示中的地址信息,可在符號表中查找對應的符號名稱 。比如知道一個錯誤地址,可在 “Symbol Name” 列找到該地址對應的源文件及符號,進而定位到具體代碼位置,快速排查問題 。
函數和變量追蹤:對于大型項目,函數和變量眾多。符號表能幫助了解每個源文件中定義的符號情況,追蹤函數調用關系和變量使用位置 。例如,想知道某個函數在哪些源文件中被引用,可借助符號表梳理 。
5.5.2.2 程序分析與優化
代碼結構理解:通過查看符號表中不同源文件對應的符號,能清晰了解項目的代碼結構 。比如哪些源文件屬于底層驅動(如stm32f1xx_hal_xxx.c
?相關 ),哪些是應用層代碼(如main.c
?等 ),有助于新開發者快速熟悉項目架構 。
在我們實際開發中寫代碼也應該分層編寫,利于梳理。
未使用符號檢測:可以發現一些未被使用的符號 。如果某個源文件對應的符號在整個項目中都沒有被引用,可能是冗余代碼,可考慮清理以優化代碼體積和提高編譯效率 。
5.5.2.3?鏈接與編譯檢查
符號沖突排查:在多文件編譯鏈接過程中,可能出現符號重名沖突 。符號表能展示所有符號信息,方便開發者檢查是否存在同名符號,避免因符號沖突導致的編譯或運行錯誤 。
鏈接正確性驗證:符號表中的信息與鏈接過程緊密相關 。它能反映鏈接器是否正確解析和處理了各個目標文件中的符號,驗證鏈接的正確性 。如果符號表中符號信息異常,可能意味著鏈接過程出現問題 。
5.6?Memory Map of the image存儲器映射
先看第一句:Image Entry point:表示程序鏡像的入口點地址,這里是0x080000ed
?,即程序開始執行時的起始地址 。
然后再往下看
5.6.1 Load Region加載區域
- Base:加載區域的起始地址為
0x08000000
?,一般對應芯片 Flash 的起始地址 。 - Size:大小為
0x0000a088
?,表示該區域占用的字節數 。 - Max:最大允許大小為
0x00010000
?,用于限定該加載區域可使用的最大空間 。 - 屬性:
ABSOLUTE
?表示該區域地址是絕對地址 。
接著就是下面的
5.6.2 Execution Region執行區域
- Exec base:執行基地址為
0x08000000
?,即程序執行時的起始地址 。 - Load base:加載基地址也是
0x08000000
?,說明加載地址和執行地址相同 。 - Size:大小為
0x00009fa4
?,是執行區域實際占用空間 。 - Max:最大允許大小
0x00010000
?。 - 屬性:
ABSOLUTE
?。
然后就是執行區域的具體解釋
- Exec Addr:執行地址 。
- Load Addr:加載地址 。
- Size:對應段的大小 。
- Type:段的類型,
Data
?表示數據段,Code
?表示代碼段 。 - Attr:屬性,
RO
?表示只讀(Read - Only) 。 - Idx:索引值 。
- Section Name:段名,如
RESET
?是存放復位向量等的段;.emb_text
?是包含部分代碼的文本段 。 - Object:該段所屬的目標文件,如
startup_stm32f103xb.o
、port.o
?等 。
看藍色箭頭,RO代表執行區內容都是只讀的,都存在Flash里。看起始地址0x80000000也可以看出,符合Flash的基地址。
執行區域對于分析內存占用情況是十分重要的。
我們接著往下看。
這段執行區里的內容都是可以讀寫的,看紅色框RW即可看出。說明該段位于SRAM中。
我們從他的基地址也可以看出。從0x20000000開始,符合我們前面講到的。
我們再看第一個 size:0x00000004,代表freertos.o占用了四個字節
然后我們再看一下我們前面提到的代碼段,在.map文件里都能找到。
?這個.constdata意思是常量數據段,保存在flash里
.bss數據段,它主要用于存放未初始化的全局變量和靜態變量,該段位于 SRAM 中。
?
?.text代碼段,存放在Flash里
?.STACK棧段,位于SRAM里
?5.7?Image component sizes鏡像組件大小
- Code (inc. data):包含代碼以及與代碼相關數據的大小總和 ,這里的代碼指編譯后的機器指令 。
- RO Data:只讀數據的大小,如程序中用
const
修飾的常量等 。存放在 Flash里。 - RW Data:可讀寫數據的大小,一般是已初始化的全局變量和靜態變量 。存放在SRAM里。
- ZI Data:未初始化的全局變量和靜態變量,程序啟動時會被初始化為 0 。存放在SRAM里。
- Debug:調試信息的大小,用于調試器輔助調試程序 。
- Object Name:目標文件名稱,代表這些數據所屬的編譯后文件 。
以alert.o
這一行數據64 32 0 0 28 1132 alert.o
?為例:
alert.o
目標文件中,包含代碼及相關數據的大小為 64 字節 ;
只讀數據大小是 32 字節 ;
沒有可讀寫數據(RW Data 為 0 )
零初始化數據(ZI Data 為 0 )
調試信息大小為 28 字節?
在目標文件后面會有一個內存總計。其實就是自己寫的代碼的內存占用情況。
其他行數據同理,分別對應不同目標文件的各類數據大小情況 。
鏡像組件大小有助于我們去了解每個目標文件對存儲空間的占用情況,分析程序的內存使用布局,排查是否存在不合理的內存占用等問題 。
這個圖里展示的是庫文件統計。
再看最后一段
Total RO Size (Code + RO Data) 為 40868 ( 39.91kB)?:是代碼和只讀數據大小之和,即存儲在 ROM 中不會被修改的內容總大小為 40868 字節,約 39.91kB 。
Total RW Size (RW Data + ZI Data) 為 15872 ( 15.50kB)?:是可讀寫數據(已初始化和未初始化)的總大小,這部分數據在程序運行時可能會存放在 SRAM 中,大小為 15872 字節,約 15.50kB 。
Total ROM Size (Code + RO Data + RW Data) 為 41096 ( 40.13kB)?:是存儲在 ROM 中的代碼、只讀數據和已初始化可讀寫數據的總大小,為 41096 字節,約 40.13kB 。
六:總結
在分析代碼的內存占用情況時,查看鏡像組件中用戶代碼的內存占用是首要步驟。用戶代碼涵蓋了開發者編寫的各類源文件經編譯后生成的目標文件內容。通過關注鏡像組件中如 “Code (inc. data)”“RO Data”“RW Data”“ZI Data” 等不同類別數據的大小,能清晰知曉每部分代碼和數據在內存中的占用情況。
對于內存占用較大的代碼,也需要慎重評估處理。
一方面,可深入分析代碼邏輯,查找冗余部分進行修改精簡。例如,檢查是否存在重復的計算邏輯、不必要的變量定義等,通過優化算法和代碼結構來降低內存開銷。
另一方面,考慮將不常變動且對讀取速度要求相對不高的部分存放在 Flash 中。因為 Flash 具有非易失性,可用于存儲程序代碼和一些只讀數據。而 SRAM 作為程序運行時用于快速讀寫數據的區域,需確保其空間充足,以保障程序運行時變量的讀寫操作能高效進行,避免因 SRAM 空間不足導致程序運行出錯或性能下降。