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_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

生成海报
点赞 0

sudaroot

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

74HC138译码器的原理和使用

前言 译码器就是将每个输入的二进制代码译成对应的输出高低电平信号,和编码器互为逆过程。 百度百科 74HC138是一款高速CMOS器件,74HC138引脚兼容低功耗肖特基TTL(LSTTL&#xf

ESP32-GY_30光照强度传感器

GY_30光照强度传感器介绍 GY-30光强传感器特点及使用介绍 一、连接引脚 GY_30光照强度传感器使用I2C传输数据 。 5根引脚,名称与功能如下; vcc 为外接供电电源输入端 GND 地线 SCL I2C通信模式时钟