IAP程序升级(源码+注释讲解)

目录

程序升级的概念

1.OTA 和 IAP

2.阿里 OTA 服务器简单体验

3.IAP 与 Bootloader

实现升级功能

1.升级方案设计

① 上位机与下位机

② Flash 使用规划

③ 下位机启动流程

2.必备知识

① Cortex M3/M4/M33 启动流程

② 异常向量表

③ CPU 内部寄存器

④ 几条汇编指令

3.编写 APP

4.编写 Bootloader 实现启动功能

5.定义下载协议

6.编写 Bootloader 实现下载功能

7.编写 Bootloader 实现烧录功能

① 烧写 Flash

② 启动


程序升级的概念

1.OTA 和 IAP

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

        IAP 是“In Application Programming”的简写,就是用户程序运行时对 Flash 的某些区域进行烧写,可以写入新版本的软件、用户数据等。IAP 主要包括 BootLoader 和应用程序两部分:在升级时运行的是 Bootloader,它接收新版本的应用程序,烧写在 Flash 上。
        要实现 OTA(就是通过网络升级软件),需要使用 IAP 技术。要使用 IAP,需要引入 Bootloader。
        上电后,先运行的程序被称为 BootLoader。它的作用和工作流程如下:
① 判断是否需要升级固件,如果无需升级,就启动 Flash 另一个区域的应用程序
② 如果需要升级,通过 OTA 数据包交互协议,接收新版本的软件、烧录到 FLASH 上,然后设置升级标志位并重启
③ 重启后运行的仍然是 Bootloader,它根据标志位启动新版本的软件。

2.阿里 OTA 服务器简单体验

        在阿里云物联网平台,可以购买 OTA 服务:


        它的 OTA 升级流程如下:

        由于要使用 MQTT 协议,所以我们暂时不深究 OTA 技术,本文讲解 IAP 并编写代码实现程序升级。 

3.IAP 与 Bootloader

        在 Linux 系统中,软件组成可以跟 Windows 进行类比:

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

        在单片机中,软件没那么复杂,一般只有一个程序,上电就运行这个程序,并不需要 BootLoader。
        但是涉及软件升级时,必须引入要 BootLoader。假设没有 BootLoader,程序无法升级自己: ① 单片机资源比较紧张,Flash 容量比较小,一般无法存储两份程序
② 当前的程序在 Flash 上运行,它无法更新自己:通过网络等手段下载程序到内存后,烧写到 Flash 不就破坏本身正在运行的程序了吗?

        所以在单片机中,涉及软件升级时,必须引入 BootLoader:

        Flash 上烧写有 BootLoader 和 APP(用户程序),启动过程如下:
① 上电时 BootLoader 先运行
② BootLoader 判断发现:Flash 上有 APP 并且无需升级,BootLoader 就会启动 APP
③ BootLoader 判断发现:Flash 上没有 APP 或者需要升级,BootLoader 执行升级操作

(理解上面这些步骤,我们需要在Flash里分配最前面的一段空间,用来烧写 Boot Loader ,上电时默认从Flash最前面取代码来执行,在Flash最后面后续会分配一小部分空间用来存放配置信息,可以根据这些配置信息来判断是否要进行程序升级等等,后续会有代码讲解,慢慢看下去 )


实现升级功能

1.升级方案设计

① 上位机与下位机

        上位机与下位机使用 USB 串口相连:

        上位机使用 sscom 串口调试助手发送固件,如下图(数据定义后面再设计):
① 先发送文件信息
② 再发送文件

        下位机:等待文件信息、读取上位机发来的数据、烧写。

② Flash 使用规划

        STM32H563RIV 内置 2MB Flash,划分如下:
① Bootloader 占据 256KB 空间
② APP 占据 1784KB 空间
③ 配置信息占据最后一个扇区 8KB 空间:用来保存 APP 版本、大小、校验码等信息。
(不同型号的芯片需要自己去查看 Flash 的大小)

