Bootstrap

STM32的OTA升级方案

简介

        本设计是基于STM32F103ZET6的HAL库的串口OTA升级方案,用的是Xmodem+串口的升级方案,代码部分有部分模块参考了别人写的,但整个项目框架是完完全全自己搭建的。接下来就开始分模块介绍本项目。

项目思路

        其实,实现OTA的方案很简单,就是把STM32的flash分成两个分区,一个分区叫BOOT分区(本项目由于是基于STM32实现,就叫SBOOT),另一个分区就是程序分区(APP),是你板子启动后运行的功能。有了这两个分区,想要实现OTA升级,就很好办了。用BOOT分区去擦写程序分区,便可实现对程序的更新。而程序分区则和我们正常写的单片机程序差别不大,主要是自己想实现的功能。所以实现整个OTA升级的关键是在BOOT分区的搭建上。

SBOOT分区的搭建

        此分区是单片机上电后运行的第一个分区,在这里我们要实现的功能有以下两点

        1、接收程序代码,烧录到APP分区。

        2、跳转到程序分区执行。

Xmodem协议接收

        先来介绍一下Xmodem协议

        通讯流程

        1、发送方等待接收方的C

        2、收到接收方的C后,发送第一包数据

        3、接收方收到数据后停止发C,在数据源校验无误后,发送ACK;校验有误,发送NACK;

        4、发送方收到ACK后发送下一包数据,收到NACK重复发送当前一包数据(在接收到ACK/NACK前发送方处在静默状态)。

        5、最后一包发送完,发送方发送EOT。

        6、接收方收到EOT回ACK.

以上便是完整一次Xmodem的传输。

        数据包

        数据包构成如下(本次采用的是128byte形式)

        帧头|第几包数据|255-第几包数据|[0-127]个数据|CRC校验高八位|CRC低八位

        帧头固定为0x01,而接收方只需要拿到数据后做一次CRC校验,便可以确定数据接收有无错误。

        ACK:发送十六进制06;

        NACK:发送十六进制15;

接收代码实现

        此处比较难的点就是对数据包的校验,和对数据包个数的处理。我在网上看很多人的代码接收了直接写入单片机的flash,我个人是非常不建议大家用这种写法的,首先对单片机的flash擦写本就有寿命,高频的擦写非常的不好。其次由于这是在中断回调函数中,涉及到Flash的擦除非常耗时,有可能会导致程序死锁(亲测),也不利于代码的编写。所以我建议大家还是在完整接收完一整个数据缓存好后,再统一的对flash进行擦写。

        norflash的驱动代码。由于我是正点原子的板子,所以就直接移植来用了,大家有其他缓存芯片也可以,不一定非要norflash。

