基于boot+双app区间的方案

前言
在普通的项目中,MCU的内存稍微大一点的项目,在项目需要留出升级功能的时候,都会选择boot+app+app缓存区这种方案,这样让MCU内存小的情况下也能使用boot+app+app缓存区(外部Flash)等任何存储方式,原理简单可行也易于移植,但此方案有个缺点是在正常接收完app固件后还需要复位到boot中将接收到的缓存固件重新复制到app区,这使得整个升级方案耗时比较长时间,并且上位机或者手机app升级控制台不好设计(需要为每一个不同大小的升级固件设置一个倒计时时间),此文将采用boot+双app区间方案,不再需要进入boot中去复制缓存固件到app区,并且固件经过更深入的设计可以实现固件秒回滚的功能。

一、分区
将MCU Flash分区,分成boot区、appC区、appD区三个区间,其中boot区大小为2K左右,本文设置为2K足够了(Program Size: Code=1868 RO-data=336 RW-data=24 ZI-data=2000),appC和appD占用剩余的Flash,如果Flash有其他功能使用时相应减除掉,本文采用STM32F103CBT6,容量为128K,除去2Kboot和2K升级配置参数剩余124K,预留4K用做其他,appC/appD分区60K大小,

二、boot设计
1 boot功能负责根据2K升级配置参数来启动相应的分区app,升级配置参数结构体包含:
typedef struct APP_FIRMWARE{
uint32_t Flag;//标志
uint32_t State;//当前升级状态
uint32_t Packet;//包数
uint32_t Version;//版本号
uint32_t FileSize;//文件大小
uint32_t CheckSumC;//校验和
uint32_t CheckSumD;//校验和
uint8_t BuilTime[4];//文件编译时间
uint8_t FileName[32];//文件名称

uint32_t PartitionNumber;//分区号

uint32_t FlashAddrBase;//Flash首地址

uint32_t BootAddrBase;//Boot首地址
uint32_t AppAddrBase;//App首地址
uint32_t AppBakAddrBase;//AppBak首地址

uint32_t BootSize;//Boot空间大小
uint32_t AppSize;//App空间大小
uint32_t AppBakSize;//AppBak空间大小

uint32_t RunState;//当前运行状态

uint32_t ConfigAddrBase;//Config首地址
uint32_t ConfigSize;//Config空间大小
}APP_FIRMWARE;

2 其中uint32_t PartitionNumber;//分区号即为当前运行的分区号,boot只需要读取Flash内容后再判断此变量即可直接启动分区app固件。

boot启动函数如下,在main函数中调用即可:

void Bootstrap(void)
{
uint32_t appAddress = AppFirmUnion.AppFirmware.AppAddrBase;
uint8_t RunState = 0;

LoadFirmwareParameter();

for(;;)
{
  if(AppFirmUnion.AppFirmware.PartitionNumber == UPGRADE_NUMBER_C)
	{
		appAddress = AppFirmUnion.AppFirmware.AppAddrBase;
		RunState |= 0x01 << 0;
	}
	if(AppFirmUnion.AppFirmware.PartitionNumber == UPGRADE_NUMBER_D)
	{
		appAddress = AppFirmUnion.AppFirmware.AppBakAddrBase;
		RunState |= 0x01 << 1;
	}
	if(SetupApp(appAddress))
	{
		//两个都运行错误
		if((RunState & (0x01 << 2)) && (RunState & (0x01 << 3)))
		{
		  while(1);
		}
		//A运行错误,调到B
		if(RunState & (0x01 << 0))
		{
		  RunState |= 0x01 << 2;
			AppFirmUnion.AppFirmware.PartitionNumber = UPGRADE_NUMBER_C;
		}
		//B运行错误,调到A
		if(RunState & (0x01 << 1))
		{
		  RunState |= 0x01 << 3;
			AppFirmUnion.AppFirmware.PartitionNumber = UPGRADE_NUMBER_D;
		}
	}
}

}

3 启动分区app函数如下:
#define __IO volatile

pFunction Jump_To_Application;
uint32_t JumpAddress;

