基于STM32实现OTA&BootLoader 第四章——三分区形式BootLoader程序设计
本文详细介绍了STM32单片机基于三分区架构的BootLoader设计与实现方案。主要内容包括:1)将Flash划分为BootLoader区(B区)和两个应用分区(A1、A2区),实现程序冗余备份;2)设计OTA升级流程,通过标志位管理实现安全升级和回滚机制;3)实现串口IAP功能,采用Xmodem协议进行程序传输和CRC校验;4)通过向量表重定位技术实现分区跳转。系统支持本地串口升级和远程OTA
一、三分区形式的规划
1、重新规划Flash分区
在AB分区的基础之上,将A区划分为A1区和A2区两块大小相同的区域,它们都是用于存放应用功能程序的

2、程序下载方式
(1)在一些项目中,出于降本的考虑,可能不会配置外部Flash存储程序,这就意味着下载程序的任务要交给BootLoader,BootLoader在接收到程序段数据时要直接将其写入A1区或者A2区(A1区和A2区也可以“互刷”,但比较常用的还是BootLoader统一刷写)。
(2)一般地,产品下线时单片机只有一个应用功能程序,假设存储在A1区中,后续如果有OTA更新,BootLoader需要将新版本程序下载到A2区中,如果升级成功,则切换BootLoader跳转的分区,如果升级失败,则不切换,这样就能保证,即使程序升级失败,在没有外部Flash的情况下也不会使得单片机没有可用的应用功能程序。
3、APP Flag和OTA Flag
(1)需要设置OTA Flag和APP Flag用于标记是否有OTA事件以及当前运行的应用程序(A1/A2),它们需要存储在EPPROM中,一旦有更新,EPPROM中的Flag也要同步更新,以保证标志位不会被异常重置。
(2)OTA Flag的工作流:
①当OTA事件发生时,应用功能程序调用NVIC_SystemReset函数重启单片机,单片机进入BootLoader后,控制通信模块连接服务器(本章暂时先用串口通信实现),将新版本程序下载至Flash中。
②当下载完成后,BootLoader清除OTA Flag,并重启单片机,然后BootLoader判断无OTA事件需要处理,将指令指针跳转至当前运行的应用程序(A1/A2)。
(3)APP Flag的工作流:
①运行BootLoader程序时,如果判断OTA Flag有被置位,则根据APP Flag判断当前运行的应用程序是A1区程序还是A2区程序:
[1]如果当前运行程序是A1区程序,则对A2区程序进行升级;如果升级成功,APP Flag指向A2区程序,然后重启单片机;如果升级失败,APP Flag保持指向A1区程序,然后重启单片机。
[2]如果当前运行程序是A2区程序,则对A1区程序进行升级;如果升级成功,APP Flag指向A1区程序,然后重启单片机;如果升级失败,APP Flag保持指向A2区程序,然后重启单片机。
②运行BootLoader程序时,如果判断OTA Flag没有被置位,则根据APP Flag判断当前运行的应用程序是A1区程序还是A2区程序:
[1]如果当前运行程序是A1区程序,则跳转至A1区,运行A1区的应用功能程序。
[2]如果当前运行程序是A2区程序,则跳转至A2区,运行A2区的应用功能程序。

