• ESA2GJK1DH1K升级篇: STM32远程乒乓升级,升级流程源码详细说明


    前言

      1.BootLoader程序,升级简要流程图

      

      2.其实主要的就是把程序文件写入环形队列,然后环形队列取出来数据写入Flash

      3.用户程序,简要流程图

        

     下面的读一下,有个印象就可以:

      说白了就是BootLoader里面通过http远程下载完程序以后, 设置更新状态是 0x01  然后重启

      重启以后还是会先执行BootLoader,然后BootLoader判断更新状态是 0x01 那么就设置更新状态是 0xFF

      然后就会执行 用户程序,用户程序判断更新状态是 0xFF 就切换下版本号.设置升级状态为0,升级完成

      假设咱更新的用户程序有问题,那么就会执行用户程序失败,然后导致重启执行了BootLoader

      BootLoader一判断还是0xFF,就说明没有正确执行用户程序,就设置更新状态是 0xEE,同时设置下切换执行文件

      然后执行另一份没有问题的用户程序.

    关于乒乓升级

    1.简要

      每次更新的时候 用户程序1和用户程序2来回的切换写入运行

      BootLoader程序主要做的工作就是如果上次运行的用户程序1

      则获取第二份用户程序,然后把程序文件写入用户程序2地址

      如果上次运行的用户程序2

      则获取第一份用户程序,然后把程序文件写入用户程序1地址

    2.关于源码中的两份用户程序

    首先,两份用户程序除了设置的中断偏移不一样以外其他完全一样!

    不同的偏移值使其能运行在不同的Flash地址上

    STM32F10xTemplate 设置的偏移值只能运行在 ↓

    STM32F10xTemplate - 副本  设置的偏移值只能运行在 ↓

    3.升级切换文件流程

    首先,咱们需要每次要升级的时候需要把运行在不同地址,

    功能完全一样的两份用户程序的bin文件放到云端,

    (编译 STM32F10xTemplate 产生的bin文件

    和编译 STM32F10xTemplate - 副本 产生的bin文件)

    以供单片机下载.

    假设当前单片机是在用户程序1 地址加载运行的程序

    那么单片机下次升级就需要下载 能在用户程序2 地址运行的程序文件

    然后写到 用户程序2 地址,然后单片机就加载用户程序2 地址上的程序运行

    下次再升级就是下载 能在用户程序1 地址运行的程序文件

     

    这里说一下为什么需要同时把两份文件放上去.

    大家可能会想,假设我单片机一开始是在用户程序1 地址加载运行的程序

    那么我单片机升级肯定是下载 能在用户程序2 地址运行的程序文件

    我直接把  能在用户程序2 地址运行的一个程序文件放到云端不就可以了

    我单片机如果接着再升级,肯定是下载 能在用户程序1 地址运行的程序文件

    我直接把  能在用户程序1 地址运行的一个程序文件放到云端不就可以了

    干嘛非要把 能在两个地址运行的执行功能一样的程序放上去呢??

     

    我只问一句:

    大家在看到软件更新的时候,大家是否真的去执行更新???

    是不是有时候都过去好几个版本了才选择升级??

     

    大白话就是:

    假设有两个用户买了咱的产品

    假设一开始都是在用户程序1 地址加载运行的程序

    然后第一次升级的时候,其中一个用户升级了

    另一个用户没有执行升级.

    假设咱又换了下版本,需要再升级!

    现在的情况就变为:

    第一个用户再次升级的时候,需要下载能在用户程序1 地址运行的程序文件

     

    第二个用户由于第一次没有升级,

    再次更新的时候需要下载能在用户程序2 地址运行的程序文件

     

    所以......

    大家一定是要把在两个地址运行的执行功能一样的程序放上去!

    BootLoader程序说明

      首先需要明确:

      总共把Flash分为了下面四部分

      

      每次更新的时候 用户程序1和用户程序2来回的切换写入运行

      BootLoader程序主要做的工作就是如果上次运行的用户程序1

      则获取第二份用户程序,然后把程序文件写入用户程序2地址

      如果上次运行的用户程序2

      则获取第一份用户程序,然后把程序文件写入用户程序1地址

    main函数初始化里面

      提醒:为了便于叙述,升级程序都写在了main里面,后面章节为了便于移植使用,全部封装成了单个函数!

      一,初始化变量

        

      二,获取存储的云端版本,获取当前设备版本

        获取更新状态:如果是0x01 设置更新状态为:0xFF

        如果更新状态是0xFF  设置更新状态为:0xEE  同时切换执行程序

        

      三,获取当前应该运行哪一份用户程序,然后设置运行和更新的Flash起始地址

        

    配网

      对于一个新的Wi-Fi模块而言,没有连接路由器的状态下,无法实现远程http访问

      所以在BootLoader里面有个配网程序

      一,BootLoader里面配网完成以后会初始化版本号,然后设置升级标志

        

    配网重启以后,便能连接上Web服务器

      

      

      

      连接TCP服务器(Web服务器)是用的自动重连的透传模式

    然后定时http询问程序版本

      定时询问云端的版本

      

      

      就是访问的云端的下面文件

      

    询问到版本信息以后处理

      一,处理获取的版本号

      因为设置的是串口TCP透传,所以获取的数据直接在串口里面获取

      

      二,获取升级文件的校验和

      校验和累加方式是每一个字节进行累加,取低八位

      

      三,如果获取的版本号和校验和都是可以的

        1.擦除对应的Flash地址

        2.把云端版本存储起来

        3.发送获取相应的程序文件http指令

        4.然后设置 IAPStructValue.PutDataFlage = 1;  (允许把数据写入环形队列)

        提示:发送完获取文件指令以后,Web服务器便会下发程序文件了

        

    到串口中断里面看下接收程序文件

      

      if(IAPStructValue.PutDataFlage && (IAPStructValue.PutDataFlage^IAPStructValue.Overflow))

      IAPStructValue.PutDataFlage: (允许把程序写入环形队列)

      IAPStructValue.Overflow:如果溢出过,就不再往里面写了

      PutData(&rb_tIAP,NULL,&Res,1) : 把接收的数据写入环形队列

      提示:主循环只要判断环形队列里面有数据,就把数据写入Flash

      提示:主循环只要判断环形队列里面有数据,就把数据写入Flash

      提示:主循环只要判断环形队列里面有数据,就把数据写入Flash

      具体里面的判断后面会说明!

      

      提醒:由于http会返回数据头,需要去掉数据头

      

    //解析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;
                
                Usart1IdleTime = 3000;//GPRS判断空闲时间需要3S左右,因为GPRS有延迟
            }
            //解析http数据-------------------------------end

    把程序文件写入Flash

      只要环形队列里面有数据,就提取数据写入Flash

      

      1: 不用多说

      2:http服务器的头信息会返回程序文件的大小,程序中获取了这个数据

      对比下写入的和http实际返回的数据个数是不是一致

      

      获取程序文件大小的程序如下:

      

      不过鉴于有的http服务器不会返回数据大小,所以

      如果用户 #define UserContentLength 才会进行判断

      3:计算校验和

      4:把数据写入Flash

    判断接收完数据

      

      如果判断 IAPStructValue.ReadDataEndFlage == 1

      才认为是接收完了数据

      由于我是串口接收数据,所以只要是判断串口出现了空闲就说明接收完了数据

      

    接收完了更新程序以后检验下整个过程

                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);//数据溢出
                    }
                    delay_ms(2000);
                    Reset_MCU();//重启
                }

      1. 如果用户使用了对比http返回的数据个数,则对比数据个数

        

      2.判断写Flash有没有错误

        

      3.判断写环形队列的时候有没有溢出过

        

      4.检测数据一开头的那几个地址是不是0x08 和 0x20

        

      5.如果有校验和,则对比下程序的校验和

        否则就不对比,然后写入更新标志是0x01,切换程序运行的地址 ,  最后重启

        

    重启以后,加载用户程序

      

      

    情况1:正常运行了用户程序

      打开用户程序

      正常运行用户程序便会执行

      

      先看一下处理更新

      IAPSetUpdateStatus(UpdateStatus_None);//清除升级状态(设置升级状态为0)

      然后判断如果是0x01,说明是运行的新程序

      然后切换下程序版本

      再看一下 GetUpdateInfo();

      

      为了便于一眼看出更新的状态

      便把更新状态,设置了对应的字符串

      

       现在就完成了升级了

    情况2:运行用户程序失败

      

      提示:大家的这个处理更新程序,最好是让程序运行一段时间,

      感觉程序运行没有问题以后再调用

      如果用户程序运行失败,则执行不到处理更新程序,然后看门狗超时重启

      接着运行 BootLoader程序

      

       BootLoader 判断还是0xFF,说明没有运行起来新更新的用户程序

      设置更新状态是 0xEE

      切换运行程序的地址(运行另一套用户程序)

      

    关于BootLoader程序里面的两个定时器

    一,下载超时定时器

      1.1下载程序的时候该定时器开始累加

        

      

        

      1.2.调用

        

      1.3.该定时器在写入Flash的时候清零

        

    二,整体运行超时定时器

      1.该定时器只要执行BootLoader程序,就开始运行,除非是Wi-Fi配网的时候才会清零,是强制的.

        

    其它: Flash调整(保持所有程序的stmflash文件一样)

    一,Flash调整(可根据自己使用的f103系列的芯片进行设置),兼容f103全系列

      1.1 可在stmflash.h 调整Flash存储位置

        

      1.2 一般,用户只需要修改

        

      1.3 可在串口打印查看两份程序文件的配置信息 

        user1ROMStart: 0x8004000   用户程序1 Flash存储的开始地址
        user1ROMSize : 0x5c00        用户程序1 程序大小

        user2ROMStart: 0x8009c00   用户程序2 Flash存储的开始地址
        user2ROMSize : 0x5c00         用户程序2 程序大小

        

        当前Flash存储分配如下图

        

      1.4 用户程序1配置

        

      1.5 用户程序2配置

        

    结语

      有什么不明白的可在下面留言,我将根据情况对文章再做更改.

  • 相关阅读:
    裁剪圆形图片原理
    Scrollview回弹效果自定义控件
    Android热修复、插件化、组件化
    Android 6.0权限
    Android如何缩减APK包大小
    Android MVP理解
    Mat转化为IplImage类型的方法
    【QT】Qaction和触发函数建立连接的方法
    【QT】【OpenCv】初始配置以及测试功能
    【MATLAB】画信号频谱的子函数
  • 原文地址:https://www.cnblogs.com/yangfengwu/p/11817442.html
Copyright © 2020-2023  润新知