文章目录[隐藏]
随言:
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闪烁的。
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功能。
先将芯片内部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_t
uint32_t Application1_Status; // APP1的状态. @Application_Status_t
uint8_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下载交流经验。感谢~!
版权声明:本文为CSDN博主「sudaroot」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sudaroot/article/details/106932736
暂无评论