MCU的启动到bootloader原理详解
认识 RO-data、RW-data、ZI-data;ROM、RAM
什么是ARM 程序:
ARM 程序是至ARM系统正在执行的程序,而非只是保存在ROM中的映像文件。
一个ARM程序包含3部分:RO-data、RW-data、和ZI-data。这只是区分数据是什么数据类型,而不是区分数据所在ROM或者RAM区域。
keil 编译后会出现以下提示
Program Size: Code=11216 RO-data=380 RW-data=68 ZI-data=2428
Code 是程序中的指令(数据类型RO、不会变)
RO-data 是程序中的常量(数据类型RO、不会变),如const int a = 5;
。
RW-data 是程序中的已初始化变量(数据类型RW、会变),但是初值不为0,如int a = 5;
。
ZI-data 是程序中的未初始化的变量(数据类型RW、会变),但是初值为0,如int a;
。
什么是 RO Size、RW Size、ROM Size:
Total RO Size(Code + RO Data)
Total RW Size(RW Data + ZI Data)
Total ROM Size(Code + RO Data + RW Data)
ROM Size 就是 Flash 所需大小。也就是说,单片机的Flash不仅仅要存放不会变的数据,也要存放会变的变量的初始值RW-data,在运行程序文件时,里面会有编译时的数据搬运指令,将RW-data搬运至RAM中。
RW Size 就是 RAM 所需大小
RO Size
ARM 映像文件 bin 的组成:
ARM映像文件就是指烧录到ROM中的bin文件,也称为image文件。以下用Image文件来称呼它。Image文件包含了RO和RW数据。之所以Image文件不包含ZI数据,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可。包含进去反而浪费存储空间。
Q:为什么Image中必须包含RO和RW?
A:因为RO中的指令和常量以及RW中初始化过的变量是不能像ZI那样“无中生有”的。
ARM程序的执行过程
从以上两点可以知道,烧录到ROM中的image文件与实际运行时的ARM程序之间并不是完全一样的。因此就有必要了解ARM程序是如何从ROM中的image到达实际运行状态的。
实际上,RO中的指令至少应该有这样的功能:
1、将RW从ROM中搬到RAM中,因为RW是变量,变量不能存在ROM中。
2、将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。
RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。
总结
什么是arm的映像文件,arm映像文件其实就是可执行文件,包括bin或hex两种格式,可以直接烧到rom里执行。在axd调试过程中,我们调试的是axf文件,其实这也是一种映像文件,它只是在bin文件中加了一个文件头和一些调试信息。映像文件一般由域组成,域最多由三个输出段组成(RO,RW,ZI)组成,输出段又由输入段组成。所谓域,指的就是整个bin映像文件所处在的区域,它又分为加载域和运行域。加载域就是映像文件被静态存放的工作区域,一般来说flash里的 整个bin文件所在的地址空间就是加载域,当然在程序一般都不会放在 flash里执行,一般都会搬到sdram里运行工作,它们在被搬到sdram里工作所处的地址空间就是运行域。
我们输入的代码,一般有代码部分和数据部分,这就是所谓的输入段,经过编译后就变成了bin文件中ro段和rw段,还有所谓的zi段,这就是输出段。对于加载域中的输出段,一般来说ro段后面紧跟着rw段,rw段后面紧跟着zi段。在运行域中这些输出段并不连续,但rw和zi一定是连着的。zi段和rw段中的数据其实可以是rw属性。
ARM中的RO、RW和ZI DATA说明-电子工程世界 (eeworld.com.cn)
SRAM的划分
SRAM首地址 0x20000000
SRAM存放 RW-data ZI-data
全局变量、态变量
HEAP 0x20000148 Section 512 startup_stm32f10x_hd.o(HEAP)
STACK 0x20000348 Section 1024 startup_stm32f10x_hd.o(STACK)
__initial_sp 0x20000748 Data 0 startup_stm32f10x_hd.o(STACK)
什么是堆栈
堆
堆,系统动态分配的对象,空间不连续,数据是有数据结构的,空间占用不连续,模型如水管。
堆生长方向由上至下(),栈生长方向由下至上。因此要注意堆栈溢出,函数嵌套,栈会覆盖堆。
栈
堆栈 LIFO 先入后出特性,模型如瓶子,常应用于保存中断断点、保存子程序调用返回点、保存CPU现场数据等,也用于程序间传递参数。
堆栈(栈stack)存放函数的参数值,局部变量值等临时变量,退出该作用域时变量会自动释放,压栈就是保存现场,保存内容是所有用到的寄存器r0-r15,局部变量,SP、LR、PC指针。
CPU内部的几种指针 SP、LR、CP
SP(Stack pointer R13)指针:是堆栈指针寄存器,用于管理堆栈的内存空间。在ARM架构中,SP指针通常是R13寄存器,SP指针有多个,如MSP\PSP。堆栈指针用于控制入栈和出栈操作,堆栈通常是向下增长的满栈模型,即从高地址向低地址扩展。执行PUSH/POP,SP指针自减/自增
LR(Link Register R14)连接寄存器:临时保存子函数的返回地址(即调用子函数时,直接把PC给LR,一般是执行子函数前的最后一个指令地址),恢复现场时需要把之前压栈保存的LR指针给PC指针。
PC(Program Counter R15)指针寄存器:用于存放下一条指令地址的寄存器。在ARM架构中,PC指针寄存器通常是R15寄存器。PC寄存器的特点是它始终指向即将取指的指令地址,而不是当前正在执行的指令地址。这是因为在流水线处理器中,当一条指令正在执行时,下一条指令已经在取指阶段了。(三级流水线即是一个时钟,同时完成取指令,译码,执行)
一条指令4字节大小,一条指令三过程,CPU取指令地址 = 当前运行指令地址(执行) + 8
在函数调用过程中,CPU会使用PC和SP指针来保存和恢复上下文。例如,当一个函数被调用时,当前的PC值会被压栈(push到堆栈中),以便在子函数执行完毕后能够返回到调用点。同时,子函数的返回地址会被保存在LR(Link Register)中,以便在执行完子函数后能够返回主函数。当子函数执行完毕后,通过修改PC指针的值来返回主函数
CP指针是指向指令的指针,起初是函数的地址,在函数的运行过程中会改变为函数内部的各种指令。
SP指针是指向操作变量,即堆栈的内存地址。
SP 就是操作草稿的草稿地址,CP就是指向当前需要的操作指令。
返回地址(指向上一条指令的地址)
单片机的返回地址通常是指在执行子程序或中断服务程序后,用来指示程序继续执行的下一条指令的地址。在大多数情况下,这个地址是在调用子程序或中断服务程序之前,由主程序或中断服务程序本身保存到堆栈中的。当子程序或中断服务程序执行完毕后,通过返回指令(如ret
),程序会从堆栈中弹出这个保存的返回地址,并将其加载到程序计数器(PC)中,从而实现返回到主程序或中断服务程序调用之前的位置继续执行。
函数的参数传递小于4个则传送到寄存器,多于4个,寄存器不够用则用栈。
单片机启动原理
理论上,CM3中规定上电后CPU是从0地址开始执行,但是这里中断向量表却被烧写在0x0800 0000地址里(Flash memory启动方式),那启动时不就找不到中断向量表了?既然CM3定下的规矩是从0地址启动,SMT32当然不能破坏ARM定下的“规矩”,所以它做了一个启动映射的过程,就是和芯片上总能见到的BOOT0和BOOT1有关了,当选择从主Flash启动模式后,芯片一上电,Flash的0x0800 0000地址被映射到0地址处,不影响CM3内核的读取,所以这时的CM3既可以在0地址处访问中断向量表,也可以在0x0800 0000地址处访问中断向量表,而代码还是在0x0800 0000地址处存储的。
CORTEX-M3上电后后检测BOOT引脚的电平来决定PC的位置。
例:
B0=0 为FLASH启动、物理地址 0x08000000
B0=1、B1=0 为片内内存启动,物理地址 0x1FFF0000,用来运行单片机厂家预制 bootloader 串口下载
B0=1、B1=1 为SRAM启动,物理地址 0x20000000,用于调试
启动后CPU会先取两个地址:一个是栈顶地址给到MSP,另一个是复位地址给到PC。
STM32的堆栈是存放在片上静态SRAM中的,地址分配可以见Keil的编译map文件。
代码与烧录固件原理
编译器编译C语言程序为汇编语言,再由汇编指令编译生成二进制bin文件,即映像文件,bin 文件可直接按顺序烧录至存储器中如内部flash。烧录后的 flash 同该二进制文件一样,分为多个区段,如RO段和RW段,RO段包含code RO-data也就是常量,RW段包含RW-data。CPU上电运行时,将自动把flash 中的 RO 段,按顺序写入单片机内部 flash 的ROM区域(默认首地址为 0x08000000),把RW段写入至单片机内部 flash 中的RAM区域,默认首地址为 0x20000000。虽然Flash是可读可写的存储器,但是程序代码(code 段)、常量(RO-data)作为随着bin文件烧录进flash中后是只读。
题外话:RAM首地址不是栈顶地址,起始栈顶地址是RAM的最高位地址+1,如 0x20010000。
ARM内核地址与外设映射
在Cortex内核中,有4G虚拟空间地址,该地址分为8个块,每个块有512MByte的地址。这些地址映射至不同的外设,CPU可根据这些地址访问不同的外设。
值得注意的是,Flash 的地址范围为 0x08000000 - 0x0807FFFF 即512KB 的flash,SRAM 的地址为 0x20000000 - 0x2000FFFF,64KB。
中断向量表与向量表的偏移
中断向量表在可执行文件的最前面,同时也是flash的最前面。CPU将从这里开始一步步寻找指令并且执行的。
可执行文件内容的起始位置存放的就是这个中断向量表。
首地址 0x0
是个指向RAM栈顶地址的指针,单片机最先读取到此自制给到MSP,定位至栈顶地址,从栈顶开始入栈出栈操作。
*(__IO uint32_t*)0x0)
就是 RAM的栈顶指针的十六进制数,也就是MSP在此获取的初始值,同时也是代表堆栈RAM的大小。
0x04
存放的是个指向复位函数的指针,上电复位后PC指针将自动从此处获取复位函数的指令地址,执行复位函数的指令。
理论复位流程
理解两个指针,PC指针与SP指针
PC指针即是指令指针,同时也是个计数器,每执行一条指令,就会自增或自减。PC指针寄存器所保存的内容就是下一条即将执行的指令的地址。
SP指针是堆栈(Stack)指针,指向被操作的堆栈地址。
CPU运算时,就是通过PC指针获取操作指令,将运算过程中、或者运算后的数据、调用子函数时的返回地址等,存放到取指定的SP指针中指向的堆栈地址中去,或者从中取出使用或调用。
bin 文件将烧入到 Flash 程序存储空间中。该空间存储内容顺序为,向量表、各种中断程序、main函数程序、用户其他程序。该空间中每条指令都有指令地址,bootloader 就是通过这些地址,跳转到用户程序。
以下的地址全为内核执行的虚拟地址,该虚拟地址可映射到内部flash、SRAM、和核内存储区。
1、从地址0x0
指向读写堆栈RAM栈顶地址,取出作为MSP的初始值,
2、从地址0x04
指向向量表中的复位向量,取出作为PC的初始值,用来执行第一条指令。上电时PC指针指向此,执行复位函数,再由复位函数自动启动main函数。
从flash中启动
理解了内核启动的流程,我们来看真实的单片机是如何从flash中启动的
由于STM32单片机厂家的代码段地址映射,可以通过外部boot引脚的设置,由flash处启动,内核从code 地址区0x0
寻址时,是指向flash起始地址0x08000000 的。(以下使用地址为物理地址flash的真实地址)
单片机上电
1、定位至程序 flash 首地址(物理地址 0x08000000),是个指针,取出该地址存放的指针的值,该指针指向APP程序的堆栈地址(如0x20010000)同时也就是MSP初始值,将此值给MSP指针。
2、首地址+偏移量4 (物理地址 0x08000004)得到复位向量复位地址,给CP指针,执行复位函数
3、由复位向量可定位至复位函数的地址
4、启动复位函数即可定位至main函数
5、函数将在main函数中开始死循环
6、若运行过程中有异常中断,将工具中断向量定位至中断服务函数
7、执行完中断服务函数后将回归main函数
Bootloader 的原理
函数在二进制文件中,是被拆分为多条指令,以及需要操作的全局变量临时变量等,根据指令来操作堆栈(外部草稿纸)和内部特殊寄存器(内部草稿纸)。堆栈存放函数调用时与函数嵌套时需要执行的指令以及临时变量。因此修改SP指针(指向堆栈的指针),以及CP指针(指向程序指令的指针),即可跳转要执行的函数,并且执行。
代码的分区
bootloader(boot) 区,boot 区起始位置为默认的中断向量表,用来上电运行。作为单片机启动的引导
App区域, 为用户程序文件运行区域,该区域起始位置为用户的中断向量表,当上电复位后,boot 引导代码在执行完引导逻辑后将跳转至此,由此开始执行用户程序。烧录时,可分批烧录代码至内部flash的不同区域。
在keil 设置中 魔术棒-> target -> IROM1 中可设置烧录的 flash 起始的绝对地址0x08000000
和分区大小0x4000
。
向量表的设置
ARM默认在0x0
处寻找向量表,并开始启动单片机的,若对用户代码烧录位置做了修改,用户代码对应的向量表的烧录的位置也随之改变,但CPU仍会在上电或者硬件复位时,在地址0x0
处寻找向量表,因此单片机将因为无正确的向量表而工作异常甚至无法启动。因此,若需要改变用户代码的烧录位置,必须先有boot引导程序,该引导程序有初始向量表,单片机将根据此向量表,启动引导程序,并根据引导程序的跳转逻辑,将执行语句跳转至用户程序所在的代码块并运行。注意的是,初始向量表是禁止改变的,因此在用户程序App中需要对向量表重定向,设置VECT_TAB_OFFSET
,添加向量表的偏移 VECT_TAB_OFFSET 0x4000
,以后CPU运行时产生的各种中断异常,将至此执行对应的App程序的中断服务函数。
注意的是,中断向量表的偏移即向量表重映射,应当在boot经行程序跳转至App程序后再执行才有用。
bootloader的跳转
在理解了前面单片机程序的启动后,程序跳转将变得简单。
1、判断目标跳转的用户程序地址的首地址 appAddr,即ARM堆栈的栈顶地址是否合法,应该在一定范围内,如64K的RAM 范围在0x20000000 - 0x20010000。若合法,则将栈顶指针给到MSP指针。
appAddr
是app程序烧录所在内存的起始地址,可以是 flash 首地址0x08000000
,也可以是flash 偏移后的首地址0x08004000
。
flash 首地址,即是栈顶地址,减去堆栈基地址*(__IO uint32_t*)appAddr) - SRAM_BASE
刚好是堆栈大小。
#define RAM_END_ADDR 0x20010000 //0x10000 为 64K
if (((*(__IO uint32_t*)appAddr) & ((RAM_END_ADDR-1)^0x0FFFFFFF) ) == 0x20000000)`//判断堆栈指针RAM的地址是否在SRAM的范围之内。
2、在地址 appAddr + 4 中取出复位服务函数的地址,并转换为函数指针并执行该用户的中断向量表中的复位函数
3、由于单片机默认是在 0x0
查找的向量表,因此软件复位后需要重映射至App用户程序的中断向量表。VECT_TAB_OFFSET 0x4000
的偏移。向量表的起始地址是有要求的(即上面所说的偏移):必须先求出系统中共有多少个向量,再把这个数字向上“圆整”到 2 的整次幂,而起始地址必须对齐到后者的边界上。例如,如果一共有 32 个中断,则共有 32+16(系统异常) =48 个向量,向上圆整到 2 的整次幂后值为 64,因此向量表重定位的地址必须能被 64*4=256 整除,从而合法的起始地址可以是: 0x0, 0x100, 0x200 等。
不管是Bootloader(这里指用户自定义的Bootloader程序)跳到APP,还是APP跳到Bootloader,再跳转之前必须保证以下几点
1.向量表正确偏移。一般偏移0x4000,需要根据中断个数去计算。
向量表偏移函数
/* 法一: 在APP 程序的 main 函数开头写下*/
SCB->VTOR=FLASH_BASE|0x4000;//向量表偏移到0x08005000
/* 法二: 在APP 程序的 keil 工程C++引入宏定义,并且跳转至 工程文件夹中 CMSIS system_stm32f1xx.c */
USER_VECT_TAB_ADDRESS //添加宏定义
#define VECT_TAB_BASE_ADDRESS FLASH_BASE
#define VECT_TAB_OFFSET 0x0004000 //修改向量表偏移量
2.栈顶指针合法(即栈顶指针必须落在你的芯片的SRAM区域内,一般64K 0x20000000-0x20010000)
3.清除用到的所有中断标志位以及失能当前中断
特别是第3点,假设Bootloader里用到了定时器更新中断,但是从bootloader跳转到APP之前没有清除该中断标志位以及未失能当前中断,那么跳过去大概率卡死,除非你的APP里也用到了该定时器中断,反过来,从APP跳回bootloader也一样,必须清除所有用到的中断标志位和失能该中断方可跳转!!!
bool JumpApp(uint32_t appAddr) {
uint32_t jumpAddr;
JumpCallback cb;//无参数无返回值函数指针作为回调函数,目的就是程序跳转
//清除Bootlower 打开的SysTick定时器
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk; /* Disable SysTick Timer 很重要*/
HAL_TIM_Base_MspDeInit(&htim3); //关闭boot中没使用的tim 可能的中断
/*****************无用代码,只是帮助理解**********************
printf("appAddr:%x\r\n",appAddr);
printf("RAM end addr:%x, range:%x\r\n",RAM_END_ADDR,((RAM_END_ADDR-1)^0x0FFFFFFF ) );
//0x2FFE0000 是 128k 即 0x20000000 - 0x20020000
//0x2FFF0000 是 64k 即 0x20000000 - 0x20010000
**********************************************************/
//用户代码的堆栈地址的合法性判断
//ApplicationAddress是0x8004000是用户程序Flash的首地址,首地址存放用户代码的堆栈地址,堆栈地址指向RAM,RAM的起始地址是0x20000000
//所以(…… & 0x2FFE0000 ) )这个是判断用户代码的堆栈地址是否落在0x20000000~0x2001ffff之间。
if (((*(__IO uint32_t*)appAddr) & 0x2FFF0000 ) == 0x20000000) {
/* Jump to user application */
//查询中断向量表得知程序代码偏移4个字节是复位中断函数的地址,jumpAddr 得到复位地址
jumpAddr = *(__IO uint32_t*) (appAddr + 4);
printf("jumpAddr:%x\r\n",jumpAddr);
//把复位地址转换成函数指针
cb = (JumpCallback)jumpAddr;
// __set_PRIMASK(1);//关闭所有中断
//Set the Main Stack Pointer
//是把用户代码的栈顶地址设为栈顶指针
__set_MSP(*(__IO uint32_t*)appAddr);
cb();//执行复位中断程序
return true;
}
return false;
}
问题:插入__set_PRIMASK(1);//关闭所有中断
后会卡,删除就不会了
问题:SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk; /* Disable SysTick Timer */
解决进程序就卡的主要原因
需要分多个工程
1、Boot 工程,用来烧写Bootloader 程序用
2、App 工程,自己运行的程序
在App 工程中的修改中断向量偏移量
#define USER_VECT_TAB_ADDRESS/*自己添加,为了IAP下载*/
#if defined(USER_VECT_TAB_ADDRESS)
/*!< Uncomment the following line if you need to relocate your vector Table
in Sram else user remap will be done in Flash. */
#if defined(VECT_TAB_SRAM)
//为了IAP下载,原值#define VECT_TAB_OFFSET 0x00000000U ,现修改为0x00004000U
#define VECT_TAB_OFFSET 0x00004000U /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
3、Factory工程,存放出厂固件包,当App 工程失去作用时可做备用程序。
关于作者:赤诚Xie
版权声明:本博客所有文章仅用于学习、交流和研究目的,欢迎转载,但请注明原文作者及出处。
奥里给!:若您觉得文章对您有帮助,请点赞、关注支持我吧😊。
药药切克闹,👇👇👇下面三连来一套(●'◡'●)
——励志作一个用单片机梳头的乖宝宝