二、分区跳转功能实现
1、规划三分区参数
(1)重新对Flash的空间做规划,B区占用20KB(0~19号扇区),A1区占用22KB(20~41号扇区),A2区占用22KB(42~63号扇区)。
(2)拷贝一份第三章的工程,清除原本main.h文件、main.c文件、Boot.h文件和Boot.c文件中的函数定义、变量定义和宏定义。
(3)在main.h文件中添加用于存放BootLoader相关及其它的比较重要的宏定义等内容。
#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_A1_PAGE_NUM ((STM32_PAGE_NUM - STM32_B_PAGE_NUM)/2) //A1区所占页数
#define STM32_A1_STAET_PAGE STM32_B_PAGE_NUM //A1区第一个扇区的编号
#define STM32_A1_SADDR STM32_FLASH_SADDR + STM32_A1_STAET_PAGE * STM32_PAGE_SIZE //A1区起始地址
#define STM32_A2_PAGE_NUM ((STM32_PAGE_NUM - STM32_B_PAGE_NUM)/2) //A2区所占页数
#define STM32_A2_STAET_PAGE STM32_B_PAGE_NUM + STM32_A1_PAGE_NUM //A2区第一个扇区的编号
#define STM32_A2_SADDR STM32_A1_SADDR + (STM32_A2_STAET_PAGE - STM32_A1_STAET_PAGE) * STM32_PAGE_SIZE //A2区起始地址

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 Flag
uint8_t OTA_ver[32]; //版本号
}OTA_InfoCB;
extern OTA_InfoCB OTA_Info;
②在main.c文件中定义OTA相关信息结构体变量。
OTA_InfoCB OTA_Info;
(2)OTA相关信息结构体的读取在AT24C02.c文件已实现对应的函数,本工程可继续沿用。
(3)在Boot.c文件编写判定OTA Flag的逻辑,并在Boot.h文件中声明函数。
①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、BOOT Flag的定义、读取及判定
(1)BOOT Flag的定义:
①在main.h文件中定义BOOT相关信息结构体,其中包含成员BOOT Flag。
#define BOOT_A1_FLAG 0x00000001 //BOOT Flag为该值时代表A1区为当前运行程序
#define BOOT_A2_FLAG 0x00000002 //BOOT Flag为该值时代表A2区为当前运行程序
typedef struct{
uint32_t BOOT_Flag; //BOOT Flag
}BOOT_InfoCB;
extern BOOT_InfoCB BOOT_Info;
②在main.c文件中定义BOOT相关信息结构体变量。
BOOT_InfoCB BOOT_Info;
(2)在AT24C02.c文件中增加读取BOOT相关信息结构体的函数,并在AT24C02.h文件中声明。BOOT相关信息结构体存储在AT24C02地址为0x60的地方。
①AT24C02.c文件增加内容:(使用memset函数需包含string.h文件)
void AT24C02_ReadBOOTInfo(void)
{
memset(&BOOT_Info, 0, sizeof(OTA_InfoCB)); //将BOOT_Info整块内存空间清零
AT24C02_ReadData(0x60, (uint8_t *)&BOOT_Info, sizeof(BOOT_InfoCB)); //读出BOOT_Info内容
}
②AT24C02.h文件增加内容:
void AT24C02_ReadBOOTInfo(void);
(3)在Boot.c文件的BootLoader_Branch函数中增加判定BOOT Flag的逻辑。
void BootLoader_Branch(void){
if(OTA_Info.OTA_Flag == OTA_SET_FLAG)
Serial_Printf("OTA更新\r\n");
else
{
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG)
Serial_Printf("跳转A1分区\r\n");
else
Serial_Printf("跳转A2分区\r\n");
}
}
4、无OTA事件时的分区跳转实现
(1)在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指针,SP指针的值通过函数参数传入,跳转A1区则传入A1区首地址,跳转A2区则传入A2区首地址。
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分区的子函数,并根据BOOT Flag判断跳转的目的地是A1区还是A2区。
void BootLoader_Branch(void){
if(OTA_Info.OTA_Flag == OTA_SET_FLAG)
{
Serial_Printf("OTA更新\r\n");
}
else
{
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG)
{
Serial_Printf("跳转A1分区\r\n");
LOAD_A(STM32_A1_SADDR);
}
else
{
Serial_Printf("跳转A2分区\r\n");
LOAD_A(STM32_A2_SADDR);
}
}
}
5、BOOT Flag的更新
(1)在AT24C02.c文件中增加写入BOOT相关信息结构体的函数,BOOT信息相关结构体存储在AT24C02地址为0x60的地方。
void AT24C02_WriteBOOTInfo(void)
{
uint8_t *wptr; //指向需写入的字节数据
wptr = (uint8_t *)&BOOT_Info; //指向OTA_Info结构体中的首字节数据
for(uint8_t i = 0;i < sizeof(OTA_InfoCB)/8;i++) //按页写入W25Q64
{
AT24C02_WritePage(0x60 + i * 8, wptr + i * 8);
Delay_ms(5);
}
}
(2)在AT24C02.h文件中声明写入BOOT相关信息结构体的函数。
void AT24C02_WriteBOOTInfo(void);
6、功能开发阶段性调试
(1)在main.c文件中添加如下调试代码。
#include "stm32f10x.h" // Device header
#include "Serial.h"
#include "MyDMA.h"
#include "Delay.h"
#include "MyI2C.h"
#include "AT24C02.h"
#include "MyFLASH.h"
#include "main.h"
#include "Boot.h"
OTA_InfoCB OTA_Info;
BOOT_InfoCB BOOT_Info;
int main(void)
{
/*串口模块初始化*/
Serial_Init();
U0Rx_PtrInit();
MyDMA_Init();
/*AT24C02模块初始化*/
MyI2C_Init();
BOOT_Info.BOOT_Flag = BOOT_A2_FLAG; //可更换为BOOT_A1_FLAG尝试
AT24C02_WriteBOOTInfo(); //写入BOOT Flag
AT24C02_ReadOTAInfo(); //读取OTA Flag
AT24C02_ReadBOOTInfo(); //读取BOOT Flag
BootLoader_Branch();
while (1)
{
}
}
(2)本章构建的工程是B分区的程序,A1分区和A2分区的程序需要另外准备。
①选择一个古早开发的成熟工程,按照下图所示将起始地址更改为0x08005000。

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

