STM32CubeIDE IAP原理讲解,及UART双APP交替升级IAP实现

2024-02-10 14:38

本文主要是介绍STM32CubeIDE IAP原理讲解,及UART双APP交替升级IAP实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

随言:

IAP应该是我唯一想写的文章,从创建这个账号开始。

但是不知不觉几年过去了,一直没去写这文章。

现在就随便写写吧~

曾做过4G模块UART协议与STM32通讯实现远程无线迭代升级,

一共2个APP,bootloader优先选择稳定高版本的APP启动。

下面文章就把这个简单实现大概,

由于我是之前使用无线模块透传+UART与服务器通讯的,功能比较多复杂。

为了简化,我不打算写个独立带协议的上位机,简单用UART实现大概双

APP的IAP模型和提及一些之前遇到的问题。简单修改一下可以用于网络IAP。

记录“美好”生活吧。

下面我不会使用Ymodem协议接收bin文件数据。

我只想以最简单的方式展示IAP的过程,不想过多复杂化IAP,微笑。

若需要Ymodem协议的,请自己移植,谢谢~!

1.什么是IAP?

英文名:in-application programming。

中文名:应用程序内编程。

作用:对于大多数基于闪存的系统,一个重要的要求是能够在最终产品中安装固件时进行更新。

           STM32微控制器可以运行用户特定的固件来对微控制器中嵌入的闪存执行IAP。

接口:此功能支持的任何通信接口。

由于不限制通信接口协议等,只要能通过任意通信接口拿到新版固件包数据(bin文件),就能自己升级固件。

这就能做到添加  外部无线模块(4G模块、wifi)做到OTA升级。

也可以使用U盘或TF卡等外部存储设备做到OTG升级。U盘升级的IAP官方有模板程序叫:FWupgrade_Standalone。

1、1  IAP适用场景:

1、设备附加功能卖点,固件新增功能,修复旧功能等,用户自己可以OTG升级固件。

2、设备笨重不利于返厂,OTA升级固件。

3、设备分布零散广泛不利人工去现场去维护固件,OTA升级。

1、2  参考官方例程:

..\STM32Cube\Repository\STM32Cube_FW_F4_V1.25.0\Projects\STM324xG_EVAL\Applications\IAP\

2、IAP执行原理:

2、1  正常程序上电执行流程:

1、从Flash相对0地址即STM32的0x8000000地址开始执行,存储的是栈顶地址。再偏移4个字节找到中断向量表起始地址,即复位中断向量,存储这复位中断程序入口,跳转到复位中断程序入口执行复位中断服务函数Reset_Handle(void)。

2、复位中断服务函数Reset_Handle(void)执行完会跳转到main函数,main函数循环运行。

3、main函数死循环时,发生中断请求,然后PC指针会跳转到中断向量表寻找对应的中断向量。

4、找到对应的中断向量后执行对应的中断服务函数xxx_Handler(void).

5、执行中断服务函数完成后,返回main函数循环运行。

2、2 加入IAP后的上电程序执行流程

不同点:

对比两图可以发现,加入IAP后其实就把芯片内的Flash分成了两个程序。IAP过程前是一个程序,IAP后是一个程序。

由于是两个程序,我们一般会把放在flash相对的0地址的程序叫bootloader引导启动程序,后面的都叫APP用户程序。

如上图的IAP程序就占用了flash的M个字节大小,然后通过跳转后APP程序的中断向量表也往后偏移了M个字节。

1、第2步的跳转是通过函数指针跳转。跳转前需要关闭所有的中断后取消外设功能,跳转后需要更新中断向量表。

特别是使用了RTOS系统,由于像FreeRTOS会用外设TIMx做系统时钟,跳转前一定要关闭系统滴答时钟中断,否则跑飞。

STM32F4xx系列更新中断向量表可以使用下面两种方法:

#define APPLICATION_ADDRESS   (uint32_t)0x08008000
SCB->VTOR = APPLICATION_ADDRESS;
#define APPLICATION_ADDRESS   (uint32_t)0x08008000
#define VECT_TAB_OFFSET  APPLICATION_ADDRESS   

2、第5步中发送中断后PC指针会跳转到默认的向量表位置,然后默认的向量表会执行新的向量表对应的中断服务函数,最后返回main函数。


 

3、做个简单的UART IAP

3、1  框图

