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

【STM32】BootLoader和IAP详解

文章目录

    • 0 前言
    • 1 基本概念
    • 2 BootLoader
    • 3 主程序相关配置
    • 4 相关理论:芯片启动与中断响应
    • 5 特殊情况:Cortex-M0内核的芯片

0 前言

  最近在研究一个RT-Thread的项目,遇到很多之前没咋遇见过的STM32相关的知识,想着顺带也整体过一遍。其中有一个很关键的部分就是BootLoader的实现,发现自己之前一直没有亲自实践过,只停留在理论阶段,于是想着亲自撸一遍代码,增加印象。

1 基本概念

  所谓BootLoader,就是自举程序,所谓自举,即可以实现自己更新自己的程序。对于用户来说,最直观的感受就是这个可以实现远程代码更新,而不用返厂维修。所以这种功能在产品中还是非常常见且必要的。

  其实芯片本身就自带了一个bootloader,可以为用户提供串口下载的功能,但这个部分是出厂就定死的,用户无法得知具体的细节,也就无法使用,所以开发者就需要自己定制一个用来更新程序的程序。

  下面将分别介绍bootloader(后文也称iap程序)和用户程序APP(主程序)各自的结构和注意事项。

2 BootLoader

  BootLoader一般在主程序的低地址,上电复位时首先执行bootloader,判断是否有更新程序的输入,如果有,则接收更新的内容,再写入到主程序区域,最后再跳转到主程序执行;如果没有,则直接跳转到主程序进行执行(实际使用时可能会加上一定的等待延时)。

  整体的流程如下图所示。

在这里插入图片描述

从这个流程图可以看出,一个bootloader必须具备的几个部分:

  • flash读写
  • 接收更新数据的外设(如串口,CAN等),包括初始化和数据收发
  • 逻辑交互,判断是否需要更新程序以及程序接收和读写的逻辑

  此外,为了提高程序的可靠性,也可以额外开辟一块区域,用来存储接收到的主程序,“等待时机成熟”再更新。类似于先下载,再安装,不受时间限制。但这样就需要有额外的存储,可以是外置的flash模块,也可以是人为划分内部flash。

flash读写

  首先是flash读写,打开官方的库文件stm32f10x_flash.h,查看其提供的函数接口:

在这里插入图片描述

可以看到,这里官方只提供了擦除页(sector),擦除全部页,以及读写半字(16bit)和全字(32bit)这几个直接操作的函数接口。如果深入看这两个写flash函数的内部实现,可以发现其实就是通过指针的方式来写:

在这里插入图片描述

但是如果上网去找相关的教程,都会提到一点:STM32内部的Flash只能由1变成0,不能由0变成1,所以其实在调用这个函数前,必须先调用上面的擦除函数(如果写入的区域原先已经有数据的话)。

  那如果只是想改动某个字节怎么办呢?一般的做法是先把整个sector读出来存到数组里面,然后修改这个数组,再擦除这个sector,再把整个数组写入到原来的位置。 确实有些麻烦。那读取flash怎么做呢?官方也没有提供函数接口,其实非常简单,既然可以用指针的方式写入,那必然可以以指针的形式读出,所以读flash时,直接用指针读取即可。

外设的使用

  对于外设,可以是串口,也可以是CAN,SPI,IIC等,但建议要加上一个调试外设,在IAP程序初始化阶段,只初始化必要的外设,另外就是要防止外设影响IAP程序的跳转。 我遇到过的主要是两个方面:

  • 跳转之前关闭外设中断 : 防止跳转时还被中断,担心出问题;
  • 跳转前避免有数据传输:遇到过一个问题,就是IAP跳转到主程序时,第一个串口输出会出现乱码。感谢论坛这位大哥的提问,最后发现问题在于跳转前串口发送的数据过多,虽然代码执行完了,但硬件的发送还没结束,就影响了主程序中的串口发送,从而导致乱码。 所以解决办法也很简单,在串口发送和跳转程序之间加上一点延时即可。

