一、简介

OTA 升级,全称为Over-the-Air Technology(空中下载技术),是一种通过无线通信链路,直接为设备实现固件或软件远程更新的技术,无需物理接触设备即可完成版本迭代。

在设备开发或小批量调试阶段,我们给单片机烧录程序通常需要依赖物理连接:先通过 J-Link、ST-Link 等专用调试工具,将电脑(PC)与单片机建立硬件连接,再借助 Keil、IAR 等集成开发环境(IDE),对单片机的片上 Flash 存储器进行擦除与写入操作,最终完成代码的下载烧录。

但当设备进入批量生产阶段,大量产品已交付至用户手中时,传统物理烧录方式便无法适用 —— 总不能要求用户拆解设备、连接专业工具来更新程序。此时,若单片机预先集成了 WiFi、蓝牙、4G 等远程通信模块,就能通过 OTA 技术解决这一难题:设备可通过通信模块接收云端或控制端下发的新版本固件包,再由单片机内部的升级程序校验固件合法性、执行 Flash 擦写与新固件写入,最终完成自动更新,高效修复 bug 或新增功能。

二、设计思路

1. 无缓冲区方案

在单片机上实现 OTA 升级时,通常采用三区划分的存储结构设计,各区域功能与升级流程如下:

分区规划

  • A 区(Bootloader 区):存储启动引导程序,负责系统初始化、升级判断与程序跳转
  • B 区(APP 区):存储主应用程序,为设备的核心功能实现代码
  • C 区(数据区):保存 OTA 状态标志、升级参数等关键信息

工作流程

  1. 正常启动流程:设备上电后先从 A 区执行 Bootloader 代码,初始化硬件并检查 C 区的 OTA 状态标志。若未检测到升级需求,立即跳转至 B 区执行应用程序,设备进入正常工作状态。
  2. OTA 升级触发:设备在 B 区运行时,通过远程通信模块(如 WiFi、蓝牙)接收 OTA 升级指令后:
    • 先将 C 区的 OTA 标志位置为 "待升级" 状态
    • 执行软件复位命令,使单片机重新启动
  3. 升级执行流程:重启后系统再次从 A 区启动,Bootloader 检测到 C 区的升级标志位已置位:
    • 进入升级模式,通过通信模块接收新固件包
    • 校验固件完整性与合法性后,将新固件写入 B 区 Flash
    • 升级完成后清除 C 区的 OTA 标志位,跳转至 B 区运行新程序

这种设计通过分区隔离与标志位控制,既保证了升级过程的安全性,又能在升级失败时通过 Bootloader 进行故障恢复,显著提升了 OTA 升级的可靠性。

2. 缓冲式方案(bootloader+app+buffer)

核心设计:在原有两区基础上增加独立的 buffer 缓冲区,升级流程为:

  • 接收新固件时先完整存储到 buffer 区
  • 完成传输并校验后,再擦除擦旧 APP 区并将 buffer 区数据写入
  • 全程保持旧 APP 区完整直至新固件验证通过

优势

  • 彻底避免传输中断导致的程序区损坏,即使掉电也仅损失 buffer 区数据
  • 校验机制可提前发现固件完整性问题,防止错误代码写入

局限

  • 需额外分配与 APP 区同等大小的 buffer 空间
  • 增加了一次 Flash 擦写拷贝过程,升级耗时略长

3. 双 APP 交替方案(bootloader+app1+app2)

核心设计:设置两个独立的应用程序区,通过数据区的跳转标志实现版本切换:

  • 正常运行时从 APP1 或 APP2 启动(由标志位指定)
  • 升级时将新固件写入当前未运行的 APP 区(如运行 APP1 时写入 APP2)
  • 验证通过后修改跳转标志,下次启动自动切换到新 APP 区
  • 版本回退仅需重置标志位,无需重新下载

优势

  • 系统始终保持至少一个可用版本,彻底消除 "变砖" 风险
  • 支持快速版本回滚,特别适合对可靠性要求极高的场景
  • 升级过程不影响当前运行的程序,可实现 "热更新" 效果

局限

  • 对 Flash 容量要求最高(需两倍 APP 空间)
  • 应用程序需考虑双区适配,增加了设计复杂度

STM32单片机OTA方案思路

以STM32F103C8T6单片机为例,本次介绍方案1的无缓冲区方案,该单片机共flash大小共128K,flash起始地址为0x08000000,结束地址为0x08001FFFF。单片机上电后代码从0x00000000开始执行,但这里存在一个地址重映射的操作。

