工作笔记-----IAP的相关内容
工作笔记-----IAP的相关内容
@@ author: 明月清了个风
@@ date: 2025.7.27
在新的项目中增加了IAP升级的功能,因此记录一下学习的过程,板子是GD32F405
什么是IAP
百度有很多介绍,这个就不写了
MCU启动程序执行流程
在MCU工程文件建立的时候,会用到一个启动文件,这里以STM32F103ZGT6为例,文件名中为startup_stm32f10x_md.s
,一般都是一个汇编文件。
在这个文件中,会完成硬件初始化并引导程序进入我们写的C语言程序,下面对该文件的内容进行一个简单的流程讲解,在我们编写完成程序后,会使用JTAG/SWD下载程序到单片机,下载地址在这
-
首先程序会进行堆栈大小的定义
Stack_Size EQU 0x00000800;0x00000400AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp; <h> Heap Configuration ; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8> ; </h>Heap_Size EQU 0x00000800;0x00000200AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit
-
然后会进行中断向量表的设置,需要注意的是在启动文件中有一句:
; Vector Table Mapped to Address 0 at Reset
表示向量表在复位后会被映射到地址0,需要注意的是,这里的地址0并不是物理地址0,而是一个逻辑地址(是不是可以理解为相对地址),因为对于每个MCU而言,一般都会有不同的启动方式,也就是BOOT,比如从FLASH启动,从SRAM启动或者系统存储器启动,那么对于不同的启动方式,中断向量表放的位置其实也是不一样的,为了统一这个地方,将其放在了逻辑地址0的位置。
BOOT0 BOOT1 映射目标 物理地址 典型用途 0 X 主Flash 0x08000000 正常运行程序 1 0 系统存储器 0x1FFFF000 内置Bootloader 1 1 SRAM 0x20000000 调试模式 在系统初始化函数
SystemInit()
中有这样的操作,将向量表根据不同的启动方式进行重定位Relocation
,到此之后,中断向量表就是在物理地址上了,这里也就是0x08000004开始至于为什么不是0x08000000,向下看可以看到向量表的第一个向量是栈顶地址,也叫主栈指针(MSP),第二个就是复位中断响应函数,当系统复位时,会首先读取栈顶地址赋值给MSP寄存器,然后读取复位向量到PC寄存器去执行,由于cortex-m为32位,因此一个指针就是32位,这里就要偏移4。
复位中断属于异常中断,在《ARM体系架构与编程》中对异常中断跳转的机制进行了介绍,原文引用如下:
每个异常中断对应的中断向量表的4个字节的空间中存放了一个跳转指令或者一个向程序计数器(PC)中赋值的数据访问指令。通过这两种指令,程序将跳转到相应的异常中断处理程序处执行。
-
然后就会执行复位中断函数,调用
SystemInit()
函数,这个函数中主要进行一些必要的初始化,比如RCC时钟,FLASH初始化,重定位中断向量表,接着就会进入__main
函数了,注意这里的这个___main
还不是我们的main
函数,而是编译器的,最后他会自动调用我们的main
函数(这个函数是编译器提供,并不可见,但是可以从编译完成中的.map
文件中看到他的存在)
在此之后,进入我们写的用户程度,当中断来时,系统就会去上面设置的中断向量表中的地址跳到该地址执行对应的中断服务函数,执行完后再次回到主程序中进行。
IAP如何改变MCU启动流程
首先讲一下IAP一般有两种,一种是只有一个APP程序的,另一种是有2个APP程序的。
-
对于单APP程序的IAP模式来说,整个存储区仅存储了一个能够运行的APP程序,示意图如下(地址仅作示意,并不代表一定要这样)
启动流程和更新如下:
上电后运行Bootloader,检查更新标志位,若标志显示APP程序为最新版本,不用升级,则直接跳转至APP程序入口处运行;若标志显示程序需要更新,那么就进行新版本程序的搬运工作,并在搬运完成后更新标志位并跳转至APP处运行。
注意:有很多的单APP程序的IAP也可能在FLASH上并没有备份APP,而是在BOOT程序中建立与上位机的连接后检查是否有更新,并通过通讯将新版本APP程序下载下来,后面的流程就是一样的,完成APP程序区的擦除和写入工作,并更新标志位再进行跳转
-
对于双APP程序的IAP模式来说,整个存储区存储了两份可用代码,对于标志位来说,需要设定更多的状态,比如目前使用的代码是A区代码还是B区代码,两个分区代码的有效性状态,目前需要进行的更新状态。
启动流程和更新如下:
上电后运行Bootloader,检查更新标志位并判断当前活动分区代码,若标志显示无需更新,那么直接跳转至活动分区代码运行;若标志显示需要更新,那么判断是下载新程序至非活动分区还是切换活动分区,接着的流程就和上面一样了,擦除分区并更新分区代码及标志位,最后跳转。
下面给出一个在项目中使用验证过的程序框架(MCU:GD32F405RGT6),不一定能直接在你的板子上运行!!!但是关键功能和修改都给出了,希望能够提供一点思路
BOOT程序和APP程序都需要的定义
首先需要定义: APP程序存放地址,更新程序暂存地址,BOOT程序存放地址。
地址根据自己的程序大小和MCU进行更改
#define APP_ADDRESS 0X08000000 #define UPDATE_ADDRESS 0X08040000 #define BOOT_ADDRESS 0X08080000
如果有运行参数需要保存的可以在后面继续定义,比如:
#define USERDATA_ADDR 0x80A0000
然后定义升级标志的存放位置,一般存在一个不可能会被改变的地方,比如FLASH最后
#define APP_FLAG_ADDR 0x80E0000 //升级程序标志
接着定义标志的值,可以定义的复杂一点
#define UPDATECOMPLETE 0x87654321 //更新完成标志 #define UPDATING 0x12345678 //正在更新标志
BOOT程序
首先,需要更改BOOT程序的下载地址为你定义的地方
然后需要将下载的选项改为擦除Sector,不能设置为擦除Full Chip,这样所有的Flash都被清空了。
然后,由于对BOOT程序下载位置进行了
0x80000
的偏移,因此需要一开始就设置中断向量表进行对应的偏移量,这里不同MCU的设置方式不同,需要根据自己的进行。nvic_vector_table_set(NVIC_VECTTAB_FLASH,0X80000);
由于BOOT程序主要实现代码的擦除和搬运,因此只需要初始化一些基本的外设即可,不管是UI还是灯还是串口,能让你知道他进boot了就行,比如串口
hal_usart_init(); printf ("wellcome to boot\r\n");
然后就是判断升级标志了,直接拿升级标志的的值就行,标志正在升级,那就擦除原APP程序区域,在擦除的过程中可以加一些交互让你知道他在擦除,比如灯效。
update_flag=*(volatile uint32_t *)(APP_FLAG_ADDR);if(update_flag == IDCMD_UPDATEING){fmc_unlock();fmc_sector_erase(CTL_SECTOR_NUMBER_0);fmc_sector_erase(CTL_SECTOR_NUMBER_1);fmc_sector_erase(CTL_SECTOR_NUMBER_2);fmc_sector_erase(CTL_SECTOR_NUMBER_3);fmc_sector_erase(CTL_SECTOR_NUMBER_4);fmc_sector_erase(CTL_SECTOR_NUMBER_5);fmc_sector_erase(CTL_SECTOR_NUMBER_11);fmc_lock();}
擦除完毕,将升级的代码搬运到APP地址
for(int i = 0; i < 255; i ++){printf("UPDATING-------%d\r\n", i);memcpy(uaUpDateBuff, (uint8_t *)(UPDATE_ADDRESS + i * 1024), 1024);if(C_OK != Data_Move(APP_ADDRESS + i * 1024, uaUpDateBuff, 1024)) {Move_flag = 1;break;} else {}}
下面是搬移的函数,需要注意的是每个MCU提供的操作FLASH能力是不同的,比如我截图了两款的FLASH简介,因此需要注意你的数据搬运函数需要符合你MCU的要求。另外,如果想提升程序的稳定性,可以增加对于数据检验的手段。
uint8_t DFlashRWBlock(unsigned long tarAddr,uint8_t*cpBuffer,unsigned long dwCount) {uint32_t i,j, Address = 0;uint32_t tmpUWord;uint32_t *cpTmp;if(tarAddr >= APP_ADDRESS){fmc_unlock();Address = tarAddr;for (i = 0, j = 0; i < dwCount; i += 4){ // 组合成32位进行操作tmpUWord = cpBuffer[j+3]<<24 |cpBuffer[j+2]<<16 |cpBuffer[j+1]<<8 | cpBuffer[j];fmc_word_program(Address+i,tmpUWord);j += 4;}cpTmp = (uint32_t*)cpBuffer; // 简单检验一下for (i = 0, j = 0; i < dwCount;i += 4){tmpUWord = *(volatile uint32_t *)(Address+i);if(tmpUWord != cpTmp[j++]){fmc_lock();return 0xff;}}fmc_lock();return 0x00;}else return 0xff; }
如果搬移程序成功,将主栈指针和中断向量表复位,进行程序跳转
if(crcWrite == 0){update_flag = IDCMD_UPDATCOMPLETE; fmc_unlock();fmc_word_program(APP_FLAG_ADDR, update_flag); fmc_flag_clear (FMC_FLAG_END );fmc_flag_clear (FMC_STAT_WPERR );fmc_flag_clear (FMC_STAT_PGMERR );fmc_flag_clear (FMC_STAT_PGSERR ); fmc_lock(); JumpAddress = *(uint32_t*) (APP_ADDRESS + 4);Jump_To_Application = (pFunction)JumpAddress;__set_MSP(*(__IO uint32_t*) APP_ADDRESS);nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0X0000);__disable_fiq();NVIC_SystemReset(); }
下面是
main.c
文件的主要内容,如果程序升级成功是不可能进主循环的,因此在主循环中使用了一个定时器输出升级错误信息。#define C_OK 0x00 // 操作成功 #define C_NOTOK 0xff // 通用错误typedef void (*pFunction)(void); // 函数指针类型 unsigned long JumpAddress; // 跳转地址 pFunction Jump_To_Application; // 跳到应用程序的函数指uint32_t update_flag; uint8_t data_buff[2000], crcWrite; //升级包缓存区int main(void) {nvic_vector_table_set(NVIC_VECTTAB_FLASH,0X80000);hal_tim_init();hal_led_init();hal_usart_init();printf ("wellcome to boot\r\n");led_spark();update_flag=*(volatile uint32_t *)(APP_FLAG_ADDR);if(update_flag == IDCMD_UPDATEING){fmc_unlock();fmc_sector_erase(CTL_SECTOR_NUMBER_0);//16Khal_led_toggle(LED_G_PORT, LED_G_PIN);fmc_sector_erase(CTL_SECTOR_NUMBER_1);//16Khal_led_toggle(LED_G_PORT, LED_G_PIN);fmc_sector_erase(CTL_SECTOR_NUMBER_2);//16Khal_led_toggle(LED_G_PORT, LED_G_PIN);fmc_sector_erase(CTL_SECTOR_NUMBER_3);//16Khal_led_toggle(LED_G_PORT, LED_G_PIN);fmc_sector_erase(CTL_SECTOR_NUMBER_4);//64Khal_led_toggle(LED_G_PORT, LED_G_PIN);fmc_sector_erase(CTL_SECTOR_NUMBER_5);//128Khal_led_toggle(LED_G_PORT, LED_G_PIN);fmc_sector_erase(CTL_SECTOR_NUMBER_11);//128Khal_led_toggle(LED_G_PORT, LED_G_PIN);fmc_lock();}Move_flag = 0;for(int i = 0; i < 255; i ++){printf("UPDATING-------%d\r\n", i);memcpy(uaUpDateBuff, (uint8_t *)(UPDATE_ADDRESS + i * 1024), 1024);if(C_OK != Data_Move(APP_ADDRESS + i * 1024, data_buff, 1024)) {Move_flag = 1;break;} else {}}if(Move_flag == 0){update_flag = IDCMD_UPDATCOMPLETE; fmc_unlock();fmc_word_program(APP_FLAG_ADDR, update_flag); fmc_flag_clear (FMC_FLAG_END );fmc_flag_clear (FMC_STAT_WPERR );fmc_flag_clear (FMC_STAT_PGMERR );fmc_flag_clear (FMC_STAT_PGSERR ); fmc_lock(); JumpAddress = *(uint32_t*) (APP_ADDRESS + 4);Jump_To_Application = (pFunction)JumpAddress;__set_MSP(*(__IO uint32_t*) APP_ADDRESS);nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0X0000);__disable_fiq();NVIC_SystemReset(); }LEDG(OFF); while(1){if(watchdog_flag == 1){printf("update failed\r\n");watchdog_flag = 0;}} }void TIMER4_IRQHandler(void) {if(RESET != timer_interrupt_flag_get(TIMER4, TIMER_INT_FLAG_UP)){watchdog_count ++;if(watchdog_count >= 10000){watchdog_flag = 1;watchdog_count = 0;}timer_interrupt_flag_clear(TIMER4, TIMER_INT_FLAG_UP);} }
APP程序中要做的修改
由于这里APP程序是下载到Flash起始位置的,因此不用在程序中重定位向量表
需要在程序开始前,查询升级标志位,判断是否升级,如果升级则需要跳转到BOOT程序,那么就需要重定位中断向量表到对应位置,如果不升级就正常进入APP程序即可。
update_flag=*(volatile uint32_t *)(APP_FLAG_ADDR);if(update_flag==IDCMD_UPDATEING) // 如果升级了,跳到boot{ JumpAddress = *(uint32_t*)(BOOT_ADDRESS+4);Jump_To_Application = (pFunction) JumpAddress;__set_MSP(*(__IO uint32_t*) BOOT_ADDRESS);nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0X80000);Jump_To_Application(); }
升级的标志位需要通过与控制方的交互来完成,可以将整个升级过程分为三个阶段,这样相对更可控,需要在与上位机的通讯中添加升级的命令码,命令码根据自己的需要定即可,然后根据自己的协议帧组帧。
#define CMD_UPCMD 0x01 #define CMD_UPDATE 0x02 #define CMD_UPFINISH 0x03
对于第一个命令,实施对应Flash的擦除工作,也就是升级代码区的擦除,同时也要擦除最后的升级标志位
/* 自己的协议处理逻辑*/ case CMD_UPCMD: {update_flag = IDCMD_UPDATEING; fmc_unlock();fmc_sector_erase(CTL_SECTOR_NUMBER_6); //备份区擦除fmc_flag_clear (FMC_FLAG_END );fmc_flag_clear (FMC_STAT_WPERR );fmc_flag_clear (FMC_STAT_PGMERR );fmc_flag_clear (FMC_STAT_PGSERR );fmc_sector_erase(CTL_SECTOR_NUMBER_7); //备份区擦除fmc_flag_clear (FMC_FLAG_END );fmc_flag_clear (FMC_STAT_WPERR );fmc_flag_clear (FMC_STAT_PGMERR );fmc_flag_clear (FMC_STAT_PGSERR );fmc_sector_erase(CTL_SECTOR_NUMBER_11);//标志区擦除fmc_flag_clear (FMC_FLAG_END );fmc_flag_clear (FMC_STAT_WPERR );fmc_flag_clear (FMC_STAT_PGMERR );fmc_flag_clear (FMC_STAT_PGSERR );fmc_lock();// 操作完成回复逻辑自定 }
对于第二个命令,完成新版本代码的存储,将其写入Flash,我这里是上位机将bin文件分包后下发的,并且数据处理出来后第一个byte是该分包的序号,每个分包1024个byte,这里需要根据自己的逻辑更改,这里用的按byte写入,也可以组成整字写入,还是比较灵活的。
case CMD_UPDATE: {uint8_t pack_num = command_data[0];fmc_unlock();for(int a = 0;a < cmd_len - 1;a++){if(FMC_READY!= fmc_byte_program(UPDATE_ADDRESS + 1024 * pack_num + a, command_data[1 + a])){// 操作失败对应逻辑break;} fmc_flag_clear (FMC_FLAG_END);fmc_flag_clear (FMC_STAT_WPERR);fmc_flag_clear (FMC_STAT_PGMERR);fmc_flag_clear (FMC_STAT_PGSERR); }fmc_lock();// 操作完成回复逻辑自定 } break ;
对于第三个命令,将APP标志位写入FLASH,并完成程序的跳转
case CMD_UPFINISH: {fmc_unlock();if(FMC_READY!= fmc_word_program(APP_FLAG_ADDR, update_flag)){// 操作失败对应逻辑} fmc_flag_clear (FMC_FLAG_END );fmc_flag_clear (FMC_STAT_WPERR );fmc_flag_clear (FMC_STAT_PGMERR );fmc_flag_clear (FMC_STAT_PGSERR ); fmc_lock(); // 在跳转之前可以回一点消息或者做灯效告知状态delay_1ms(200);__disable_fiq();//关闭中断NVIC_SystemReset(); //复位 } break;
然后程序就会进入BOOT了,并且在BOOT中完成新版本程序的搬运以及重新跳回新版本程序.
主要需要注意的点有:
- 根据使用的MCU以及程序大小确定你自己的保存地址和留出的保存区域大小.
- 根据使用的MCU确定FLASH写入和擦除逻辑.
- 根据自己的业务逻辑确定消息帧形式和处理逻辑.
调试建议:
- 在每一步操作下面都添加一些效果告知程序运行状态
- 升级失败可以调试查看FLASH是否成功写入,检查写入逻辑以及写入形式,可以先烧写到对应位置看应该是什么样的,然后搬运到对应位置看差异.