//这里是截取自我代码中的串口DMA空闲中断回调函数
        else if(BootStructINFO.BootFlag==UPDATE)                                                    //如果是更新模式
        {
            if(rx_buf[0]==0x01&&data_leng==133)                                                     //如果包头是0x01,包长度为133,则一包数据初步认为正确
            {
                BootStructINFO.Event_bit&=~EVENT_C_BIT;                                             //把发C事件标志位给清零,停止发C
                BootStructINFO.XmodeCRC= Xmodem_crc16(&rx_buf[3],128);                              //对128字节数据校验
                if(BootStructINFO.XmodeCRC==(rx_buf[131]*256+rx_buf[132]))                          //如果我们的CRC和数据包给的CRC一致,可以确认这是一包完全正确的数据
                {
                    BootStructINFO.XmodeRecNum+=1;                                                  //接收数+1,这里的接收数是接收数据包的个数,方便后期统计接收数据量
                    memcpy(&Updatebuff[0+128*((BootStructINFO.XmodeRecNum-1)%16)],&rx_buf[3],128);  //把中间的数据段给复制到UpdaeBuff中(这里我设置的数组大小为2048,也就是说16包数据才能填满一个缓冲区)
                    if((BootStructINFO.XmodeRecNum)%16==0)                                          //如果接收到16包数据,即此刻缓冲区已经满了
                    {
                        BootStructINFO.XmodeRecPge+=1;                                              //接收到的页数+1,也是方便后期进行数据搬运
                        norflash_write(Updatebuff,(BootStructINFO.XmodeRecPge-1)*2048,2048);        //把接收到的数据写入Norflash里(外置Norflash)
                        
                    }
                    printf("\x06");                                                                 //发送ACK
                }
            else
                {
                    printf("\x15");                                                                 //如果不是数据正确的情况,就发送NACK   
                }
            }
            else if(data_leng==1&&rx_buf[0]==0x04)                                                  //如果接收到长度为1,且为0x04的数据,则表明已经完成一次协议通讯了
            {
                printf("\x06");                                                                     //发送一个ACK高职
                if(BootStructINFO.XmodeRecNum%16!=0)                                                //这里,由于我们接收到的数据量不可能是2048的整数倍,可能会多出几包数据这里进行一个判断
                {
                    BootStructINFO.XmodeRceLstPN = BootStructINFO.XmodeRecNum%16;                   //如果这里把多出来的数据包数量存起来,方便后期搬运
                    norflash_write(Updatebuff,BootStructINFO.XmodeRecPge*2048,(BootStructINFO.XmodeRceLstPN)*128);  //把多出来的数据写入Norflash
                    BootStructINFO.XmodeRecPge+=1;                                                  //因为我们是新开一页写的,这里不要忘记页数+1
                }
                BootStructINFO.Event_bit|=EVENT_FLASH_BIT;                                          //使能FLASH更新标志事件
            }
            else
            {
                printf("\x15");
            }
//CRC校验,没什么好讲的,大家感兴趣自己去学一下
uint16_t Xmodem_crc16(uint8_t *data,uint16_t datalen)
{
    uint8_t i;
    uint16_t Crcinit = 0x0000;
    uint16_t Crcipoly = 0x1021;//这个是通讯协议规定了的
    while(datalen--)
    {
        Crcinit = (*data<<8)^Crcinit;
        for(i=0;i<8;i++)
        {
            if(Crcinit&0x8000)
            {
                Crcinit = (Crcinit<<1)^Crcipoly;
            }
            else
            {
                Crcinit = (Crcinit<<1);
            }
        }
        data++;
    }
    return  Crcinit;
    
}

 Flash的写入

        在接收完一整包数据后,就开始写入单片机的flash了,这里补充一点前置知识。

关于单片机的Flash

        1、单片机的flash的数据只能从1变为0,不能从0变为1,所以在写入之前要先擦除

        2、单片机的flash只能按页擦除。

        3、stm32单片机的flash页大小分为两种情况,128k以下的一页1k,128k以上的2k一页。我的是zet6 512k以2k为一页有256页

        4、单片机flash寻找是从0x08000000开始的。

有了以上知识,大家看我这里关于flash空间操作的宏定义应该就不难了,这里我们分了20页flash给SBOOT用,合计20*2048byte(实际代码用不了这么多),剩余的全用作程序分区。

     else if(BootStructINFO.BootFlag==UPDATE)
     {
         if(BootStructINFO.Event_bit&EVENT_MSG_BIT)//这里是一个状态机的开始标志位,切换到UPdata模式后,先输出提示信息
         {
            printf("Warning:Cant quit before finished\r\n");
            printf("Please Use the XModem Dowload\r\n");
            BootStructINFO.Event_bit &= ~EVENT_MSG_BIT;
            BootStructINFO.Event_bit |= EVENT_C_BIT;
         }
         else if(BootStructINFO.Event_bit&EVENT_C_BIT)//跳转到发C状态
         {
             BootStructINFO.XmodeTimer++;
             if(BootStructINFO.XmodeTimer==2)
             {
                 BootStructINFO.XmodeTimer=0;
                 printf("C");
             }
         }
        else if(BootStructINFO.Event_bit&EVENT_FLASH_BIT)//如果接收完成使能了写入位开始写入数据
        {
            printf("ready write %d pack\r\n",BootStructINFO.XmodeRecNum);
            HAL_Delay(3000);
            eraseflash(SYS_START_PAG,SYS_PAGE);         //先对主程序部分的256-20页全部擦除
            printf("Erase Done\r\n");
            BootStructINFO.Event_bit&=~EVENT_FLASH_BIT; //把写入先失能,避免重复触发
            memset(Updatebuff,0,2048);                  //把updatebuf清零,后续要用这个做数据搬运桥梁
            for(uint16_t i=0;i<BootStructINFO.XmodeRecPge-1;i++)//这里我们先用一个for循环写入除最后一页外的其他页(因为最后一页有可能并不满2048个数据,所以要单独判断)
            {
                norflash_read(Updatebuff,0x00000000+i*PAGE_SIZE,PAGE_SIZE);
                writeflash(SYS_START_ADDR+i*PAGE_SIZE,(uint32_t *)Updatebuff,PAGE_SIZE);
            }
            if(BootStructINFO.XmodeRceLstPN==0)//如果接收的余出来的数据包为0即,恰好是放满整页的的(128byte的数据包)
            {
                norflash_read(Updatebuff,0x00000000+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,PAGE_SIZE);//直接写一页
                writeflash(SYS_START_ADDR+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,(uint32_t *)Updatebuff,PAGE_SIZE);
            }
            else//如果有余出来的
            {
                norflash_read(Updatebuff,0x00000000+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,BootStructINFO.XmodeRceLstPN*128);//把余出来的单独读写
                writeflash(SYS_START_ADDR+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,(uint32_t *)Updatebuff,BootStructINFO.XmodeRceLstPN*128);
            }
            printf("Write Finished\r\n");//写完以后打印
            HAL_Delay(2000);
            HAL_NVIC_SystemReset();//重启系统
        }
     }

flash写入和擦除的代码是经过封装的,现在把这些函数贴出来。

uint8_t eraseflash(uint16_t addr,uint16_t nbPages)
{
    unsigned int pageError = 0;
    uint32_t pageAddress = 0x08000000+addr*2048;
    FLASH_EraseInitTypeDef eraseInit;
    eraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
    eraseInit.PageAddress = pageAddress;
    eraseInit.Banks = FLASH_BASE;
    eraseInit.NbPages = nbPages;
    HAL_FLASH_Unlock();
    if(HAL_FLASHEx_Erase(&eraseInit,&pageError) != HAL_OK)
    {
        HAL_FLASH_Lock();
        return 1;
    }
    HAL_FLASH_Lock();
    return 0;

}


void writeflash(uint32_t saddr,uint32_t *wdata,uint32_t wnum)
{
    HAL_FLASH_Unlock();
    while(wnum)
    {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,saddr,*wdata);
        wnum -=4;
        saddr+=4;
        wdata++;
    }
    HAL_FLASH_Lock();
}