STM32单片机可以通过配置BOOT引脚选择不同的启动方式,如图所示:

这里的启动模式选择实际上就是改变0x00000000的重映射地址。当选择从主闪存存储器启动,则0x00000000地址被映射到0x08000000,当上电后代码从0x00000000开始执行实际上是执行的0x08000000地址的代码。

本次设计采用方案一:bootloader+app的OTA方案,首先规划存储空间划分

代码首先从bootloader开始执行,通过判断data区域的OTA标志选择是执行OTA功能下载程序到APP区域还是跳转到APP区域开始执行应用代码。

三、bootloader跳转APP

前言

如何实现程序跳转? 首先ARM系列单片机代码编译生成的固件包.bin文件最开头部分是一个中断向量表。这个中断向量表包含了该单片机所有中断任务函数的入口地址。当产生中断信号后,单片机通过中断序号找到中断向量表对应的序号偏移位置,这个地址存储的数据就是该中断任务处理函数的地址,程序跳转执行中断任务,当中断结束任务处理完成就恢复返回正常代码继续执行。

如图所示为ARM单片机的中断向量表,这里我们只关注前两个地址的信息即MSP初始化复位向量

单片机上电后从中断向量表开始执行,将MSP初始值赋值给MSP寄存器,将复位向量赋值给PC寄存器。MSP代表系统堆栈区指针,堆栈区用于保存局部变量和一些其他信息,PC指针则指向单片机下一条需要执行的命令。上电后两指针赋值完成则代码即可从复位向量开始执行。

然后从代码的启动文件开始分析中断向量表和复位向量

如图所示的startup_stm32f103xb.s就是stm32f103系列单片机的启动文件,最开始的注释就介绍了这个文件的功能,其中就有对PC指针和SP指针的赋值操作。

然后第60行的__initial_sp就是MSP指针的地址信息,61行的Reset_Handler就是复位向量的地址信息,上电后分别将这两个值赋值给MSP指针和PC指针。

DEBUG模式可以看见MSP指针被赋值为0x08000000地址上的数据0x20000500,而PC指针的值则是0x08000004地址上的数据值-1,至于为什么-1,我猜测和PC指针永远指向下一条需要执行的指令的性质有关,说明当前代码暂停在0x08000100的地址上,下一条需要执行的指令地址是0x08000100+1。

至于Reset_Handler如上图所示自128行开始,程序先执行SystemInit进行系统初始化,后执行__main函数跳转到main.c文件的int main(void)函数入口进入我们熟悉的C语言环境开始执行。

好,前言结束,那么代码怎么从bootloader跳转到APP呢?开始正文介绍


理论步骤

关键内容有三个

  1. MSP指针赋值

假设APP固件包被下载到了flash的0x08004000的地址,0x08004000地址就是APP固件包的中断向量表的位置,修改MSP的值为0x08004000地址存储的值,就是单片机启动的第一步:对MSP指针进行初始化。

  1. PC指针赋值

单片机启动第二步:给PC指针赋值让代码从Reset_Handler开始执行。这里说明一下实际上函数的调用就是通过修改PC指针的值指向被调用的函数的起始地址的方式实现的,当调用完成后又将PC指针恢复,所以,我们修改PC指针可以将0x08004004存储的数据(Reset_Handler的起始地址)赋值给一个函数指针,然后调用这个函数指针就可以让PC指针指向Reset_Handler的起始地址。这样我们就实现了单片机启动的第二步:对PC指针初始化

  1. 中断向量表

还没完,虽然我们已经完成了MSP和PC指针的赋值,程序也能正常运行APP代码了。现在假设APP代码中调用串口功能产生了串口发送中断,程序会自动在中断向量表按照当前的中断序号查找对应的中断执行函数并调用这个串口中断执行函数,但是,目前代码中存在两份中断向量表,分别是位于0x08000000起始地址的bootloader代码的中断向量表和位于0x08004000地址的APP代码的中断向量表,那么查表查的是谁的表呢?答案是查的是bootloader代码的中断向量表,所以这是不对的。

因此我们需要修改单片机指向的中断向量表的地址,让代码在APP中执行时查找APP代码的中断向量表。

ARM单片机允许向量表重定位,与之相关的寄存器如图所示:

