ESA2GJK1DH1K升级篇: STM32远程乒乓升级,基于Wi-Fi模块AT指令TCP透传方式,MQTT通信控制升级-BootLoader程序制作过程(V1.0)
前言
这一节实现的功能是使用MQTT通信控制模块去升级
这一节还是着重讲解一下如何移植升级程序文件到自己的项目
我做的单片机升级封装文件的目的是希望大家直接移植到自己的项目使用!
关于实用性:
现在的封装适应所有的升级操作,无论用的啥东西来控制的啥单片机升级,无论用的啥方式升级都可以使用
如果大家不希望每用一个芯片实现升级就需要费劲脑子写一套程序,你就吃透我的升级方案!
关于稳定性:
所有的方案,代码都是我一点一点敲出来的,该方案也在很多人的项目上跑着,大家可以放心移植.
准备一个已经实现了TCP的工程,拷贝升级处理文件
1.准备的工程,该工程程序可以控制Wi-Fi模块发送http请求
2.把BootLoader需要用到的文件拷贝到自己的工程
拷贝到自己的项目里面
整理下工程
1.自行添加到工程,还有设置头文件位置
2.注:
可能自己的项目中已经有了上面的一些文件,建议大家把自己以前使用的替换掉!!
注意: 为使得升级稳定可靠 stmflash文件 必须使用我提供的!!
在自己工程的定时器里面添加以下信息
if(IAPStructValue.PutDataFlage && IAPStructValue.UpdateFlage)IAPStructValue.DownloadTimeout++;
else IAPStructValue.DownloadTimeout=0;
IAPStructValue.MainTimeout++;
在自己工程的主函数添加如下信息
#include "IAP.h"
IAP();
IAPLoadAPPProgram();
IAPDownloadTimeoutFunction();
IAPMainTimeoutFunction();
IAPWriteData();
大家把当前的程序下载到单片机,然后看一下串口1的打印信息
user1ROMStart: 0x8004000 用户程序1 Flash存储的开始地址
user1ROMSize : 0x5c00 用户程序1 程序大小
user2ROMStart: 0x8009c00 用户程序2 Flash存储的开始地址
user2ROMSize : 0x5c00 用户程序2 程序大小
大家可以在下面这个文件根据自己的芯片进行设置
所选芯片Flash大小:这个根据自己的芯片设置
BootLoader程序大小: BootLoader程序产生的bin文件大小
假设自己的BootLoader程序的bin文件大小是 15K
则可以设置上面的值 为16,18,20等
假设自己的BootLoader程序的bin文件大小是 20K
则可以设置上面的值 为22,24,26等
存储用户数据所用Flash大小: 这个根据自己需要的设置,
但是必须设置,因为咱升级的时候也需要记录数据
可以是2,4,6,8等等等等
设置好以后系统便会根据大家的设置打印出来APP用户程序的信息
当前Flash存储分配如下图
BootLoader程序占用 16KB
两份用户程序各占23KB,升级的时候,就是两块区域来回的倒腾.(名词:乒乓升级)
乒乓升级的好处是,如果运行新更新的程序失败,可以切换到原先的程序运行
第一份APP用户程序从0x08004000开始存储
第二份APP用户程序从0x08009C00开始存储
剩余的2KB用来存储其它信息
注:当前的配置不是绝对的,需要做完BootLoader程序和用户程序以后
看一下程序所占空间,然后再做调整.
获取云端当前升级的版本
以下是我Web服务器里面存放的文件
程序文件路径:
Web服务器根目录的 hardware文件夹->STM32_MQTT_AT8266_A文件夹
STM32_MQTT_AT8266_A:这个代表着设备的型号
这个型号要和用户程序里面的型号保持一致
我的模块配置成了串口TCP透传,
串口发送的数据,网络模块直接发给服务器
服务器返回的数据直接通过串口发给单片机
所以串口发送的http协议,http协议便转发给了Web服务器
我在BootLoader里面定时发送协议询问程序版本文件
//不是处于升级状态 配置模块连接了Web服务器
if(!IAPStructValue.PutDataFlage && ConfigModuleNoBlockFlage)
{
if(GetVersionInfoCnt > 3000)//3S //每隔3S 访问一次程序版本
{
GetVersionInfoCnt=0;
//获取程序版本
printf("GET %s HTTP/1.1\r\nHost: %s\r\n\r\n","/hardware/STM32_MQTT_AT8266_A/updatainfo.txt","47.92.31.46");
}
}
处理获取的信息
1.按照上面的指令,便获取到了
2.现在把信息丢给一个函数处理 IAPVersionDispose
进入上面的那个函数
该程序处理版本号以后,如果版本不一致,则发送请求相应的程序文件
/**
* @brief 处理从服务器获取的版本号,并获取另一份用户程序
* @warn
* @param data 传入从云端获取的版本号信息
* @param None
* @param None
* @retval None
* @example
**/
void IAPVersionDispose(char *data)
{
if(!IAPStructValue.PutDataFlage)//升级状态下不再进入判断
{
if(strstr(data,"version"))//接收到版本//{"version":"1.02.56"}
{
IAPStructValue.Str = StrBetwString(data,"version\":\"","\"");//提取版本号
if(IAPStructValue.Str != NULL && strlen(IAPStructValue.Str)<=20)//版本号没有问题,设置的版本号最长20位
{
memset(IAPStructValue.VersionServer,0,sizeof(IAPStructValue.VersionServer));
memcpy(IAPStructValue.VersionServer,IAPStructValue.Str,strlen(IAPStructValue.Str));//获取当前云端版本
if(memcmp(IAPStructValue.VersionServer,IAPStructValue.VersionDevice,20)==0)//云端版本和当前版本一致
{
IAPSetUpdateStatus(UpdateStatus_VersionAlike);//版本号和服务器上面的一致
IAPResetMCU();//重启
}
else
{
cStringRestore();
if(IAPStructValue.RunProgram == 1)//运行的第一份程序
{
IAPStructValue.Str = StrBetwString(data,"SumBin2\":\"","\"");//提取第二份bin文件的数据和
}
else//运行的第二份程序
{
IAPStructValue.Str = StrBetwString(data,"SumBin1\":\"","\"");//提取第一份bin文件的数据和
}
if(IAPStructValue.Str != NULL)//有数据
{
IAPStructValue.Len = strlen(IAPStructValue.Str);//获取字符串长度
if(IAPStructValue.Len == 1 && IAPStructValue.Str[0]>='0'&& IAPStructValue.Str[0]<='9')
{//1位
IAPStructValue.SumBin = IAPStructValue.Str[0]-'0';
}
else if(IAPStructValue.Len==2 && IAPStructValue.Str[0]>='0'&& IAPStructValue.Str[0]<='9' && IAPStructValue.Str[1]>='0'&& IAPStructValue.Str[1]<='9')
{//2位
IAPStructValue.SumBin = (IAPStructValue.Str[0]-'0')*10+(IAPStructValue.Str[1]-'0');
}
else if(IAPStructValue.Len==3&&IAPStructValue.Str[0]>='0'&& IAPStructValue.Str[0]<='9'&& IAPStructValue.Str[1]>='0'&& IAPStructValue.Str[1]<='9'&& IAPStructValue.Str[2]>='0'&& IAPStructValue.Str[2]<='9')
{//3位
IAPStructValue.SumBin = (IAPStructValue.Str[0]-'0')*100+(IAPStructValue.Str[1]-'0')*10+(IAPStructValue.Str[2]-'0');
}
else
{
IAPStructValue.Len = 4;
}
if(IAPStructValue.Len>3 || IAPStructValue.SumBin < 0 || IAPStructValue.SumBin >255)
{
IAPSetUpdateStatus(UpdateStatus_SumBinRangeErr);//校验和范围错误
IAPResetMCU();//重启
}
}
if(FlashErasePage(IAPStructValue.UpdateAddress,FLASH_USER_SIZE)!=4)//擦除接收用户程序Flash地址
{
IAPSetUpdateStatus(UpdateStatus_FlashEraseErr);//Flash 擦除失败
IAPResetMCU();//重启
}
IAPSetUpdateVersionServer(IAPStructValue.VersionServer);//存储云端版本
if(IAPStructValue.RunProgram == 1)//运行的第一份程序
{
//发送请求第二份程序文件指令
}
else//运行的第二份程序
{
//发送请求第一份程序文件指令
}
IAPStructValue.PutDataFlage = 1;//可以向环形队列写入数据
}
cStringRestore();
}
else
{
IAPSetUpdateStatus(UpdateStatus_VersionLenErr);//版本号长度错误
IAPResetMCU();//重启
}
}
}
}
这个地方根据自己的服务器路径更改
把程序文件写入Flash
由于我是串口返回的数据,所以我把该程序放到串口中断里面
但是,
大家注意一点,大家无论用什么网络模块也好,什么通信方式也好,或者是内存卡升级,U盘升级也好
你必须保证传入环形队列的数据只是程序数据
Res 代表着程序数据
但是 http返回的数据
需要加点代码去掉数据头.
/*处理HTTP数据*/
u8 HttpHeadCnt = 0;
u8 HttpHeadOK = 0;//接收到正常的http数据
u8 HttpDataLengthOK= 0;//获取了数据长度
u32 HttpDataLength = 0;//http数据长度
u8 HttpHeadEndOK = 0;//http的heap接收完成,后面发过来的是数据
u8 HttpDataStartFlage = 0;//下次传进来的是消息体
//#define UserContentLength //自己的Web服务器返回 Length: XXXXXXXX (本次的数据长度);如果自己服务器不返回,请屏蔽
USART_Ex_ u32 HttpDataLength;//http数据长度
然后串口中断里面处理
void USART1_IRQHandler(void)//串口1中断服务程序
{
u8 Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据
Usart1ReadBuff[Usart1ReadCnt] = Res; //接收的数据存入数组
Usart1ReadCnt++;
if(Usart1ReadCnt > Usart1ReadLen -10)//防止数组溢出
{
Usart1ReadCnt = 0;
}
Usart1IdleCnt = 0;
if(HttpDataStartFlage)//后面接收的都是真实的数据了
{
//可以往环形队列里面写数据,同时没有溢出
if(IAPStructValue.PutDataFlage && (IAPStructValue.PutDataFlage^IAPStructValue.Overflow))
{
if(PutData(&rb_tIAP,NULL,&Res,1) == -1)
{
IAPStructValue.Overflow = 1;//环形队列溢出
}
}
}
//解析http数据-------------------------------Start
//HTTP/1.1 200 OK
if(!HttpHeadOK && IAPStructValue.PutDataFlage)
{
if(Res=='H' && HttpHeadCnt==0)HttpHeadCnt++;
else if(Res=='T' && HttpHeadCnt==1)HttpHeadCnt++;
else if(Res=='T' && HttpHeadCnt==2)HttpHeadCnt++;
else if(Res=='P' && HttpHeadCnt==3)HttpHeadCnt++;
else if(Res=='/' && HttpHeadCnt==4)HttpHeadCnt++;
else if(Res=='1' && HttpHeadCnt==5)HttpHeadCnt++;
else if(Res=='.' && HttpHeadCnt==6)HttpHeadCnt++;
else if(Res=='1' && HttpHeadCnt==7)HttpHeadCnt++;
else if(Res==' ' && HttpHeadCnt==8)HttpHeadCnt++;
else if(Res=='2' && HttpHeadCnt==9)HttpHeadCnt++;
else if(Res=='0' && HttpHeadCnt==10)HttpHeadCnt++;
else if(Res=='0' && HttpHeadCnt==11)HttpHeadCnt++;
else if(Res==' ' && HttpHeadCnt==12)HttpHeadCnt++;
else if(Res=='O' && HttpHeadCnt==13)HttpHeadCnt++;
else if(Res=='K' && HttpHeadCnt==14){HttpHeadOK = 1;HttpHeadCnt=0;HttpDataLength=0;}
else
{
HttpHeadCnt=0;
}
}
#ifdef UserContentLength
//Content-Length: XXXXXXXX
if(HttpHeadOK && !HttpDataLengthOK)//获取http发过来的数据个数
{
if(Res=='-' && HttpHeadCnt==0) HttpHeadCnt++;
else if(Res=='L' && HttpHeadCnt==1)HttpHeadCnt++;
else if(Res=='e' && HttpHeadCnt==2)HttpHeadCnt++;
else if(Res=='n' && HttpHeadCnt==3)HttpHeadCnt++;
else if(Res=='g' && HttpHeadCnt==4)HttpHeadCnt++;
else if(Res=='t' && HttpHeadCnt==5)HttpHeadCnt++;
else if(Res=='h' && HttpHeadCnt==6)HttpHeadCnt++;
else if(Res==':' && HttpHeadCnt==7)HttpHeadCnt++;
else if(Res==' ' && HttpHeadCnt==8)HttpHeadCnt++;
else if(HttpHeadCnt>=9 && HttpHeadCnt<=16 )//最大99999999个字节. 16:99999999 17:999999999 18:9999999999
{
if(Res!=0x0D)
{
HttpDataLength = HttpDataLength*10 + Res - '0';
HttpHeadCnt++;
}
else
{
HttpDataLengthOK = 1;
HttpHeadCnt = 0;
}
}
else
{
HttpHeadCnt = 0;
}
}
if(HttpHeadOK && HttpDataLengthOK && HttpDataLength && !HttpHeadEndOK)
#else
if(HttpHeadOK && !HttpHeadEndOK)
#endif
{//0D 0A 0D 0A
if(Res==0x0D && HttpHeadCnt==0)HttpHeadCnt++;
else if(Res==0x0A && HttpHeadCnt==1)HttpHeadCnt++;
else if(Res==0x0D && HttpHeadCnt==2)HttpHeadCnt++;
else if(Res==0x0A && HttpHeadCnt==3){HttpHeadEndOK = 1;}
else HttpHeadCnt = 0;
}
if(HttpHeadEndOK == 1)//http数据的head已经过去,后面的是真实数据
{
HttpHeadEndOK=0;
HttpHeadCnt = 0;
HttpDataLengthOK=0;
HttpDataStartFlage=1;
}
//解析http数据-------------------------------end
}
}
现在传入环形队列的数据只是咱的程序文件数据了
最后
上面在传输着程序文件,大家需要告诉我数据接收完了
大家需要在确认数据接收完的地方写上
if(IAPStructValue.PutDataFlage)//写入环形队列的标志位置位了
{
IAPStructValue.ReadDataEndFlage=1;//接收完了程序
}
由于我是单片机串口接收数据
只要是判定串口等了一段时间都没有接收到数据,就说明接收完数据了
现在 BootLoader 已经做好了
然后说一下另外的细节
咱的Web服务器有的会返回当前数据的长度
我的程序也使用了这个判断
如果想使用这个判断,就去掉屏蔽
我就直接屏蔽了哈,因为咱有数据校验,用不上这个了
希望大家预留一个按钮
1.虽然我的升级模板可以保证可靠的把程序写入Flash并且如果检测有问题则自动切换到上一份程序运行,
但是需要避免另一件事情(用户程序本身执行了一段时间以后出问题了.....就是说,自己的程序本身就有问题....)
谁也不能保证百分之百没有问题呀!!
2.在BootLoader里面我写了一个按钮检测程序,在进入BootLoader的时候,如果检测到按钮按下
则不加载用户程序,按下一定时间以后控制模块重新升级
因为我的Wi-Fi需要配网,我直接设置的按下按钮3S是配网
配网成功以后重置版本号,写入升级标志,重启.
如果大家希望,有升级标志的时候再去访问升级,
就是设置下有升级标志的时候在去控制模块连接Web服务器
做好以后再编译下,然后看一下生成的bin文件大小
说明咱上面设置的16是可以的
接着看下一节 APP用户程序需要哪些配合