系统完整性搭建

main函数(HAL库生成的)

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "oled.h"
#include "stdio.h"
#include "crc.h"
#include "flash_ctrl.h"
#include "boot.h"
#include "norflash.h"
#include "string.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
uint8_t rx_buf[270];
uint8_t Updatebuff[PAGE_SIZE];
struct BOOT_Struct {
    uint8_t BootFlag;
    uint8_t Event_bit;
    uint32_t XmodeTimer;
    uint32_t XmodeRecNum;
    uint32_t XmodeCRC;
    uint32_t XmodeRecPge;
    uint32_t XmodeRceLstPN;
}BootStructINFO;

struct Wait_struct{
    uint8_t Wait_Cmd;
    uint8_t Cmd_Count;
}WaitTimeCount;

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
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_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buf,270);
  BootStructINFO.BootFlag = WAIT_CMD;
  norflash_init();
  printf("Press a within 3 seconds to enter sboot\r\n");
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
     HAL_Delay(100);
     if(BootStructINFO.BootFlag==WAIT_CMD)
     {
         WaitTimeCount.Wait_Cmd++;
         if(WaitTimeCount.Wait_Cmd%10==0)
         {
             printf("Press a within %1d seconds to enter sboot\r\n",3-(WaitTimeCount.Wait_Cmd/10));
         }
         if(WaitTimeCount.Wait_Cmd==30)//倒计时结束跳转到主分区
         {
             HAL_DeInit();
             __set_PRIMASK(1);
             Load_SYS_User(SYS_START_ADDR);
         }
     }
     else if(BootStructINFO.BootFlag==INTO_CMD)//输出命令行
     {
         WaitTimeCount.Cmd_Count++;
         if(WaitTimeCount.Cmd_Count%50==0)
         {
             WaitTimeCount.Cmd_Count=0;
             printf("/***********************************************************/\r\n");
             printf("[A] Update the System\r\n");
             printf("[B] Run into the MainSys\r\n");
             printf("[C] Restart the Sysyem\r\n");
             printf("\r\n\r\n\r\n\r\n");
         }

     }
     else if(BootStructINFO.BootFlag==UPDATE)
     {
         if(BootStructINFO.Event_bit&EVENT_MSG_BIT)//这里是一个状态机的开始标志位,切换到UPdata模式后,先输出提示信息
         {
            printf("Warning:Cant quit before finished\r\n");
            printf("Please Use the XModem Dowload\r\n");
            BootStructINFO.Event_bit &= ~EVENT_MSG_BIT;
            BootStructINFO.Event_bit |= EVENT_C_BIT;
         }
         else if(BootStructINFO.Event_bit&EVENT_C_BIT)//跳转到发C状态
         {
             BootStructINFO.XmodeTimer++;
             if(BootStructINFO.XmodeTimer==2)
             {
                 BootStructINFO.XmodeTimer=0;
                 printf("C");
             }
         }
        else if(BootStructINFO.Event_bit&EVENT_FLASH_BIT)//如果接收完成使能了写入位开始写入数据
        {
            printf("ready write %d pack\r\n",BootStructINFO.XmodeRecNum);
            HAL_Delay(3000);
            eraseflash(SYS_START_PAG,SYS_PAGE);         //先对主程序部分的256-20页全部擦除
            printf("Erase Done\r\n");
            BootStructINFO.Event_bit&=~EVENT_FLASH_BIT; //把写入先失能,避免重复触发
            memset(Updatebuff,0,2048);                  //把updatebuf清零,后续要用这个做数据搬运桥梁
            for(uint16_t i=0;i<BootStructINFO.XmodeRecPge-1;i++)//这里我们先用一个for循环写入除最后一页外的其他页(因为最后一页有可能并不满2048个数据,所以要单独判断)
            {
                norflash_read(Updatebuff,0x00000000+i*PAGE_SIZE,PAGE_SIZE);
                writeflash(SYS_START_ADDR+i*PAGE_SIZE,(uint32_t *)Updatebuff,PAGE_SIZE);
            }
            if(BootStructINFO.XmodeRceLstPN==0)//如果接收的余出来的数据包为0即,恰好是放满整页的的(128byte的数据包)
            {
                norflash_read(Updatebuff,0x00000000+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,PAGE_SIZE);//直接写一页
                writeflash(SYS_START_ADDR+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,(uint32_t *)Updatebuff,PAGE_SIZE);
            }
            else//如果有余出来的
            {
                norflash_read(Updatebuff,0x00000000+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,BootStructINFO.XmodeRceLstPN*128);//把余出来的单独读写
                writeflash(SYS_START_ADDR+(BootStructINFO.XmodeRecPge-1)*PAGE_SIZE,(uint32_t *)Updatebuff,BootStructINFO.XmodeRceLstPN*128);
            }
            printf("Write Finished\r\n");//写完以后打印
            HAL_Delay(2000);
            HAL_NVIC_SystemReset();//重启系统
        }
     }
    /* USER CODE END WHILE */

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

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if(huart->Instance==USART1)
    {
        uint8_t data_leng = 270-__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);//计算本次接收到的字符串个数
        if(BootStructINFO.BootFlag==WAIT_CMD)//如果在等a模式下
        {
            if(data_leng==1&&rx_buf[0]=='a')//接收到a
            {
                BootStructINFO.BootFlag=INTO_CMD;//进入输出控制台模式
                WaitTimeCount.Cmd_Count=49;
            }
        }
        else if(BootStructINFO.BootFlag==INTO_CMD)//如果在控制台模式下
        {
            if(data_leng==1)
            {
                switch(rx_buf[0])//收到以下字符
                {
                    case 'A':{BootStructINFO.BootFlag=UPDATE;BootStructINFO.Event_bit |= EVENT_MSG_BIT;}break;
                    case 'B':Load_SYS_User(SYS_START_ADDR);break;
                    case 'C':HAL_NVIC_SystemReset();break;
                    default:printf("cmd erro\r\n");break;
                }
            }
            else
            {
                printf("cmd erro\r\n");
            }
        }
        else if(BootStructINFO.BootFlag==UPDATE)                                                    //如果是更新模式
        {
            if(rx_buf[0]==0x01&&data_leng==133)                                                     //如果包头是0x01,包长度为133,则一包数据初步认为正确
            {
                BootStructINFO.Event_bit&=~EVENT_C_BIT;                                             //把发C事件标志位给清零,停止发C
                BootStructINFO.XmodeCRC= Xmodem_crc16(&rx_buf[3],128);                              //对128字节数据校验
                if(BootStructINFO.XmodeCRC==(rx_buf[131]*256+rx_buf[132]))                          //如果我们的CRC和数据包给的CRC一致,可以确认这是一包完全正确的数据
                {
                    BootStructINFO.XmodeRecNum+=1;                                                  //接收数+1,这里的接收数是接收数据包的个数,方便后期统计接收数据量
                    memcpy(&Updatebuff[0+128*((BootStructINFO.XmodeRecNum-1)%16)],&rx_buf[3],128);  //把中间的数据段给复制到UpdaeBuff中(这里我设置的数组大小为2048,也就是说16包数据才能填满一个缓冲区)
                    if((BootStructINFO.XmodeRecNum)%16==0)                                          //如果接收到16包数据,即此刻缓冲区已经满了
                    {
                        BootStructINFO.XmodeRecPge+=1;                                              //接收到的页数+1,也是方便后期进行数据搬运
                        norflash_write(Updatebuff,(BootStructINFO.XmodeRecPge-1)*2048,2048);        //把接收到的数据写入Norflash里(外置Norflash)
                        
                    }
                    printf("\x06");                                                                 //发送ACK
                }
            else
                {
                    printf("\x15");                                                                 //如果不是数据正确的情况,就发送NACK   
                }
            }
            else if(data_leng==1&&rx_buf[0]==0x04)                                                  //如果接收到长度为1,且为0x04的数据,则表明已经完成一次协议通讯了
            {
                printf("\x06");                                                                     //发送一个ACK高职
                if(BootStructINFO.XmodeRecNum%16!=0)                                                //这里,由于我们接收到的数据量不可能是2048的整数倍,可能会多出几包数据这里进行一个判断
                {
                    BootStructINFO.XmodeRceLstPN = BootStructINFO.XmodeRecNum%16;                   //如果这里把多出来的数据包数量存起来,方便后期搬运
                    norflash_write(Updatebuff,BootStructINFO.XmodeRecPge*2048,(BootStructINFO.XmodeRceLstPN)*128);  //把多出来的数据写入Norflash
                    BootStructINFO.XmodeRecPge+=1;                                                  //因为我们是新开一页写的,这里不要忘记页数+1
                }
                BootStructINFO.Event_bit|=EVENT_FLASH_BIT;                                          //使能FLASH更新标志事件
            }
            else
            {
                printf("\x15");
            }
        }
        else
        {
            printf("\x15");
        }
        memset(rx_buf,0,270);
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1,(uint8_t *)rx_buf,270);
    }
}


