基于STM32实现OTA&BootLoader 第三章——构建BootLoader程序
参考教程:https://www.bilibili.com/video/BV1SatHeBEVG/?spm_id_from=333.1387.favlist.content.click
一、分区跳转功能实现
1、规划AB分区参数
(1)STM32F103C8T6的Flash容量是64KB,一个扇区的大小为1KB,则扇区的编号为0~63。
(2)首先对Flash的空间做一个初步的规划,B区占用20KB(0~19号扇区),A区占用44KB(20~63号扇区)。
(3)在User组中添加main.h文件,用于存放BootLoader相关及其它的比较重要的宏定义等内容。
#ifndef __MAIN_H
#define __MAIN_H
#define STM32_FLASH_SADDR 0x08000000 //FLASH程序区起始地址
#define STM32_PAGE_SIZE 1024 //FLASH一个扇区的字节数
#define STM32_PAGE_NUM 64 //FLASH的扇区数(页数)
#define STM32_B_PAGE_NUM 20 //B区所占页数
#define STM32_A_PAGE_NUM STM32_PAGE_NUM-STM32_B_PAGE_NUM //A区所占页数
#define STM32_A_STAET_PAGE STM32_B_PAGE_NUM //A区第一个扇区的编号
#define STM32_A_SADDR STM32_FLASH_SADDR + STM32_A_STAET_PAGE * STM32_PAGE_SIZE //A区起始地址
#endif

2、OTA Flag的定义、读取及判定
(1)OTA Flag的定义:
①在main.h文件中定义OTA相关信息结构体,其中包含成员OTA Flag。
#define OTA_SET_FLAG 0xAABB1122 //FLAG为该值时代表OTA Flag置位
typedef struct{
uint32_t OTA_Flag;
}OTA_InfoCB;
extern OTA_InfoCB OTA_Info;
②在main.c文件中定义OTA信息相关结构体变量。
OTA_InfoCB OTA_Info;
(2)在AT24C02.c文件中增加读取OTA相关信息结构体的函数,并在AT24C02.h文件中声明。OTA相关信息结构体存储在AT24C02地址为0的地方。
①AT24C02.c文件增加内容:(使用memset函数需包含string.h文件)
void AT24C02_ReadOTAInfo(void)
{
memset(&OTA_Info, 0, sizeof(OTA_InfoCB)); //将OTA_Info整块内存空间清零
AT24C02_ReadData(0, (uint8_t *)&OTA_Info, sizeof(OTA_InfoCB)); //读出OTA_Info内容
}
②AT24C02.h文件增加内容:
void AT24C02_ReadOTAInfo(void);
(3)在Hardware组中添加Boot.c文件和Boot.h文件,并编写判定OTA Flag的逻辑。
①Boot.c文件:
#include "stm32f10x.h" // Device header
#include "main.h"
#include "Serial.h"
#include "Boot.h"
void BootLoader_Branch(void){
if(OTA_Info.OTA_Flag == OTA_SET_FLAG){
Serial_Printf("OTA更新\r\n");
}
else{
Serial_Printf("跳转A分区\r\n");
}
}
②Boot.h文件:
#ifndef __BOOT_H
#define __BOOT_H
void BootLoader_Branch(void);
#endif
3、跳转A分区前的工作
(1)Corte-M3处理器拥有R0‐R15的寄存器组,其中R13作为堆栈指针SP。SP有两个,但在同一时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。

①R0~R12都是32位通用寄存器,用于数据操作。
②Cortex-M3拥有两个堆栈指针,然而它们是banked,因此任一时刻只能使用其中的一个。
主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)
进程堆栈指针(PSP):由用户的应用程序代码使用
③当呼叫一个子程序时,由R14存储返回地址。
④R15指向当前的程序地址,如果修改它的值,就能改变程序的执行流。
(2)运行B区的程序时,通用寄存器中会保存一些数据用于BootLoader程序,如果要从B区跳转到A区,那么本次单片机上电循环内,不会再返回运行BootLoader程序,也就是说,从B区跳转到A区时,可以直接舍弃B区在通用寄存器中保存的数据、SP指针、PC指针以及R14中保存的返回地址,不需要“保存现场”。
(3)当指令指针跳转到A区时,对于A区的程序而言相当于按下了复位按键,在进入A区程序的main函数以前,会先运行__main函数对通用寄存器进行初始化,所以不需要再去手动初始化通用寄存器。

(4)实际上,前面说“单片机上电/复位后总是从Flash的起始位置开始运行第一条指令”,这个说法并不准确,上电/复位后,CPU首先从内存的固定位置(即向量表的起始地址)获取两样东西,先设置好主堆栈指针(MSP),再拿到复位中断服务函数的入口地址(Reset_Handler),然后跳转过去执行,所以,当指令指针跳转到A区时,实际上并非真正意义上的复位,应仿照按下复位按键的逻辑,手动设置SP指针和PC指针的初始值。

①如果不人为干预,程序的向量表将存放在Flash的起始位置,这是默认没有BootLoader程序的情况,如果有BootLoader程序,那么A区程序的向量表应该存放在A区的起始位置(0x08005000)。

②向量表的第一个成员__initial_sp就是SP指针的初始值,初始值为多少取决于编译结果,而编译结果则取决于程序中设置了多少全局变量等。无论A区程序SP指针的初始值为多少,它都是可以通过一段固定代码访问到的(也就是存放的位置固定,为0x08005000),那么从B区跳转到A区时,可以读出此值,并赋给SP指针,以完成初始值的设置。
③向量表的第二个成员Reset_Handler就是PC指针的初始值,同样的,无论A区程序PC指针的初始值为多少,它都是可以通过一段固定代码访问到的(也就是存放的位置固定,为0x08005004),那么从B区跳转到A区时,可以读出此值,并赋给PC指针,以完成初始值的设置。

(5)在BootLoader中初始化了一些外设,在将“运行权”交给A区应用功能程序前,BootLoader应当将使用过的外设恢复为默认状态,若A区应用功能程序需要使用这些外设,则由A区应用功能程序重新初始化。
4、无OTA事件时的分区跳转实现
(1)在STM32中,若需要直接访问寄存器,须通过汇编指令。在Boot.c文件中封装函数,负责初始化跳转A区后的SP指针。
__asm void MSR_SP(uint32_t addr)
{
MSR MSP, r0
BX r14 //主调函数返回,相当于return语句
}
//进入该函数后,函数的第一个形参addr会存在通用寄存器R0中,汇编指令仅支持访问寄存器,不支持访问变量
(2)编写将使用过的外设恢复为默认状态的函数。
①在Boot.c文件中定义外设恢复默认状态函数。
void BootLoader_Clear(void)
{
USART_DeInit(USART1);
GPIO_DeInit(GPIOA);
GPIO_DeInit(GPIOB);
}
②在Boot.h文件中声明刚刚定义的函数。
void BootLoader_Clear(void);
(3)由于无法直接修改PC寄存器的值,需要定义一个指向函数的指针,指针指向A区程序的复位向量,调用该函数指针,就相当于调用A区的复位中断服务函数,从而间接修改PC寄存器的值为A区复位中断服务函数的入口地址。
①在Boot.c文件中定义函数指针并封装函数,负责初始化跳转A区后的SP指针和PC指针。
load_a load_A; //函数指针
void LOAD_A(uint32_t addr)
{
/* 先判断addr索引得到的__initial_sp是否在RAM的地址范围中,是则对SP指针和PC指针赋初始值 */
if((*(uint32_t *)addr >= 0x20000000)&&((*(uint32_t *)addr <= 0x20004FFF)))
{
MSR_SP(*(uint32_t *)addr); //对SP指针赋初始值
load_A = (load_a)(*(uint32_t *)(addr + 4)); //取出复位中断服务函数的地址
BootLoader_Clear(); //恢复外设默认状态
load_A(); //直接访问复位中断服务函数,修改PC指针
}
}
②在Boot.h文件中重命名void类型的函数指针,并声明刚刚定义的函数。
typedef void (*load_a)(void);
__asm void MSR_SP(uint32_t addr);
void LOAD_A(uint32_t addr);
③更新BootLoader_Branch函数,判断无OTA事件时调用跳转A分区的子函数。
void BootLoader_Branch(void)
{
if(OTA_Info.OTA_Flag == OTA_SET_FLAG)
{
Serial_Printf("OTA更新\r\n");
}
else
{
Serial_Printf("跳转A分区\r\n");
LOAD_A(STM32_A_SADDR);
}
}
5、功能开发阶段性调试
(1)在main.c文件中添加如下调试代码。
#include "stm32f10x.h" // Device header
#include "Serial.h"
#include "MyDMA.h"
#include "Delay.h"
#include "MyI2C.h"
#include "W25Q64.h"
#include "AT24C02.h"
#include "MyFLASH.h"
#include "main.h"
#include "Boot.h"
OTA_InfoCB OTA_Info;
int main(void)
{
/*串口模块初始化*/
Serial_Init();
U0Rx_PtrInit();
MyDMA_Init();
/*AT24C02模块初始化*/
MyI2C_Init();
/*W25Q64模块初始化*/
W25Q64_Init();
AT24C02_ReadOTAInfo(); //读取OTA Flag
BootLoader_Branch();
while (1)
{
}
}
(2)本章构建的工程是B分区的程序,A分区的程序需要另外准备。
①选择一个古早开发的成熟工程,按照下图所示将起始地址更改为0x08005000。