uint8_t SetupApp(uint32_t appAddress)
{
if((((__IO uint32_t )(0x08000000 + appAddress+4))&0xFF000000) == 0x08000000)//判断是否为0X08XXXXXX.
{
JumpAddress = (__IO uint32_t) (0x08000000 + appAddress + 4); //===Jump to user application
Jump_To_Application = (pFunction) JumpAddress;
Mcu_Misc_Set_MSP(
(__IO uint32_t
)(0x08000000 + appAddress)); //===Initialize user application’s Stack Pointer
__disable_irq(); //关闭中断

	if (appAddress == CHIP_APP_BAK_FLASH_ADDRESS*1024)
	{
	  Mcu_Misc_SetVTOR(CHIP_APP_BAK_FLASH_ADDRESS*1024);
	}
	else
	{
	  Mcu_Misc_SetVTOR(CHIP_APP_FLASH_ADDRESS*1024);
	}
	Jump_To_Application();                                                //===Jump to application
}

return 1;

}

boot因为功能简单,无需太多代码,一般占用内存会很少,这使得内存使用率极大提高。

4 app设计
首先上位机发送升级的一些信息(固件的大小,包数,编译时间、两个分区的固件校验码等,注意此处给的是单个app的信息,合并app时候会说明此处)给MCU,MCU收到后回复请求下一包的命令并带有当前的运行区间信息,此时上位机可以根据请求的包数和当前运行区间的信息读取前半部分或者后半部分的数据(固件), app在需要升级的时候将数据(固件)接收后,先判断当前运行的区间,如果是运行在C区间,则将D区间擦除,然后不断的写入D区间,当写入数据(固件)完成后将接收到的固件校验和和接收数据计算出来的校验和比对一样就激活另一个区间,然后软复位即可。

case PROTOCOL_COMMAND_UPGRADE_START://开始升级
StartUpgrade(&data[1],&length);

  packets = 0;
	T1Protocol_Obj.length = 6;
T1Protocol_Obj.data[0] = PROTOCOL_COMMAND_UPGRADE_WRITE;
	T1Protocol_Obj.data[1] = packets >> 0;
T1Protocol_Obj.data[2] = packets >> 8;
T1Protocol_Obj.data[3] = packets >> 16;
T1Protocol_Obj.data[4] = packets >> 24;
	T1Protocol_Obj.data[5] = AppFirmUnion->AppFirmware.PartitionNumber;
vTaskDelay(200);
	break;
case PROTOCOL_COMMAND_UPGRADE_WRITE://写入数据(固件)
	packets  = data[4];packets <<= 8;
	packets |= data[3];packets <<= 8;
	packets |= data[2];packets <<= 8;
	packets |= data[1];
	
LogE("Source packets:%d\r\n",packets);

WriteUpgradeData(packets,&data[5],length-5);
	{
		packets ++;
		T1Protocol_Obj.length = 6;
		T1Protocol_Obj.data[1] = packets >> 0;
		T1Protocol_Obj.data[2] = packets >> 8;
		T1Protocol_Obj.data[3] = packets >> 16;
		T1Protocol_Obj.data[4] = packets >> 24;
  T1Protocol_Obj.data[5] = AppFirmUnion->AppFirmware.PartitionNumber;
	}
	Tpackets = packets;
	if(xUpgradeTimerUser == NULL)
	{
		xUpgradeTimerUser = xTimerCreate(
												"Timer's name",
												2000, 
												pdTRUE,
												(void *)0,
												vUpgradeTimerCallback);
	}
	xTimerStart(xUpgradeTimerUser,0);
	ReTpackets = 20;
	break;

case PROTOCOL_COMMAND_UPGRADE_ACTIVATE://激活分区
T1Protocol_Obj.length = 2;
T1Protocol_Obj.data[1] = 1;

  UpgradeActivate((UPGRADE_NUMBER)data[2]);
  LogE("激活升级分区:%d\r\n",data[2]);
  xTimerStop(xUpgradeTimerUser,0);
  vTaskDelay(2000);
Mcu_Misc_Soft_Reset();
	break;

激活分区函数:
void UpgradeActivate(UPGRADE_NUMBER partition)
{
AppFirmUnion.AppFirmware.PartitionNumber = partition;
Drive_Memory_WriteDatas(AppFirmUnion.AppFirmware.ConfigAddrBase,(uint32_t *)&AppFirmUnion.UData,sizeof(APP_FIRMWARE_UNION)/4);//===写回固件状态
}

