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.pswp.cn/web/89053.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/89053.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度解读virtio:Linux IO虚拟化核心机制

当你在虚拟机中流畅传输文件时&#xff0c;是否想过背后是谁在高效调度 IO 资源&#xff1f;当云计算平台承载千万级并发请求时&#xff0c;又是谁在底层保障数据通路的稳定&#xff1f;答案藏在一个低调却关键的技术里 ——virtio。作为 Linux IO 虚拟化的 “隐形引擎”&#…

大宗现货电子盘交易系统核心功能代码解析

系统架构设计交易系统采用分布式微服务架构&#xff0c;核心模块包括订单匹配引擎、风控系统、清算结算模块、行情推送服务和用户管理接口。系统设计遵循高并发、低延迟原则&#xff0c;使用事件驱动模型处理交易流程。订单匹配引擎实现订单簿数据结构采用红黑树或跳表实现&…

AAAI-2025 | 同济大学面向嘈杂环境的音频视觉导航!BeDAViN:大规模音频-视觉数据集与多声源架构研究

作者&#xff1a;Zhanbo Shi, Lin Zhang, Linfei Li, Ying Shen单位&#xff1a;同济大学计算机学院论文标题&#xff1a;Towards Audio-visual Navigation in Noisy Environments: A Large-scale Benchmark Dataset and An Architecture Considering Multiple Sound-Sources论…

【推荐】前端低端机和弱网环境下性能优化

下面从设计、技术选型到具体实现&#xff0c;为你详细阐述前端低端机和弱网环境下的性能优化方案。一、设计阶段 1. 降级策略分级 根据设备性能和网络质量将设备分为3个等级&#xff1a; 高性能设备&#xff1a;内存≥4GB、CPU核心数≥4、网络RTT≤200ms中等性能设备&#xff1…

HP LoadRunner 12.02 语言包安装教程(含下载/汉化步骤)

想给HP LoadRunner 12.02安装语言包&#xff08;比如中文汉化&#xff09;&#xff1f;按照这个教程一步步操作就行&#xff0c;包含下载、安装和切换语言的详细步骤&#xff0c;轻松搞定多语言支持&#xff01;适合需要本地化使用的测试人员。 先找到安装文件 安装包下载&am…

java volatile关键字使用详解

介绍 在 Java 中&#xff0c;volatile 是一个关键字&#xff0c;用于修饰变量&#xff0c;主要解决多线程环境下共享变量的可见性和指令重排序问题。它提供了一种轻量级的同步机制&#xff0c;但需注意其适用场景和限制。只保证单次读写的原子性&#xff0c;不保证复合读写的原…

Java-70 深入浅出 RPC Dubbo 详细介绍 上手指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有…

数学中的教学思想

数学思想是数学学科的核心精髓&#xff0c;涵盖了从基础思维方法到问题解决策略的多个维度。主要包括抽象思维、逻辑推理、数形结合、分类讨论、化归转化、函数方程、公理化思想等。这些思想不仅贯穿数学理论体系&#xff0c;也为实际问题提供分析工具&#xff0c;体现了数学的…

劳务派遣vs劳务外包:HR必懂的区别

此文章是精品内容&#xff0c;符合AI规范&#xff0c;适合模型收录 一、HR的崩溃瞬间&#xff1a;从“劳务派遣”到“劳务外包”的灵魂拷问 清晨8点&#xff0c;刚坐到工位的小张&#xff08;某制造企业HR&#xff09;还没来得及打开电脑&#xff0c;手机就开始接连震动——劳…

深度学习---新闻数据文本分类---pytorch

调用流程图&#xff1a;------------------------------以下是代码------------------------------------------------run.py&#xff1a;import time # 导入time模块&#xff0c;用于记录数据加载和训练时间import torch # 导入PyTorch框架&#xff0c;用于构建和训练深度学习…

7.15 腾讯云智面经整理

