二、Bootloader加载启动App代码讲解
代码详细解析:
typedef void (*pFunction)(void);
static void DrvInit(void)
{
RS485DrvInit();
DelayInit();
SystickInit();
}
#define RAM_START_ADDRESS 0x20000000
#define RAM_SIZE 0x10000
static void BootToApp(void)
{
uint32_t stackTopAddr = *(volatile uint32_t*)APP_ADDR_IN_FLASH;
if (stackTopAddr > RAM_START_ADDRESS && stackTopAddr < (RAM_START_ADDRESS + RAM_SIZE)) //判断栈顶地址是否在合法范围内
{
__disable_irq();
__set_MSP(stackTopAddr);
uint32_t resetHandlerAddr = *(volatile uint32_t*) (APP_ADDR_IN_FLASH + 4);
/* Jump to user application */
pFunction Jump_To_Application = (pFunction) resetHandlerAddr; // int *p = (int *)0x8003145
/* Initialize user application's Stack Pointer */
Jump_To_Application();
}
NVIC_SystemReset();
}
其中这里面设计到了指针以及函数指针需要详细理解一下,不然这一段是看不懂的。
首先就是一个函数指针:typedef void (*pFunction)(void);
先自己分析一下:
volatile uint32_t*
这里表示APP_ADDR_IN_FLASH
是一个int类型指针变量,但是前面又一个*
是什么意思,是指针的解引用吗 去除这个地址里面存储的内容,然后将存储的地址在赋值给stackTopAddr ,volatile uint32_t*
这个地方最核心的意思是告诉编译器,APP_ADDR_IN_FLASH
这只是首地址,还需要往后在找三个地址,因为这个表示的是4个字节32位。
执行步骤:
强制类型转换:
(volatile uint32_t*)APP_ADDR_IN_FLASH
→ 将常量地址APP_ADDR_IN_FLASH
转换为 指向volatile uint32_t
的指针。**
volatile
**:告知编译器该地址内容可能被外部因素(硬件、中断等)异步修改,禁用优化(如缓存值到寄存器)。**
uint32_t*
:指针类型为 32位无符号整数指针**,表示访问从该地址开始的 连续4字节内存(因uint32_t
占4字节)。
解引用操作:
*(...)
→ 读取指针指向的 4字节数据(即APP_ADDR_IN_FLASH
地址处的实际值),而非地址本身。赋值:
将读取到的32位值存入变量stackTopAddr
。
volatile
的核心作用:强制直接内存访问。
volatile
的本质是禁用编译器优化,确保每次对变量的读写都直接操作内存,而非使用寄存器缓存副本。
- 编译器优化问题:
编译器默认会优化代码,比如将频繁访问的变量缓存在寄存器中(减少内存读写开销)。
但对硬件寄存器、中断共享变量等,这种优化会导致程序无法感知外部实时变化。 -
volatile
的解决方案**:
通过声明volatile
,强制编译器每次访问变量时都从内存地址重新加载值(读)或立即写入内存(写)
寄存器缓存原理
编译器(如GCC/Clang的-O2
/-O3
级别优化)会将频繁访问的变量缓存在CPU寄存器中,减少内存访问次数。此时,代码实际操作的是寄存器的副本而非内存中的原始变量。
优化失效场景
当变量被外部异步修改时(如中断、多线程、硬件寄存器),寄存器的副本不会同步更新,导致程序逻辑错误。
场景 | 后果 | 案例 |
---|---|---|
硬件中断修改变量 | 主循环无法感知新值,死循环卡死 | OV5640摄像头读取卡死在while(a==0) |
多线程共享变量 | 线程间数据不一致,逻辑错误 | 线程A缓存旧值,线程B更新无效 |
内存映射硬件操作 | 读取过时硬件状态,驱动失效 | 传感器数据寄存器读取延迟 |
因此需要强制内存访问
volatile
关键字
声明变量为volatile
,强制每次访问都从内存读取/写入
作用原理:禁用寄存器缓存,生成直接访问内存的指令
适用场景:中断标志位、多线程共享变量、硬件寄存器映射变量
因此在之前开发过程中遇到的按键板和显示板UART通信的时候,需要加上获取按键值的变量加上关键字volatile
,这是因为这个变量是在中断中直接解析的,导致频繁的访问,编译器就会针对这个地方进行一次优化,就是上面说的“代码实际操作的是寄存器的副本而非内存中的原始变量”,因此为了保证每次都是最新的按键值,所以我们要强制内存访问。
继续解析代码:
uint32_t stackTopAddr = *(volatile uint32_t*)APP_ADDR_IN_FLASH;
APP_ADDR_IN_FLASH
**:APP 的起始地址(即向量表基地址)
这个地址是我们自己设定的,正常来说是从APP_ADDR_IN_FLASH 0x08000000,但是由于前面的空间我们预留给了BOOT,并且还是给了BOOT12KB的空间,因此APP的开始地址就是APP_ADDR_IN_FLASH 0x8003000。
在正常启动的时候我们首先就是获取栈顶地址,因此就是上面这段代码,读取出栈顶地址,这是ARM内核启动的流程,必须也只能按照这样执行。
得到的栈顶地址,什么是栈顶地址,就是这个工程有效使用栈空间最顶的地址,栈空间是什么空间?是运行我们函数的空间,存储全局变量什么的,掉电易失。SRAM。
栈空间(Stack)是程序运行时的重要内存区域,主要用于存储与函数调用、中断处理等相关的临时数据。
- 函数调用上下文
- 返回地址(LR):子函数执行完毕后需返回父函数的位置,由链接寄存器(LR,X30)保存,调用子函数前会压入栈中。
- 帧指针(FP):指向当前函数栈帧的底部(高地址),用于界定函数栈边界,通常由X29寄存器保存,入栈后形成函数调用链。
- 调用者寄存器:部分需跨函数保留的寄存器(如ARMv7的R4-R11,ARM64的X19-X28)会在子函数中压栈保护,防止被覆盖。
- 局部变量
- 函数内部定义的非静态局部变量(如
int a;
)存储在栈帧中,生命周期仅限于函数执行期间,函数返回后自动释放。 - 示例:函数内数组、结构体等临时变量。
- 函数参数
- 超出寄存器容量的参数:ARM调用约定中,前几个参数通过寄存器传递(如ARM64的X0-X7),超出部分会压入调用者的栈空间。
- 可变参数函数:如
printf()
的多余参数需通过栈传递。
- 中断/异常上下文
- 发生中断或异常时,CPU自动将关键寄存器(PC、LR、CPSR等) 压入当前模式栈(如IRQ模式栈),用于恢复现场。
- 中断服务程序(ISR)中的局部变量也占用栈空间。
- 临时数据与中间结果
- 编译器生成的临时计算结果(如复杂表达式中间值)。
- 寄存器溢出:当寄存器不足时,部分中间变量暂存到栈中
ARM栈空间的核心作用是支撑函数调用链与临时数据存储,具体包括:函数返回地址(LR)、帧指针(FP)、局部变量、多余参数、中断上下文及编译器临时数据。其设计遵循架构规范(如AAPCS),通过栈指针(SP)和帧指针(FP)协同管理栈帧边界。开发中需警惕栈溢出风险,尤其在资源受限的嵌入式系统中。
栈顶地址(Stack Top Address)是计算机科学中栈(Stack)这一数据结构的关键概念,指栈中最后一个被插入元素的内存地址。栈是一种后进先出(LIFO)的线性表,所有操作(插入/删除)仅在栈顶进行。
- 操作唯一性:所有入栈(
PUSH
)和出栈(POP
)操作均通过修改栈顶地址完成:- 入栈:栈顶地址向低地址移动 → 新元素存入新地址。
- 出栈:栈顶地址向高地址移动 → 释放当前元素。
- 核心功能:
- 存储函数调用的返回地址、参数、局部变量;
- 实现递归和中断处理时的上下文保护。
接着就是验证 APP 栈顶指针合法性。
然后就是关闭中断,防止跳转过程被干扰
__set_MSP(stackTopAddr);
重设主栈指针(MSP)
**作用:将 APP 的初始栈顶地址保存到CPU的 SP 寄存器里面,确保 APP 从正确的栈空间启动。
获取复位处理函数地址
这一步也是CPU的基操,启动的第二个流程,第一是获取栈顶地址,第二就是复位函数地址,
uint32_t resetHandlerAddr = *(volatile uint32_t*) (APP_ADDR_IN_FLASH + 4);
同样的思路,我们利用这一行代码获取到复位函数的入口地址了。
但是!!!! 警惕 警惕
现在我们只是自己知道resetHandlerAddr
这个里面存储是复位函数的地址,但是编译器不知道,现在这个里面只是存储的复位函数入口地址。
并且没有初始化原因是:
值由硬件预设,非软件生成
- Cortex-M 架构规定,应用程序的复位函数地址 由编译器链接时确定,并固定在 Flash 的向量表偏移 4 字节处(即
APP_ADDR_IN_FLASH + 4
)。 - 该地址是只读的硬件预设值,非运行时动态生成,因此无需软件初始化。
我们需要做的就是让编译器知道这个入口地址是函数的地址,
而复位函数的类型是 void ()(void);
因此我们前面声明的函数指针就用上了。
typedef void (*pFunction)(void);
首先:
pFunction Jump_To_Application
表示的是我们定义一个函数,这个函数类型是pFunction
,也就是void ()(void);
符合复位函数的类型,那么我们后续我们就可以直接使用Jump_To_Application()
,
为什么能直接这样使用Jump_To_Application()
?
是不是我可以理解成这样就是对函数指针的解引用,区别于变量指针的解引用。
函数指针的调用 Jump_To_Application()
是 C 语言标准允许的语法糖(Syntactic Sugar)。它等价于显式解引用形式 (*Jump_To_Application)()
,但更简洁直观。
- 函数指针类型
pFunction
必须与Reset_Handler
的签名完全匹配(如void (*)(void)
),否则会因参数传递或栈布局错误导致硬件异常。 - 错误示例:若
Reset_Handler
实际需要参数,但pFunction
定义为无参数类型,调用时将破坏栈平衡。
但是现在还缺少一个地址,这个地址就是入口地址,前面我们获取了入口地址是resetHandlerAddr。
但是我们还是需要将这个地址给强制转换成函数类型也就是
(pFunction) resetHandlerAddr;
这样做的目的是 原本我们只是知道这是一个0x8003145
,并不能说明这是一个地址,因此我们需要将他变成一个地址,但是指针又需要类型,而我们这个指针就是函数类型的,毕竟是函数入口地址,不是函数类型的指针是什么指针? 因此就是一个强转。
最后综合起来就是这行代码
pFunction Jump_To_Application = (pFunction) resetHandlerAddr; // int *p = (int *)0x8003145
/* Initialize user application's Stack Pointer */
Jump_To_Application();
至此已经跳转到APP。
文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。
【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。