②将向量表偏移字段由0x0更改为0x5000(注意是偏移量,应为相对于Flash起始地址的差值)。

(3)依次将A分区程序和B分区程序下载到单片机中,理想情况下,串口助手输出“跳转A分区”字样,A分区程序随后开始运行。
二、程序更新功能实现
1、OTA更新相关变量的定义和保存
(1)修改main.h文件中OTA相关信息结构体的定义,增加一个数组成员,用于记录本次OTA程序及W25Q64中其它程序的大小(单位为字节),其中数组的0号成员为本次OTA程序大小。
typedef struct{
uint32_t OTA_Flag;
uint32_t APPlen[11];
}OTA_InfoCB;
(2)更新程序按照每次1KB的大小进行“分片式”更新(不管是OTA更新还是从W25Q64中读取程序并切换)。
①需在main.h文件中增加更新程序片段结构体的定义,用于缓存从W25Q64中读出的程序片内容以及本次更新程序在W25Q64中存储的块的编号。
typedef struct{
uint8_t Updatabuff[STM32_PAGE_SIZE];
uint32_t W25Q64_BlockNumber; //本次更新程序在W25Q64中存储的块的编号
}UpDataA_CB;
extern UpDataA_CB UpDataA;
②在main.c文件中定义更新程序片段结构体变量。
UpDataA_CB UpDataA;
(3)在AT24C02.c文件中增加写入OTA相关信息结构体的函数,并在AT24C02.h文件中声明。OTA信息相关结构体存储在AT24C02地址为0的地方。
①AT24C02.c文件增加内容:
void AT24C02_WriteOTAInfo(void)
{
uint8_t *wptr; //指向需写入的字节数据
wptr = (uint8_t *)&OTA_Info; //指向OTA_Info结构体中的首字节数据
for(uint8_t i = 0;i < sizeof(OTA_InfoCB)/8;i++) //按页写入W25Q64
{
AT24C02_WritePage(i * 8, wptr + i * 8);
Delay_ms(5);
}
}
②AT24C02.h文件增加内容:
void AT24C02_WriteOTAInfo(void);
2、从W25Q64更新A区程序
(1)在main.c文件中定义Boot状态变量BootStatus,并在main.h文件中进行外部声明,同时修改Boot.c文件中的BootLoader_Branch函数,当发生OTA事件时,BootLoader_Branch函数将状态变量BootStatus置为“A区程序更新”状态,代表需要更新A区程序,同时将更新程序片段结构体中的块号更改为0,即更新的程序从W25Q64的第0块取出。
①main.c文件增加内容:
uint32_t BootStatus;
②main.h文件增加内容:
#define UPDATA_A_FLAG 0x00000001 //A区程序更新状态
extern uint32_t BootStatus;
③Boot.c文件中的BootLoader_Branch函数修改为:
void BootLoader_Branch(void)
{
if(OTA_Info.OTA_Flag == OTA_SET_FLAG)
{
Serial_Printf("OTA更新\r\n");
BootStatus |= UPDATA_A_FLAG;
UpDataA.W25Q64_BlockNumber = 0;
}
else
{
Serial_Printf("跳转A分区\r\n");
LOAD_A(STM32_A_SADDR);
}
}
(2)从W25Q64更新A区程序需分为几步,首先判断是否需要更新A区程序,然后获取需要更新的程序存放在W25Q64中的哪一块,并判断合法性,确认合法后再将其中的程序按片的形式更新到A区的Flash空间中,更新完成后,如果此次更新是由OTA引起,则需要清除OTA Flag。
①根据Boot状态变量BootStatus判断是否需要更新A区程序。

②需要更新的程序存放在W25Q64中的哪一块,可通过访问更新程序片段结构体的W25Q64_BlockNumber成员获取;本次更新程序的大小,可根据程序在W25Q64中存储的块的编号,访问更新程序片段结构体的APPlen[UpDataA.W25Q64_BlockNumber]成员获取。

③程序大小不为4的倍数(以字节为单位)都是不合法的,不能将其更新到A区中,同时应该清除状态变量BootStatus的“A区程序更新”状态,执行相应的后处理方案(比如直接跳转至A区运行老版本应用功能程序,具体方案需根据项目情况确定,以下代码暂无后处理方案设计),在得到合法的程序前,都不能将状态变量BootStatus恢复为“A区程序更新”状态。

④为了加快程序更新效率,首先按照1KB一片的大小进行“分片式”更新,最后更新不足1KB大小的一片(如有),另外在更新过程中,每从W25Q64中读取一片,应该将其暂存在更新程序片段结构体中的Updatabuff成员数组里,然后再写入A区的Flash空间中,写入前需擦除Flash中A区的所有内容。

⑤接着根据更新程序在W25Q64中的块号判断是否属于OTA更新,如果是,则清除OTA Flag,最后再重启单片机,完成更新。

(3)在main.c文件的主函数中增加A区程序更新相关代码。
uint8_t i = 0;
int main(void)
{
/*串口模块初始化*/
Serial_Init();
U0Rx_PtrInit();
MyDMA_Init();
/*AT24C02模块初始化*/
MyI2C_Init();
/*W25Q64模块初始化*/
W25Q64_Init();
AT24C02_ReadOTAInfo(); //读取OTA信息相关结构体
BootLoader_Branch(); //判断是否进行OTA更新
while (1)
{
if(BootStatus & UPDATA_A_FLAG) //判断是否为A区程序更新状态
{
Serial_Printf("程序长度%d字节\r\n",OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]);
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 4 == 0) //判断程序长度是否合法
{
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
for(i = 0;i < OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]/STM32_PAGE_SIZE;i++)
{
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024,
UpDataA.Updatabuff, STM32_PAGE_SIZE); //读取程序片段,暂存更新程序片段结构体中
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE,
(uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //按页写入Flash的A区空间
}
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024 != 0) //判断1024整除程序大小后是否有余数
{
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024,
UpDataA.Updatabuff, OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024); //读取最后一段,暂存更新程序片段结构体中
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE,
(uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //将最后一段写入Flash的A区空间
}
if(UpDataA.W25Q64_BlockNumber == 0) //判断是否属于OTA更新
{
OTA_Info.OTA_Flag = 0; //清除OTA Flag
AT24C02_WriteOTAInfo(); //将OTA Flag保存在24C02中
}
NVIC_SystemReset(); //重启单片机
}
else
{
Serial_Printf("程序长度错误\r\n");
BootStatus &= ~UPDATA_A_FLAG; //清除A区程序更新状态
}
}
}
}
三、串口交互式命令行功能实现
1、串口命令行进入方式及提示菜单
(1)在单片机上电后,约定一个时间段,在时间段内,如果上位机通过串口发出特定的命令,则进入命令行,并打印命令菜单提示,否则直接判断是否有OTA更新事件。