我使用的是STM32F429IGT6;环境是STM32CubeIDE 1.42版本。

由于实现IAP实际上写两个程序,分别是bootloader和APP.

首先先将芯片内部flash分区两个区域,分别存放bootloader和APP程序。

IAP功能就坐在bootloader程序上面,bootloader使用UART实现更新程序;

APP做一个1秒 LED闪烁的。

STM32F429IG内部flash分区

简单的IAP程序流程图

APP就没什么流程图可言的了,毕竟只有一个LED闪烁。

3、1  关于Ymodem协议(纯主观)

官网例程都是用的Ymodem协议接收bin文件数据。

YModem协议是由XModem协议演变而来的,每包数据可以达到1024字节,是一个非常高效的文件传输协议

我就不用了,还是自由舒服。

3、3  Bootloader编程

STM32CubeIDE初始化了外部晶振时钟和UART(无中断)。

main代码:

int main(void)
{/* USER CODE BEGIN 1 */uint8_t key = 0;uint32_t temp = 0;uint32_t timeout = 0;uint32_t userapplen = 0;/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */printf("\r\nSudaroot, This is Bootloader\r\n");/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){printf("\r\n=================== Main Menu ============================\r\n\n");printf("  Download user application to the internal Flash ------ 1\r\n\n");printf("  Execute the loaded application ----------------------- 2\r\n\n");/* Clean the input path */__HAL_UART_FLUSH_DRREGISTER(&huart1);/* Receive key */HAL_UART_Receive(&huart1, &key, 1, HAL_MAX_DELAY);switch(key){case '1':/* Download user application in the Flash *//* 1. erase user application area */printf("Wait for the internal Flash erase to complete\r\n");if(FLASH_If_Erase(APPLICATION_ADDRESS, 10) == 1){printf("Erase the internal Flash is fail\r\n");Error_Handler();}printf("Erase the internal Flash is complete\r\n");/* 2. download a file via serial port */printf("Waiting for the file to be sent ... \r\n");userapplen = 0;timeout = HAL_MAX_DELAY;/* Clean the input path */__HAL_UART_FLUSH_DRREGISTER(&huart1);while(1){if(HAL_UART_Receive(&huart1, uart_buf, UART_BUF_SIZE, timeout) != HAL_OK){temp = UART_BUF_SIZE - (huart1.RxXferCount + 1);if(FLASH_If_Write(APPLICATION_ADDRESS + userapplen, (uint32_t*)uart_buf, temp / 4) != FLASHIF_OK){printf("Write the internal Flash is fail\r\n");Error_Handler();}userapplen = userapplen + temp;break;}timeout = 1000;if(FLASH_If_Write(APPLICATION_ADDRESS + userapplen, (uint32_t*)uart_buf, UART_BUF_SIZE / 4)){printf("Write the internal Flash is fail\r\n");Error_Handler();}userapplen = userapplen + UART_BUF_SIZE;}printf("Programming Completed Successfully! %ldBtye\r\n", userapplen);break;case '2':printf("Start program execution......\r\n\n");/** 关闭或反初始化前面用到的外设和中断* 1、反初始化UART* 2、关闭系统滴答定时器中断* */HAL_UART_DeInit(&huart1);HAL_SuspendTick();/* execute the new program */JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);/* Jump to user application */JumpToApplication = (pFunction) JumpAddress;/* Initialize user application's Stack Pointer */__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);JumpToApplication();break;default:printf("Invalid Number ! ==> The number should be either 1 or 2\r");break;}/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}

1、上电后打印功能菜单。

2、发送‘1’就会进入IAP模式;首先会擦除APP空间,然后等待用串口助手发送APP的bin文件写入flash,直至完成。

3、发送‘2’就会直接跳转到APP运行。

注意:编译完成后注意一下生成的bin文件是否超过了定义的32kB大小,超过后就要重新定义分区。

2021.10.25

关于temp = UART_BUF_SIZE - (huart1.RxXferCount + 1);这句代码获取一包数据剩余长度。

为什么加1呢?和我使用的“HAL库版本有关系”。我用的是当时写博客时的最新版本,但是现在肯定不是最新了。

主要是在 HAL_UART_Receive 这个库函数内部,huart1.RxXferCount先减1再等待超时,而新版本的已经变了,先等待超时再huart1.RxXferCount减1。具体情况具体分析,根据你自己的HAL库版本修改。