③ 下位机启动流程

        Bootloader 流程图如下:

        所以进入升级模式有两种方式:
① 根本没有应用程序,即没有配置信息,这时就会进入升级模式,烧写应用程序
② 上位机发来的固件信息里的版本号比当前应用程序的版本号要高,也会进入升级模式

2.必备知识

① Cortex M3/M4/M33 启动流程

        上电后,CPU 默认从 0 地址开始启动:
① 地址 0 就是默认的异常向量表基地址,使用 Flash 启动时 0 地址被映射到 Flash 基地址 0x08000000。
② CPU 读取异常向量表第 1 个 word(4 字节),写入 SP 寄存器
③ CPU 读取异常向量表第 2 个 word(4 字节),跳转执行:这就是 CPU 运行的第 1 个指令

② 异常向量表

        当发生各类异常、中断时:
① 硬件会从异常向量表中,根据异常号、中断号找到一项,这项里保存的是“处理函数的地址”
② 硬件跳转执行这个处理函数。 
        以 SysTick 中断为例,SysTick 中断发生时,硬件会调用如下函数:

        能正确使用中断的前提是:
① 把异常向量表的基地址告诉 CPU:这可以设置 SCB 里的 VTOR 寄存器(寄存器地址为 0xE000ED08)
② 在异常向量表里,填充中断处理函数

③ CPU 内部寄存器

        无论是 cortex-M3/M4/M33,CPU 内部都有 R0、R1、……、R15 寄存器;它们可以用来“暂存”数据。
        对于 R13、R14、R15,还另有用途:
① R13:别名 SP(Stack Pointer),栈指针
② R14:别名 LR(Link Register),用来保存返回地址
③ R15:别名 PC(Program Counter),程序计数器,表示当前指令地址,写入新值即可跳转

④ 几条汇编指令

读内存:Load

# 示例

LDR R0, [R1, #4] ; 读地址"R1+4", 得到的4字节数据存入R0

写内存:Store

# 示例

STR R0, [R1, #4] ; 把R0的4字节数据写入地址"R1+4" 

加减:

ADD R0, R1, R2 ; R0=R1+R2

ADD R0, R0, #1 ; R0=R0+1

SUB R0, R1, R2 ; R0=R1-R2

SUB R0, R0, #1 ; R0=R0-1 

比较:

CMP R0, R1 ; 结果保存在PSR(程序状态寄存器) 

跳转:

B main ; Branch, 直接跳转

BL main ; Branch and Link, 先把返回地址保存在LR寄存器里再跳转

BX R1 ; 先在R1里保存地址再跳转 

3.编写 APP

        编写一个 APP(点灯)

h5_app:

        烧写在 Flash 开头时能看到灯闪烁。然后修改 RO 地址为0x08040000,烧写后灯不闪烁(需要先擦除 Flash 开头的程序)。

        要点:
① 设置 RO 地址为 0x08040000: 

② 不要使用默认的异常向量表:

        这段代码表示使用Flash开头的异常向量表,但是我们修改了应用程序的烧写位置。以后我们要在Flash开头一段空间内烧写bootloader程序,并且将异常向量表的基地址放在属于应用程序的Flash空间的开头。接下来我们来编写 bootloader 的代码。

4.编写 Bootloader 实现启动功能

(注意:要重新创建另外一个工程,boot loader的代码和app的代码不在同一个工程里)

        Bootloader 要启动 APP,需要模仿硬件上电后做的事情:
① 读取异常向量表的第 1 个 word,设置进 SP 寄存器
② 读取异常向量表的第 1 个 word,跳转执行

h5_bootloader:

汇编代码:
Jump.S

                AREA    |.text|, CODE, READONLY


; Reset Handler

start_app   PROC
                EXPORT  start_app
                ; SET vector, r0(0x08040000)==>VTOR
                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

        为了使用新的异常向量表,Bootloader 还要设置 VTOR 寄存器为新的异常向量表。参考《ARM Cortex-M3 与 Cortex-M4 权威指南.pdf》,如下图:

        并且,APP 工程里不应该再设置 VTOR(上一个小节讲过):

         我们使用 STM32CubeMX 创建工程时经常看到如下警告:

        如果不想看到上述警告,可以使能 ICACHE,如下操作:

        注意!!Bootloader 和 APP,只能让一个程序使能 ICACHE。如果两个程序都使能 ICACHE 的话,APP 再次初始化 ICACHE 时会导致死机。
        最终效果:
① 编译、烧写 h5_app:灯不闪烁。(app烧写在0x80400000)
② 编译、烧写 h5_bootloader:灯闪烁。(bootloader烧写在Flash开头)

5.定义下载协议

        下载协议可以自己定义,根据使用流程定义如下:
① 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];//文件名
    /*共32字节*/
};