(2)在Boot.c文件中修改BootLoader_Branch函数,增加判断是否不需要进入命令行的逻辑。
#include "Delay.h"
void BootLoader_Branch(void){
if(BootLoader_Enter(20)== 0) //判断是否不需要进入命令行
{
if(OTA_Info.OTA_Flag == OTA_SET_FLAG){
Serial_Printf("OTA更新\r\n");
BootStatus |= UPDATA_A_FLAG;
UpDataA.W25Q64_BlockNumber = 0;
}
else{
Serial_Printf("跳转A分区\r\n");
LOAD_A(STM32_A_SADDR);
}
}
else
{
Serial_Printf("进入BootLoader命令行\r\n");
BootLoader_Info(); //打印命令菜单
}
}
uint8_t BootLoader_Enter(uint8_t timeout){
Serial_Printf("%dms内输入小写字母“w”,则进入BootLoader命令行\r\n",timeout*100);
while(timeout--) //如果形参timeout初始值为20,则2000ms内等待上位机指令
{
Delay_ms(100);
if(U0_RxBuff[0] == 'w') return 1; //返回1,表示进入命令行
}
return 0; //返回0,表示不进入命令行
}
void BootLoader_Info(void){
Serial_Printf("\r\n");
Serial_Printf("[1]擦除A区程序\r\n");
Serial_Printf("[2]串口IAP下载A区程序\r\n");
Serial_Printf("[3]设置OTA版本号\r\n");
Serial_Printf("[4]查询OTA版本号\r\n");
Serial_Printf("[5]向外部Flash下载程序\r\n");
Serial_Printf("[6]使用外部Flash内的程序\r\n");
Serial_Printf("[7]重启\r\n");
}
(3)在Boot.h文件中声明新增的子函数。
uint8_t BootLoader_Enter(uint8_t timeout);
void BootLoader_Info(void);
2、擦除A区程序功能
(1)在Boot.c文件中新增BootLoader_Event函数,函数可根据上位机输入的命令执行相应的处理逻辑。首先实现擦除A区程序的功能。
#include "MyFLASH.h"
void BootLoader_Event(uint8_t *data, uint16_t datalen){
if((datalen == 1)&&(data[0] == '1'))
{
Serial_Printf("擦除A区程序\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
}
}
(2)在Boot.h文件中声明BootLoader_Event函数,以便主函数调用。
void BootLoader_Event(uint8_t *data, uint16_t datalen);
(3)由于计划实现通过命令行擦除A区程序的功能,那么就会存在擦除A区程序后,A区没有程序的情况,因此需要修改Boot.c文件中的LOAD_A函数,如果判断A区向量表中的SP初始值不合法,就发出信息提示——“跳转A分区失败”,并且不能跳转至A分区。
void LOAD_A(uint32_t addr)
{
/* 先判断addr索引得到的__initial_sp是否在RAM的地址范围中,是则对SP指针和PC指针赋初始值 */
if((*(uint32_t *)addr >= 0x20000000)&&((*(uint32_t *)addr <= 0x20004FFF)))
{
MSR_SP(*(uint32_t *)addr); //对SP指针赋初始值
load_A = (load_a)(*(uint32_t *)(addr + 4)); //取出复位中断服务函数的地址
BootLoader_Clear(); //恢复外设默认状态
load_A(); //直接访问复位中断服务函数,修改PC指针
}
else
{
Serial_Printf("跳转A分区失败\r\n");
}
}
(4)更新主函数,在判断是否需要更新A区程序之前,先判断上位机是否有发出命令或其它数据,如有,则调用BootLoader_Event函数,对收到的命令或数据进行处理。
int main(void){
Serial_Init(); U0Rx_PtrInit(); MyDMA_Init(); MyI2C_Init(); W25Q64_Init();
AT24C02_ReadOTAInfo(); //读取OTA信息相关结构体
BootLoader_Branch(); //判断是否进行OTA更新
while (1){
if(U0CB.URxDataOUT != U0CB.URxDataIN){ //判断是否有接收数据未处理
BootLoader_Event(U0CB.URxDataOUT->start, U0CB.URxDataOUT->end - U0CB.URxDataOUT->start + 1);
U0CB.URxDataOUT++; //读出一组数据,OUT指针右移
if(U0CB.URxDataOUT == U0CB.URxDataEND)
U0CB.URxDataOUT = &U0CB.URxDataPtr[0]; //OUT指针越界回滚
}
if(BootStatus & UPDATA_A_FLAG){ //判断是否为A区程序更新状态
Serial_Printf("程序长度%d字节\r\n",OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]);
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 4 == 0){
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM);
for(i = 0;i < OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]/STM32_PAGE_SIZE;i++){
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024, UpDataA.Updatabuff, STM32_PAGE_SIZE);
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE);
}
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024 != 0){
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024, UpDataA.Updatabuff, OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024);
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE);
}
if(UpDataA.W25Q64_BlockNumber == 0){
OTA_Info.OTA_Flag = 0; AT24C02_WriteOTAInfo();
}
NVIC_SystemReset();
}
else{
Serial_Printf("程序长度错误\r\n"); BootStatus &= ~UPDATA_A_FLAG;
}
}
}
}
3、重启单片机功能
更新Boot.c文件中的BootLoader_Event函数,增加重启单片机功能
void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
if((datalen == 1)&&(data[0] == '1'))
{
Serial_Printf("擦除A区程序\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
}
else if((datalen == 1)&&(data[0] == '7'))
{
Serial_Printf("重启\r\n");
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
4、串口IAP下载A区程序功能
(1)因为接下来会使用Xmdoem协议,所以后续的实验需要借助比较高级的“串口助手”SecureCRT,打开SecureCRT后,配置如下图所示的快速连接(端口需要选择与STM32连接的端口),然后点击“连接”即可。

(2)Xmdoem协议单次数据传输流程如下图所示,它用于大容量数据分片传输,将每一片数据都分别通过一个数据包传输。

①首先接收方发送字符C,然后间隔一段时间(不一定是3s,需要接收方和发送方约定),接收方再发送一次字符C,发送方能成功收到两个字符C后,开始传输数据包。(这一步相当于“握手”)
②数据包的格式如下,从发送顺序看,依次为:
[1]SOH = 0x01,标识数据包的起始标志,占1个字节。
[2]本轮传输中数据包的编号,标识这是第几片数据,占1个字节。
[3]本轮传输中数据包的编号的反码,占1个字节。
[4]数据包的主体数据,也就是被“分割成一片”的数据,占128个字节。
[5]16位CRC校验码,占2个字节。
③整包数据的大小为133字节,接收方收到数据包后,如果通过CRC校验,则发送肯定应答标志ACK = 0x06,告知发送方数据传输无误,继续下一组传输,如果未通过CRC校验,则发送否定应答标志NAK = 0x15,告知发送方数据传输有误,发送方须将本组数据重传一遍。
④当发送方完成数据传输后,发送结束标志EOT = 0x04,接收方收到结束标志后发送肯定应答标志ACK = 0x06,发送方收到ACK后再发送一次结束标志EOT = 0x04,接收方收到该标志后,结束本轮数据传输。
(3)Xmdoem协议中CRC数据校验的实现:
①由于STM32F103C8T6的硬件只支持32位CRC校验,因此16位CRC校验只能通过软件实现。
②循环冗余校验(CRC,Cyclic Redundancy Check)算法介绍:
[1]收发双方约定好一个生成多项式G(x),发送方基于待发送的数据和生成多项式计算出差错检测码(冗余码),将其添加到待传输数据后面一起传输,接收方通过生成多项式计算收到的数据是否产生了误码。
[2]发送方的处理:
待发送的数据作为被除数的一部分,后面添加生成多项式最高次个0(下面的生成多项式举例中生成多项式最高次个0为4个0)以构成被除数,生成多项式各项系数构成的比特串作为除数,进行除法运算,得到商和余数
余数就是所计算出的冗余码,将其添加到待发送数据的后面一起发送
冗余码的长度与生成多项式最高次数相同,商仅作为标记防止计算过程中对错位
[3]接收方的处理:
将接收到的数据作为被除数,除数仍是生成多项式各项系数构成的比特串,进行除法运算,得到商和余数
如果余数为0,可判定传输过程中没有产生误码;如果余数不为0,可判定传输过程中产生了误码

[4]生成多项式的各项系数只能是0或1;CRC算法要求生成多项式必须包含最低次项。

[5]循环冗余校验CRC举例:


③在Boot.c文件中添加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;
}
④在Boot.h文件中添加CRC校验函数声明。
uint16_t Xmodem_CRC16(uint8_t *data, uint16_t datalen);
(4)Xmdoem协议中发送字符C的实现:
①发送字符C分为事件触发和事件处理两个步骤,如下所示。

②更新main.h文件中UpDataA_CB结构体的定义,增加成员Xmdoem计时器,用于管理接收数据前两个字符C的发送间隔。
typedef struct{
uint8_t Updatabuff[STM32_PAGE_SIZE];
uint32_t W25Q64_BlockNumber; //本次更新程序在W25Q64中存储的块的编号
uint32_t XmodemTimer;
}UpDataA_CB;
③在main.h文件中添加宏定义,指示Xmodem协议中发送字符C事件。
#define IAP_XMODEMC_FLAG 0x00000002 //Xmodem协议中发送字符C事件标志
④更新Boot.c文件中的BootLoader_Event函数,增加串口IAP下载A区程序功能分支,进入此分支后,先擦除A区程序,然后置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理,同时重置Xmdoem计时器。
void BootLoader_Event(uint8_t *data, uint16_t datalen){
if(BootStatus == 0){
if((datalen == 1)&&(data[0] == '1')){
Serial_Printf("擦除A区程序\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM);
}
else if((datalen == 1)&&(data[0] == '2')){
Serial_Printf("使用Xmodem协议,串口IAP下载A区程序,请添加bin格式文件\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
BootStatus |= IAP_XMODEMC_FLAG; //置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理
UpDataA.XmodemTimer = 0;
}
else if((datalen == 1)&&(data[0] == '7')){
Serial_Printf("重启\r\n"); Delay_ms(100); NVIC_SystemReset();
}
}
}
⑤更新主函数,在判断是否需要更新A区程序前,增加字符C事件的处理逻辑。
int main(void)
{
/*串口模块初始化*/
Serial_Init();
U0Rx_PtrInit();
MyDMA_Init();
/*AT24C02模块初始化*/
MyI2C_Init();
/*W25Q64模块初始化*/
W25Q64_Init();
AT24C02_ReadOTAInfo(); //读取OTA信息相关结构体
BootLoader_Branch(); //判断是否进行OTA更新
while (1)
{
Delay_ms(10);
if(U0CB.URxDataOUT != U0CB.URxDataIN) //判断是否有接收数据未处理
{
BootLoader_Event(U0CB.URxDataOUT->start, U0CB.URxDataOUT->end - U0CB.URxDataOUT->start + 1);
U0CB.URxDataOUT++; //读出一组数据,OUT指针右移
if(U0CB.URxDataOUT == U0CB.URxDataEND)
{
U0CB.URxDataOUT = &U0CB.URxDataPtr[0]; //OUT指针越界回滚
}
}
if(BootStatus & IAP_XMODEMC_FLAG) //判断是否有发送字符C事件未处理
{
if(UpDataA.XmodemTimer >= 100) //判断计时时间是否达到1s
{
Serial_Printf("C");
UpDataA.XmodemTimer = 0; //重置XmodemTimer
}
UpDataA.XmodemTimer++; //XmodemTimer递增计时
}
if(BootStatus & UPDATA_A_FLAG) //判断是否为A区程序更新状态
{
Serial_Printf("程序长度%d字节\r\n",OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]);
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 4 == 0) //判断程序长度是否合法
{
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
for(i = 0;i < OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]/STM32_PAGE_SIZE;i++)
{
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024,
UpDataA.Updatabuff, STM32_PAGE_SIZE); //读取程序片段,暂存更新程序片段结构体中
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE,
(uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //按页写入Flash的A区空间
}
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024 != 0) //判断1024整除程序大小后是否有余数
{
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024,
UpDataA.Updatabuff, OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024); //读取最后一段,暂存更新程序片段结构体中
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE,
(uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //将最后一段写入Flash的A区空间
}
if(UpDataA.W25Q64_BlockNumber == 0) //判断是否属于OTA更新
{
OTA_Info.OTA_Flag = 0; //清除OTA Flag
AT24C02_WriteOTAInfo(); //将OTA Flag保存在24C02中
}
NVIC_SystemReset(); //重启单片机
}
else
{
Serial_Printf("程序长度错误\r\n");
BootStatus &= ~UPDATA_A_FLAG; //清除A区程序更新状态
}
}
}
}
(5)接收方Xmdoem协议实现:
①更新main.h文件中UpDataA_CB结构体的定义,增加成员XmdoemNB记录当前接收的数据包个数,增加成员XmdoemCRC记录接收数据包的CRC校验码。
typedef struct{
uint8_t Updatabuff[STM32_PAGE_SIZE];
uint32_t W25Q64_BlockNumber; //本次更新程序在W25Q64中存储的块的编号
uint32_t XmodemTimer; //字符C发送间隔
uint32_t XmodemNB; //当前接收的数据包个数
uint32_t XmodemCRC; //接收数据包的CRC校验码
}UpDataA_CB;
②在main.h文件中添加宏定义,指示Xmodem协议中接收数据事件。
#define IAP_XMODEMD_FLAG 0x00000004 //Xmodem协议中接收数据事件标志
③更新Boot.c文件中的BootLoader_Event函数,增加接收数据事件的处理逻辑,在实现Xmodem协议接收数据的同时,将程序“分片式”地写入A区(在实际开发中需对接收的数据做充分校验,比如检查数据包编号等,此处仅做CRC校验)。接收数据事件的处理逻辑如下图所示。