3、4 APP编程

STM32CubeIDE初始化了外部晶振时钟、UART(无中断)和一个LED的GPIO.

main函数代码:

int main(void)
{/* USER CODE BEGIN 1 */SCB->VTOR = APPLICATION_ADDRESS;/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */printf("Sudaroot, This is IAP Application\r\n");/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){printf("This is IAP Application running\r\n");HAL_GPIO_TogglePin(RUN_LED_GPIO_Port, RUN_LED_Pin);HAL_Delay(500);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}

 这行代码的时候是   SCB->VTOR = APPLICATION_ADDRESS; 上面提到的更新中断向量表偏移地址。

注意:编译完成后注意一下生成的bin文件是否超过了定义的分区大小,超过后就要重新定义分区。

还要修改一下程序flash链接地址。

1、打开下面这个文件

2、我用的是STM32F429IG,片内flash有1MB大小,前面32kB给了boot loader,即APP剩下992kB,起始地址是0x08008000.

修改成功后:

3、5 实验效果:

B站链接:STM32简单的IAP_哔哩哔哩_bilibili

3、6 源码链接:

简单的UARTIAP.rar_stm32iap双app-嵌入式文档类资源-CSDN下载

4、写UART双APP迭代升级的IAP

4、1 IAP单APP和双APP区别

IAP双APP和单APP相比之下就是更稳定。

我做的是网络升级的IAP,毕竟要确保设备软件的正常稳定工作,重中之重。

1、单APP的话,如果网络IAP升级失败(断网或断电)的话,基本可能变成了“砖头”,大忌(同事会很辛苦)。

2、双APP的话,首先启动肯定是一个稳定运行的版本,至于是不是最新版是次要,主要是稳定。

比如当运行稳定的是APP0,那么网络IAP升级的时候,肯定不会升级正在运行的APP0。会升级

另一个没有运行APP1,即使APP1网络IAP过程升级失败了,也没关系。毕竟稳定的APP0的没有

改动,还能稳定工作,等待下一次APP1正常升级完成后再尝试启动。只有升级后APP1能稳定工

作且版本号也比APP0高的时候才会去升级APP0。无论如何两个APP都能至少有一个稳定的版本

能运行,确保设备正常工作。

4、2 框图

我使用的是STM32F429IGT6;环境是STM32CubeIDE 1.42版本。

由于是bootloader + 双APP,所以UART IAP功能会放在APP处,且存放系统APP版本的参数十分重要,必须也需要一个备份区。

即bootloader主要负责选择启动哪APP,还有最重要的是负责对于系统APP版本参数检查及矫正修改。

APP功能:LED闪烁 + IAP功能。

STM32F429IGT6内部flash扇区表

先将芯片内部flash 1MB分成五个区域,分别存放bootloader,系统参数存储区、系统参数备份区域和两个APP程序。

前面32kB放bootloader;由于STM32F4擦除最小单位是扇区,后面两个16kB用于系统参数存储区、系统参数备份区域;

剩下的空间都用于存放APP,但是剩下1个64kB 和 7个128kB没法均分,故分成APP0 448kB 和 APP1 512kB.


 

4、3 bootloader编写

做双APP的话,最重要的就是保护系统参数的正确性,否则丢失的话可能导致启动APP失败。

看门狗的作用是当跳转APP后程序跑飞后能复位重新选择启动;只有当APP程序正常启动后能重新初始化看门狗及喂狗。

流程图:

结构体定义:

typedef enum {APPLICATION_NORMAL = 0,		// APP能正常稳定运行APPLICATION_UPDATED,		// APP刚更新完成,等待测试启动APPLICATION_ERROR,			// APP错误,不能正常工作
}Application_Status_t;typedef	__PACKED_STRUCT{uint32_t Device_id;					// 设备号uint32_t Hardware_Version;			// 硬件版本信息uint32_t Application0_Version;		// APP0软件版本uint32_t Application1_Version;		// APP1软件版本uint32_t Application0_Status;		// APP0的状态. @Application_Status_tuint32_t Application1_Status;		// APP1的状态. @Application_Status_tuint8_t  Server_Address[64];		// 服务器的地址uint32_t Server_Port;				// 服务器的端口号uint8_t  Server_Key[4];				// 服务器的登录密钥,随时更新uint32_t SystemParamCRC;
}SystemParamTypeDef;

