ESA2GJK1DH1K升级篇: STM32远程乒乓升级,基于(Wi-Fi模块AT指令TCP透传方式),MQTT通信控制升级(含有数据校验)

2019-12-20 16:50:34 浏览数 (1)

前言

  这一节实现的功能是使用MQTT通信控制模块去升级

  其实和这一节实现的功能一样  https://cloud.tencent.com/developer/article/1538812

  这一节还是着重讲解一下如何移植升级升级程序到自己的项目

  我做的单片机远程升级封装文件的目的是希望大家直接移植到自己的项目使用!

准备一个已经实现了TCP的工程,拷贝升级处理文件

  1.准备的工程

  2.把BootLoader需要用到的文件拷贝到自己的工程

  拷贝到自己的项目里面

整理下工程

  1.自行添加到工程,还有设置头文件位置

  2.注: 

    可能自己的项目中已经有了上面的一些文件,建议大家把自己以前使用的替换掉!!

    注意: 为使得升级稳定可靠     stmflash文件   必须使用我提供的!!

在自己工程的定时器里面添加以下信息

代码语言:javascript复制
    if(IAPStructValue.PutDataFlage && IAPStructValue.UpdateFlage)IAPStructValue.DownloadTimeout  ;
    else IAPStructValue.DownloadTimeout=0;
    
    IAPStructValue.MainTimeout  ;

在自己工程的主函数添加如下信息

代码语言:javascript复制
#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用来存储其它信息

  大家调整完以后编译一下自己的APP用户程序,看下自己的APP用户程序大小

  超了就重新调整,要么换芯片.....

获取云端当前升级的版本

  程序升级是先获取新程序的版本,如果和本地的不一致,则再获取程序文件

  这是我Web服务器里面存放的文件

  路径:

Web服务器根目录的 hardware文件夹->STM32_MQTT_AT8266_SUM文件夹

  STM32_MQTT_AT8266_SUM:

这个代表着改设备的型号,这节咱控制升级是先用APP通过MQTT询问下设备的型号,和当前版本

     APP获取型号和版本以后,访问对应的updatainfo.txt文件,如果版本不一致则提示用户

  updatainfo.txt文件:

记录当前程序的版本号,程序文件的校验值,升级提示信息

  {"version":"1.0.456"}   标准JSON格式  版本号最长20个字符

  加上校验: {"version":"1.0.456","SumBin1":XX,"SumBin2":XX}

  注:校验不是强制的,如果有该参数,则程序就去判断校验. 无,则不判断校验

  SumBin1: 第一份用户程序的校验值

  SumBin2: 第二份用户程序的校验值

  后面制作完用户程序,根据计算的bin文件校验值填写

  我的模块配置成了串口TCP透传,

  串口发送的数据,网络模块直接发给服务器

  服务器返回的数据直接通过串口发给单片机

  所以串口发送的http协议,http协议便转发给了Web服务器

  我在BootLoader里面定时发送协议询问程序版本文件

代码语言:javascript复制
        //不是处于升级状态                 配置模块连接了Web服务器
        if(!IAPStructValue.PutDataFlage && ConfigModuleNoBlockFlage)
        {
            if(GetVersionInfoCnt > 3000)//3S //每隔3S 访问一次程序版本
            {
                GetVersionInfoCnt=0;
                //获取程序版本
                printf("GET %s HTTP/1.1rnHost: %srnrn","/hardware/STM32_MQTT_AT8266_SUM/updatainfo.txt","47.92.31.46");
            }
        }

处理信息

  1.按照上面的指令,便获取到了

  2.现在把信息丢给一个函数处理  IAPVersionDispose

进入处理版本号函数,添加获取程序文件程序

  该程序处理版本号以后,如果版本不一致,则发送请求相应的程序文件

