当前位置: 首页 > news >正文

ARM单片机OTA解析(二)

文章目录

  • 二、Bootloader加载启动App代码讲解


二、Bootloader加载启动App代码讲解

代码详细解析:


typedef void (*pFunction)(void);static void DrvInit(void)
{RS485DrvInit();DelayInit();SystickInit();
}#define RAM_START_ADDRESS    0x20000000
#define RAM_SIZE             0x10000static 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();
}

其中这里面设计到了指针以及函数指针需要详细理解一下,不然这一段是看不懂的。

深入理解C语言内存空间、函数指针(三)(重点是函数指针)

首先就是一个函数指针:typedef void (*pFunction)(void);

先自己分析一下:

volatile uint32_t*这里表示APP_ADDR_IN_FLASH是一个int类型指针变量,但是前面又一个*是什么意思,是指针的解引用吗 去除这个地址里面存储的内容,然后将存储的地址在赋值给stackTopAddr ,volatile uint32_t*这个地方最核心的意思是告诉编译器,APP_ADDR_IN_FLASH这只是首地址,还需要往后在找三个地址,因为这个表示的是4个字节32位。

执行步骤​:

  1. 强制类型转换​:
    (volatile uint32_t*)APP_ADDR_IN_FLASH → 将常量地址 APP_ADDR_IN_FLASH 转换为 ​指向 volatile uint32_t 的指针

    • ​**volatile**​:告知编译器该地址内容可能被外部因素(硬件、中断等)异步修改,禁用优化(如缓存值到寄存器)。

    • ​**uint32_t*​:指针类型为 ​32位无符号整数指针**,表示访问从该地址开始的 ​连续4字节内存​(因 uint32_t 占4字节)。

  2. 解引用操作​:
    *(...) → 读取指针指向的 ​4字节数据​(即 APP_ADDR_IN_FLASH 地址处的实际值),而非地址本身。

  3. 赋值​:
    将读取到的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)是程序运行时的重要内存区域,主要用于存储与函数调用、中断处理等相关的临时数据。

  1. 函数调用上下文
  • 返回地址(LR)​​:子函数执行完毕后需返回父函数的位置,由链接寄存器(LR,X30)保存,调用子函数前会压入栈中。
  • 帧指针(FP)​​:指向当前函数栈帧的底部(高地址),用于界定函数栈边界,通常由X29寄存器保存,入栈后形成函数调用链。
  • 调用者寄存器​:部分需跨函数保留的寄存器(如ARMv7的R4-R11,ARM64的X19-X28)会在子函数中压栈保护,防止被覆盖。
  1. 局部变量
  • 函数内部定义的非静态局部变量​(如int a;)存储在栈帧中,生命周期仅限于函数执行期间,函数返回后自动释放。
  • 示例:函数内数组、结构体等临时变量。
  1. 函数参数
  • 超出寄存器容量的参数​:ARM调用约定中,前几个参数通过寄存器传递(如ARM64的X0-X7),超出部分会压入调用者的栈空间。
  • 可变参数函数​:如printf()的多余参数需通过栈传递。
  1. 中断/异常上下文
  • 发生中断或异常时,CPU自动将关键寄存器(PC、LR、CPSR等)​​ 压入当前模式栈(如IRQ模式栈),用于恢复现场。
  • 中断服务程序(ISR)中的局部变量也占用栈空间。
  1. 临时数据与中间结果
  • 编译器生成的临时计算结果​(如复杂表达式中间值)。
  • 寄存器溢出​:当寄存器不足时,部分中间变量暂存到栈中

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 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。

http://www.dtcms.com/a/274617.html

相关文章:

  • cesium添加原生MVT矢量瓦片方案
  • 在 Spring Boot 中使用 WebMvcConfigurer
  • 【SpringBoot】配置文件学习
  • linux kernel struct regmap_config结构详解
  • 力扣242.有效的字母异位词
  • MySQL5.7版本出现同步或插入中文出现乱码或???显示问题处理
  • vector之动态二维数组的底层
  • django queryset 去重
  • JavaSE -- StreamAPI 详细介绍(上篇)
  • Java开发新宠!飞算JavaAI深度体验评测
  • 获取华为开源3D引擎 (OpenHarmony),把引擎嵌入VUE中
  • string模拟实现
  • 信号肽预测工具PrediSi本地化
  • 《打破预设的编码逻辑:Ruby元编程的动态方法艺术》
  • 内存踩踏全解析:原理 + 实战案例 + 项目排查技巧
  • 2025十大免费销售管理软件推荐
  • 基于物联网的智能体重秤设计与实现
  • 测试第一定律
  • 如何通过公网IP访问部署在kubernetes中的服务?
  • AVL平衡二叉树
  • 为什么必须掌握Java异常处理机制?——从代码健壮性到面试必考题全解析
  • 阿里云服务器,CentOS7.9上安装YApi 接口管理平台
  • Linux修炼:权限
  • vue2往vue3升级需要注意的点(个人建议非必要别直接升级)
  • 基于规则匹配的文档标题召回
  • Leaflet面试题及答案(21-40)
  • PHT-CAD 笔记
  • 【每日算法】专题八_分治_归并排序
  • k8s新增jupyter服务
  • 7.11 dp 图