在物联网(IoT)和嵌入式系统领域,设备的远程升级能力变得越来越重要。它允许开发者在不直接物理接触设备的情况下,通过网络更新固件或软件,极大地提高了产品的可维护性和用户体验。本文将详细介绍如何在STM32微控制器上实现基本OTA升级,涵盖从环境搭建、协议选择、固件打包、传输及更新流程等关键环节。
一、环境搭建与工具选择
1. 硬件平台
- STM32开发板:本次采用的是STM32F103RCT6为主控的开发板,选好主控后需要了解主控的Flash
- STM32F103RCT6的Flash为256KB,属于大容量产品,其Flash的一页大小为2K,共128页
- stm32f1系列芯片参考:《stm32f10xxx 闪存编程参考手册》https://www.st.com.cn/resource/en/programming_manual/pm0075-stm32f10xxx-flash-memory-microcontrollers-stmicroelectronics.pdf
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 协议的基本操作流程可以分为以下几个步骤:
- 传输初始化:由接收方发起,发送一个字符 'C'(ASCII 码为 0x43)作为传输开始的信号。发送方在收到该信号后,准备发送起始帧。
- 起始帧发送:发送方发送起始帧,包含文件名、文件大小等信息。起始帧以 SOH(Start of Header,0x01)作为帧头,后跟包号(固定为 0)、包号反码、文件名、文件大小、填充区以及 CRC 校验值。
- 数据帧发送:在接收方确认起始帧无误后,发送方开始发送数据帧。数据帧以 SOH 或 STX(Start of Text,0x02,表示 1024 字节数据块)作为帧头,后跟包号、包号反码、数据块和 CRC 校验值。
- 接收确认:接收方在收到数据帧后,进行 CRC 校验。如果校验通过,则发送 ACK(Acknowledge)信号给发送方;如果校验失败,则发送 NAK 信号,要求重发当前数据块。
- 结束帧发送:当所有数据块发送完毕后,发送方发送一个结束帧(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
程序, 因为BootLoader
和App1
这两个程序的向量表不一样, 所以跳转到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文件
- 升级完成后串口打印升级后程序信息