跳到主要内容位置

IAP_Bootloader固件升级实现方法

1. 升级方案#

1.1 基本概念#

IAP“In Application Programming”的简写,就是用户程序运行时对 Flash 的某些区域进行烧写,可以写入新版本的软件、用户数据等。

IAP 主要包括 BootLoader应用程序两部分:在升级时运行的是 Bootloader,它接收新版本的应用程序,烧写在 Flash 上。

OTAOver-the-Air 的简写,即空中下载技术,通过移动通信网络(2G/3G/4G 或 Wifi)对设备终端上固件、数据及应用进行远程管理的技术。简单来说 OTA 技术实现分三步:首先将更新软件上传到 OTA 中心,然后 OTA 中心无线传输更新软件到设备端,最后设备端自动更新软件。

因此,OTA 技术的基础是 IAP 技术,需要基于 IAP 实现程序的自动下载。

OTA 流程如下:

Bootloader完整功能包括:

  • 初始化硬件:比如设置时钟、初始化内存
  • 启动内核:从 Flash 读出内核、存入内存、给内核设置参数、启动内核
  • 调试作用:在开发产品时需要经常调试内核,使用 BootLoader 可以方便地更新内核

在单片机中,我们可以简单一点,因为不涉及内核,我只是借助它实现 IAP 功能。

1.2 Flash 规划#

STM32H563RIV 内置 2MB Flash,划分如下:

  • Bootloader 占据 256KB 空间
  • APP 占据 1784KB 空间
  • 配置信息占据最后一个扇区 8KB 空间:用来保存 APP 版本、大小、校验码等信息。

1.3 Bootloader 程序流程#

2. Bootloader 实现启动功能(跳转)#

2.1 必备知识#

  • 地址 0 就是默认的异常向量表基地址,使用 Flash 启动时 0 地址被映射到 Flash 基地址 0x08000000。

  • CPU 读取异常向量表第 1 个 word(4 字节),写入 SP 寄存器

  • CPU 读取异常向量表第 2 个 word(4 字节),跳转执行:这就是 CPU 运行的第 1 个指令

发生各类异常、中断时,硬件会从异常向量表中,根据异常号、中断号找到一项,这项里保存的是“处理函数的地址”

