STM32固件升级OTA

在物联网(IoT)和嵌入式系统领域,设备的远程升级能力变得越来越重要。它允许开发者在不直接物理接触设备的情况下,通过网络更新固件或软件,极大地提高了产品的可维护性和用户体验。本文将详细介绍如何在STM32微控制器上实现基本OTA升级,涵盖从环境搭建、协议选择、固件打包、传输及更新流程等关键环节。

一、环境搭建与工具选择

1. 硬件平台

2. 软件与工具

  • STM32CubeMX
  • MDK

3. 功能说明

Bootloader程序:开机3s内发送升级文件,自动升级,也可以配合APP程序进行升级(在APP程序中需要写接收函数)

APP程序:使用Ymodem接收函数实时接收升级文件(如资源不够可以只使用Bootloader程序完成开机3s内升级)

二、基本概念原理

1. IAP编程

  • STM32的IAP(In-Application Programming,在应用编程)编程原理主要涉及在微控制器(MCU)运行时,通过某种通信接口(如USB、USART等)对微控制器内部Flash存储器的部分区域进行编程或更新固件程序。

2. Flash存储器分区

为了实现IAP功能,通常需要将STM32的内部Flash存储器划分为多区域:

  • Bootloader区:存放引导加载程序(Bootloader),这是程序执行的初始入口。Bootloader负责检测外部是否有固件更新请求,并在满足条件时执行固件更新操作。Bootloader程序一般出厂后固定下来,不轻易更改。
  • User Application 1区:存放用户应用程序(User Application),这是设备的主要功能代码。当需要更新固件时,这部分代码会被Bootloader擦除并重新写入新的固件。
  • User Application 2区:存放用户最新新的固件。
  • 在本次使用的rct6内部Flash分配如下
    • 因此在开发过程中需要准备3个程序:bootloader升级程序、APP1初始程序、需要更新的程序

3. Bootloader的工作流程

Bootloader的工作流程通常包括以下几个步骤:

  • 上电启动:STM32上电后,首先从Flash的0x0800 0000地址开始执行Bootloader程序。
  • 检测更新条件:Bootloader检查是否有固件更新的条件被触发,如特定按键被按下、串口接收到特定数据等,本次实验中通过检查flash特定区域的值来判断程序状态。
  • 固件更新:如果检测到更新条件,Bootloader通过预留的通信接口接收新的固件数据,并将其写入APP1区。
  • 跳转执行:固件更新完成后,Bootloader将程序指针跳转到APP1区的中断向量表,开始执行新的固件程序。

4. Ymodem 协议的操作流程

Ymodem 协议的基本操作流程可以分为以下几个步骤:

  1. 传输初始化:由接收方发起,发送一个字符 'C'(ASCII 码为 0x43)作为传输开始的信号。发送方在收到该信号后,准备发送起始帧。
  2. 起始帧发送:发送方发送起始帧,包含文件名、文件大小等信息。起始帧以 SOH(Start of Header,0x01)作为帧头,后跟包号(固定为 0)、包号反码、文件名、文件大小、填充区以及 CRC 校验值。
  3. 数据帧发送:在接收方确认起始帧无误后,发送方开始发送数据帧。数据帧以 SOH 或 STX(Start of Text,0x02,表示 1024 字节数据块)作为帧头,后跟包号、包号反码、数据块和 CRC 校验值。
  4. 接收确认:接收方在收到数据帧后,进行 CRC 校验。如果校验通过,则发送 ACK(Acknowledge)信号给发送方;如果校验失败,则发送 NAK 信号,要求重发当前数据块。
  5. 结束帧发送:当所有数据块发送完毕后,发送方发送一个结束帧(EOT,End of Transmission,0x04)。接收方在第一次收到 EOT 时,以 NAK 应答进行二次确认。发送方在收到 NAK 后,重发 EOT,接收方第二次收到 EOT 时,以 ACK 应答,表示文件传输结束。