地址的话,手册上标的有点问题我后面debug看了一下实际地址应该是0xE000Ed00+0x08

寄存器名称是SCB_VTOR(向量表偏移量寄存器)

通过这个寄存器可以实现将中断向量映射到地址0x08004000的地址上从而保证APP代码中执行的中断任务函数是从APP代码的中断向量表查询的。

STM32F103C8T6代码例程

bootloader

bootloader中执行MSP和PC指针重新赋值和一些额外操作,具体函数如下:

typedef void (*pFunction)(void);
uint32_t JumpAddress;
pFunction Jump_To_Application;
void jump_to_APP(uint32_t APP_ADDR)
{
	//堆栈指针起始地址合法性校验
	if (((*(__IO uint32_t*)APP_ADDR) & 0x2FFE0000 ) == 0x20000000)
	{ 
		//关中断
		__disable_irq();
		/* Jump to user application */
		//获取APP代码Reset_Handler的地址
		JumpAddress = *(__IO uint32_t*) (APP_ADDR + 4);
		//Reset_Handler地址强转为函数指针并赋值
		Jump_To_Application = (pFunction) JumpAddress;
		/* Initialize user application's Stack Pointer */
		//MSP指针赋值
		__set_MSP(*(__IO uint32_t*) APP_ADDR);
		//__ASM volatile ("MSR msp, %0" : : "r" (0x8004000) : );
		//函数指针调用,程序跳转到APP的Reset_Handler开始执行
		Jump_To_Application();
	}
}
int main(void)
{

 ......
  while (1)
  {
		if(Flash_Read(APP_OTA_FLAG) == 0xffffffff)
		{
			xmodem_receive();
		}else
		{
			jump_to_APP(APP_ADDR1);
		}

  }
}

当执行完Jump_To_Application();后单片机代码就开始从APP中开始执行了。

这里有个注意点:

调试过程中发现,如果我将变量JumpAddress和Jump_To_Application放在函数jump_to_APP中作为一个局部变量则当执行完__set_MSP函数后变量JumpAddress和Jump_To_Application将会发生变化。

原因是,MSP指向系统堆栈区,而局部变量就是存储在堆栈区的,当我修改了MSP指针指向的地址那么当前存储在堆栈区的变量信息将会指向一个错误的位置,如果是局部变量,执行Jump_To_Application();程序会跳转到一个未知的地址造成程序跑飞。

所以,绝对不要在Jump_To_Application中定义局部变量。

app

在该代码中需要修改中断向量偏移寄存器的值,完成之后打开全局中断,APP代码即可正常执行

方式一:

int main(void)
{
	SCB->VTOR = 0x08000000 | 0x4000; /* Vector Table Relocation in Internal SRAM. */
	__enable_irq();
	.......
}

方式二:

增加宏定义:USER_VECT_TAB_ADDRESS

修改#define VECT_TAB_OFFSET 0x00004000U /*!< Vector Table base offset field.

则单片机初始化时会自动执行SCB->VTOR = 0x08000000 | 0x4000语句,后续只需要重新打开全局中断即可

/**
  * @brief  Setup the microcontroller system
  *         Initialize the Embedded Flash Interface, the PLL and update the 
  *         SystemCoreClock variable.
  * @note   This function should be used only after reset.
  * @param  None
  * @retval None
  */
void SystemInit (void)
{
#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) || defined(STM32F103xE) || defined(STM32F103xG)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl(); 
  #endif /* DATA_IN_ExtSRAM */
#endif 

  /* Configure the Vector Table location -------------------------------------*/
#if defined(USER_VECT_TAB_ADDRESS)
  SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#endif /* USER_VECT_TAB_ADDRESS */
}

四、flash读写

flash读取很简单,指定一个要读取的flash存储地址,将这个地址强转成32位无符号整形指针,用*号访问指针指向地址上的值即可。

uint32_t Flash_Read(uint32_t ReadAddr)
{
	return  (*(__IO uint32_t *)ReadAddr);
}

 flash无校验写

void Flash_Write_NoCheck(uint32_t WriteAddr, uint32_t *pBuffer, uint16_t NumToWrite) 
{
	uint16_t i;
    
    // 解锁Flash
    HAL_FLASH_Unlock();
    // 执行写入操作
    for(i = 0; i < NumToWrite; i++)
    {
        // 写入字(32位)
        if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,WriteAddr, pBuffer[i]) != HAL_OK)
        {
            // 此处仅做简单处理,实际应用中可根据需要添加错误处理
            break;
        }
        WriteAddr += 4;
    }
    // 锁定Flash
    HAL_FLASH_Lock();
}