③再选择一个古早开发的成熟工程,按照下图所示将起始地址更改为0x0800A800。

④将向量表偏移字段(相对于Flash起始地址的差值)由0x0更改为0xA800。

(3)依次将A2分区程序、A1分区程序和B分区程序下载到单片机中,理想情况下,串口助手输出“跳转A2分区”字样,A2分区程序随后开始运行。
三、程序更新功能实现
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");
}
else{
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG){
Serial_Printf("跳转A1分区\r\n");
LOAD_A(STM32_A1_SADDR);
}
else{
Serial_Printf("跳转A2分区\r\n");
LOAD_A(STM32_A2_SADDR);
}
}
}
else{
Serial_Printf("进入程序升级流程,请请添加bin格式文件\r\n");
}
}
uint8_t BootLoader_Enter(uint8_t timeout)
{
Serial_Printf("%dms内输入小写字母“w”,则进入程序升级流程\r\n",timeout*100);
while(timeout--) //如果形参timeout初始值为20,则2000ms内等待上位机指令
{
Delay_ms(100);
if(U0_RxBuff[0] == 'w')
return 1; //返回1,表示进入命令行
}
return 0; //返回0,表示不进入命令行
}
2、串口IAP下载A区程序功能
(1)因为接下来会使用Xmdoem协议,所以后续的实验需要借助比较高级的“串口助手”SecureCRT,打开SecureCRT后,配置如下图所示的快速连接(端口需要选择与STM32连接的端口),然后点击“连接”即可。