具体过程如下:

发送端----------------------------------------------------------------接收端

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C

SOH 00 FF “foo.c” "1064’’ NUL[118] CRC CRC >>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C

STX 01 FE data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

STX 02 FD data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

STX 03 FC data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

STX 04 FB data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

SOH 05 FA data[100] 1A[28] CRC CRC>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

EOT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< NAK

EOT>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C

SOH 00 FF NUL[128] CRC CRC >>>>>>>>>>>>>>>>>>>>>>>

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK

命令

命令码

备注

YMODEM_SOH

0x01

133字节长度帧头

YMODEM_STX

0x02

1024字节长度帧头

YMODEM_EOT

0x04

文件传输结束命令

YMODEM_ACK

0x06

接受正确应答命令

YMODEM_NAK

0x15

重传当前数据包请求命令

YMODEM_CAN

0x18

取消传输命令,连续发送5个该命令

YMODEM_C

0x43

字符C

三、代码编写

在实现过程中我们共用到3个程序,BootLoader程序、APP1(初始程序)、APP2(更新程序)

  • 执行BootLoader程序,判断是否有新文件接收,如果有接收文件;判断APP2区域的状态,是否有更新程序,如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
  • 执行App1程序, 因为BootLoaderApp1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
  • 在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:

1. BootLoader的编写

将APP2区的最后一个字(0x80018FFC)用来表示APP2区是否需要升级的状态,

    • 0xFFFFFFFF 表示没有需要升级的程序,STM32在擦除之后Flash的数据存放是0xFFFFFFFF
    • 0xAAAAAAAA 表示有需要更新的程序

流程图如下:

/*串口2接收升级文件采用DMA+空闲中断接收*/
/*串口1打印文件信息*/
/*主函数部分*/
void Board_Run(void)
{
/*Ymodem recv updata--start*/
	if(Get_state()==TO_START)
	{
		send_command(&huart2,CCC);
		HAL_Delay(1000);
		times++;
	}
	/*usart2--接收*/
	if(g_USART2_Recv_Flag)
	{
		g_USART2_Recv_Flag=0;
		/* USER CODE BEGIN 解析数据 内容:g_USART2_Recv_Data_BAK,长度:g_usart2_recv_len*/
		printf("> UPLoad APP ......\r\n");
		ymodem_fun(&huart2,g_USART2_Recv_Data_BAK,g_usart2_recv_len);
		/* USER CODE END */
	}
/*Ymodem recv updata--end*/	
	
/* jump to app*/
	
	printf("> Wait %d s ......\r\n",times);	
	
	if(times == 3)//等待3s
	{
		times = 0;
		Start_BootLoader();
	}	
}

/*部分函数*/
void Start_BootLoader(void)
{
	/*==========打印消息==========*/  
	printf("> Read startup mode......\r\n");	
	switch(Read_Start_Mode())										//读取是否启动应用程序
	{
		case Startup_Normol:										//正常启动
		{
			printf("> Normal start......\r\n");
			Jump_APP_Flag = 1;
			Wait_NewFile_Flag = 0;
			break;
		}
		case Startup_Update:										//升级再启动
		{
			printf("> Start update......\r\n");		
			MoveCode(Application_2_Addr, Application_1_Addr, Application_Size);
			Jump_APP_Flag = 1;
			Wait_NewFile_Flag = 0;
			printf("> Update down......\r\n");
			break;
		}
		default:													//启动失败
		{
			printf("> Error:%X!!!......\r\n", Read_Start_Mode());
			return;			
		}
	}
	
	/* 跳转到应用程序 */
	printf("> Start up......\r\n\r\n");	
	IAP_ExecuteApp(Application_1_Addr);
}

2. APP程序Ymodem接收升级文件

2.1. 修改中断向量

由于APP代码运行的起始位置不在0x08000000,其栈顶地址发生偏移,对应的中断向量表地址也整体发生偏移,因此,需要对APP代码的中断向量表偏移进行设置