注意:为了方便在串口里操作,上位机发送 uint32_t 的整数时,先发送高字节(大字节序)。 
③ Bootloader 发出获取固件的请求:发出“2”字符给上位机
④ 上位机发送 bin 文件
⑤ Bootloader 在烧写过程中,可以发送进度:“$1%”、“$2%”、“$100%”。以字符“$”开头、字符“%”结束。(可有可无)

        在 Keil 里生成 bin、反汇编文件:

fromelf --bin --output app.bin app.axf

        在 keil 里添加用户命令生成 bin 文件后,可以使用这个工具生成固件信息:

https://pan.baidu.com/s/1prNlDVciWKLF6_DpibVmdQ?pwd=epqw 提取码: epqw

        它的用法为(尖括号表示的参数是不可省略的,中括号表示的参数可以省略,version是整数):

create_firmware_info.exe [load_addr]

        它会分析 bin 文件,打印出长度、加载地址、校验码、文件名。 

        示例如下:
        把“create_firmware_info.exe”复制到 bin 文件目录下,然后在命令行执行如下命令:

6.编写 Bootloader 实现下载功能

bootloader.h

#ifndef _BOOTLOADER_H
#define _BOOTLOADER_H

#include <stdint.h>

typedef struct FirmwareInfo {
    uint32_t version;
    uint32_t file_len;
    uint32_t load_addr;
    uint32_t crc32;
    uint8_t file_name[16];
}FirmwareInfo, *PFirmwareInfo;

/*实现这个任务,用freeRTOS创建*/
void BootLoaderTask( void *pvParameters );

#endif /* _BOOTLOADER_H */

        在 bootloader.h 里定义结构体,后续我们就通过结构体里的数据来实现bootloader。

bootloader.c

(重点看BootLoaderTask函数)

/*
 * 包含各种头文件
 */

/*存放配置信息(固件信息)的地址*/
#define CFG_OFFSET 0x081FE000

#define UPDATE_TIMEOUT 1000

/*用来存放操作串口的句柄,前几个博客经常看到*/
static struct UART_Device *g_pUpdateUART;

/*大字节序转小字节序*/
static uint32_t BE32toLE32(uint8_t *buf)
{
    return ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | ((uint32_t)buf[3] << 0);
}

/*获得本地固件信息*/
static int GetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
    PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
    
    /*出错*/
    if (ptFlashInfo->file_len == 0xFFFFFFFF)
        return -1;
    
    /*如果没错,将固件信息赋给传入的参数*/
    *ptFirmwareInfo = *ptFlashInfo;
    return 0;
}

/*获得服务器的固件信息(本文用串口程序代替服务器)*/
static int GetServerFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
    uint8_t data = '1';
    uint8_t buf[sizeof(FirmwareInfo)];

    /* 发送字符1给PC,表示自己开始等待接收固件信息 */
    if (0 != g_pUpdateUART->Send(g_pUpdateUART, &data, 1, UPDATE_TIMEOUT))
        return -1;

    /* wait for response */
    while (1)
    {
        /*由于上位机我们用串口软件来模拟,手动发送固件信息没那么快,所以等待10s*/
        if (0 != g_pUpdateUART->RecvByte(g_pUpdateUART, &data, UPDATE_TIMEOUT*10))
            return -1;

        /*一开始我们会在固件信息前随便加几个0x5a,接收到5a就把它丢掉,我们要等待真正的数据*/
        if (data != 0x5a)
        {
            /*进入这里表示接收到了第一个真正的字节,存到数组里*/
            buf[0] = data;
            break;
        }
    }

    /* get firmware info */
    for (int i = 1; i < sizeof(FirmwareInfo); i++)
    {
        /*buf[0]已经有数据了,所以从buf[1]开始,存放固件信息*/
        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;
    
}

