一、三分区形式的规划

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)本教程中的代码仅实现了基础功能,在实际开发中,应考虑数据传输过程中的所有潜在差错,软件中应有充分的检错算法和相应的后处理措施,如中断传输、请求重传等。

Logo

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

更多推荐