写入数据(固件)函数:
int WriteUpgradeData(uint32_t packets,uint8_t dat[],uint16_t length)
{
static uint32_t Address=0,CheckSum = 0,LastPacket=0;
APP_FIRMWARE_UNION *AppFirmUnion = GetAppFirmUnion();

//LogI("packets:[%d]\r\n",packets);

if(packets == LastPacket && packets != 0)
{
//LogE(“[重传包]\r\n”);
return -1;
}
if(packets >= AppFirmUnion->AppFirmware.Packet && packets != 0)
{
//LogE(“[包大于设定值]\r\n”);
return -2;
}
if(packets == 0)
{
CheckSum = 0;
SetUpDataState(UPDATE_STATE_UPGRADE_UPDATAING);

	if(AppFirmUnion->AppFirmware.PartitionNumber == UPGRADE_NUMBER_C)
	{
		Drive_Memory_ErasePages(AppFirmUnion->AppFirmware.AppBakAddrBase,
													AppFirmUnion->AppFirmware.AppBakSize/DRIVE_MEMORY_SECTOR_SIZE);
	}
	else 
	{
		Drive_Memory_ErasePages(AppFirmUnion->AppFirmware.AppAddrBase,
													AppFirmUnion->AppFirmware.AppSize/DRIVE_MEMORY_SECTOR_SIZE);
	}
}
//计算校验值
for(uint32_t i = 0;i < length;i ++)
{
	CheckSum += dat[i];
}
//计算包所在的内存地址
if(AppFirmUnion->AppFirmware.PartitionNumber == UPGRADE_NUMBER_C)
{
	Address = AppFirmUnion->AppFirmware.AppBakAddrBase + packets * 1024;
}
else 
{
	Address = AppFirmUnion->AppFirmware.AppAddrBase + packets * 1024;
}
//LogI("Address:[%08x] CheckSum:[%08x]\r\n",Address,CheckSum);
Drive_Memory_WritePage(Address,(uint32_t *)&dat[0],length/4);	

if(AppFirmUnion->AppFirmware.PartitionNumber == UPGRADE_NUMBER_C)
{
	if((packets == (AppFirmUnion->AppFirmware.Packet - 1)) &&
		 (AppFirmUnion->AppFirmware.CheckSumD != CheckSum))
	{
		//LogE("CheckSum Err:%08x %08x\r\n",AppFirmUnion->AppFirmware.CheckSum,CheckSum);
		SetUpDataState(UPDATE_STATE_UPGRADE_CHECKSUM_ERROR);
		return -3;
	}
}
else 
{
	if((packets == (AppFirmUnion->AppFirmware.Packet - 1)) &&
		 (AppFirmUnion->AppFirmware.CheckSumC != CheckSum))
	{
		//LogE("CheckSum Err:%08x %08x\r\n",AppFirmUnion->AppFirmware.CheckSum,CheckSum);
		SetUpDataState(UPDATE_STATE_UPGRADE_CHECKSUM_ERROR);
		return -3;
	}
}


LastPacket = packets;

if(packets == (AppFirmUnion->AppFirmware.Packet - 1))
{
//LogA(“Upgrade Complete\r\n”);
SetUpDataState(UPDATE_STATE_UPGRADE_OVER);
return 1;
}

return 0;

}

此方案在升级的过程中上位机都是在第一次开始升级作为主动方,其他情况都视为MCU作为主动方,当传输数据图中有中断传输流程时,MCU会不断的重置定时器,以便定时器超时后会再次主动向上位机请求当前的包数来完成重发机制。
关于升级的文件:此方案需要将appC和appD合并后制作成双app升级的bin固件,可以手动将appD放在appC的末尾达到合并的目的或者参考以下脚本并如何设置次脚本可参考“基于KEIL 的合并boot.bin&app.bin的脚本文件”一文中介绍的原理来实现自动编译并合并bin文件。

https://download.csdn.net/download/weixin_44654285/86406694

注意:此方案的appC和appD不同点只是启动的地址和映射的中断不一样,其他的地方都是一模一样。

总结
此方案不同于一般升级方案,不需要再次到boo中复制文件的等待时间,并且经过进一步的设计可以实现固件回滚,上位机传输完成固件后可以及时与新的app通讯并获取到升级后的信息,给实际用户体验到传输完成既可查看升级的结果。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