深入底层学习
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=32^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 __VectorsEXPORT __Vectors_EndEXPORT __Vectors_SizeEXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。如果是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指针的地址(复位中断地址)

0x080004A14字节对齐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_CTRLCOUNTFLAG:当定时器数值为0时,此位为1CLKSOURCE:选择时钟源,0:AHB/8 1:AHBTICKINT:1使能定时器异常请求,即定时器回到0不会重装载ENABLE:使能定时器重装载,同时受TICKINT控制
-
装载值寄存器STK_LOADRELOAD:重装值为真实值N-1
-
当前值寄存器STK_VALCURRENT:可手动清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/81: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; //清空计数器
}