void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
if(BootStatus == 0)
{
if((datalen == 1)&&(data[0] == '1'))
{
Serial_Printf("擦除A区程序\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
}
else if((datalen == 1)&&(data[0] == '2'))
{
Serial_Printf("使用Xmodem协议,串口IAP下载A区程序,请添加bin格式文件\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
BootStatus |= IAP_XMODEMC_FLAG; //置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理
BootStatus |= IAP_XMODEMD_FLAG; //置位BootStatus的IAP_XMODEMD_FLAG标志,表示有接收数据事件等待处理
UpDataA.XmodemNB = 0; //接收数据包个数重置为0
UpDataA.XmodemTimer = 0;
}
else if((datalen == 1)&&(data[0] == '7'))
{
Serial_Printf("重启\r\n");
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
if(BootStatus & IAP_XMODEMD_FLAG) //判断是否有接收数据事件未处理
{
if((datalen== 133) && (data[0] == 0x01)) //判断数据包是否合法
{
BootStatus &= ~IAP_XMODEMC_FLAG; //清除发送字符C事件标志
UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128); //根据接收数据计算CRC校验码
if(UpDataA.XmodemCRC == data[131]* 256 + data[132])
{
Serial_Printf("\x06"); //通过CRC校验,发送ACK
UpDataA.XmodemNB++; //接收数据包数加1
memcpy(&UpDataA.Updatabuff[(UpDataA.XmodemNB - 1)%(STM32_PAGE_SIZE/128)* 128],&data[3], 128); //接收的有效数据拷贝至缓冲区
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))== 0) //判断接收的数据是否足够1KB
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128) - 1) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //写入A区
}
}
else
{
Serial_Printf("\x15"); //未通过CRC校验,发送NAK
}
}
if((datalen== 1) && (data[0] == 0x04)) //判断接收的内容是否为EOT
{
Serial_Printf("\x06"); //发送ACK
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))!= 0) //判断是否有剩余不足够1KB的数据未写入A区
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128)) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,(UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))*128);
}
BootStatus &= ~IAP_XMODEMD_FLAG; //清除接收数据事件标志
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
}
(6)功能开发阶段性测试:
①打开另一个成熟的古早工程(中断向量表的位置需要配置为0x08005000),按照下图所示配置,其中指示处输入的命令如下。
D:\Keil(MDK)\ARM\ARMCC\bin\fromelf.exe --bin -o.\Objects\Project.bin .\Objects\Project.axf