/*获得服务器发来的固件(编译出来的bin文件)*/
static int GetServerFirmware(uint8_t *buf, uint32_t len)
{
    uint8_t data = '2';

    /* 发送字符2表示可以接收固件了 */
    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;
}

/*计算校验码可以看下面这个网址,不做过多阐述*/
/* https://lxp32.github.io/docs/a-simple-example-crc32-calculation/ */
static int GetCRC32(const char *s,size_t n)
{
    uint32_t crc=0xFFFFFFFF;

    for(size_t i=0;i<n;i++) {
            char ch=s[i];
            for(size_t j=0;j<8;j++) {
                    uint32_t b=(ch^crc)&1;
                    crc>>=1;
                    if(b) crc=crc^0xEDB88320;
                    ch>>=1;
            }
    }

    return ~crc;
}

/************************************************************************************/
void BootLoaderTask( void *pvParameters )	
{
    struct UART_Device *pUSBUART = GetUARTDevice("usb");
    FirmwareInfo tLocalInfo;
    FirmwareInfo tServerInfo;
    int err;
    int need_update = 0;
    uint8_t *firmware_buf;

    /*先等待一会,PC可能要进行某些操作*/
    vTaskDelay(10000); /* wait for pc ready */
    pUSBUART->Init(pUSBUART, 115200, 'N', 8, 1);

    g_pUpdateUART = pUSBUART;

    while (1)
    {
        /* 获取本地的固件信息 */
        err = GetLocalFirmwareInfo(&tLocalInfo);
        if (err)
        {
            /* 如果本地没有固件信息,就设置更新标志位 */
            need_update = 1;
        }
    
        /*不管成功还是失败,都去获取服务器的固件信息*/
        err = GetServerFirmwareInfo(&tServerInfo);

        if (!err)
        {
            /* 如果获取到服务器发来的固件信息,就比较 */
            if (tServerInfo.version > tLocalInfo.version)
            {
                /* 服务器发来的版本号比本地的版本号高,就设置更新标志位 */
                need_update = 1;
            }
        }
        else
        {
            need_update = 0;
        }


        /*
         * 进入更新模式
         */
        if (need_update)
        {
            /*开辟空间存放固件,大小从服务器发来的固件信息里得到*/
            firmware_buf = pvPortMalloc(tServerInfo.file_len);

            /*获得服务器发来的固件*/            
            err = GetServerFirmware(firmware_buf, tServerInfo.file_len);
            if (!err)
            {
                /* 根据发来的固件计算校验码 */                
                uint32_t crc = GetCRC32((const char *)firmware_buf, tServerInfo.file_len);
                if (crc == tServerInfo.crc32)
                {
                    /* OK */
                    /* burn */
                    pUSBUART->Send(pUSBUART, (uint8_t *)"Download OK\r\n", 13, UPDATE_TIMEOUT);
                }
            }
        }
        else
        {
            /* 进入这里表示不用进入升级模式,跳转执行应用程序 */
        }
        
    }
}

        注释写的比较清除,逻辑不是很复杂,我把代码精简了一部分,把调试信息和出错的分支删减了一些,自己写的时候可以加一些打印信息,这样就知道代码在哪一步出错了。

注意:烧写(burn)和跳转执行应用程序的代码我们还没有写,会在下个小节写,这小节先实现下载功能,看能不能成功获取固件信息及固件(编译出来的bin文件)。

        上机实验:
① 生成 h5_app.bin 后,制作固件信息,在串口工具中粘贴待用:

② 烧写 h5_bootloader_download 程序
③ 观察串口工具,接收到“1”字符时,点击“发送”:发送固件信息
④ 观察串口工具,接收到“2”字符时,点击“发送文件”:发送 bin 文件
⑤ 观察串口工具,可以看到“Download OK”

7.编写 Bootloader 实现烧录功能

        本节要实现 2 个功能:
① 烧录
② 启动

① 烧写 Flash

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

        HAL_FLASHEx_Erase函数比较复杂,要构造相关结构体,需要对Flash结构有一定了解,这里就不展开,感兴趣的可以自己学习一下。

HAL_FLASH_Program

        烧写固件和固件信息的函数如下(在“Core\Src\bootloader.c”):

#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;
    
    /*先要解锁才能操作Flash,返回之前要lock*/
    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;
}

        代码大概讲一下:在烧写之前先擦除;对于烧写固件,擦除的时候要根据文件大小判断擦除哪个bank(有两个bank,一个bank大小为1M),然后16字节16字节去烧写;对于烧写固件信息,就不需要判断bank了,我们人为设定bank2最后一个扇区存放的是配置信息,同样擦除后再烧写,感兴趣的可以自己看看。(Flash首地址为0x80000000)

        然后补全上一小节的下载功能的代码,等会我们就要编写启动应用程序的代码。

② 启动

        启动 APP 之前,应该让系统“尽量”处于初始状态。比如:关闭各类中断、让各类设备处于初始状态。有一个办法可以轻松实现这点:软件复位
        所以,Bootloader 启动 APP 时,可以这样改进:
① 触发软件复位
② 会再次运行 Bootloader
③ Bootloader 在初始各类硬件之前判断复位原因,发现是软件服务时,启动 APP

关键代码如下(在“Core\Src\bootloader.c”):

/*声明为外部可调用,复位后进入main函数时,判断是否为软件复位*/
int isSoftReset(void)
{
    return HAL_RCC_GetResetSource() & RCC_RESET_FLAG_SW;
}

/*声明为外部可调用*/
/*获得app的异常向量表*/
uint32_t get_app_vector(void)
{
    /*CFG_OFFSET = 0x081FE000*/
    PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
    /*返回加载地址*/
    return ptFlashInfo->load_addr;
}

static void SoftReset(void)
{
    __set_FAULTMASK(1);//关闭所有中断
    HAL_NVIC_SystemReset();
}

static void start_app_c(void)
{
    /* 触发软件复位 */
    SoftReset();
}

在 main 函数前面,判断是否软件复位,是的话启动 APP:

int main(void)
{

  /* USER CODE BEGIN 1 */
    /*如果是软件复位,就跳转执行应用程序*/
    if (isSoftReset())
    {
        /*声明我们之前写的汇编文件*/
        extern void start_app(uint32_t vector);
        
        /*传入加载地址,执行应用程序,start_app是我们之前编写的汇编代码*/
        start_app(get_app_vector());
    }
  /* USER CODE END 1 */
/*略*/
}

注意:start_app是我们之前编写的汇编文件。

        上机实验:
① 生成 h5_app.bin 后,制作固件信息,在串口工具中粘贴待用:

② 烧写 h5_bootloader_ok 程序
③ 观察串口工具,接收到“1”字符时,点击“发送”:发送固件信息
④ 观察串口工具,接收到“2”字符时,点击“发送文件”:发送 bin 文件
⑤ 观察串口工具,可以看到“Download OK”
⑥ 观察串口工具,可以看到“start app”,并且看到 LED 闪烁
⑦ 修改 h5_app,让 LED 闪烁更快,重新编译:

⑧ 重新制作固件信息,在串口里粘贴待用:

⑨ 手工复位开发板(一定要手工复位,手工复位才会进入bootloader),重复步骤③④,可以观察到烧录成功后,程序被启动(LED 闪烁更快)。 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sakabu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值