这个函数System_ParamReadCheck();用于每次上电检测系统参数存放的两个区的参数正确性,

若不正确及时修正。  我用的是STM32内部CRC校验数据的正确性, 用到了CRC校验的话一定要注意

结构体的字节对齐,尽量内部参数用4字节对齐。我以前做这个吃过字节对齐亏,hahahahh~

1、读取主区和备份区的系统参数分别进行CRC校验。

2、   (1)系统参数主存储区的数据校验正确,
         (2)开始校验系统参数备份区的数据:
         (3)如果备份区的数据CRC校验正确,则进行对比数据一致性,不一致则将主区数据写入备份区
         (4)如果备份区的数据CRC校验错误,则将主区的系统参数据写入备份区;

3、   (1)系统参数主存储区的数据校验错误
         (2) 开始校验系统参数备份区的数据:
         (3)如果备份区的数据CRC校验正确,则将备份区数据写入主区
         (4)如果备份区的数据CRC校验错误,则将默认系统参数数据写入两个区域,强行启动APP0;

void System_ParamReadCheck(SystemParamTypeDef *pData)
{uint32_t crcdatalen = 0;SystemParamTypeDef sysparam, sysparambackup;/* 读取主区和备份区的系统参数 */FLASH_If_Read(SYSTEMPARAM_ADDRESS, (uint32_t*) &sysparam, sizeof(SystemParamTypeDef) / 4);FLASH_If_Read(SYSTEMPARAM_BACKUPADDRESS, (uint32_t*) &sysparambackup, sizeof(SystemParamTypeDef) / 4);// 去掉系统参数CRC的数据校验长度crcdatalen = sizeof(SystemParamTypeDef) / 4 - 1;/* 主系统参数区放的的数据CRC校验  */if (HAL_CRC_Calculate(&hcrc, (uint32_t*) &sysparam, crcdatalen)	== sysparam.SystemParamCRC){/**  @ 系统参数主存储区的数据校验正确,*  @ 开始校验系统参数备份区的数据:*    1、如果备份区的数据CRC校验正确,则进行对比数据一致性,不一致则将主区数据写入备份区*    2、如果备份区的数据CRC校验错误,则将主区的系统参数据写入备份区;* */printf("System parameter main sector data checked OK\r\n");if (HAL_CRC_Calculate(&hcrc, (uint32_t*) &sysparambackup, crcdatalen) == sysparambackup.SystemParamCRC){printf("System parameter backup sector data checked OK\r\n");if (memcmp(&sysparam, &sysparambackup, sizeof(SystemParamTypeDef)) != 0){printf("System parameter main sector and backup sector data are different, update backup sector data\r\n");memcpy(&sysparambackup, &sysparam, sizeof(SystemParamTypeDef));System_ParamUpdate(SYSTEMPARAM_BACKUPADDRESS, &sysparambackup);}else printf("System parameter main sector and backup sector data are the same\r\n");}else{printf("System parameter backup sector data checked Fail, update backup sector data\r\n");memcpy(&sysparambackup, &sysparam, sizeof(SystemParamTypeDef));System_ParamUpdate(SYSTEMPARAM_BACKUPADDRESS, &sysparambackup);}}else{/**  @ 系统参数主存储区的数据校验错误*  @ 开始校验系统参数备份区的数据:*    1、如果备份区的数据CRC校验正确,则将备份区数据写入主区*    2、如果备份区的数据CRC校验错误,则将默认系统参数数据写入两个区域,强行启动APP0;* */printf("System parameter main sector data checked Fail\r\n");if (HAL_CRC_Calculate(&hcrc, (uint32_t*) &sysparambackup, crcdatalen) == sysparambackup.SystemParamCRC){printf("System parameter backup sector data checked OK, update master sector data\r\n");memcpy(&sysparam, &sysparambackup, sizeof(SystemParamTypeDef));System_ParamUpdate(SYSTEMPARAM_ADDRESS, &sysparam);}else{printf("System parameter main sector and backup sector data checked Fail, Restore defaults\r\n");SystemParam_default.SystemParamCRC = HAL_CRC_Calculate(&hcrc, (uint32_t*) &SystemParam_default, crcdatalen);memcpy(&sysparam, &SystemParam_default, sizeof(SystemParamTypeDef));memcpy(&sysparambackup, &SystemParam_default, sizeof(SystemParamTypeDef));System_ParamUpdate(SYSTEMPARAM_ADDRESS, &sysparam);System_ParamUpdate(SYSTEMPARAM_BACKUPADDRESS, &sysparambackup);}}memcpy(pData, &sysparam, sizeof(SystemParamTypeDef));printf("Hardware_Version = 0x%08lX\r\n", pData->Hardware_Version);printf("Application0_Version = 0x%08lX\r\n", pData->Application0_Version);printf("Application0_Status  = 0x%08lX\r\n", pData->Application0_Status);printf("Application1_Version = 0x%08lX\r\n", pData->Application1_Version);printf("Application1_Status  = 0x%08lX\r\n", pData->Application1_Status);
}