void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    printf("error now\r\n");
}


/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

最后就是跳转代码,这里用了点汇编,也是借鉴了一个大佬的写法。

typedef void (*load_sys)(void);



load_sys Load_SYS;




__ASM void MSR_SP(uint32_t addr)//汇编传参,addr是第一个变量,值直接给到r0寄存器
{
    MSR MSP,r0//设置sp指针
    BX r14  //恢复现场
}


void Load_SYS_User(uint32_t addr)
{
    if((*(uint32_t *)addr>0x20000000-1)&&((*(uint32_t *)addr<(0x20000000+0x10000))))//看看指针是否在合理区间(RAM中)
    {
        MSR_SP(*(uint32_t *)addr);//sp跳转
        Load_SYS = (load_sys)*(uint32_t *)(addr+4);//地址强转为函数,这里转的是(程序区)复位Reset_Handler回调函数
        BOOT_Clear();
        Load_SYS();//执行复位函数
    }
}

void BOOT_Clear(void)//初始化的清理
{
    HAL_UART_DeInit(&huart1);
    HAL_SPI_DeInit(&g_spi2_handler);
    HAL_DeInit();
}

基本上分区搭建的代码差不多就到这里了,后续还有一个细节要修改。

这里改为整片擦除,然后先烧录SBOOT程序再烧录APP

