单片机 IAP 功能基础开发篇之APP升级(二)

1、前言

上一篇 单片机 IAP 功能基础开发篇之APP升级(一)讲到了单片机 IAP 功能给 APP 程序升级的设计思路,这篇介绍的是具体实现方式。

下一篇 单片机 IAP 功能基础开发篇之APP升级(三)


1.1、目的

本文所写的是如何不通过下载器的方式实现单片机中的程序更新,目前介绍的是 STM32 的 BootLoader 简易设计方案(便于初步理解)。


2、设计方案


2.1、升级方式

在开发单片机的 bootloader 功能时,首先需要选择通过什么样的方式进行升级,在单片机 IAP 功能基础开发篇之APP升级(一)介绍了三种升级方式,该篇先介绍串口通信升级方式。

选定升级方式后,就需要选择串口通信协议,这里我们可以选择 Modbus通讯协议(当然,可以自己定义协议,这样更快,但是需要对应的上位机也支持该协议)。

选择Modbus通讯协议的原因:

1、完善健壮的通信协议内容,属于工业领域通信协议的业界标准(肯定要有所了解和使用业界标准协议)

2、 嵌入式软件开发之常用软件(六)介绍的串口调试助手 XCOM 就有该协议的功能,方便使用

扩展:选择串口通信可以通过连接蓝牙模块,从而实现无线升级

2.2、分区划分

升级方式选定后,则需要划分 Bootloader 和 app 的 Flash 空间,分别用来存放 bootloader 和 app 的程序,在没有开发 bootloader 之前,整个 Flash 都可以是 app 的程序空间。

以 STM32F103ZET6 为例,STM32 的 flash启动地址为 0x0800 0000,则根据下表进行划分(分区大小可根据芯片内存大小自行决定):

MCU分区

描述

起始地址

大小备注

Bootloader 程序区

BOOT 可执行程序(程序启动区)

0x0800 0000

32K

用户程序区(APP)

APP 可执行程序(应用程序区)

0x0800 8000

480K

疑问:为什么 BootLoader 的起始地址为 0x0800 0000,难道 app 的起始地址不能是 0x08000000 吗?

首先了解 STM32 芯片上电的时候,首先会从内存地址位 0x0800 0000(由启动模式决定)的地方加载栈顶地址(4字节),从 0x0800 0004 的地方加载程序复位地址(4字节),然后跳转到对应的复位地址去执行。

假设 app 的起始地址是 0x0800 0000,那么芯片上电就会直接执行 app 程序(没有 BootLoader 功能的就是这种情况),升级的时候肯定就是由 APP 程序跳转 BootLoader 中,但因为 BootLoader 是用来升级 app 程序,那就意味着 app 程序不一定时刻都存在,如在升级过程中若出现断电或其他问题引起升级失败导致 app 程序被擦除且掉电,那么下次启动时就因为没有 app 程序而进入硬件错误中断(堆栈溢出,程序指针指飞),即无法进入 BootLoader 重新升级,只能重新通过下载器烧写 app 程序。但如果 BootLoader 先启动,那这种情况就不会存在。

2.2.1、编译器设置

Bootloader 和 app 属于两个独立的工程,不是一个工程!!!

独立的工程意味着程序可以独立运行(将两个工程的起始地址设置 0x0800 0000),不受另一个工程影响,只是程序的功能不一样。

但是 APP 工程的起始地址变了,所以跑不起来,需要 bootloader 进行引导。

  • BootLoader 的编译器设置

  • App 的编译器设置

2.2.1、APP 程序有效标志

程序启动后,首先进入 bootloader 程序中,再跳入APP 程序中,如果APP 程序不存在,则一直在bootloader 中等待升级;那么,如何识别 APP 程序是否存在或者有效呢?

通常升级 APP 程序后,即通过 bootloader 烧录 APP 程序后,会在 APP 中的指定位置设置标志,然后 bootloader 去检查该标志是否有效。

为确保该标志不影响 APP 可执行程序,一般放在 APP可执行程序之后。

可执行程序包括:代码、初始化数据段和未初始化数据段等不仅仅只是代码

目前不清楚APP可执行程序的结束地址怎么办?

可以在 APP 程序预留分区的最后地址预留 4 个字节的标志位,标志可设置为 0x5a5a(该方式每次擦除 APP 程序时必须将 APP 分区全部擦除,否则该标志就没有意义)。

下篇会讲如何在程序编译完之后得到 APP 可执行程序结束地址,将标志放置 APP 可执行程序结束地址后,这样擦除时就只需要擦除升级程序需要的内存了,节约擦除时间(擦除较耗时),当然优点不止这些。

2.3、流程图


2.4、Bootloader 设计

2.4.1、初始化