flash页擦除

注意,此处STM32F103共128Kflash每页大小是1K,当页擦除函数传入一个地址,后将会擦除该地址所属页全部的数据,假设第一页地址为0x08000000~0x080003FF吗,若传入0x08000124地址执行页擦除函数,则第一页将被全部擦除置为全F。

void FLASH_Erase_Page(uint32_t PageAddress)
{
	FLASH_EraseInitTypeDef EraseInitStruct;
	uint32_t PageError;

	EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
	EraseInitStruct.PageAddress = PageAddress;
	EraseInitStruct.NbPages = 1;

	HAL_FLASH_Unlock();
	if (HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK) {
		// 错误处理
		Error_Handler();
	}
	HAL_FLASH_Lock();
}

五、x_modem通信

本次OTA方案采用X_modem CRC方案

当需要通过x_modem接收固件包时,单片机给上位机发送字符串‘C’表示这是X_modem CRC进行传输。上位机收到C后,逐帧发送固件包,当收到ACK应答则继续发送下一帧数据,该通信协议整体还算简单,就不讲解理论了,直接贴代码

#ifndef __XMODEM_OTA_H__
#define __XMODEM_OTA_H__

#include "main.h"
#include "flash.h"

#include "usart.h"

#include <stdio.h>

/* XModem (128 bytes) packet format
 * Byte  0:         Header
 * Byte  1:         Packet number
 * Byte  2:         Packet number complement
 * Bytes 3-132:     Data
 * Bytes 132-133:   CRC
 */

/* 数据接收缓冲区大小(用户定义,至少要大于 3 + 128 + 2) */
#define X_PROT_FRAME_LEN_RECV  150

/* 最大允许错误(用户定义). */
#define X_MAX_ERRORS ((uint8_t)10u)
/* 检测超时错误配置 */
#define X_MAX_TIMEOUT ((uint8_t)10u)

/* 数据包大小. */
#define X_PACKET_128_SIZE   ((uint16_t)128u)
#define X_PACKET_CRC_SIZE   ((uint16_t)2u)

/* 包的相对位置(包括报头). */
#define X_PACKET_NUMBER_INDEX             ((uint16_t)1u)
#define X_PACKET_NUMBER_COMPLEMENT_INDEX  ((uint16_t)2u)
#define X_PACKET_DATA_INDEX               ((uint16_t)3u)
#define X_PACKET_DATA_CRC1 								((uint16_t)131u)
#define X_PACKET_DATA_CRC2 								((uint16_t)132u)
/* 协议定义的字节. */
#define X_SOH ((uint8_t)0x01u)  /**< 包头 (128 bytes). */
#define X_STX ((uint8_t)0x02u)  /**< 包头 (1024 bytes). */
#define X_EOT ((uint8_t)0x04u)  /**< 传输结束. */
#define X_ACK ((uint8_t)0x06u)  /**< 应答. */
#define X_NAK ((uint8_t)0x15u)  /**< 非应答. */
#define X_CAN ((uint8_t)0x18u)  /**< 取消. */
#define X_C   ((uint8_t)0x43u)  /**< ASCII“C”,要通知上位机,我们要用CRC16. */

/* 功能的状态报告. */
typedef enum {
  X_OK            = 0x00u, /**< 传输成功. */
  X_ERROR_CRC     = 0x01u, /**< CRC 校验误差. */
  X_ERROR_NUMBER  = 0x02u, /**< 包数量不匹配错误. */
  X_ERROR_UART    = 0x04u, /**< 传输错误. */
  X_ERROR_FLASH   = 0x06u, /**< Flash 错误. */
  X_ERROR         = 0xFFu  /**< 其他错误. */
} xmodem_status;

typedef enum {
    X_SEND_NULL ,
    X_SEND_C  ,
    X_RECEIVE  ,
    X_END     , 
    X_FAULT ,
} xmodem_step_TYPE;

#define X_UNUSED(Y) (void)Y      /* To avoid gcc/g++ warnings */
  
/***************************** 对外函数 ***************************************/
/* 用户调用 */
void xmodem_receive(void);