这个System_SelectBootAddress()函数用于选择启动哪个APP,返回APP地址进行调整。

1、选择哪个APP版本高。

2、判读APP的状态标志位。

      (1)APPLICATION_NORMAL:表示APP可以正常稳定工作。

       (2)APPLICATION_UPDATED:表示刚IAP更新的程序固件,把APP的status改成

                错误标志位 APPLICATION_ERROR。这么做的是为了如果首次启动成功后APP会

                修正这个标志位为APPLICATION_NORMAL,表示正常稳定工作,下次可以启动。

                如果首次启动失败后,则看门狗复位后再次选择启动的时候这个APP的状态标志位

                是错误就不会启动这个APP了。

       (3)APPLICATION_ERROR:表示这个APP是不能启动的。只能强行启动另一个APP,无论结果。

uint32_t System_SelectBootAddress(SystemParamTypeDef *pData)
{uint32_t BootAddress = 0;if (pData->Application0_Version >= pData->Application1_Version){if (pData->Application0_Status == APPLICATION_NORMAL){/* 正常启动 */printf("APP0 APPLICATION_NORMAL. Run the APP0.\r\n");BootAddress = APPLICATION0_ADDRESS;}else if (pData->Application0_Status == APPLICATION_UPDATED){/** 刚IAP更新的程序固件,把APP的status改成 错误标志位 @APPLICATION_ERROR,* 1、若APP跳转运行正常后会修改成正常启动标志 @APPLICATION_NORMAL。* 2、若APP跳转运行失败(跑飞),那么看门狗会复位,重启后该APP是错误标志不会启动。* 3、清空版本信息,待启动修正。* 4、更新系统参数,写入flash* */printf("APP0 APPLICATION_UPDATED. Clear APP0 system parameters, Run the APP0.\r\n");pData->Application0_Version = 0;pData->Application0_Status = APPLICATION_ERROR;System_ParamUpdate(SYSTEMPARAM_ADDRESS, pData);System_ParamUpdate(SYSTEMPARAM_BACKUPADDRESS, pData);BootAddress = APPLICATION0_ADDRESS;}else{/* 由于APP0信息错误,强制启动APP1 */printf("APP0 APPLICATION_ERROR. Forced the APP1 to run.\r\n");BootAddress = APPLICATION1_ADDRESS;}}else{if (pData->Application1_Status == APPLICATION_NORMAL){printf("APP1 APPLICATION_NORMAL. Run the APP1.\r\n");BootAddress = APPLICATION1_ADDRESS;}else if (pData->Application1_Status == APPLICATION_UPDATED){printf("APP1 APPLICATION_UPDATED. Clear APP1 system parameters, Run the APP1.\r\n");pData->Application1_Version = 0;pData->Application1_Status = APPLICATION_ERROR;System_ParamUpdate(SYSTEMPARAM_ADDRESS, pData);System_ParamUpdate(SYSTEMPARAM_BACKUPADDRESS, pData);BootAddress = APPLICATION1_ADDRESS;}else{/* 由于APP1信息错误,强制启动APP0 */printf("APP1 APPLICATION_ERROR. Forced the APP0 to run.\r\n");BootAddress = APPLICATION0_ADDRESS;}}return BootAddress;
}

bootloader 主函数:

其中跳转前要做的是bootloader用到什么外设和中断一定要关闭。

原因就是如果在APP端如果外设和中断复用基本没问题。