初始化和正常没有bootloader 的程序一下,该咋样就咋样,为了避免无效功耗损失,一般是需要什么就初始化什么。

  • 时钟初始化
  • 串口初始化
  • 定时器初始化(如果有用到定时轮询或者定时器延时等可以初始化)

2.4.2、检查APP 程序是否存在

       初始化完成后,首先检测 APP 分区中最后地址的标志位是否符合(如是否等于0x5a5a),如果符合,启动倒计时(时间大约几百毫秒就行,根据实际情况决定),等待进入 APP 程序,在此期间,也会等待接收升级的指令;否则,则一直等待接收升级的指令。

2.4.3、升级

       接收到升级指令后,则启动升级流程,升级期间不允许进入 APP 程序。升级流程如上流程图

2.4.3、跳转APP程序

升级完成后,需要跳转到 APP 程序(可以自动跳转,也可以增加跳转指令手动跳转),跳转前的准备:

  1. 检测 APP 程序是否存在,若存在,则继续往下执行
  2. 关闭全局中断
  3. 清除所有中断标志位
  4. 跳转 APP 启动程序地址

当然,也能直接跳转到 APP 程序的 main 函数,但在跳转前需要重新初始化堆栈等内容,所以最好还是交给启动文件处理


2.5、App 设计

2.5.1、初始化

APP 程序初始化之前,需要重定向中断向量表。

重定向中断向量表的原因:由于 bootloader 占用了原来的芯片中断向量表地址,为了防止进入 APP 程序区后产生的中断进入 bootloader 的中断处理程序,所以需要重新定向到 APP 程序的中断向量表。

注1:并不是说进入 APP 程序后产生的中断就会执行 APP 中断程序,跳转 APP 程序可以简单理解成为 bootloader 的一个 APP 函数,进入函数执行后只不过无法退出而已,难道执行函数会影响原有的中断程序吗!!!

注2:STM32 可以设置重定向中断向量表寄存器,不代表所有芯片都有该寄存器功能,比如 STM8 就不行,需要通过额外的处理做到类似重定向中断向量表的处理(相对复杂)。

重新定向中断向量表后,重新清除所有中断标志为,接着就可以正常初始化,正常执行了。

2.5.1、热启动升级

什么?APP 程序还能跳转 bootloader?

是的,当然可以,有两种方式:

1、通过函数指针跳转,即和跳转 APP 是一样的方式,跳转之前的做法是一样的,只不过没有检测 bootloader 存在标志(都能运行 APP 怎么可能没有),但是这样需要在 bootloader 也要做重定向中断向量表功能,麻烦!!!

2、软复位:通过软复位让程序复位,这样啥都不要处理,快速(程序复位,啥数据都没了,不关心无所畏惧)

跳转的目的是?都执行 APP 程序了,还跳转回去干嘛,闲的?

目的是为了下发升级固件时不需要冷启动(单片机重新上电),即在 APP 程序中需要实现接收升级准备指令(避免误触发,通常时一定时间接收到多少升级准备指令),然后通过上述的方式进入 bootloader 执行升级,不需要再像 51 单片机一样下载程序还需要重新上电才能下载。


3、代码片段

3.1、APP 跳转代码片段

#define APP_FLASH_CODE_BASE 0x08008000
#define APP_FLASH_CODE_SIZE 0x78000

int CheckIfAPPCanJump(void)
{
    uint32_t *pFlag = (uint32_t *)(APP_FLASH_CODE_BASE + APP_FLASH_CODE_SIZE - 4);

    if (*pFlag == 0x5A5A)
    {
        return 1;
    }

    return 0;
}

void JumpJumpApplication(void)
{
    typedef  void (*iapfun)(void);  /* 定义一个函数类型的参数 */
    iapfun jump2app; 

    if (CheckIfAPPCanJump())        /* 检测APP应用程序段是否可正常跳转 */
    {
        __disable_irq();                      /* 全局中断关闭 */
        jump2app = (iapfun)*(vu32*)(APP_FLASH_CODE_BASE + 4);/* 用户代码区第二个字为程序开始地址(复位地址) */
        MSR_MSP(*(vu32*)APP_FLASH_CODE_BASE);/* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
        jump2app();		  /* 跳转到APP */   
    }
}

3.2、重定向中断向量表

void SetVectorTable(void)
{
    uint8_t i;

    SCB->VTOR = APP_FLASH_CODE_BASE;

    /* 清除所有中断标志 */
    for(i= 0; i < CRS_IRQn; i++)
    {
        NVIC_ClearPendingIRQ((IRQn_Type)i);
    }
    
    /* 在BOOT中跳转之前关闭了全局中断, 因此需要重新打开 */
    __enable_irq();
}

posted @ 2022-06-10 19:04  大橙子疯  阅读(774)  评论(0编辑  收藏  举报