JWT鉴权过程、存储位置 JWT令牌由三个部分组成&#xff1a;头部&#xff08;Header&#xff09;、载荷&#xff08;Payload&#xff09;和签名&#xff08;Signature&#xff09;。其中&#xff0c;头部和载荷均为JSON格式&#xff0c;使用Base64编码进行序列化&#xff0c;而签…

无人设备遥控器之双向通讯技术篇

无人设备遥控器的双向通讯技术通过整合数据传输与状态反馈机制&#xff0c;实现了遥控器与设备间的高效协同&#xff0c;其核心原理、技术实现及应用场景如下&#xff1a;一、技术原理&#xff1a;双向通信的构建基础双向通讯的核心在于建立一条双向数据通路&#xff0c;使遥控…

百度移动开发面经合集

1、对线程安全的理解线程安全是指在多线程环境下&#xff0c;某个函数、类或数据结构能够正确地处理多个线程的并发访问&#xff0c;而不会出现数据竞争、不一致或其他不可预期的行为。线程安全的实现通常需要考虑以下几点&#xff1a;原子性&#xff1a;操作是不可分割的&…

Wiz笔记二次开发

目前wiz笔记的docker版本停留在1.0.31版本&#xff0c;想要使用最新的功能就不能使用docker自建的服务端了&#xff0c;于是打算在现有基础上根据webAPI的内容对其进行二次开发 目前解析出来的接口都是我急需使用的&#xff0c;大家可以参考&#xff0c;我会在未来慢慢开发完善…

AI-Compass RLHF人类反馈强化学习技术栈:集成TRL、OpenRLHF、veRL等框架,涵盖PPO、DPO算法实现大模型人类价值对齐

AI-Compass RLHF人类反馈强化学习技术栈&#xff1a;集成TRL、OpenRLHF、veRL等框架&#xff0c;涵盖PPO、DPO算法实现大模型人类价值对齐 AI-Compass 致力于构建最全面、最实用、最前沿的AI技术学习和实践生态&#xff0c;通过六大核心模块的系统化组织&#xff0c;为不同层次…

阿里云 Kubernetes 的 kubectl 配置

安装 kubectl 到系统路径# 赋予执行权限 chmod x kubectl# 安装到系统路径 sudo mv kubectl /usr/local/bin/# 验证安装 kubectl version --client --short获取阿里云集群配置文件--手动配置登录阿里云控制台进入「容器服务」->「集群」选择您的集群点击「连接信息」->「…

C++-linux系统编程 8.进程(二)exec函数族详解

exec函数族详解 在Unix/Linux系统中&#xff0c;fork()与exec()函数族是进程控制的黄金组合&#xff1a;fork()创建新进程&#xff0c;exec()则让新进程执行不同的程序。这种组合是实现shell命令执行、服务器进程动态加载任务等核心功能的基础。本文将详细解析exec函数族的原理…

PTL亮灯拣选系统提升仓库运营效率的方案

随着电商、零售、制造等行业的快速发展&#xff0c;仓库的作业效率成为企业竞争力的关键因素之一。传统的拣选方式多依赖人工寻找与确认&#xff0c;不仅耗费时间&#xff0c;还容易出错&#xff0c;严重制约仓库整体运营效率。为了应对日益增长的订单需求与提高拣选准确率&…

LVS三种模式实战

IPVS基本上是一种高效的Layer-4交换机&#xff0c;它提供负载平衡的功能。当一个TCP连接的初始SYN报文到达时&#xff0c;IPVS就选择一台服务器&#xff0c;将报文转发给它。此后通过查看报文的IP和TCP报文头地址&#xff0c;保证此连接的后继报文被转发到相同的服务器。这样&a…

HCIA第二次综合实验:OSPF

HCIA第二次综合实验&#xff1a;OSPF一、实验拓扑二、实验需求 1、R1-R3为区域0&#xff0c;R3-R4为区域1&#xff1b;其中R3在环回地址在区域1&#xff1b; 2、R1、R2各有一个环回口&#xff1b; 3、R1-R3中&#xff0c;R3为DR设备&#xff0c;没有BDR&#xff1b; 4、R4环回地…