最近在写项目的时候学到了有关IAP这方面的知识,所以决定分享出来。
我的开发环境是STM32H743+STM32CudeIDE
网上也有很多关于IAP的知识,我也找了很多,也踩了很多的坑。
我先来说说什么是IAP以及问什么要用IAP。IAP是In Application Programming的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。对我来说,我需要使用IAP实现无线下载程序。
IAP是怎么实现的呢?通常我们在使用IAP的时候需要准备两份代码,将它们烧写到Flash的两个不同的地址上。通常芯片上电后运行的第一份代码我们叫做boot,第二份叫做app。boot需要做两件事情:跳转到app和擦写app部分的代码。通常我们在boot中完成应用代码的更新。想要完成代码的跳转就要先知道芯片的启动流程。
STM32H7 的内部闪存(FLASH)地址起始于 0X08000000, 一般情况下,程序文件就从此地址开始写入。此外 STM32H743 是基于 Cortex-M7 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动, 而这张“中断向量表”的起始地址是 0x08000004,当中断来临, STM32H743的内部硬件机制亦会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。在下图中, STM32H743 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生了中断),此时 STM32H743 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
在下图所示流程中, STM32H743 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,此部分同图 60.1.1 一样;在执行完 IAP 以后(即将新的 APP 代码写入STM32H743 的FLASH,灰底部分。新程序的复位中断向量起始地址0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数, 如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32H743 的 FLASH,在不同位置上,共有两个中断向量表。在 main 函数执行过程中,如果 CPU 得到一个中断请求, PC 指针仍然会强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
对比两个流程可以知道我们需要一个新中断向量表,对于STM32来说它并不知道什么boot什么app,默认情况下对于所有的代码的中断向量表都是同一个地址:0x08000004。所以这需要我们手动进行修改。在文件system_stm32h7xx.c文件中有一个宏定义
/* 修改前 */
#define VECT_TAB_OFFSET 0x00000000UL
/* 修改后 */
#define VECT_TAB_OFFSET 0x00040000UL
或者直接在主函数中直接修改向量表的地址:
SCB->VTOR = FLASH_BANK1_BASE | 0x00040000UL
VECT_TAB_OFFSET为偏移地址,这个地址是根据自己个人需要而定的,最小为一个Flash扇区的大小。修改了中断表的地址后就应该来修改app代码的起始地址了。对于STM32来说,一般代码都是保存在Flash中的,默认情况是从0x08000000开始的,但如果要同时存在两份代码时,app的代码就得从另一个地址开始。具体的地址与中断表的一样,也是偏移0x00040000,也就是0x08040000。因为我使用的是STM32CudeIDE,所以需要修改链接文件STM32H743IITX_FLASH.ld。
修改前:
修改后:
以上修改的部分均为app的部分,boot部分无需做这些修改。
以上准备工作已经完成,接下来就该写代码了。但是在写之前我们需要捋一捋流程。
以上为两份代码的总方向,我们还需要对IAP升级过程进行细化。app是我们的应用代码,所以芯片一般都在运行app的代码,当我们需要更新的时候需要向app下发boot联机和boot跳转命令,这样就能转到boot代码,接着擦除app代码,然后接受app升级程序数据并写入Flash,最后跳转到app。
首先是boot联机,boot联机就是一个擦除固件的过程。只要擦除了固件,然后跳转到boot,这样boot就检测不到固件,就不会跳转到app了。
IAP_StatusTypeDef IAP_Clear_Flag(void)
{
uint32_t temp = APP_CONFIG_CLEAR_VALUE;
STMFLASH_Write(APP_CONFIG_ADDR, &temp, 8); // 清除固件
return IAP_OK;
}
然后是boot跳转,因为我的app代码用了比较多的外设,我用了很多方法来写这部分的代码,结果都是跳转后跑飞。因为boot有些外设没用到,当外设中断来临的时候,发现boot没有这部分的代码,所以就会跑飞。所以要关闭这些外设,但又因为外设比较多,我也不想一个个来关,并且boot代码的起始地址和向量表并没有修改,所以我直接使用系统复位来实现boot跳转。
void APP_Jump_Boot()
{
__set_FAULTMASK(1); // 关闭总中断
NVIC_SystemReset(); // 使用复位代替boot跳转
}
对于app擦除和app升级程序数据我还没有测试过,等之后测试了在贴出来。接着是app跳转,在跳转之前要关闭外设时钟,将MSP指针指向app,再将PC指针指向app。
IAP_StatusTypeDef Boot_Jump_APP(void)
{
if(tIAP_Updata.AppClearFlag == 1 && tIAP_Updata.AppUpdataFlag != 1)
{
return IAP_BUSY;
}
if(((*(__IO uint32_t*)FLASH_APP1_ADDR)&0x2FF00000)==0x24000000) //检查栈顶地址是否合法.0x20000000是sram的起始地址,也是程序的栈顶地址
{
__HAL_RCC_ETH1MAC_CLK_DISABLE();
__HAL_RCC_ETH1TX_CLK_DISABLE();
__HAL_RCC_ETH1RX_CLK_DISABLE();
__set_FAULTMASK(1); // 关闭总中断
HAL_SuspendTick(); // 挂起滴答定时器
jump2app=(iapfun)*(__IO uint32_t*)(FLASH_APP1_ADDR+4); //用户代码区第二个字为程序开始地址(复位地址)
__set_MSP(*(__IO uint32_t*)FLASH_APP1_ADDR); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
else
{
return IAP_ADD_ERROR;
}
return IAP_OK;
}
目前已经测试的只有boot联机、boot跳转和app跳转,一切正常。对了还有app联机,增加这个接口是方便用户的,如果用户下发了boot联机指令后又不想升级了,如果没有app联机的话,在重启之后又要重新下发旧的程序代码,然后再跳转app。有了app联机就可以直接下发app联机,这样boot在运行的时候就会自动跳转到app。app联机就是一个烧写固件的过程。
IAP_StatusTypeDef IAP_Set_Flag(void)
{
uint32_t temp = APP_CONFIG_SET_VALUE;
STMFLASH_Write(APP_CONFIG_ADDR, &temp, 8); // 烧写固件
return IAP_OK;
}
由于现在的代码里有公司项目代码,等以后写个新的IAP程序再贴出来。