SCB->VTOR = FLASH_BASE | 0x00005000UL;/* 更改中断向量表地址,此文中APP1的起始地址为0x800 5000 */
2.2. Ymodem接收函数
/*YModem升级需要结合串口2接收一起使用*/
/**
 * @bieaf YModem升级
 *
 * @param none
 * @return none
 */
void ymodem_fun(UART_HandleTypeDef *huart,uint8_t* data,int len)
{	
	switch(data[0])
	{
		case SOH:///<数据包开始
		{
			static unsigned char data_state = 0;
			static unsigned int app2_size = 0;
			if(Check_CRC(data, len)==1)///< 通过CRC16校验
			{					
				if((Get_state()==TO_START)&&(data[1] == 0x00)&&(data[2] == (unsigned char)(~data[1])))///< 开始
				{
					printf("> Receive start...\r\n");

					Set_state(TO_RECEIVE_DATA);
					data_state = 0x01;						
					send_command(huart,ACK);
					send_command(huart,CCC);

					/* 擦除App2 */							
					Erase_page(Application_2_Addr, 40);
				}
				else if((Get_state()==TO_RECEIVE_END)&&(data[1] == 0x00)&&(data[2] == (unsigned char)(~data[1])))///< 结束
				{
					printf("> Receive end...\r\n");

					Set_Update_Down();						
					Set_state(TO_START);
					send_command(huart,ACK);
					HAL_NVIC_SystemReset();
				}					
				else if((Get_state()==TO_RECEIVE_DATA)&&(data[1] == data_state)&&(data[2] == (unsigned char)(~data[1])))///< 接收数据
				{
					printf("> Receive data bag:%d byte\r\n",data_state * 128);
					
					/* 烧录程序 */
					WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&data[3]), 32);
					data_state++;
					
					send_command(huart,ACK);		
				}
			}
			else
			{
				printf("> Notpass crc\r\n");
			}
			
		}break;
		case EOT://数据包开始
		{
			if(Get_state()==TO_RECEIVE_DATA)
			{
				printf("> Receive EOT1...\r\n");
				
				Set_state(TO_RECEIVE_EOT2);					
				send_command(huart,NACK);
			}
			else if(Get_state()==TO_RECEIVE_EOT2)
			{
				printf("> Receive EOT2...\r\n");
				
				Set_state(TO_RECEIVE_END);					
				send_command(huart,ACK);
				send_command(huart,CCC);
			}
			else
			{
				printf("> Receive EOT, But error...\r\n");
			}
		}break;	
	}
}

四、下载与升级

1. BootLoader的下载

  • BootLoader不需要特别设置代码的下载位置
  • 按照下图, 修改擦除方式为Erase Sectors, 空间大小为0X5000(20K)
  • 下载成功后串口1会输出相关过程信息

2. APP程序下载

  • 根据FLASH分区,APP代码存放在紧挨着BootLoader之后,因此APP的起始位置应该为0x08000000+0x5000 = 0x08005000,
  • 占用内存大小为0xA000(40kB),在MDK中打开options for target,设置IROM1 Start = 0x08005000,Size = 0xA000,在Flash Download中设置下载Flash位置,参数与IROM1中设置的Start和Size相同

3. 生成更新bin文件

需要更新的程序是需要bin文件进行传输下载,利用MDK可以生成,在user选项卡中填写生成命令:

$K\ARM\ARMCC\bin\fromelf.exe --bin --output=Bin\@L.bin !L

4. 传输升级文件

通过Ymodem协议传输bin文件的上位机软件有很多,本次选用的是Xshell进行传输,

  • 新建会话,连接串口
  • 右键传输选择Ymodem发送
  • 连接成功后,会看到Ymodem协议出发的"C",右键传输选择Ymodem发送,选择刚刚生成的bin文件
  • 升级完成后串口打印升级后程序信息

五、源码地址:

https://download.csdn.net/download/LJ_96/89770628