深入底层学习
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; //清空计数器
}