能正确使用中断的前提是:

  • 把异常向量表的基地址告诉 CPU:这可以设置 SCB 里的 VTOR 寄存器(寄存器地址为0xE000ED08
  • 在异常向量表里,填充中断处理函数

2.2 APP#

在 APP 中我们需要设置 ROM 的地址为 0x08040000,因为在 1.2 中我们把这里起始的 1784KB 分给 APP 了。

同时,需要注意,后面 system 函数中又重置了 VTOR 寄存器

2.3 Bootloader(jump.S)#

为了保证中断和异常的使用,先设置 VTOR 寄存器为新的异常向量表,如果不设置的话,默认就会使用 Bootloader 的异常向量表。

  • 设置 VTOR 寄存器为新的异常向量表

Bootloader 要启动 APP,需要模仿硬件上电后做的事情:

  • 读取异常向量表的第 1 个 word,设置进 SP 寄存器

  • 读取异常向量表的第 2 个 word,跳转执行

编写jump.S

AREA |.text|, CODE, READONLY
; Reset Handler
start_app PROC
EXPORT start_app
; SET vector, r0(0x08040000)==>VTOR(0xE000ED08)
LDR R1, =0xE000ED08
STR R0, [R1]
; READ val of address(0x08040000), set to SP
LDR R1, [R0]
MOV SP, R1
; READ val of address(0x08040004), jump
LDR R1, [R0, #4]
BX R1
ENDP
END

3. Bootloader 实现下载功能#

3.1 下载协议#

下载协议可以自己定义,根据使用流程定义如下:

  • Bootloader 发出获取固件信息的请求:发出“1”字符给上位机

  • 上位机发送固件信息:先发出 5 个“0x5a”数据给下位机,用于同步,再发送固件信息。

固件信息如下定义:

struct FirmwareInfo {
uint32_t version;
uint32_t file_len;
uint32_t load_addr;
uint32_t crc32;
uint8_t file_name[16];
};

注意:为了方便在串口里操作,上位机发送 uint32_t 的整数时,先发送高字节(大字节序)。

  • Bootloader 发出获取固件的请求:发出“2”字符给上位机
  • 上位机发送 bin 文件
  • Bootloader 在烧写过程中,可以发送进度:“$1%”“$2%”“$100%“。以字符“$”开头、字符“%”结束。

Bootloader 基本流程在 1.3 中已经展现。这里展示细节。

3.2 GetLocalFirmwareInfo#

#define CFG_OFFSET 0x081FE000
static int GetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
if (ptFlashInfo->file_len == 0xFFFFFFFF)
return -1;
*ptFirmwareInfo = *ptFlashInfo;
return 0;
}

获取本地固件信息,只需要到最后一个扇区读取就可以,即地址 CFG_OFFSET (0x081FE000)处。

3.3 GetServerFirmwareInfo#

static int GetServerFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
uint8_t data = '1';
uint8_t buf[sizeof(FirmwareInfo)];
/* send '1' cmd to PC */
if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
return -1;
/* wait for response */
while (1)
{
if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &data, UPDATE_TIMEOUT*10))
return -1;
if (data != 0x5a)
{
buf[0] = data;
break;
}
}
/* get firmware info */
for (int i = 1; i < sizeof(FirmwareInfo); i++)
{
if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT))
return -1;
}
ptFirmwareInfo->version = BE32toLE32(&buf[0]);
ptFirmwareInfo->file_len = BE32toLE32(&buf[4]);
ptFirmwareInfo->load_addr = BE32toLE32(&buf[8]);
ptFirmwareInfo->crc32 = BE32toLE32(&buf[12]);
strncpy((char *)ptFirmwareInfo->file_name, (char *)&buf[16], 16);
return 0;
}

获取上位机(服务器)的固件信息:先发送‘1’字符给上位机,然后等待上位机发送固件信息的头部标志0x5a,最后读取sizeof(FirmwareInfo)个字节的数据,完成后存入ptFirmwareInfo返回。(注意字节流是大端的,需要转换为小端)。

3.4 GetServerFirmware#

如果需要更新固件,就需要获得固件文件:

static int GetServerFirmware(uint8_t *buf, uint32_t len)
{
uint8_t data = '2';
/* send 0x02 cmd to PC */
if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
return -1;
/* get firmware info */
for (int i = 0; i < len; i++)
{
if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &buf[i], UPDATE_TIMEOUT*10))
return -1;
}
return 0;
}

先发送’2‘字符过去,循环读取 len 个字节就可以了。

4. Bootloader 实现烧录功能#

获得完服务器的固件,进行 CRC 校验成功后,就要进行烧录了。首先就需要了解 STM32H5 的 Flash 分区。

4.1 Flash 分区#

内部 Flash 有 2M 字节的空间,分为两个 Bank,每个 Bank 有 128 个分区(sector),每个分区为 8KB。(8KB 128 2 = 2MB)

4.2 烧录#

对于烧录,要先擦除(使用“HAL_FLASHEx_Erase”函数),再烧写(使用“HAL_FLASH_Program”函数)。

基本原理就是需要分 Bank 分 Sector 烧录。

烧录固件 APP:

