深入底層學習
1 STM32啟動過程分析
1.0 預備知識
1、RAM、FLASH
ram
即隨機存儲器,掉電即丟失信息,常用於存放寄存器和運行中的數據。flash
是和rom
類似的存儲器,掉電不會丟失信息,用於存放代碼。芯片手冊
可以查到STM32F407ZGT6
的Flash大小為1024Kbyte
,SRAM的大小為192k
2、三種啟動方式
打開STM32F407中文手冊
,找到2.4 自舉配置
STM32有三種啟動方式,分別是從FLASH(閃存)
、SRAM
、系統存儲器
啟動,由BOOT0
和BOOT1
設置
- 從
Flash
啟動,將Flash
地址0x08000000
映射到0x00000000
,一般我們所寫的程序都放在STM32的內部Flash中,是我們最常用的模式 - 從
SRAM
啟動,將SRAM
地址0x20000000
映射到0x00000000
,一般用於調試(PS:遇到過flash被鎖定,則可通過SRAM解鎖Flash,參考【解鎖FLASH】如何給STM32芯片解鎖) - 從
系統存儲器
啟動,根據中文手冊
中2.4 自舉配置
的表4
可得系統存儲器即存儲器地址0x1FFFF000
起到0x1FFF77FF
的這一段地址。再往上看,解釋這段空間存放着一段嵌入式自舉程序
,這個程序出廠就燒錄在里面。可以理解為電腦的bios程序,這個程序提供了串口下載的功能,也就是串口下載要切換boot的原因。
3、編譯完后的信息和map文件分析
在keil中編譯完程序之后
-
Code:是程序中代碼所占字節大小;
-
RO-data:程序只讀(
readonly
)的變量,也就是帶const的,和已初始化的字符串等; -
RW-data:已初始化的可讀寫(
readwrite
)全局/靜態變量; -
ZI-data:未初始化(
Zero-initialized
)的可讀寫全局/靜態變量;
程序所占Flash空間大小=Code+RO data+RW-data=生成的bin
文件大小。
程序固定占用RAM大小=RW data+ZI data
在生成文件中可以找到一個map
文件,這個文件記錄了編譯鏈接以及內存分配等信息。keil
可以設置map
的輸出信息。
keil
的Project -> Options for Target -> Listing
主要包含配置:
- Memory Map:內存映射
- Callgraph:圖像映射
- Symbols:符號
- Cross Reference:交叉引用
- Size Info:大小信息
- Totals Info:統計信息
- Unused Section Info:未調用模塊信息
- Veneers Info:裝飾信息
map文件主要結構
map文件里面內容大致分為五大類(按照map文件分類的順序):
1.Section Cross References:模塊、段(入口)交叉引用
2.Removing Unused input sections from the image:移除未使用的模塊
3.Image Symbol Table:映射符號表
4.Memory Map of the image:內存(映射)分布
5.Image component sizes:存儲組成大小
文件名詞:
段(section):描述映像文件的代碼和數據塊
RO:Read-Only的縮寫,包括RO-data(只讀數據)和RO-code(代碼)
RW:Read-Write的縮寫,主要是RW-data,RW-data由程序初始化初始值
ZI:Zero-initialized的縮寫,主要是ZI-data,由編譯器初始化為0。
.text:與RO-code同義
.constdata:與RO-data同義
.bss:與ZI-data同義
.data:與RW-data同義
Section Cross References
模塊、段(入口)交叉引用(需勾上Cross Reference
)
- 表達不同文件中函數的調用關系
- 例如:
main.o(.text) refers to delay.o(.text) for delay_init
表達main
文件里的一段語句,調用了delay
文件delay_init
函數
Removing Unused input sections from the image
移除未使用的模塊(需勾上Unuaed Sections Info
)
- 最后一句統計信息
23 unused section(s) (total 104 bytes) removed from the image.
表示總共23段沒有調用,沒有調用的大小為104字節
- 最后一句統計信息
Image Symbol Table
映射符號表(需勾上Symbols
)
- 映射符號表,也就是各個段所存儲對應地址的表(這一項比較重要)
Symbols
分為兩大類1.Local Symbols
局部、2.Global Symbols
全局- 1、
Symbol Name
:符號名 - 2、
Value
:存儲對應的地址;(0x08
開頭表示存儲在Flash
中,0x2
開頭表示存儲在SRAM
中) - 3、
Ov Type
:符號對應的類型符號類型大概有幾種:Number
、Section
、Thumb Code
、Data
等;(全局、靜態變量等位於0x2000xxxx
的內存RAM
中) - 4、
Size
:存儲大小(懷疑內存溢出,可以查看代碼存儲大小來分析) - 5、
.Object(Section)
:段目標(這里一般指所在模塊(所在源文件))
Memory Map of the image
:內存(映射)分布(需勾上Memory Map
)
- 1、
Base Addr
:存儲地址(0x0800xxxxFLASH地址和0x2000xxxx內存RAM地址) - 2、
Size
:存儲大小 - 3、
Type
:類型(Data
:數據類型Code
:代碼類型Zero
:未初始化變量類型PAD
:“補充類型”。) - 4、
Attr
:屬性(RO:存儲與ROM中的段 RW:存儲與RAM中的段) - 5、
Section Name
:段名這里也可以說為入口分類名,與第一章節“Section Cross References”指的模塊、段一樣。大概包含:RESET、.ARM、 .text、 i、 .data、 .bss、 HEAP、 STACK等。 - 6、
Object
:目標模塊
- 1、
Image component sizes
:存儲組成大小(需勾上Size Info
)
- 對信息進行匯總,和開頭分析的那個信息是一樣的。
1.1 啟動過程分析
1 啟動文件解讀
野火哥的啟動文件視頻講解,超詳細
- 初始化堆和棧
第一段定義了一個棧空間(用於存放局部變量,函數)
Stack_Size EQU 0x00000400
;偽指令Stack_Size=1KBAREA STACK, NOIIT, READWRITE, ALIGN=3
;STACK
新的棧段,NOIIT
表示存放到SRAM,READWRITE
可讀寫ALIGN=3
2^3字節即8字節對齊Stack_Mem SPACE Stack_Size
;分配空間__initial_sp
表示棧的結束地址,即棧頂地址;棧是由高向低生長的
第二段定義了一個堆空間(動態內存分配)
Heap_Size EQU 0x00000200
;Heap_Size=512字節AREA HEAP, NOINIT, READWRITE, ALIGN=3
同上__heap_base
;堆的起始地址Heap_Mem SPACE Heap_Size
;分配空間__heap_limit
;堆的結束地址;堆是由低向高生長的,和棧相反PRESERVE8
;指定當前文件的堆棧按照8 字節對齊。THUMB
;表示后面指令兼容THUMB 指令。THUBM是ARM以前的指令集
- 初始化向量表
-
AREA RESET, DATA, READONLY
;定義一個數據段,名字為RESET,存放在flash,只讀。 -
聲明
__Vectors
、__Vectors_End
和__Vectors_Size
這三個標號具有全局屬性,可供外部的文件調用。EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
EXPORT
:聲明一個標號可被外部的文件使用,使標號具有全局屬性。如果是IAR
編譯器,則使用的是GLOBAL
這個指令。
-
DCD __initial_sp ;
DCD
簡單來說就是給后面的標號分配了一個地址,並存放在該位置- 第一個存放棧頂指針
SP
,第二個存放PC
指針,指向復位RESET中斷子服務函數
- 復位中斷子服務函數及其他中斷子服務函數
EXPORT Reset_Handler [WEAK]
;WEAK:表示弱定義,如果外部文件優先定義了該標號則首先引用該標號,如果外部文件沒有聲明也不會出錯。這里表示復位子程序可以由用戶在其他文件重新實現,這里並不是唯一的。IMPORT SystemInit
;IMPORT
:表示該標號來自外部文件,跟C 語言中的EXTERN
關鍵字類似。- 然后下面兩個分別進入外部的
SystemInit
函數和__main
函數(__main
集成在MDK自帶的庫里面了,主要的功能是軟件設置SP、加載.data.bss並初始化棧區)
下面是一些其他的中子服務函數,並且具有弱WEAK性,即如果外部有,優先采用外部的,不會產生沖突
- 用戶堆棧初始化
給用戶自己定義堆棧
2 內部Flash啟動過程
啟動過程主要為:
1、初始化堆棧指針SP=_initial_sp和PC指針=Reset_Handler
2、配置系統時鍾,調用C 庫函數_main 初始化用戶堆棧調用main
1、初始化堆棧指針SP=_initial_sp
和PC指針=Reset_Handler
、初始化中斷向量表
上電后默認將0x0000 0000
的位置讀出為SP
指針,將0x0000 0004
的位置數據讀出為PC
指針
根據前面,我們知道以flash啟動時0x0000 0000
被映射到0x0800 0000
的位置,所以根據啟動文件可以得知,向量表也被映射到了0x0800 0000
,由此可以讀出棧頂地址
和復位中斷地址
這里用軟件STM32 ST-LINK Utility
下載地址1(官網) 下載地址2
打開hex
文件,可以查到0x0800 0000
起頭兩個的數據,第一個0x20000758
是分配給SP的地址(棧頂地址
),第二個0x080004A1
是分配給PC指針的地址(復位中斷地址
)
0x080004A1
4字節對齊0x080004A0
,在.map
文件中找到地址0x080004A0
對應得模塊(復位中斷函數就放在這個啟動文件里)
PC
指針跳轉到復位中斷子服務函數Reset_Handler
2、配置系統時鍾,調用C 庫函數_main 初始化用戶堆棧調用main
復位中斷子服務函數里完成這些事
參考資料
- 系統分析STM32的上電啟動過程
- 深入分析STM32單片機的RAM和FLASH
- Keil綜合(03)_map文件全解析
- stm32--啟動文件(.s)與啟動過程
- 單片機STM32的啟動文件詳解--學習筆記
[應用]IAP、ISP
見專欄IAP
2 SysTick系統定時器
系統定時器非常重要,在非操作系統里,系統定時器用來做延時使用,在操作系統里,系統定時器用來產生任務調度的定時。
SysTick系統定時器是Cortex M4架構里都有的外設,所以我們查看Cortex M4內核編程手冊
打開Cortex M4內核編程手冊
,找到4 Core peripherals(內核特性)
-4.5 SysTick Timer
它告訴我們SysTick是個24位的系統定時器,從預裝載值向下計數到0,相關寄存器有控制狀態寄存器STK_CTRL
、裝載值寄存器STK_LOAD
、當前值寄存器STK_VAL
、校准數值寄存器STK_CALIB
-
控制狀態寄存器STK_CTRL
COUNTFLAG
:當定時器數值為0時,此位為1CLKSOURCE
:選擇時鍾源,0:AHB/8 1:AHBTICKINT
:1使能定時器異常請求,即定時器回到0不會重裝載ENABLE
:使能定時器重裝載,同時受TICKINT
控制
-
裝載值寄存器STK_LOAD
RELOAD
:重裝值為真實值N-1
-
當前值寄存器STK_VAL
CURRENT
:可手動清0
固件庫中的Systick
相關函數
-
SysTick_CLKSourceConfig()
Systick時鍾源選擇(在misc.c
文件中) -
SysTick_Config(uint32_t ticks)
初始化systick
,時鍾為HCLK
,並開啟中斷 (在core_cm3.h
中) -
void SysTick_Handler(void);
SysTick中斷函數 -
SysTick_CLKSourceConfig()
選擇時鍾源,0:AHB/8
1:AHB
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
/* Check the parameters */
assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));
if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
{
SysTick->CTRL |= SysTick_CLKSource_HCLK;
}
else
{
SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
}
}
SysTick_Config(uint32_t ticks)
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */
SysTick->LOAD = ticks - 1; /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* set Priority for Systick Interrupt */
SysTick->VAL = 0; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0); /* Function successful */
}
if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk) return (1);
預裝載值不能超過最大值SysTick->LOAD = ticks - 1;
設置重裝載寄存器的值NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
設置中斷寄存器SysTick->VAL = 0;
當前值為0SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
設置控制寄存器
應用:延時delay函數
- 初始化延遲函數
delay_init
//初始化延遲函數
//當使用OS的時候,此函數會初始化OS的時鍾節拍
//SYSTICK的時鍾固定為AHB時鍾的1/8
//SYSCLK:系統時鍾頻率
void delay_init(u8 SYSCLK)
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
fac_us=SYSCLK/8; //不論是否使用OS,fac_us都需要使用
fac_ms=(u16)fac_us*1000; //非OS下,代表每個ms需要的systick時鍾數
}
指定時鍾源為AHB/8
- us延時
delay_us
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //時間加載
SysTick->VAL=0x00; //清空計數器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //開始倒數
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待時間到達
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //關閉計數器
SysTick->VAL =0X00; //清空計數器
}