【STM32】BootLoader和IAP详解

文章目录

    • [0 前言](#0 前言)
    • [1 基本概念](#1 基本概念)
    • [2 BootLoader](#2 BootLoader)
    • [3 主程序相关配置](#3 主程序相关配置)
    • [4 相关理论:芯片启动与中断响应](#4 相关理论:芯片启动与中断响应)
    • [5 特殊情况:Cortex-M0内核的芯片](#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,然后找到对应的向量表,进而执行中断服务程序。

c 复制代码
// 函数模板
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系列芯片有。

相关推荐
智商偏低40 分钟前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen2 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
白鱼不小白4 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D4 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术7 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt8 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘8 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang8 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c
几个几个n11 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件
Despacito0o14 小时前
ESP32-s3摄像头驱动开发实战:从零搭建实时图像显示系统
人工智能·驱动开发·嵌入式硬件·音视频·嵌入式实时数据库