比如APP用freeRTOS,bootloader不带实时操作系统,那么freeRTOS一般会用外设定时器TIMx,而不用滴答时钟作为系统时钟。在bootloader调整前如果不关闭滴答时钟的定时器的话,则在APP端会跑飞;原因就是APP由于flash链接地址变了,APP会进行中断向量表的偏移,而新的中断向量表滴答时钟中断为空。

int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();MX_CRC_Init();MX_IWDG_Init();/* USER CODE BEGIN 2 */printf("\r\nSudaroot, This is a advanced bootloader\r\n");/* system param read check */System_ParamReadCheck(&SystemParam);/* execute the new program */HAL_IWDG_Refresh(&hiwdg);JumpAddress = System_SelectBootAddress(&SystemParam);JumpAddress = *(__IO uint32_t*) (JumpAddress + 4);printf("Start program execution......\r\n\n\n");/** 关闭或反初始化前面用到的外设和中�?* 1、反初始化UART* 2、关闭系统滴答定时器中断* */HAL_UART_DeInit(&huart1);HAL_SuspendTick();/* Refresh the IWDG. */HAL_IWDG_Refresh(&hiwdg);/* Jump to user application */JumpToApplication = (pFunction) JumpAddress;/* Initialize user application's Stack Pointer */__set_MSP(*(__IO uint32_t*) (JumpAddress - 4));JumpToApplication();/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}

4、4 APP程序编写

流程图:

首先说明一点:APP的版本号是由程序写死的,不能说由外部指定。外部可以告知有没有新版更新,

即使告知新版本的具体版本号也只是仅供参考,不能当做实际版本号。如下程序定义了APP0的版本号。

/* Define APP0 software version */
#define	APPLICATION0_VERSION		0x00000010U

这个函数System_ParamReadCheckUpdate()作用就是检查APP的软件版本号和状态标志位,

如果需要修改标志,则修改完成后需要重启后进入bootloader重新进行APP选择,确保启动的是版本稳定且最新。

这个的作用就和bootloader修改首次启动的APP相关联,APP启动成功后会自己修正。

void System_ParamReadCheckUpdate(SystemParamTypeDef *pData)
{uint32_t flash_update = 0;FLASH_If_Read(SYSTEMPARAM_ADDRESS, (uint32_t*)pData, sizeof(SystemParamTypeDef) / 4);/* APP0的软件版本号与固件内部软件版本号不一致,更新版本号并写入Flash */if(pData->Application0_Version != APPLICATION0_VERSION){flash_update = 1;printf("Update APP0 Software Version : 0x%08X\r\n", APPLICATION0_VERSION);pData->Application0_Version = APPLICATION0_VERSION;}/* APP0 启动了,但是APP0的状态是错误标志.* 可能是由于更新完成后,第一次启动标记的,* 修正为APPLICATION_NORMAL,重新写入flash* */if(pData->Application0_Status != APPLICATION_NORMAL){flash_update = 1;printf("Update APP0 Status, APPLICATION_NORMAL\r\n");pData->Application0_Status = APPLICATION_NORMAL;}/* 如果flash_update 等于 1, 即flash需要更新主存储区和备份区的系统参数数据* 更新数据后,需要重启* 注意:更新数据时,必须擦除一个扇区后写入完成后,才能更新另一个区域数据,否则有概率丢失两个扇区数据* */if(flash_update == 1){pData->SystemParamCRC = HAL_CRC_Calculate(&hcrc, (uint32_t *)pData, sizeof(SystemParamTypeDef) / 4 - 1);System_ParamUpdate(SYSTEMPARAM_ADDRESS, pData);System_ParamUpdate(SYSTEMPARAM_BACKUPADDRESS, pData);printf("device reboot\r\n");HAL_NVIC_SystemReset();}
}

这个函数SystemParam_IAP主要作用主要升级APP。记住一点就行了:升级的永远是没有运行的那个APP区域。

写入bin文件完成后把版本号改成0xFFFFFFFF,上面说过了,这个版本号无所谓。升级完成后重启,进入bootloader。

