前言
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配置
結語
有什么不明白的可在下面留言,我將根據情況對文章再做更改.