逻辑交互

  试想一下,IAP程序必然会有这么一部分:接收外设的数据,然后写入到某个地方(可能是外部的存储,也可能就是内部的flash),那么就会存在一个问题:假如发送端发送数据太快,在进行写入操作时就有可能遗漏掉了一部分数据。所以这里可以采用中断+双缓冲的形式。当然,也可以着手解决“发送太快”的问题:自己写一个上位机,来控制发送速度,这当然没有问题,但相比前者可能只需要一个可以发送文件的串口调试助手来说,还是要麻烦很多。

3 主程序相关配置

  相比于普通的程序,如果要使用bootloader写入的程序,需要在原基础上做一些配置,就两个部分:

  • 修改VTOR寄存器
    system_stm32f10x.c文件中,前面百来行的地方,修改这个OFFSET即可:
    在这里插入图片描述
    如果不想改动官方文件,也可以在main函数第一行执行NVIC_SetVectorTable(FLASH_BASE, 0x2000);,第二个参数就是地址偏移量,根据需要进行设置。

  • 在魔法棒中修改起始地址(和size)
    在这里插入图片描述
    这一步必须设置,因为他会直接影响中断向量所在位置。【中断向量存储的位置由起始地址决定

配置好了上面两步,直接编译得到bin文件,这就是可以传给bootloader程序的数据文件了。

如何得到bin文件可以参考这篇文章

以上是标准库的处理方式,如果是HAL库呢?

  如果是CubeMX生成的仍然用Keil打开,配置方式基本一致,只是需要将这个注释打开而已:

在这里插入图片描述

  如果是CubeIDE,设置中断向量表偏移和上面的方式一致,但设置Flash起始地址有点麻烦,不仅要修改宏定义,还需要修改链接文件:

  • 宏定义
    在这里插入图片描述

    找不到这个文件的话,在任意文件敲上FLASH_BASE,然后按住Ctrl单击鼠标即可定位到该宏定义

  • 链接文件 xxxx.ld
    在这里插入图片描述

都改为加上偏移之后的地址。

4 相关理论:芯片启动与中断响应

  以上是操作部分,可能对于这些设置有疑问,需要理解一些理论。首先来复习一下芯片的启动过程。

在这里插入图片描述

在这里插入图片描述

首先芯片是从0x0000 0000开始运行的,然后根据BOOT引脚的状态来决定跳转的位置。芯片一上电,系统时钟起振,在系统时钟的第4个上升沿就锁定了BOOT引脚的状态,然后根据这个状态来决定跳转的地址。

在这里插入图片描述

  其实可以把0x0000 0000看作一个受BOOT引脚控制的指针变量,0x0是这个变量所在地址,这个变量本身存储的也是一个地址,要么是flash的首地址,要么是boot程序首地址,要么是ram的首地址。此即所谓的映射,即A地址存储的内容是B地址,表示A地址映射到B地址,访问A地址,也就等价于访问B地址。

如果是执行用户程序,即跳转flash首地址,之后的代码结构和执行逻辑是怎样的?其实这个可以从项目的启动文件得知。

在这里插入图片描述

在确定堆栈大小之后,紧接着就是设置向量表(Vector Table),设置在DATA段,且为只读格式。可以看到,第一个向量就是栈顶地址,即程序首先获取栈顶地址。这一步可以理解为初始化RAM,因为后续代码执行需要使用RAM。第二个向量是复位处理函数,相当于是整个程序的起点,在后面有关于这个“函数”的定义:

在这里插入图片描述

即使不懂汇编语法,也大概知道,这个函数首先执行一个SystemInit函数,然后跳转到main函数中执行。

所以虽然说main函数是整个程序的起点,但其实到main函数已经运行了一些代码。

而这个SystemInit函数实际上在system_stm32fxx.c文件中定义好了,可以通过跳转定义的方式定位到:

在这里插入图片描述

可以看出这个函数主要是设置系统时钟以及中断向量表偏移(后面会提)。

这就是程序的运行过程,那中断是怎么触发的呢?

  以上向量表除了第一个是存储栈顶地址外,其他的都是中断向量(复位本质上也是一种中断),每个向量占4字节,存储的是一个32位的地址,可以理解为是对应的中断函数的地址,如果没有定义中断服务函数,那么里面存储的就是一个默认值(可以理解为防止程序错误的一个预防措施,总之不执行程序,不在讨论范围内)。

  当程序发生中断时,程序其实是首先是跳转到0x0000 0000这个地址,但是这个地方映射到了flash首地址,即0x0800 0000,所以实际上相当于在0x0800 0000这个地址查找中断向量(可能是采用固定地址偏移的方式来查找,比如某个中断和FLASH首地址的地址偏移是0xxx),找到之后,再根据中断向量存储的值,跳转到中断服务程序中执行。