#define SECTOR_SIZE (8*1024)
static int WriteFirmware(uint8_t *firmware_buf, uint32_t len, uint32_t flash_addr)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t sectors = (len + (SECTOR_SIZE - 1)) / SECTOR_SIZE;
uint32_t flash_offset = flash_addr - 0x08000000;
uint32_t bank_sectors;
uint32_t erased_sectors = 0;
HAL_FLASH_Unlock();
/* erase bank1 */
if (flash_offset < 0x100000)
{
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_1;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
if (sectors <= bank_sectors)
erased_sectors = sectors;
else
erased_sectors = bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASHEx_Erase Failed\r\n",
strlen("HAL_FLASHEx_Erase Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
flash_offset += erased_sectors*SECTOR_SIZE;
}
sectors -= erased_sectors;
flash_offset -= 0x100000;
/* erase bank2 */
if (sectors)
{
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
if (sectors <= bank_sectors)
erased_sectors = sectors;
else
erased_sectors = bank_sectors;
tEraseInit.NbSectors = erased_sectors;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASHEx_Erase Failed\r\n",
strlen("HAL_FLASHEx_Erase Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
}
/* program */
len = (len + 15) & ~15;
for (int i = 0; i < len; i+=16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)firmware_buf))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASH_Program Failed\r\n",
strlen("HAL_FLASH_Program Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
firmware_buf += 16;
}
HAL_FLASH_Lock();
return 0;
}

烧录固件信息:

static int WriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
FLASH_EraseInitTypeDef tEraseInit;
uint32_t SectorError;
uint32_t flash_addr = CFG_OFFSET;
uint8_t *src_buf = (uint8_t *)ptFirmwareInfo;
HAL_FLASH_Unlock();
/* erase bank2 */
tEraseInit.TypeErase = FLASH_TYPEERASE_SECTORS;
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = (flash_addr - 0x08000000 - 0x100000) / SECTOR_SIZE;
tEraseInit.NbSectors = 1;
if (HAL_OK != HAL_FLASHEx_Erase(&tEraseInit, &SectorError))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASHEx_Erase Failed\r\n",
strlen("HAL_FLASHEx_Erase Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
/* program */
for (int i = 0; i < sizeof(FirmwareInfo); i+=16)
{
if (HAL_OK != HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr,
(uint32_t)src_buf))
{
g_pUpdateUART->Send(g_pUpdateUART, (uint8_t *)"HAL_FLASH_Program Failed\r\n",
strlen("HAL_FLASH_Program Failed\r\n"), UPDATE_TIMEOUT);
HAL_FLASH_Lock();
return -1;
}
flash_addr += 16;
src_buf += 16;
}
HAL_FLASH_Lock();
return 0;
}

5. Bootloader 实现启动功能(软复位)#

在启动 APP 之前,应该让系统“尽量”处于初始状态。比如:关闭各类中断、让各类设备处于初始状态。有一个办法可以轻松实现这点:软件复位。

所以,Bootloader 启动 APP 时,可以这样改进:

  • 触发软件复位
  • 会再次运行 Bootloader
  • Bootloader 在初始各类硬件之前判断复位原因,发现是软件服务时,启动 APP

5.1 main 函数#

在 main.c 开头添加:

int main(void)
{
/* USER CODE BEGIN 1 */
if (isSoftReset()) // 注意使用keil下载调试时,也会触发软件复位;如果需要调试,把这个去掉
{
/* 软件复位:去启动应用程序 */
uint32_t app_vector = get_app_vector();
extern void start_app(uint32_t);
start_app(app_vector);
}
...

start_app 就是之前在第 2 小节实现的汇编函数。

isSoftReset 用于判断是否是软件复位;get_app_vector 获取需要跳转过去的地址。

int isSoftReset(void)
{
return HAL_RCC_GetResetSource() & RCC_RESET_FLAG_SW;
}
uint32_t get_app_vector(void)
{
PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
return ptFlashInfo->load_addr;
}

5.2 Bootloader#

start_app_c();使用 hal 库函数HAL_NVIC_SystemReset

static void SoftReset(void)
{
__set_FAULTMASK(1);//关闭所有中断
HAL_NVIC_SystemReset();
}
static void start_app_c(void)
{
/* 触发软件复位 */
SoftReset();
}