(2)Xmdoem协议中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);
(3)Xmdoem协议中发送字符C的实现:
①在main.h文件中定义UpDataA_CB结构体,其中Updatabuff用于缓存串口接收的程序段数据。
typedef struct{
uint8_t Updatabuff[STM32_PAGE_SIZE];
uint32_t XmodemTimer; //字符C发送间隔
uint32_t XmodemNB; //当前接收的数据包个数
uint32_t XmodemCRC; //接收数据包的CRC校验码
}UpDataA_CB;
extern UpDataA_CB UpDataA;
②在main.h文件中添加宏定义,指示Xmodem协议中发送字符C事件和本地串口程序更新事件,并定义状态变量BootStatus。
#define IAP_XMODEMC_FLAG 0x00000001 //Xmodem协议中发送字符C事件标志
#define IAP_USARTLOAD_FLAG 0x00000002 //本地串口程序更新事件
extern uint32_t BootStatus;
③在main.c文件中定义UpDataA_CB结构体和状态变量BootStatus。
UpDataA_CB UpDataA;
uint32_t BootStatus;
④更新Boot.c文件中的BootLoader_Branch函数,进入程序升级流程的分支后,根据BOOTFlag判断需要更新的是A1区还是A2区,先擦除该区程序,然后置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理,再置位BootStatus的IAP_USARTLOAD_FLAG标志,表示有本地串口程序更新事件等待处理,再同时重置Xmdoem计时器和接收数据包个数。
void BootLoader_Branch(void){
if(BootLoader_Enter(20)== 0) //判断是否不需要进入命令行
{
if(OTA_Info.OTA_Flag == OTA_SET_FLAG){
Serial_Printf("OTA更新\r\n");
}
else{
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG){
Serial_Printf("跳转A1分区\r\n");
LOAD_A(STM32_A1_SADDR);
}
else{
Serial_Printf("跳转A2分区\r\n");
LOAD_A(STM32_A2_SADDR);
}
}
}
else{
Serial_Printf("进入程序升级流程\r\n");
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG){
Serial_Printf("开始擦除A2区\r\n");
MyFLASH_EraseFlash(STM32_A2_STAET_PAGE, STM32_A2_PAGE_NUM); //擦除Flash的A2区
}
else{
Serial_Printf("开始擦除A1区\r\n");
MyFLASH_EraseFlash(STM32_A1_STAET_PAGE, STM32_A1_PAGE_NUM); //擦除Flash的A1区
}
Serial_Printf("擦除完成,请添加bin格式文件\r\n");
BootStatus |= IAP_XMODEMC_FLAG; //置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理
BootStatus |= IAP_USARTLOAD_FLAG; //置位BootStatus的IAP_USARTLOAD_FLAG标志,表示有本地串口程序更新事件等待处理
UpDataA.XmodemTimer = 0; //重置XmodemTimer计时器
UpDataA.XmodemNB = 0; //接收数据包个数重置为0
}
}
⑤更新主函数,增加字符C事件的处理逻辑。
int main(void)
{
/*串口模块初始化*/
Serial_Init();
U0Rx_PtrInit();
MyDMA_Init();
/*AT24C02模块初始化*/
MyI2C_Init();
AT24C02_ReadOTAInfo(); //读取OTA Flag
AT24C02_ReadBOOTInfo(); //读取BOOT Flag
BootLoader_Branch();
while (1)
{
Delay_ms(10);
if(BootStatus & IAP_XMODEMC_FLAG){ //判断是否有发送字符C事件未处理
if(UpDataA.XmodemTimer >= 100){ //判断计时时间是否达到1s
Serial_Printf("C"); //发送字符C
UpDataA.XmodemTimer = 0; //重置XmodemTimer
}
else{
UpDataA.XmodemTimer++; //XmodemTimer递增计时
}
}
}
}
(4)接收方Xmdoem协议实现:
①在Boot.c文件中增加BootLoader_Event函数,实现接收程序段数据事件的处理逻辑,在实现Xmodem协议接收数据的同时,将程序“分片式”地写入A区,具体是A1区还是A2区,根据BOOT Flag判断,完成刷写后,修改BOOT Flag并重启单片机。
#include <string.h>
void BootLoader_Event(uint8_t *data, uint16_t datalen){
uint32_t StartAddress;
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG) StartAddress = STM32_A2_SADDR;
else StartAddress = STM32_A1_SADDR;
if(BootStatus & IAP_USARTLOAD_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){
MyFLASH_WriteFlash(StartAddress + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128) - 1) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,STM32_PAGE_SIZE);
}
}
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(StartAddress + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128)) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,(UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))*128);
}
BootStatus &= ~IAP_USARTLOAD_FLAG; //清除本地串口程序更新事件标志
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG){
BOOT_Info.BOOT_Flag = BOOT_A2_FLAG; //当前运行程序更新为A2区程序
AT24C02_WriteBOOTInfo(); //写入BOOT Flag
}
else{
BOOT_Info.BOOT_Flag = BOOT_A1_FLAG; //当前运行程序更新为A1区程序
AT24C02_WriteBOOTInfo(); //写入BOOT Flag
}
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
}
3、配置VTOR实现向量表重定位
(1)在STM32中,中断向量表的位置是由VTOR(向量表偏移寄存器)控制的。要让同一个应用功能程序在A1、A2分区中都能正确找到中断向量表的位置,需要在BootLoader跳转前,根据应用功能程序实际运行的地址,动态设置VTOR。
(2)更新Boot.c文件中的BootLoader_Branch函数,在跳转A分区前,根据BOOT Flag动态设置VTOR。
void BootLoader_Branch(void){
if(BootLoader_Enter(20)== 0){ //判断是否不需要进入命令行
if(OTA_Info.OTA_Flag == OTA_SET_FLAG){
Serial_Printf("OTA更新\r\n");
}
else{
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG){
Serial_Printf("跳转A1分区\r\n");
SCB->VTOR = STM32_A1_SADDR;//修改中断向量表首地址为A1区起始地址
LOAD_A(STM32_A1_SADDR);
}
else{
Serial_Printf("跳转A2分区\r\n");
SCB->VTOR = STM32_A2_SADDR;//修改中断向量表首地址为A2区起始地址
LOAD_A(STM32_A2_SADDR);
}
}
}
else{
Serial_Printf("进入程序升级流程\r\n");
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG){
Serial_Printf("开始擦除A2区\r\n");
MyFLASH_EraseFlash(STM32_A2_STAET_PAGE, STM32_A2_PAGE_NUM);
}
else{
Serial_Printf("开始擦除A1区\r\n");
MyFLASH_EraseFlash(STM32_A1_STAET_PAGE, STM32_A1_PAGE_NUM);
}
Serial_Printf("擦除完成,请添加bin格式文件\r\n");
BootStatus |= IAP_XMODEMC_FLAG;
BootStatus |= IAP_USARTLOAD_FLAG;
UpDataA.XmodemTimer = 0; //重置XmodemTimer计时器
UpDataA.XmodemNB = 0; //接收数据包个数重置为0
}
}
(3)由于VTOR统一由BootLoader设置,因此应用功能程序中的SystemInit函数不应再设置VTOR,需要将相关语句注释(特别需要注意,应用功能程序中引用宏定义VECT_TAB_OFFSET的地方,如果语句不得不保留,需要将VECT_TAB_OFFSET替换为变量,根据BOOT Flag确定当前所在分区首地址,该地址即为该变量的值)。