需要注意的是,fromelf.exe文件是在Keil的安装目录下的,也就是说,需要参考Keil的安装路径,在该安装路径下找到fromelf.exe文件,再将“fromelf.exe”及其之前的路径替换为电脑中存放fromelf.exe文件的路径,另外文件夹Objects需要在项目文件夹中

还需要注意的是,Objects文件夹需要在工程文件夹中存在,Project.bin和Project.axf需与Keil工程同名

②打开“Create HEX File”选项,然后编译生成应用功能程序,在对应目录下找到bin文件。

③重启单片机,先将BootLoader程序下载到单片机中,然后按照命令提示进入串口IAP下载A区程序功能,等待单片机开始发送字符C,然后操作上位机,使用Xmodem协议传输应用功能程序bin文件。

5、设置及查询OTA版本号功能
(1)更新main.h文件中OTA_InfoCB结构体的定义,增加成员用于记录OTA版本号(版本号格式定义为“VER-1.0.0-2026/03/01-11:45”),并添加宏定义,指示OTA版本号设置事件。
#define SET_VERSION_FLAG 0x00000008 //OTA版本号设置事件标志
typedef struct{
uint32_t OTA_Flag;
uint32_t APPlen[11];
uint8_t OTA_ver[32];
}OTA_InfoCB;
(2)更新Boot.c文件中的BootLoader_Event函数,增加设置及查询OTA版本号的处理逻辑。
#include "AT24C02.h"
void BootLoader_Event(uint8_t *data, uint16_t datalen){
int temp;
if(BootStatus == 0)
{
if((datalen == 1)&&(data[0] == '1'))
{
Serial_Printf("擦除A区程序\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
}
else if((datalen == 1)&&(data[0] == '2'))
{
Serial_Printf("使用Xmodem协议,串口IAP下载A区程序,请添加bin格式文件\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM);
BootStatus |= IAP_XMODEMC_FLAG;
BootStatus |= IAP_XMODEMD_FLAG;
UpDataA.XmodemNB = 0;
UpDataA.XmodemTimer = 0;
}
else if((datalen == 1)&&(data[0] == '3'))
{
Serial_Printf("设置版本号\r\n");
BootStatus |= SET_VERSION_FLAG; //置位BootStatus的SET_VERSION_FLAG标志,表示有设置OTA版本号事件等待处理
}
else if((datalen == 1)&&(data[0] == '4'))
{
Serial_Printf("查询版本号\r\n");
AT24C02_ReadOTAInfo(); //读出EEPROM中的版本号
Serial_Printf("版本号:%s\r\n",OTA_Info.OTA_ver);
BootLoader_Info(); //重新打印命令菜单
}
else if((datalen == 1)&&(data[0] == '7'))
{
Serial_Printf("重启\r\n");
Delay_ms(100);
NVIC_SystemReset();
}
}
else if(BootStatus & IAP_XMODEMD_FLAG)
{
if((datalen== 133) && (data[0] == 0x01))
{
BootStatus &= ~IAP_XMODEMC_FLAG;
UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128);
if(UpDataA.XmodemCRC == data[131]* 256 + data[132])
{
Serial_Printf("\x06");
UpDataA.XmodemNB++;
memcpy(&UpDataA.Updatabuff[(UpDataA.XmodemNB - 1)%(STM32_PAGE_SIZE/128)* 128],&data[3], 128);
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))== 0)
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128) - 1) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE);
}
}
else
{
Serial_Printf("\x15");
}
}
if((datalen == 1) && (data[0] == 0x04))
{
Serial_Printf("\x06");
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))!= 0)
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128)) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,(UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))*128);
}
BootStatus &= ~IAP_XMODEMD_FLAG;
Delay_ms(100);
NVIC_SystemReset();
}
}
else if(BootStatus & SET_VERSION_FLAG) //判断是否有设置OTA版本号事件未处理
{
if(datalen == 26) //判断版本号长度是否合法
{
if(sscanf((char*)data,"VER-%d.%d.%d-%d/%d/%d-%d:%d",&temp,&temp,&temp,&temp,&temp,&temp,&temp,&temp)== 8) //判断版本号格式是否合法
{
memset(OTA_Info.OTA_ver, 0, 32);
memcpy(OTA_Info.OTA_ver, data, 26); //将请求设置的版本号赋给OTA_ver
AT24C02_WriteOTAInfo(); //版本号存入EEPROM
Serial_Printf("设置版本号成功\r\n");
BootStatus &= ~SET_VERSION_FLAG; //清除设置OTA版本号事件标志位
BootLoader_Info(); //重新打印命令菜单
}
else
{
Serial_Printf("设置版本号格式错误\r\n");
}
}
else
{
Serial_Printf("设置版本号长度错误\r\n");
}
}
}
6、使用外部Flash内的程序功能
(1)在main.h文件中添加宏定义,分别指示向外部Flash下载程序事件和使用外部Flash内的程序事件。
#define DOWNLOAD_TOFALSH 0x00000010 //向外部Flash下载程序事件标志
#define UPLOAD_FROMFLASH 0x00000020 //使用外部Flash内的程序事件标志
(2)向外部Flash下载程序的命令执行流程:

(3)将外部Flash内的程序写入STM32Flash的命令执行流程:

(4)更新Boot.c文件中的BootLoader_Event函数,增加向外部Flash下载程序以及将外部Flash内的程序写入STM32Flash的处理逻辑。
void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
int temp;
if(BootStatus == 0)
{
if((datalen == 1)&&(data[0] == '1'))
{
Serial_Printf("擦除A区程序\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
}
else if((datalen == 1)&&(data[0] == '2'))
{
Serial_Printf("使用Xmodem协议,串口IAP下载A区程序,请添加bin格式文件\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
BootStatus |= IAP_XMODEMC_FLAG; //置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理
BootStatus |= IAP_XMODEMD_FLAG; //置位BootStatus的IAP_XMODEMD_FLAG标志,表示有接收数据事件等待处理
UpDataA.XmodemNB = 0; //接收数据包个数重置为0
UpDataA.XmodemTimer = 0;
}
else if((datalen == 1)&&(data[0] == '3'))
{
Serial_Printf("设置版本号\r\n");
BootStatus |= SET_VERSION_FLAG; //置位BootStatus的SET_VERSION_FLAG标志,表示有设置OTA版本号事件等待处理
}
else if((datalen == 1)&&(data[0] == '4'))
{
Serial_Printf("查询版本号\r\n");
AT24C02_ReadOTAInfo(); //读出EEPROM中的版本号
Serial_Printf("版本号:%s\r\n",OTA_Info.OTA_ver);
BootLoader_Info(); //重新打印命令菜单
}
else if((datalen == 1)&&(data[0] == '5'))
{
Serial_Printf("向外部FLASH下载程序,请输入需要使用的块编号,范围1~9\r\n");
BootStatus |= DOWNLOAD_TOFALSH; //置位BootStatus的DOWNLOAD_TOFALSH标志,表示有程序包要写入外部Flash的事件等待处理
}
else if((datalen == 1)&&(data[0] == '6'))
{
Serial_Printf("使用外部Flash内的程序,请输入需要使用的块编号,范围1~9\r\n");
BootStatus |= UPLOAD_FROMFLASH; //置位BootStatus的UPLOAD_FROMFLASH标志,表示有从外部Flash写程序入内部FLASH的事件等待处理
}
else if((datalen == 1)&&(data[0] == '7'))
{
Serial_Printf("重启\r\n");
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
else if(BootStatus & IAP_XMODEMD_FLAG) //判断是否有接收数据事件未处理
{
if((datalen== 133) && (data[0] == 0x01)) //判断数据包是否合法
{
BootStatus &= ~IAP_XMODEMC_FLAG; //清除发送字符C事件标志
UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128); //根据接收数据计算CRC校验码
if(UpDataA.XmodemCRC == data[131]* 256 + data[132])
{
Serial_Printf("\x06"); //通过CRC校验,发送ACK
UpDataA.XmodemNB++; //接收数据包数加1
memcpy(&UpDataA.Updatabuff[(UpDataA.XmodemNB - 1)%(STM32_PAGE_SIZE/128)* 128],&data[3], 128); //接收的有效数据拷贝至缓冲区
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))== 0) //判断接收的数据是否足够1KB
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128) - 1) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //写入A区
}
}
else
{
Serial_Printf("\x15"); //未通过CRC校验,发送NAK
}
}
if((datalen == 1) && (data[0] == 0x04)) //判断接收的内容是否为EOT
{
Serial_Printf("\x06"); //发送ACK
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))!= 0) //判断是否有剩余不足够1KB的数据未写入A区
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128)) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,(UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))*128);
}
BootStatus &= ~IAP_XMODEMD_FLAG; //清除接收数据事件标志
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
else if(BootStatus & SET_VERSION_FLAG) //判断是否有设置OTA版本号事件未处理
{
if(datalen == 26) //判断版本号长度是否合法
{
if(sscanf((char*)data,"VER-%d.%d.%d-%d/%d/%d-%d:%d",&temp,&temp,&temp,&temp,&temp,&temp,&temp,&temp)== 8) //判断版本号格式是否合法
{
memset(OTA_Info.OTA_ver, 0, 32);
memcpy(OTA_Info.OTA_ver, data, 26); //将请求设置的版本号赋给OTA_ver
AT24C02_WriteOTAInfo(); //版本号存入EEPROM
Serial_Printf("设置版本号成功\r\n");
BootStatus &= ~SET_VERSION_FLAG; //清除设置OTA版本号事件标志位
BootLoader_Info(); //重新打印命令菜单
}
else
{
Serial_Printf("设置版本号格式错误\r\n");
}
}
else
{
Serial_Printf("设置版本号长度错误\r\n");
}
}
else if(BootStatus & DOWNLOAD_TOFALSH) //判断是否有向外部Flash下载程序事件未处理
{
if((datalen== 133) && (data[0] == 0x01)) //判断数据包是否合法
{
BootStatus &= ~IAP_XMODEMC_FLAG; //清除发送字符C事件标志
UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128); //根据接收数据计算CRC校验码
if(UpDataA.XmodemCRC == data[131]* 256 + data[132])
{
Serial_Printf("\x06"); //通过CRC校验,发送ACK
UpDataA.XmodemNB++; //接收数据包数加1
memcpy(&UpDataA.Updatabuff[(UpDataA.XmodemNB - 1)%(STM32_PAGE_SIZE/128)* 128],&data[3], 128); //接收的有效数据拷贝至缓冲区
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))== 0) //判断接收的数据是否足够1KB
{
for(uint8_t i = 0;i < 4;i++)
{
W25Q64_PageProgram((UpDataA.XmodemNB / 8 - 1)* 1024 + i * 256 + UpDataA.W25Q64_BlockNumber * 64 * 1024,&UpDataA.Updatabuff[i * 256], 256); //写入外部FLASH
}
}
}
else
{
Serial_Printf("\x15"); //未通过CRC校验,发送NAK
}
}
else if((datalen == 1) && (data[0] == 0x04)) //判断接收的内容是否为EOT
{
Serial_Printf("\x06"); //发送ACK
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))!= 0) //判断是否有剩余不足够1KB的数据未写入A区
{
for(uint8_t i = 0;i < 4;i++)
{
W25Q64_PageProgram((UpDataA.XmodemNB / 8)* 1024 + i * 256 + UpDataA.W25Q64_BlockNumber * 64 * 1024,&UpDataA.Updatabuff[i * 256], 256); //写入外部FLASH
}
}
BootStatus &= ~DOWNLOAD_TOFALSH; //清除向外部Flash下载程序事件标志
OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] = UpDataA.XmodemNB * 128; //记录程序字节数
AT24C02_WriteOTAInfo(); //将程序字节数保存在EEPROM中
Delay_ms(100);
BootLoader_Info(); //重新打印命令菜单
}
else //上位机发送的不是数据包,则应该是块编号
{
if((datalen == 1)&&(data[0]>= 0x31)&&(data[0]<= 0x39)) //判断块编号是否合法(由于取值1~9,则可根据长度判断)
{
UpDataA.W25Q64_BlockNumber = data[0] - 0x30; //记录块编号
UpDataA.XmodemTimer = 0;
UpDataA.XmodemNB = 0;
OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] = 0;
W25Q64_Erase64K(UpDataA.W25Q64_BlockNumber); //擦除对应块的原始数据
Serial_Printf("使用Xmodem协议,向外部FLASH第%d块下载程序,请添加bin格式文件\r\n",UpDataA.W25Q64_BlockNumber);
BootStatus |= IAP_XMODEMC_FLAG; //触发发送字符C事件,与上位机完成握手
}
else
{
Serial_Printf("块编号不合法\r\n");
}
}
}
else if(BootStatus & UPLOAD_FROMFLASH) //判断是否有使用外部Flash内的程序事件未处理
{
if((datalen == 1)&&(data[0]>= 0x31)&&(data[0]<= 0x39)) //判断块编号是否合法(由于取值1~9,则可根据长度判断)
{
UpDataA.W25Q64_BlockNumber = data[0] - 0x30; //更新操作的块编号
BootStatus |= UPDATA_A_FLAG; //触发更新A区程序事件
BootStatus &= ~UPLOAD_FROMFLASH; //清除使用外部Flash内的程序事件标志
}
}
}
四、关键代码概览
1、Boot.c文件
#include "stm32f10x.h" // Device header
#include "main.h"
#include "Boot.h"
#include "Serial.h"
#include "Delay.h"
#include "MyDMA.h"
#include "MyI2C.h"
#include "W25Q64.h"
#include "MyFLASH.h"
#include "AT24C02.h"
#include <string.h>
load_a load_A;
void BootLoader_Branch(void)
{
if(BootLoader_Enter(20)== 0) //判断是否不需要进入命令行
{
if(OTA_Info.OTA_Flag == OTA_SET_FLAG)
{
Serial_Printf("OTA更新\r\n");
BootStatus |= UPDATA_A_FLAG;
UpDataA.W25Q64_BlockNumber = 0;
}
else
{
Serial_Printf("跳转A分区\r\n");
LOAD_A(STM32_A_SADDR);
}
}
else
{
Serial_Printf("进入BootLoader命令行\r\n");
BootLoader_Info(); //打印命令菜单
}
}
uint8_t BootLoader_Enter(uint8_t timeout)
{
Serial_Printf("%dms内输入小写字母“w”,则进入BootLoader命令行\r\n",timeout*100);
while(timeout--) //如果形参timeout初始值为20,则2000ms内等待上位机指令
{
Delay_ms(100);
if(U0_RxBuff[0] == 'w')
{
return 1; //返回1,表示进入命令行
}
}
return 0; //返回0,表示不进入命令行
}
void BootLoader_Info(void)
{
Serial_Printf("\r\n");
Serial_Printf("[1]擦除A区程序\r\n");
Serial_Printf("[2]串口IAP下载A区程序\r\n");
Serial_Printf("[3]设置OTA版本号\r\n");
Serial_Printf("[4]查询OTA版本号\r\n");
Serial_Printf("[5]向外部Flash下载程序\r\n");
Serial_Printf("[6]使用外部Flash内的程序\r\n");
Serial_Printf("[7]重启\r\n");
}
void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
int temp;
if(BootStatus == 0)
{
if((datalen == 1)&&(data[0] == '1'))
{
Serial_Printf("擦除A区程序\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
}
else if((datalen == 1)&&(data[0] == '2'))
{
Serial_Printf("使用Xmodem协议,串口IAP下载A区程序,请添加bin格式文件\r\n");
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
BootStatus |= IAP_XMODEMC_FLAG; //置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理
BootStatus |= IAP_XMODEMD_FLAG; //置位BootStatus的IAP_XMODEMD_FLAG标志,表示有接收数据事件等待处理
UpDataA.XmodemNB = 0; //接收数据包个数重置为0
UpDataA.XmodemTimer = 0;
}
else if((datalen == 1)&&(data[0] == '3'))
{
Serial_Printf("设置版本号\r\n");
BootStatus |= SET_VERSION_FLAG; //置位BootStatus的SET_VERSION_FLAG标志,表示有设置OTA版本号事件等待处理
}
else if((datalen == 1)&&(data[0] == '4'))
{
Serial_Printf("查询版本号\r\n");
AT24C02_ReadOTAInfo(); //读出EEPROM中的版本号
Serial_Printf("版本号:%s\r\n",OTA_Info.OTA_ver);
BootLoader_Info(); //重新打印命令菜单
}
else if((datalen == 1)&&(data[0] == '5'))
{
Serial_Printf("向外部FLASH下载程序,请输入需要使用的块编号,范围1~9\r\n");
BootStatus |= DOWNLOAD_TOFALSH; //置位BootStatus的DOWNLOAD_TOFALSH标志,表示有程序包要写入外部Flash的事件等待处理
}
else if((datalen == 1)&&(data[0] == '6'))
{
Serial_Printf("使用外部Flash内的程序,请输入需要使用的块编号,范围1~9\r\n");
BootStatus |= UPLOAD_FROMFLASH; //置位BootStatus的UPLOAD_FROMFLASH标志,表示有从外部Flash写程序入内部FLASH的事件等待处理
}
else if((datalen == 1)&&(data[0] == '7'))
{
Serial_Printf("重启\r\n");
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
else if(BootStatus & IAP_XMODEMD_FLAG) //判断是否有接收数据事件未处理
{
if((datalen== 133) && (data[0] == 0x01)) //判断数据包是否合法
{
BootStatus &= ~IAP_XMODEMC_FLAG; //清除发送字符C事件标志
UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128); //根据接收数据计算CRC校验码
if(UpDataA.XmodemCRC == data[131]* 256 + data[132])
{
Serial_Printf("\x06"); //通过CRC校验,发送ACK
UpDataA.XmodemNB++; //接收数据包数加1
memcpy(&UpDataA.Updatabuff[(UpDataA.XmodemNB - 1)%(STM32_PAGE_SIZE/128)* 128],&data[3], 128); //接收的有效数据拷贝至缓冲区
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))== 0) //判断接收的数据是否足够1KB
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128) - 1) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //写入A区
}
}
else
{
Serial_Printf("\x15"); //未通过CRC校验,发送NAK
}
}
if((datalen == 1) && (data[0] == 0x04)) //判断接收的内容是否为EOT
{
Serial_Printf("\x06"); //发送ACK
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))!= 0) //判断是否有剩余不足够1KB的数据未写入A区
{
MyFLASH_WriteFlash(STM32_A_SADDR + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128)) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,(UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))*128);
}
BootStatus &= ~IAP_XMODEMD_FLAG; //清除接收数据事件标志
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
else if(BootStatus & SET_VERSION_FLAG) //判断是否有设置OTA版本号事件未处理
{
if(datalen == 26) //判断版本号长度是否合法
{
if(sscanf((char*)data,"VER-%d.%d.%d-%d/%d/%d-%d:%d",&temp,&temp,&temp,&temp,&temp,&temp,&temp,&temp)== 8) //判断版本号格式是否合法
{
memset(OTA_Info.OTA_ver, 0, 32);
memcpy(OTA_Info.OTA_ver, data, 26); //将请求设置的版本号赋给OTA_ver
AT24C02_WriteOTAInfo(); //版本号存入EEPROM
Serial_Printf("设置版本号成功\r\n");
BootStatus &= ~SET_VERSION_FLAG; //清除设置OTA版本号事件标志位
BootLoader_Info(); //重新打印命令菜单
}
else
{
Serial_Printf("设置版本号格式错误\r\n");
}
}
else
{
Serial_Printf("设置版本号长度错误\r\n");
}
}
else if(BootStatus & DOWNLOAD_TOFALSH) //判断是否有向外部Flash下载程序事件未处理
{
if((datalen== 133) && (data[0] == 0x01)) //判断数据包是否合法
{
BootStatus &= ~IAP_XMODEMC_FLAG; //清除发送字符C事件标志
UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128); //根据接收数据计算CRC校验码
if(UpDataA.XmodemCRC == data[131]* 256 + data[132])
{
Serial_Printf("\x06"); //通过CRC校验,发送ACK
UpDataA.XmodemNB++; //接收数据包数加1
memcpy(&UpDataA.Updatabuff[(UpDataA.XmodemNB - 1)%(STM32_PAGE_SIZE/128)* 128],&data[3], 128); //接收的有效数据拷贝至缓冲区
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))== 0) //判断接收的数据是否足够1KB
{
for(uint8_t i = 0;i < 4;i++)
{
W25Q64_PageProgram((UpDataA.XmodemNB / 8 - 1)* 1024 + i * 256 + UpDataA.W25Q64_BlockNumber * 64 * 1024,&UpDataA.Updatabuff[i * 256], 256); //写入外部FLASH
}
}
}
else
{
Serial_Printf("\x15"); //未通过CRC校验,发送NAK
}
}
else if((datalen == 1) && (data[0] == 0x04)) //判断接收的内容是否为EOT
{
Serial_Printf("\x06"); //发送ACK
if((UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))!= 0) //判断是否有剩余不足够1KB的数据未写入外部FLASH
{
for(uint8_t i = 0;i < 4;i++)
{
W25Q64_PageProgram((UpDataA.XmodemNB / 8)* 1024 + i * 256 + UpDataA.W25Q64_BlockNumber * 64 * 1024,&UpDataA.Updatabuff[i * 256], 256); //写入外部FLASH
}
}
BootStatus &= ~DOWNLOAD_TOFALSH; //清除向外部Flash下载程序事件标志
OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] = UpDataA.XmodemNB * 128; //记录程序字节数
AT24C02_WriteOTAInfo(); //将程序字节数保存在EEPROM中
Delay_ms(100);
BootLoader_Info(); //重新打印命令菜单
}
else //上位机发送的不是数据包,则应该是块编号
{
if((datalen == 1)&&(data[0]>= 0x31)&&(data[0]<= 0x39)) //判断块编号是否合法(由于取值1~9,则可根据长度判断)
{
UpDataA.W25Q64_BlockNumber = data[0] - 0x30; //记录块编号
UpDataA.XmodemTimer = 0;
UpDataA.XmodemNB = 0;
OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] = 0;
W25Q64_Erase64K(UpDataA.W25Q64_BlockNumber); //擦除对应块的原始数据
Serial_Printf("使用Xmodem协议,向外部FLASH第%d块下载程序,请添加bin格式文件\r\n",UpDataA.W25Q64_BlockNumber);
BootStatus |= IAP_XMODEMC_FLAG; //触发发送字符C事件,与上位机完成握手
}
else
{
Serial_Printf("块编号不合法\r\n");
}
}
}
else if(BootStatus & UPLOAD_FROMFLASH) //判断是否有使用外部Flash内的程序事件未处理
{
if((datalen == 1)&&(data[0]>= 0x31)&&(data[0]<= 0x39)) //判断块编号是否合法(由于取值1~9,则可根据长度判断)
{
UpDataA.W25Q64_BlockNumber = data[0] - 0x30; //更新操作的块编号
BootStatus |= UPDATA_A_FLAG; //触发更新A区程序事件
BootStatus &= ~UPLOAD_FROMFLASH; //清除使用外部Flash内的程序事件标志
}
}
}
__asm void MSR_SP(uint32_t addr)
{
MSR MSP, r0
BX r14 //主调函数返回,相当于return语句
}
void BootLoader_Clear(void)
{
USART_DeInit(USART1);
GPIO_DeInit(GPIOA);
GPIO_DeInit(GPIOB);
}
void LOAD_A(uint32_t addr)
{
/* 先判断addr索引得到的__initial_sp是否在RAM的地址范围中,是则对SP指针和PC指针赋初始值 */
if((*(uint32_t *)addr >= 0x20000000)&&((*(uint32_t *)addr <= 0x20004FFF)))
{
MSR_SP(*(uint32_t *)addr); //对SP指针赋初始值
load_A = (load_a)(*(uint32_t *)(addr + 4)); //取出复位中断服务函数的地址
BootLoader_Clear(); //恢复外设默认状态
load_A(); //直接访问复位中断服务函数,修改PC指针
}
else
{
Serial_Printf("跳转A分区失败\r\n");
}
}
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;
}
2、Boot.h文件
#ifndef __BOOT_H
#define __BOOT_H
typedef void (*load_a)(void); //定义函数指针
void BootLoader_Branch(void);
__asm void MSR_SP(uint32_t addr);
void LOAD_A(uint32_t addr);
void BootLoader_Clear(void);
uint8_t BootLoader_Enter(uint8_t timeout);
void BootLoader_Info(void);
void BootLoader_Event(uint8_t *data, uint16_t datalen);
uint16_t Xmodem_CRC16(uint8_t *data, uint16_t datalen);
#endif
3、main.c文件
#include "stm32f10x.h" // Device header
#include "Serial.h"
#include "MyDMA.h"
#include "Delay.h"
#include "MyI2C.h"
#include "W25Q64.h"
#include "AT24C02.h"
#include "MyFLASH.h"
#include "main.h"
#include "Boot.h"
OTA_InfoCB OTA_Info;
UpDataA_CB UpDataA;
uint32_t BootStatus;
uint8_t i = 0;
int main(void)
{
/*串口模块初始化*/
Serial_Init();
U0Rx_PtrInit();
MyDMA_Init();
/*AT24C02模块初始化*/
MyI2C_Init();
/*W25Q64模块初始化*/
W25Q64_Init();
AT24C02_ReadOTAInfo(); //读取OTA信息相关结构体
BootLoader_Branch(); //判断是否进行OTA更新
while (1)
{
Delay_ms(10);
if(U0CB.URxDataOUT != U0CB.URxDataIN) //判断是否有接收数据未处理
{
BootLoader_Event(U0CB.URxDataOUT->start, U0CB.URxDataOUT->end - U0CB.URxDataOUT->start + 1);
U0CB.URxDataOUT++; //读出一组数据,OUT指针右移
if(U0CB.URxDataOUT == U0CB.URxDataEND)
{
U0CB.URxDataOUT = &U0CB.URxDataPtr[0]; //OUT指针越界回滚
}
}
if(BootStatus & IAP_XMODEMC_FLAG) //判断是否有发送字符C事件未处理
{
if(UpDataA.XmodemTimer >= 100) //判断计时时间是否达到1s
{
Serial_Printf("C"); //发送字符C
UpDataA.XmodemTimer = 0; //重置XmodemTimer
}
else
{
UpDataA.XmodemTimer++; //XmodemTimer递增计时
}
}
if(BootStatus & UPDATA_A_FLAG) //判断是否为A区程序更新状态
{
Serial_Printf("程序长度%d字节\r\n",OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]);
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 4 == 0) //判断程序长度是否合法
{
MyFLASH_EraseFlash(STM32_A_STAET_PAGE, STM32_A_PAGE_NUM); //擦除Flash的A区
for(i = 0;i < OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber]/STM32_PAGE_SIZE;i++)
{
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024, UpDataA.Updatabuff, STM32_PAGE_SIZE); //读取程序片段,暂存更新程序片段结构体中
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //按页写入Flash的A区空间
}
if(OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024 != 0) //判断1024整除程序大小后是否有余数
{
W25Q64_ReadData(i * 1024 + UpDataA.W25Q64_BlockNumber * 64 * 1024, UpDataA.Updatabuff, OTA_Info.APPlen[UpDataA.W25Q64_BlockNumber] % 1024); //读取最后一段,暂存更新程序片段结构体中
MyFLASH_WriteFlash(STM32_A_SADDR + i * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE); //将最后一段写入Flash的A区空间
}
if(UpDataA.W25Q64_BlockNumber == 0) //判断是否属于OTA更新
{
OTA_Info.OTA_Flag = 0; //清除OTA Flag
AT24C02_WriteOTAInfo(); //将OTA Flag保存在24C02中
}
NVIC_SystemReset(); //重启单片机
}
else
{
Serial_Printf("程序长度错误\r\n");
BootStatus &= ~UPDATA_A_FLAG; //清除A区程序更新状态
}
}
}
}
4、main.h文件
#ifndef __MAIN_H
#define __MAIN_H
#define STM32_FLASH_SADDR 0x08000000 //FLASH程序区起始地址
#define STM32_PAGE_SIZE 1024 //FLASH一个扇区的字节数
#define STM32_PAGE_NUM 64 //FLASH的扇区数(页数)
#define STM32_B_PAGE_NUM 20 //B区所占页数
#define STM32_A_PAGE_NUM STM32_PAGE_NUM - STM32_B_PAGE_NUM //A区所占页数
#define STM32_A_STAET_PAGE STM32_B_PAGE_NUM //A区第一个扇区的编号
#define STM32_A_SADDR STM32_FLASH_SADDR + STM32_A_STAET_PAGE * STM32_PAGE_SIZE //A区起始地址
#define UPDATA_A_FLAG 0x00000001 //A区程序更新状态
#define IAP_XMODEMC_FLAG 0x00000002 //Xmodem协议中发送字符C事件标志
#define IAP_XMODEMD_FLAG 0x00000004 //Xmodem协议中接收数据事件标志
#define SET_VERSION_FLAG 0x00000008 //OTA版本号设置事件标志
#define DOWNLOAD_TOFALSH 0x00000010 //向外部Flash下载程序事件标志
#define UPLOAD_FROMFLASH 0x00000020 //使用外部Flash内的程序事件标志
#define OTA_SET_FLAG 0xAABB1122 //OTA Flag为该值时代表OTA Flag置位
typedef struct{
uint32_t OTA_Flag;
uint32_t APPlen[11];
uint8_t OTA_ver[32];
}OTA_InfoCB;
typedef struct{
uint8_t Updatabuff[STM32_PAGE_SIZE];
uint32_t W25Q64_BlockNumber; //本次更新程序在W25Q64中存储的块的编号
uint32_t XmodemTimer; //字符C发送间隔
uint32_t XmodemNB; //当前接收的数据包个数
uint32_t XmodemCRC; //接收数据包的CRC校验码
}UpDataA_CB;
extern OTA_InfoCB OTA_Info;
extern UpDataA_CB UpDataA;
extern uint32_t BootStatus;
#endif
5、说明
(1)本章中的代码是为了实现逻辑而实现,使用的是事件触发和事件处理的编程思想,代码架构可能并不是最优的,在实际开发中,对每一个复杂的功能,应该建立状态机(满足特定条件才能从某种状态流转到其它特定状态,每个状态下有对应的行为),才能提高代码的可维护性。
(2)本教程中的代码仅实现了基础功能,在实际开发中,应考虑数据传输过程中的所有潜在差错,软件中应有充分的检错算法和相应的后处理措施,如中断传输、请求重传等。
更多推荐
所有评论(0)