前言
這一節實現的功能是使用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_SUM文件夾
STM32_MQTT_AT8266_SUM:這個代表着設備的型號
這個型號要和用戶程序里面的型號保持一致
我的模塊配置成了串口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_SUM/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用戶程序需要哪些配合