void SystemParam_IAP(SystemParamTypeDef *pData)
{uint32_t temp = 0;uint32_t file_length= 0;uint32_t timeout = HAL_MAX_DELAY;printf("System IAP start, update APP1\r\n");/* Download user application in the Flash *//* 1. erase user application1 area */printf("Wait for the internal Flash erase to complete\r\n");if(FLASH_If_Erase(APPLICATION1_ADDRESS, APPLICATION1_SECTOR_NUM) == 1){printf("Erase the internal Flash is fail\r\n");Error_Handler();}printf("Erase the internal Flash is complete\r\n");/* 2. download a file via serial port *//* Clean the input path */__HAL_UART_FLUSH_DRREGISTER(&huart1);printf("Waiting for the file to be sent ... \r\n");while(1){if(HAL_UART_Receive(&huart1, uart_buf, UART_BUF_SIZE, timeout) != HAL_OK){temp = UART_BUF_SIZE - (huart1.RxXferCount + 1);FLASH_If_Write(APPLICATION1_ADDRESS + file_length, (uint32_t*)uart_buf, file_length / 4);file_length = file_length + temp;break;}timeout = 1000;FLASH_If_Write(APPLICATION1_ADDRESS + file_length, (uint32_t*)uart_buf, UART_BUF_SIZE / 4);file_length = file_length + UART_BUF_SIZE;}printf("Programming Completed Successfully! %ldBtye\r\n", file_length);printf("Update system parameters\r\n");pData->Application1_Version = 0xFFFFFFFF;pData->Application1_Status  = APPLICATION_UPDATED;pData->SystemParamCRC = HAL_CRC_Calculate(&hcrc, (uint32_t *)pData, sizeof(SystemParamTypeDef) / 4 - 1);System_ParamUpdate(SYSTEMPARAM_ADDRESS, pData);System_ParamUpdate(SYSTEMPARAM_BACKUPADDRESS, pData);printf("device reboot\r\n");HAL_NVIC_SystemReset();
}

APP的主函数:

APP运行后发送‘1’即可进入IAP模式。

int main(void)
{/* USER CODE BEGIN 1 */uint8_t key = 0;SCB->VTOR = APPLICATION0_ADDRESS;/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();MX_IWDG_Init();MX_CRC_Init();MX_TIM2_Init();/* USER CODE BEGIN 2 */HAL_TIM_Base_Start_IT(&htim2);printf("\r\nSudaroot, This is IAP Application0\r\n");System_ParamReadCheckUpdate(&SystemParam);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){printf("\r\n=================== Main Menu ============================\r\n\n");printf("  IAP:Download user application to the internal Flash ----- 1\r\n\n");printf("  Current System APP0 Version: 0x%08lX\r\n", SystemParam.Application0_Version);/* Clean the input path */__HAL_UART_FLUSH_DRREGISTER(&huart1);if(HAL_UART_Receive(&huart1, &key, 1, HAL_MAX_DELAY) == HAL_OK){if(key == '1'){SystemParam_IAP(&SystemParam);}}/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}

4、5 演示效果:

视频链接:STM32 UART双APP的IAP_哔哩哔哩_bilibili

STM32 UART双APP的IAP

4、6 源码下载:

STM32UART双APP的IAP.rar_stm32双app-嵌入式文档类资源-CSDN下载

5 过程经验分享

1、芯片内部Flash擦写的时候是无法响应中断的,故在发送需要写入flash的数据 间隔时间 要相应拉长。非常重要~~~@@@!!!

2、CRC校验问题与字节对齐问题,CRC校验与字节数相关的。

当时我在裸板上写bootloader和APP带RTOS的校验结果不一样,导致数据一直不通过,最后发现是裸板默认是4字节对齐,RTOS是单字节对齐。最后我全改成4字节对齐了。

3、芯片内部Flash擦写次数是十分有限的,大概是10K次,故如果某些数据是频繁写入内部芯片flash,建议最好改用外部flash。

4、关于生产的问题,由于是多个程序烧写,为了生产的高效性只能想办法合成一个程序下发生产。

有两种选择:(1)使用bin文件合成工具。

                        (2)自己用系统编程的方式编写一个程序用于合成。

                     (3)把所有程序都烧录进ST芯片,再整个芯片程序读出,这样就合成一个程序了,且十分稳定。

5、程序跳转前必须关闭用到的外设和中断,关键是中断,否则跑飞。

再次提醒特别是使用了RTOS系统,由于像FreeRTOS会用外设TIMx做系统时钟,跳转前一定要关闭系统滴答时钟中断,否则跑飞。

        特别注意:HAL_Init()会重新初始化systick时钟,故在APP跳转后的初始化需注释掉。

         