uint32_t x_get_tick(void);
int x_transmit_ch(uint8_t ch);
int receive_file_data_callback(uint8_t *file_data, uint32_t w_size);
int receive_file_callback(void *ptr);
/***************************** 对外函数 ***************************************/

#endif

#include "xmodem_ota.h"
#include <string.h>
#include "flash.h"

/* 全局变量. */
static uint8_t recv_buf[X_PROT_FRAME_LEN_RECV];       /* 接收数据缓冲区 */
static uint32_t recv_len;                             /* 接收到数据的长度 */
static uint8_t xmodem_packet_number = 0u;             /* 包计数. */
static xmodem_step_TYPE xmodem_step = X_SEND_NULL;
/* 局部函数. */
static uint16_t xmodem_calc_crc(uint8_t *data, uint16_t length);
static void reset_recv_len(void);
static uint32_t get_recv_len(void);

/**
 * @brief   这个函数是Xmodem协议的基础.
 *          接收数据并处理数据.
 * @param   rec_num:需要接收的文件数量
 * @return  0:文件接收成功 -1:文件接收失败
 */
void xmodem_receive(void)
{
  static uint32_t tickstart = 0;
  static uint32_t err_rev_timeout = 0;
  static uint32_t err_rev_miss = 0;
  // if(初始状态) 发C
  switch (xmodem_step)
  {
  case X_SEND_NULL:
    // 初始化OTA,UART,flash,定时器等相关配置
    xmodem_packet_number = 1;
    reset_recv_len();
	  HAL_UARTEx_ReceiveToIdle_IT(&huart1, recv_buf, 150);
    //HAL_UART_Receive_IT(&huart1, recv_buf, 5);
    tickstart = x_get_tick();
    xmodem_step = X_SEND_C;
    break;
  case X_SEND_C:
    if (
        get_recv_len() >= 133 &&
        recv_buf[0] == X_SOH &&
        recv_buf[1] == xmodem_packet_number)
    {
      xmodem_step = X_RECEIVE;
    }

    if (x_get_tick() - tickstart >= 1000)
    {
      tickstart = x_get_tick();
      /* send C */
      x_transmit_ch(X_C);
    }

    break;
  case X_RECEIVE:
    if (get_recv_len())
    {
      // 检查数据长度,CRC,序号,包头数据信息
      if (
          recv_buf[0] == X_SOH &&
          recv_buf[1] == xmodem_packet_number &&
          xmodem_calc_crc(&recv_buf[X_PACKET_DATA_INDEX], X_PACKET_128_SIZE) == (recv_buf[X_PACKET_DATA_CRC1] << 8 | recv_buf[X_PACKET_DATA_CRC2]))
      {
        // 传输完成执行回调函数 数据下载
        receive_file_data_callback(&recv_buf[X_PACKET_DATA_INDEX], X_PACKET_128_SIZE);
        xmodem_packet_number++;
        x_transmit_ch(X_ACK);
        err_rev_miss = 0;
      }
      else if (recv_buf[0] == X_EOT)
      {
        xmodem_step = X_END;
      }
      else
      {
        x_transmit_ch(X_NAK);// 发送NACK
        xmodem_step = err_rev_miss++>X_MAX_ERRORS?X_FAULT:X_RECEIVE;
      }
      reset_recv_len();
      err_rev_timeout = 0;
    }
    else
    {
      if (x_get_tick() - tickstart >= 1000)
      {
        tickstart = x_get_tick();
        err_rev_timeout++;// 超时计数
        xmodem_step = err_rev_timeout++>X_MAX_TIMEOUT?X_FAULT:X_RECEIVE;
      }
    }
    break;
  case X_END:
    HAL_FLASH_Unlock();
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,APP_OTA_FLAG, 0xAAAAAAAA);
		HAL_FLASH_Lock();
  break;
  case X_FAULT:
    if (x_get_tick() - tickstart >= 1000)
    {
      tickstart = x_get_tick();
      x_transmit_ch(X_CAN);
    }
  break;
  default:

    break;
  }

}

/**
 * @brief   计算接收到包的 CRC-16.
 * @param   *data:  要计算的数据的数组.
 * @param   length: 数据的大小,128字节或1024字节.
 * @return  status: 计算CRC.
 */