APP分区的搭建

         这部分没什么特别的,改一下程序的起始地址和中断向量表,设置一个脚本生成.bin文件就好了。

        修改程序起始地址,改成SBOOT分区之后

        设置中断向量表,hal库下设置中断向量表不在.s文件下,而是在system_stm32f1xx.c下,找到,把这里的注释去掉。

写上偏移量(注意,是偏移量,不是地址值)

        添加生成bin文件脚本:fromelf --bin -o ".\bin_file\@L.bin" "#L"

自此整一个OTA的环境就搭建完成了,现在开始测试烧录。

测试

上电提示:

3s无操作跳转到APP区:

按下a呼出控制台

准备烧录(换了个串口软件):

重启后输出:

非常成功

项目踩坑点总结

        1、在DMA空闲回调函数里直接烧写flash没有norflash缓冲,程序直接跑飞不可取。

        2、在Load_SYS_User(SYS_START_ADDR);加载到APP分区之前,没对BOOT分区初始化的外设去初始化,导致进入到APP后出现各种异常。

        3、接收的数据包的数量换算,很容易算错。

工程共享

        Github:GitHub - Norginx/stm32f103_OTA

        Xmodem工具:链接: https://pan.baidu.com/s/1o4WoXOs3o4Xp21EhCHx6rQ

                                        提取码: u1mi 

;