代码语言:javascript复制
/**
* @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

  发送完上面的 printf("GET %s HTTP/1.1rnHost: %srnrn","/hardware/STM32_MQTT_AT8266_SUM/Progect2.bin","47.92.31.46");

  或者 printf("GET %s HTTP/1.1rnHost: %srnrn","/hardware/STM32_MQTT_AT8266_SUM/Progect.bin","47.92.31.46");

  服务器便会返回程序文件数据了

  咱只需要:

  由于是串口返回的数据,所以咱该程序放到串口中断里面

  有人会问,这样就把数据写入Flash??

  我怎么没有看到写入的地方???

  嘿嘿,我封装好了,大家如果只是用的话,不需要关心怎么做到的

  如果想了解详细的步骤,就从升级篇的第一节开始看

  其实写入Flash是调用的这个函数

代码语言:javascript复制
/**
* @brief  把接收到的程序文件写入Flash
* @warn   
* @param  
* @param  None
* @param  None
* @retval None
* @example
**/
void IAPWriteData(void)
{
    if(rbCanRead(&rb_tIAP)>1)//接收到更新程序就开始写入
    {
        IAPResetDownloadTimeoutFunction();//重置程序下载超时
        
        rbRead(&rb_tIAP, &IAPStructValue.ReadDat, 2);//读取两个数据
        IAPStructValue.ReadDate = (u16)IAPStructValue.ReadDat[1]<<8;
        IAPStructValue.ReadDate = IAPStructValue.ReadDate|IAPStructValue.ReadDat[0];//拼接数据
        
        if(IAPStructValue.UpdateAddressCnt< FLASH_DATA_ADDR)//不能超过 数据区
        {
            #ifdef UserContentLength   //自己的Web服务器返回 Length: XXXXXXXX (本次的数据长度)
            //写入的数据个数不能超出,http实际返回的数据个数
            if( (IAPStructValue.UpdateAddressCnt - IAPStructValue.UpdateAddress) <= HttpDataLength) 
            #endif    
            {
                if(!IAPStructValue.FlashWriteErrFlage)//写Flash没有错误
                {
                    //计算数据累加和
                    IAPStructValue.Sum = IAPStructValue.Sum   IAPStructValue.ReadDat[0]   IAPStructValue.ReadDat[1];
                    
                    if(WriteFlashHalfWord(IAPStructValue.UpdateAddressCnt,IAPStructValue.ReadDate) != 0)//写Flash
                    {
                        IAPStructValue.FlashWriteErrFlage = 1;//写Flash错误
                    }
                }
                IAPStructValue.UpdateAddressCnt =2;//地址增加
            }
        }
    }
    else//环形队列里面没有数据了.并不证明接收完了数据,可能写入环形队列慢,读的快
    {
        if(IAPStructValue.ReadDataEndFlage)//接收完更新程序
        {
            IAPStructValue.PutDataFlage = 0;//停止向环形队列写入数据
            IAPStructValue.UpdateFlage = 0; //更新标志清零
            IAPStructValue.ReadDataEndFlage = 0;//清零接收完更新程序标志
            
            #ifdef UserContentLength   //自己的Web服务器返回 Length: XXXXXXXX (本次的数据长度)
            //写入的数据个数和http实际返回的数据个数不相等
            if( (IAPStructValue.UpdateAddressCnt - IAPStructValue.UpdateAddress) != HttpDataLength) 
            {
                IAPSetUpdateStatus(UpdateStatus_MissingData);//数据错误
            }
            else if(IAPStructValue.FlashWriteErrFlage == 1)//Flash写错误
            #else
            if(IAPStructValue.FlashWriteErrFlage == 1)//Flash写错误
            #endif    
            {
                IAPSetUpdateStatus(UpdateStatus_FlashWriteErr);//Flash写错误
            }
            else if(!IAPStructValue.Overflow)//没有溢出过
            {
                if(IAPCheckRamFlashAddress(IAPStructValue.UpdateAddress))//检测某些位置的Flash的高位地址是不是0x08        //RAM的高位地址是不是0x20    
                {
                    if(IAPStructValue.SumBin != -1)//获取了云端的校验和
                    {
                        if(IAPStructValue.SumBin == IAPStructValue.Sum)//校验和正确
                        {
                            IAPSetUpdateChangeProgram();//切换运行程序地址
                            IAPSetUpdateStatus(UpdateStatus_WriteAppOk);//写入0x01标志
                        }
                        else
                        {
                            IAPSetUpdateStatus(UpdateStatus_SumCheckErr);//数据和校验错误
                        }    
                    }
                    else
                    {                
                        IAPSetUpdateChangeProgram();//切换运行程序地址
                        IAPSetUpdateStatus(UpdateStatus_WriteAppOk);//写入0x01标志
                    }
                }
                else
                {
                    IAPSetUpdateStatus(UpdateStatus_DataAddressError);//数据错误
                }
            }
            else//数据溢出
            {
                IAPSetUpdateStatus(UpdateStatus_DataOverflow);//数据溢出
            }
            IAPResetMCU();//重启
        }
    }
}

注:大家如果想详细了解就从第一节开始看

大约花费一天时间(针对开发人员),如果是初学者.....看自己的学习能力了...

但是,

  大家注意一点...无论用什么过来的数据,你必须保证传入环形队列的数据只是程序数据

  http返回的数据是有数据头的

  咱需要去掉数据头,

代码语言:javascript复制
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
        
        
    } 
} 

现在传入环形队列的数据只是咱的程序文件数据了

最后

  上面在传输着程序文件,大家需要告诉我数据接收完了

  大家需要在确认数据接收完的地方写上

代码语言:javascript复制
    if(IAPStructValue.PutDataFlage)//写入环形队列的标志位置位了
    {
        IAPStructValue.ReadDataEndFlage=1;//接收完了程序
    }

由于我是单片机串口接收数据

只要是判定串口等了一段时间都没有接收到数据,就说明接收完数据了

现在 BootLoader 已经做好了

然后说一下另一个细节

0 人点赞