static uint16_t xmodem_calc_crc(uint8_t *data, uint16_t length)
{
  uint16_t crc = 0u;
  while (length)
  {
    length--;
    crc = crc ^ ((uint16_t)*data++ << 8u);
    for (uint8_t i = 0u; i < 8u; i++)
    {
      if (crc & 0x8000u)
      {
        crc = (crc << 1u) ^ 0x1021u;
      }
      else
      {
        crc = crc << 1u;
      }
    }
  }
  return crc;
}

/**
 * @brief   复位数据接收长度
 * @param  void.
 * @return  void.
 */
static void reset_recv_len(void)
{
  recv_len = 0;
}

/**
 * @brief   获取数据接收长度
 * @param   void.
 * @return  接收到数据的长度.
 */
static uint32_t get_recv_len(void)
{
  return recv_len;
}

static uint32_t get_xmodem_packet_number(void)
{
  return xmodem_packet_number;
}

/**
 * @brief   Xmodem 发送一个字符的接口.
 * @param   ch :发送的数据
 * @return  返回发送状态
 */
int x_transmit_ch(uint8_t ch)
{
  HAL_UART_Transmit_IT(&huart1, &ch, 1);
  return 1;
}

/**
 * @brief   文件数据接收完成回调.
 * @param   *ptr: 控制句柄.
 * @param   *file_name: 文件名字.
 * @param   file_size: 文件大小,若为0xFFFFFFFF,则说明大小无效.
 * @return  返回写入的结果,0:成功,-1:失败.
 */
 int receive_file_data_callback(uint8_t *file_data, uint32_t w_size)
{
	uint32_t flash_add = 0X4000+((get_xmodem_packet_number()-1)*X_PACKET_128_SIZE);
	uint32_t flash_buffer[X_PACKET_128_SIZE/4] = {0};
	memcpy(flash_buffer,file_data,w_size);
	if(flash_add%0x400 == 0)
	{
		FLASH_Erase_Page(0X08000000+flash_add+w_size);
	}
	Flash_Write_NoCheck(0X08000000+flash_add, flash_buffer, w_size/4);
	
  X_UNUSED(file_data);
  X_UNUSED(w_size);

  /* 用户应该在外部实现这个函数 */
  return -1;
}

/**
 * @brief   一个文件接收完成回调.
 * @param   *ptr: 控制句柄.
 * @return  返回写入的结果,0:成功,-1:失败.
 */
__weak int receive_file_callback(void *ptr)
{
  X_UNUSED(ptr);

  /* 用户应该在外部实现这个函数 */
  return -1;
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if (huart == &huart1)
  {
    recv_len ++;
    HAL_UART_Receive_IT(&huart1, recv_buf,5);
  }
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  if (huart == &huart1)
  {
    recv_len = Size;
    HAL_UARTEx_ReceiveToIdle_IT(&huart1, recv_buf, 150);
  }
}

/**
 * @brief   获取毫秒时间戳.
 * @param   void
 * @return  时间戳
 */
uint32_t x_get_tick(void)
{
  return HAL_GetTick();
}

六、效果展示

1. 芯片全片擦除后下载bootloader

bootloader main函数代码如下:

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();
  /* USER CODE BEGIN 2 */
	uint32_t APP_OTA_FLAG = 0x801fc00;	
	uint32_t APP_ADDR1 = 0x8004000;	
	uint32_t APP_ADDR2 = 0;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		if(Flash_Read(APP_OTA_FLAG) == 0xffffffff)
		{
			xmodem_receive();
		}else
		{
			jump_to_APP(APP_ADDR1);
		}
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

当最后一页的OTA标志是全F,自动进入固件包接收模式,准备开始x_modem协议接收固件包,此时串口应该持续发字符串’C’

2. X_modem发送APP固件包

选择APP代码编译生成的.bin文件x_modem传输

传输完成后会在OTA标志位写入0XAAAAAAAA,代码将会执行jump_to_APP跳转到APP

如图所示:发送hello的部分为app代码。

至此,bootloader跳转APP,以及通过x_modem协议接收app固件包更新app固件版本的基本原理介绍完成。

后续有些细节的东西没写,比如:keil编译如何生成.bin文件,bootloader和app代码如何合并烧录,keil合并工程简化多工程管理,app接收OTAS升级请求擦除OTA标志位并命令重启等等内容。

整篇文章显得虎头蛇尾,原因是非要把细节补充清楚又要写个好几天的,太浪费时间了,总之我大搞讲清楚OTA的基本原理和bootloader跳转APP的原理这两个核心知识点就可以了。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