在这里插入图片描述

  所以,总结来说,中断向量表的存储位置是在0x0800 0000,但是是从0x0000 0000找过去的。

那在IAP这种情况下,该怎么使用呢?

  需要明确的是,当程序改动FLASH起始位置之后(比如在Keil魔法棒中的配置),向量表(Vector Table)也会整体平移,也就是说,原来的向量表:栈顶指针,复位向量,,,,是从0x0800 0000这个地址开始的,现在变成从0x0800 3000开始(假定偏移量是0x3000),其存放的内容还是中断服务函数的地址。但中断的机制仍然是不变的,即当中断触发时,程序仍然是从0x0000 0000->0x0800 0000查找中断向量,但是此时这些中断向量存放的并不是主程序中断服务函数的地址,而是IAP程序中的中断服务函数的地址,因此,这会导致混乱。

  那怎么办呢?

  非常简单,就是在程序去找中断向量的时候,告诉它要偏移对应地址查找中断向量——不再是跳转到0x0800 0000,而是0x0800 3000了,而这就是通过设置VTOR寄存器来实现的,也即上面展示的各种设置方法。

5 特殊情况:Cortex-M0内核的芯片

  因为Cortex-M0内核的芯片,如STM32F0系列,是不带这个VTOR寄存器的,也就是说它不能设置这个地址偏移,那就会出现上述提到的用户程序发生中断结果调用的是IAP程序的中断服务程序 的问题。于是就需要借助RAM,可以先把向量表复制到RAM,然后调整启动方式——原来是从Flash启动的,

在这里插入图片描述

IAP跳转的时候,首先执行用户程序的Reset_Handler,在Flash上,然后进入到main函数,在这里首先是将Flash首地址附近的中断向量表复制到RAM上,然后改变启动配置,那么这当发生中断的时候,首先还是从0x0000 0000 开始查找向量,但由于启动配置的改变,此时是从0x0000 0000->0x2000 0000,然后找到对应的向量表,进而执行中断服务程序。

// 函数模板
memcpy((void*)0x20000000, (void*)0x08004000, VECTOR_SIZE);
SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_SRAM);

// HAL库使用案例
memcpy((void*)0x20000000, (void*)FLASH_MAIN_FW_START_ADDR, 0xB4); 
__HAL_SYSCFG_REMAPMEMORY_SRAM();

以上这个设置SYSCFG的函数好像只有F0系列芯片有。


http://www.kler.cn/a/547083.html

相关文章:

  • linux 下连接mysql(下)
  • openGauss 3.0 数据库在线实训课程13: 学习逻辑结构:表管理1
  • 【前端框架】Vue3 中 `setup` 函数的作用和使用方式
  • npm install 失败
  • 深入理解 Qt 信号与槽机制:原理、用法与优势
  • 21vue3实战-----git husky和git commit规范
  • Baklib知识中台构建企业智能运营核心架构
  • 《Python与C/C++:运行时与编译时的量子纠缠》
  • 《Python 中 JSON 的魔法秘籍:从入门到精通的进阶指南》
  • 智能体或是GPT、Deepseek等大模型的发展方向
  • 深度学习与增强现实的完美邂逅:开启未来智能交互的新篇章
  • unity删除了安卓打包平台,unityhub 还显示已经安装,怎么解决
  • lightning.pytorch.callbacks内置的Callbacks介绍
  • LeapMotion第2代 Unity示范代码(桌面开发)
  • DeepSeekApi对接流式输出异步聊天功能:基于Spring Boot和OkHttp的SSE应用实现
  • 1.5 企业级AI大模型四阶技术全景解析:从Prompt到Pre-training的进化路径
  • 23. AI-大语言模型
  • 【AI系列】从零开始学习大模型GPT (2)- Build a Large Language Model (From Scratch)
  • Springboot整合ES
  • Linux系统使用ollama本地安装部署DeepSeekR1 + open-webui