6、文章中APP程序分别使用了两个程序,有人提出疑问说是否需要维护两个APP程序?

其实我在实际项目中也只是维护一个程序代码。原因是APP0和APP1的代码差异非常小,用宏定义#define区分即可,但是每次编译必须检查编译器设置的flash地址是否与宏定义一致。 文章中使用两个APP是我想两个APP代码表现的更加简单明了。

7、调试APP程序的时候,不要带bootloader,把APP的flash地址改回0x08000000,待功能调试完成后再修改设置编译下发测试。

8、判断下发的bin文件是不是所需的bin文件?比如说在APP0中升级APP1,怎么知道下方的bin文件是APP0还是APP1呢?

其实很简单,如果你对STM32启动流程熟悉的话,就会发现bin文件的第2个(4字节)复位地址,这个地址会与APP的flash链接地址有关。下面可以看图,APP0的flash地址是0x08010000,APP1的flash地址是0x08080000。接收第一包bin文件后判断复位地址即可知道下发的APP bin文件是否是所需的。

9、如果测试我的代码时发现用串口助手下发APP bin文件,然后启动下发APP,启动不了。原因就是APP下发丢包了。

那么你肯定没认真理解第1点经验分享的重要性。解决如下:把下发的延时适当拉长。

10、如果是用于项目生产,无线升级过程中,必须考虑   断电断网  和   网络接收到的数据错误请求重发  的问题。

断电和断网是一类问题,归APP固件数据接收不完全。解决:加入下发APP固件前,先发APP固件的长度和CRC校验  ,还有加上  数据接收超时就行,超时就中止升级。最后才更新芯片内部flash对应的标志位。

网络接收到的数据错误请求重发。解决:重发咯。重发可以是包(整个APP固件肯定分包下发的)重发,并不一定是整个APP固件重发。

我上面的垃圾代码没有实现这几个功能,给点思路参考,最后自己动手动脑。

11、APP的版本号是由固件程序写死的,不能说由外部指定。外部可以告知有没有新版更新,即使告知新版本的具体版本号也只是仅供参考,不能当做实际版本号。

  全篇完。

本人是一个嵌入式未入门小白,博客仅仅代表我个人主观见解,记录成长笔记。
笔记是以最简单的方式,只展示最核心的原理。
若有与 大神大大 见解有歧义,我绝对坚信 大神大大 见解是对的,我的是错的。
若无积分等无法下载源码,可加入QQ群657407920下载交流经验。感谢~!

 

这篇关于STM32CubeIDE IAP原理讲解,及UART双APP交替升级IAP实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/697371

相关文章

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur

C#实现文件读写到SQLite数据库

《C#实现文件读写到SQLite数据库》这篇文章主要为大家详细介绍了使用C#将文件读写到SQLite数据库的几种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下... 目录1. 使用 BLOB 存储文件2. 存储文件路径3. 分块存储文件《文件读写到SQLite数据库China编程的方法》博客中,介绍了文

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

JAVA利用顺序表实现“杨辉三角”的思路及代码示例

《JAVA利用顺序表实现“杨辉三角”的思路及代码示例》杨辉三角形是中国古代数学的杰出研究成果之一,是我国北宋数学家贾宪于1050年首先发现并使用的,:本文主要介绍JAVA利用顺序表实现杨辉三角的思... 目录一:“杨辉三角”题目链接二:题解代码:三:题解思路:总结一:“杨辉三角”题目链接题目链接:点击这里

基于Python实现PDF动画翻页效果的阅读器

《基于Python实现PDF动画翻页效果的阅读器》在这篇博客中,我们将深入分析一个基于wxPython实现的PDF阅读器程序,该程序支持加载PDF文件并显示页面内容,同时支持页面切换动画效果,文中有详... 目录全部代码代码结构初始化 UI 界面加载 PDF 文件显示 PDF 页面页面切换动画运行效果总结主

SpringBoot实现基于URL和IP的访问频率限制

《SpringBoot实现基于URL和IP的访问频率限制》在现代Web应用中,接口被恶意刷新或暴力请求是一种常见的攻击手段,为了保护系统资源,需要对接口的访问频率进行限制,下面我们就来看看如何使用... 目录1. 引言2. 项目依赖3. 配置 Redis4. 创建拦截器5. 注册拦截器6. 创建控制器8.