4、功能开发阶段性调试
(1)打开另一个成熟的古早工程,按照下图所示配置,其中指示处输入的命令如下。
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工程同名

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

(3)按照如下流程进行调试:
①重启单片机,先将BootLoader程序下载到单片机中。
②按照命令提示进入串口程序升级流程,等待单片机开始发送字符C,然后操作上位机,使用Xmodem协议传输应用功能程序bin文件。
③完成一次传输后,测试是否能跳转执行应用功能程序,然后复位单片机。
④重复步骤②和③。

四、关键代码概览
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 "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");
}
else
{
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG)
{
Serial_Printf("跳转A1分区\r\n");
SCB->VTOR = STM32_A1_SADDR; //修改中断向量表首地址为A1分区起始地址
LOAD_A(STM32_A1_SADDR);
}
else
{
Serial_Printf("跳转A2分区\r\n");
SCB->VTOR = STM32_A2_SADDR; //修改中断向量表首地址为A2分区起始地址
LOAD_A(STM32_A2_SADDR);
}
}
}
else
{
Serial_Printf("进入程序升级流程\r\n");
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG)
{
Serial_Printf("开始擦除A2区\r\n");
MyFLASH_EraseFlash(STM32_A2_STAET_PAGE, STM32_A2_PAGE_NUM); //擦除Flash的A2区
}
else
{
Serial_Printf("开始擦除A1区\r\n");
MyFLASH_EraseFlash(STM32_A1_STAET_PAGE, STM32_A1_PAGE_NUM); //擦除Flash的A1区
}
Serial_Printf("擦除完成,请添加bin格式文件\r\n");
BootStatus |= IAP_XMODEMC_FLAG; //置位BootStatus的IAP_XMODEMC_FLAG标志,表示有发送字符C事件等待处理
BootStatus |= IAP_USARTLOAD_FLAG; //置位BootStatus的IAP_USARTLOAD_FLAG标志,表示有本地串口程序更新事件等待处理
UpDataA.XmodemTimer = 0; //重置XmodemTimer计时器
UpDataA.XmodemNB = 0; //接收数据包个数重置为0
}
}
uint8_t BootLoader_Enter(uint8_t timeout)
{
Serial_Printf("%dms内输入小写字母“w”,则进入程序升级流程\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_Event(uint8_t *data, uint16_t datalen)
{
uint32_t StartAddress;
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG)
{
StartAddress = STM32_A2_SADDR;
}
else
{
StartAddress = STM32_A1_SADDR;
}
if(BootStatus & IAP_USARTLOAD_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(StartAddress + (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(StartAddress + (UpDataA.XmodemNB/(STM32_PAGE_SIZE/128)) * STM32_PAGE_SIZE, (uint32_t*)UpDataA.Updatabuff,(UpDataA.XmodemNB%(STM32_PAGE_SIZE/128))*128);
}
BootStatus &= ~IAP_USARTLOAD_FLAG; //清除本地串口程序更新事件标志
if(BOOT_Info.BOOT_Flag == BOOT_A1_FLAG)
{
BOOT_Info.BOOT_Flag = BOOT_A2_FLAG; //当前运行程序更新为A2区程序
AT24C02_WriteBOOTInfo(); //写入BOOT Flag
}
else
{
BOOT_Info.BOOT_Flag = BOOT_A1_FLAG; //当前运行程序更新为A1区程序
AT24C02_WriteBOOTInfo(); //写入BOOT Flag
}
Delay_ms(100);
NVIC_SystemReset(); //重启单片机
}
}
}
__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_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 "AT24C02.h"
#include "MyFLASH.h"
#include "main.h"
#include "Boot.h"
OTA_InfoCB OTA_Info;
BOOT_InfoCB BOOT_Info;
UpDataA_CB UpDataA;
uint32_t BootStatus;
int main(void)
{
/*串口模块初始化*/
Serial_Init();
U0Rx_PtrInit();
MyDMA_Init();
/*AT24C02模块初始化*/
MyI2C_Init();
AT24C02_ReadOTAInfo(); //读取OTA Flag
AT24C02_ReadBOOTInfo(); //读取BOOT Flag
BootLoader_Branch();
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递增计时
}
}
}
}
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_A1_PAGE_NUM ((STM32_PAGE_NUM - STM32_B_PAGE_NUM)/2) //A1区所占页数
#define STM32_A1_STAET_PAGE STM32_B_PAGE_NUM //A1区第一个扇区的编号
#define STM32_A1_SADDR STM32_FLASH_SADDR + STM32_A1_STAET_PAGE * STM32_PAGE_SIZE //A1区起始地址
#define STM32_A2_PAGE_NUM ((STM32_PAGE_NUM - STM32_B_PAGE_NUM)/2) //A2区所占页数
#define STM32_A2_STAET_PAGE STM32_B_PAGE_NUM + STM32_A1_PAGE_NUM //A2区第一个扇区的编号
#define STM32_A2_SADDR STM32_A1_SADDR + (STM32_A2_STAET_PAGE - STM32_A1_STAET_PAGE) * STM32_PAGE_SIZE //A2区起始地址
#define OTA_SET_FLAG 0xAABB1122 //OTA Flag为该值时代表OTA Flag置位
typedef struct{
uint32_t OTA_Flag; //OTA Flag
uint8_t OTA_ver[32]; //版本号
}OTA_InfoCB;
extern OTA_InfoCB OTA_Info;
#define BOOT_A1_FLAG 0x00000001 //BOOT Flag为该值时代表A1区为当前运行程序
#define BOOT_A2_FLAG 0x00000002 //BOOT Flag为该值时代表A2区为当前运行程序
typedef struct{
uint32_t BOOT_Flag; //BOOT Flag
}BOOT_InfoCB;
extern BOOT_InfoCB BOOT_Info;
typedef struct{
uint8_t Updatabuff[STM32_PAGE_SIZE];
uint32_t XmodemTimer; //字符C发送间隔
uint32_t XmodemNB; //当前接收的数据包个数
uint32_t XmodemCRC; //接收数据包的CRC校验码
}UpDataA_CB;
extern UpDataA_CB UpDataA;
#define IAP_XMODEMC_FLAG 0x00000001 //Xmodem协议中发送字符C事件标志
#define IAP_USARTLOAD_FLAG 0x00000002 //本地串口程序更新事件
extern uint32_t BootStatus;
#endif
5、说明
(1)本章中的代码是为了实现逻辑而实现,使用的是事件触发和事件处理的编程思想,代码架构可能并不是最优的,在实际开发中,对每一个复杂的功能,应该建立状态机(满足特定条件才能从某种状态流转到其它特定状态,每个状态下有对应的行为),才能提高代码的可维护性。
(2)本教程中的代码仅实现了基础功能,在实际开发中,应考虑数据传输过程中的所有潜在差错,软件中应有充分的检错算法和相应的后处理措施,如中断传输、请求重传等。
更多推荐



所有评论(0)