Fork me on GitHub

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。

STM32代码烧写到哪里去了?是ROM?还是RAM?还是flash?它们都是啥?代码具体占了多少空间?超没超芯片的范围?KEI里如何设置芯片flash、RAM可用大小呢?_stm32烧录程序是写到flash妈-CSDN博客

ARM内核地址与外设映射

在Cortex内核中,有4G虚拟空间地址,该地址分为8个块,每个块有512MByte的地址。这些地址映射至不同的外设,CPU可根据这些地址访问不同的外设。

image.png

值得注意的是,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 工程失去作用时可做备用程序。

posted @ 2024-06-25 18:41  赤诚Xie  阅读(15)  评论(0编辑  收藏  举报