手把手教你如何實現自動固件更新-嵌入式篇

※已刊登在“無線電”07月刊上手把手教你如何實現自動固件更新

—— 嵌入式篇


作者:常席正,魏文龍

我們在上期 “手把手教你如何實現自動固件更新——服務器篇”那篇文章中介紹了通過雲服務器更新固件的方法,並着重介紹了服務器端的前期準備以及軟件設計。這次小熊和大家分享下嵌入式端的軟件設計。相對於服務器端的軟件設計,嵌入式軟件設計需要更為嚴謹,因為固件升級出錯的後果會非常嚴重,因為這個功能一般使用在批量的設備上,而固件是控制系統的核心軟件,因此固件更新出錯的話,會造成設備大面積癱瘓。所以嚴重性不言而喻。

本期我們來介紹一下客戶端的具體實現過程,如圖1所示,根據我們的自動固件更新協議,在與更新服務器建立連接後,服務器會要求客戶端進行一系列驗證,嵌入式設備在通過驗證後,更新服務器會告知此嵌入式設備的最新固件信息,嵌入式設備根據這些信息下載並更新固件

圖1 自動固件更新協議
圖1 自動固件更新協議

“下載並更新固件”幾個字囊括了我們所要執行的所有步驟。我們將按照以下步驟分別介紹。

1.下載準備–對Flash進行分區

為了實現‘下載並更新固件’我們先要做一些準備工作,我們把MCU的Flash分為三個區分別為BOOT區,APP區和Backup區,如圖2所示

圖1 自動固件更新協議
圖2 內存空間分配

表1 空間分配

了解了空間分配之後,我們再來看一下我們這個演示中各部分的主要功能:

  1. BOOT區:
  • 清空APP區,為新APP寫入做準備;
  • 把暫存在Backup區的新版本程序拷貝到APP區;
  1. APP區

APP區是應用程序運行區域,實現正常的網絡連接,並更新固件。

  • 配置網絡參數;
  • 在線固件升級;

每次上電都會從Boot區引導,若判斷上層APP區載入程序是否成功,成功則直接從Boot區跳轉到APP區,正常運行主程序。

  1. Backup區
  • 從服務器接收並備份需要更新的新應用程序,也就是固件存儲區域。

備註:由於備區的大小為112K,所以意味着APP的大小最大為112K ;

  1. 程序流程設計

我們完成了對閃存的分區規劃後,就要設計我們程序的流程,圖3是程序執行的流程圖。

圖3 嵌入式設備固件更新流程圖
圖3嵌入式設備固件更新流程圖

每次啟動嵌入式設備,均從首地址開始執行程序:

(1)啟動進入BOOT區,若BOOT檢測APP區的不為空,則跳轉到APP區的首地址執行主程序;

(2) APP內的代碼主要實現:

配置網絡參數:配置IP地址,MAC地址,建立網絡連接。

遠程更新固件:客戶端向服務器發送固件版本查詢報文條件符合設定則進入步驟(3)

(3)當APP將新版本的固件下載完成後,進入步驟(4)

(4)跳轉到BOOT區,執行更新操作;

(5) BOOT將APP區擦除,並將新APP從備份區寫入到APP區,寫入完畢後擦除備份區的固件;

(6)重啟,重新執行程序。

我們將程序主要分為兩個部分,分別為BOOT程序和APP程序.APP程序即是我們的固件下載程序對應流程圖的(1)(2)(3)步,BOOT程序既是固件更新程序對應流程圖的(4)(5)(6)步:

  1. APP 程序設計(固件驗證與下載)

APP代碼程序初始化網絡配置參數,實現嵌入式設備與服務器的正常連接,下載固件到備區,圖4描述的為嵌入式設備與服務器的通信過程。

figure-4-schematic-diagram-of-server-embedded-device-communication-process
圖4服務器-嵌入式設備通信過程示意圖

固件服務器-嵌入式設備的通信過程大致分為三步:

  1. 連接:嵌入式設備分配socket並連接到服務器。
  2. 通信:連接建立後。服務器在接收到來自嵌入式設備的請求後發送應答。
  3. 關閉:請求/應答完成後關閉連接。

我們APP區的函數主要做的就是下載固件,在程序里我們是通過w5500_version()和w5500_update()兩個函數來實現的,w5500_version()用來驗證當前的版本號與服務器上的版本號是否相同,如果當前版本號小於服務器上的版本號就進行更新。

[sourcecode language=”c”]void w5500_version(void)
{
uint8 recv_buffer[2048];
uint8 version[10];
switch (getSn_SR(W5500_UPDATE)) {
case SOCK_ESTABLISHED:
if (getSn_IR(W5500_UPDATE) & Sn_IR_CON) {
setSn_IR(W5500_UPDATE, Sn_IR_CON);
}
send(W5500_UPDATE,(const uint8 *)postH,sizeof(postH));//發送驗證
Delay_ms(5000);
if ((len = getSn_RX_RSR(W5500_UPDATE)) > 0) {
len = recv(W5500_UPDATE, (uint8*)recv_buffer, len); //接收數據
if (strstr((char*)recv_buffer,"\"error\"")) { //報文內包含error,就結束函數
printf("upload error\r\n");
return;
}
printf("%s\r\n",recv_buffer);//打印服務器響應報文
mid((char*)recv_buffer,"\"version\":",",",(char*)version);//可以獲取路徑
/*********讀取版本號************/
if (strncmp(ver_num,version,7)<0) {
update_flag=1;
mid((char*)recv_buffer,"\"http://W5500.com/fw_update/upload/","\",",(char*)bin_name);//可以獲取路徑
snprintf(post_msg,sizeof(post_msg),
"POST /fw_update/upload/%s HTTP/1.1\r\n"\
"Host:w5500.com\r\n"\ "Accept:image/gif,image/x-xbitmap,image/jpeg,image/pjpeg,*/*\r\n"\
"Pragma:no-cache\r\n"\
"Accept-Encoding: gzip,deflate\r\n"\
"Connection:keep-alive\r\n"\
"\r\n",bin_name);
printf("The version is %s\r\n",ver_num);
} else {
printf("The version is %s\r\n",version);
printf("The version is no need to update\r\n");
return;
}
}
close(W5500_UPDATE);
break;
case SOCK_CLOSE_WAIT:
break;
case SOCK_CLOSED:
socket(W5500_UPDATE,Sn_MR_TCP,30000,Sn_MR_ND);
break;
case SOCK_INIT:
connect(W5500_UPDATE, server_ip ,server_port);
break;
}
}[/sourcecode]

上述函數中W5500先與固件服務器建立TCP Socket 連接,然後通過send函數發送固件查詢報文“postH”,該報文主要功能是把W5500的MAC地址發送給服務器。

[sourcecode language=”c”]char postH[]= {
"POST /fw_update/2.php HTTP1.1\r\n"\
"Host:w5500.com\r\n"\
"Accept:image/gif,image/x-xbitmap,image/jpeg,image/pjpeg,*/*\r\n"\
"User-Agent: Mozilla/4.0 (compatible;MSTE 5.5;Windows 98)\r\n"\
"Content-Length:21\r\n"\
"Content-Type:application/x-www-form-urlencoded\r\n"\
"Cache-Control:no-Cache\r\n"\
"Connection:close\r\n"\
"\r\n"\
"mac=00:08:DC:11:12:13"\
};[/sourcecode]

服務器端會根據傳送的MAC地址檢查設備是否註冊以及是否有相關類型的固件。如果驗證不通過固件服務器就會向嵌入式設備發送error信息。如果驗證通過服務器會向嵌入式設備回復如圖5所示的報文,報文中包含最新的版本號、下載路徑、固件大小、文件Hash校驗值等4個關鍵信息:{“version”:”X.X.X”,”path”:”…”,”size”:”XX”,”hash”:”…”},客戶端收到這4個關鍵信息後,先提取版本號與當前版本號比較如果比當前版本號新則置位更新標誌位並且拼接下載固件的報文。

HTTP/1.1 200 OK
Date: Mon, 12 Jun 2017 08:21:41 GMT
Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2h PHP/5.6.24
X-Powered-By: PHP/5.6.24
Content-Length: 177
Connection: close
Content-Type: text/html; charset=utf-8

{"version”:“1.0.1", “path”: “http: //w5500. com/fw_update/upload/e9564feal7bd5b8ebd195b2ca94a08aa34ca45ce.bin”, “size”: “11",“hash”:“e9564feal7bd5b8ebd195b2ca94a08aa34ca45ce"}

圖5 查詢服務器響應報文

通過檢查固件更新標誌位,當發現此標誌位置位後就運行W5500_update()函數進行固件的下載,首先我們要為嵌入式設備分配一個Socket W5500_UPDATE,這個Socket初始狀態為SOCK_CLOSED,我們通過調用函數socket(W5500_UPDATE, Sn_MR_TCP,30000,Sn_MR_ND),打開Socket,Socket狀態改變為SOCK_INIT,打開Socket後調用connect(W5500_UPDATE,server_ip ,server_port)連接服務器,server_ip和server_port分別為服務器的IP地址和端口號,Socket狀態變為SOCK_ESTABLISHED,在此狀態下我們調用函數Firmware_download()進行固件的下載,具體代碼如下。

[sourcecode language=”c”] void & nbsp;
Firmware_download(void)
{
if (getSn_IR(W5500_UPDATE) & amp; Sn_IR_CON)
{
setSn_IR(W5500_UPDATE, Sn_IR_CON);
}
send(W5500_UPDATE, (const uint8 * ) post_msg, sizeof(post_msg)); //發送驗證
Delay_ms(3000);
if ((len = getSn_RX_RSR(W5500_UPDATE)) & gt; 0)
{
len = recv(W5500_UPDATE, (uint8 * ) Buffer, len); //接收數據
mid((char * ) Buffer, "Content-Length: ", "\r\n", sub); //獲取字符串長度
p = strstr((char * ) Buffer, "\r\n\r\n");
tmplen = len – (p – (char * ) Buffer) – 4; //第一個包內的數據長度
content_len = ATOI32(sub, 10);
write_flag(content_len); //將固件長度寫入eeprom
while (rxlen != content_len)
{
if (rxlen == 0)
{
Erase_Page(); & nbsp; & nbsp; & nbsp; //擦除flash內的數據
if ((tmplen % 2) != 0) //如果是奇數個數據
{
flg = 1;
tail = Buffer[len – 1]; //保留最後一字節的數據
}
for (i = 0; i & lt; tmplen – 1; i = i + 2) //半字寫入
{
data = * (p + 4 + i + 1);
data = (data & lt; & lt; 8) + ( * (p + 4 + i));
FLASH_ProgramHalfWord(flashdest, data);
recv_count++;
flashdest += 2;
}
rxlen = tmplen;
tmplen = 0;
} else if (rxlen & gt; 0)
{
memset(Buffer, 0xff, 2048);
tmplen = getSn_RX_RSR(W5500_UPDATE);
if (tmplen & gt; 0)
{
if (flg == 1) //判斷上個包是否有數據
{
tmplen = recv(W5500_UPDATE, (uint8 * )(Buffer + 1), tmplen);
Buffer[0] = tail; //拼接數據
if (((tmplen + 1) % 2) != 0) //總字節數是奇數,flg置位,取出最後一個字節
{
flg = 1;
tail = Buffer[tmplen];
data_len = tmplen + 1; //總字節數
} else //總字節數為偶數
{
flg = 0; //清除標誌位
tail = 0;
data_len = tmplen + 1;
}
} else
{
//上個包為偶數個數據
tmplen = recv(W5500_UPDATE, (uint8 * ) Buffer, tmplen);
if ((tmplen % 2) != 0) //本次數據包為奇數個
{
flg = 1; //標誌位置位
tail = Buffer[tmplen – 1]; //保存最後一個數據
}
data_len = tmplen;
}
if ((rxlen + tmplen) == content_len) //判斷是否為最後一個包
{
data_len = & nbsp;
data_len + 2;
}
for (i = 0; i & lt; data_len – 1; i += 2)
{
data = Buffer[i + 1];
data = (data & lt; & lt; 8) + Buffer[i];
FLASH_ProgramHalfWord(flashdest, data);
flashdest += 2;
recv_count++;
}
rxlen += tmplen;
}
}
}
FLASH_Lock();
update_flag = 2;
}
}[/sourcecode]

在Firmware_download()函數中,向服務器發送請求下載固件的報文post_msg,服務器在收到報文以後,會向嵌入式設備發如圖5所示的下載送響應報文,並開始傳送固件,我們通過下載響應報文中Content-Length獲取待傳送固件的長度content_len然後根據文件長度去獲取數據並將數據寫入flash的Backup區。通過以上的函數操作,我們已經從服務器成功的獲取了固件文件即完成我們的APP下載的步驟。

HTTP/1.1 200 OK
Date: Mon, 12 Jun 2017 08:21:43 GMT
Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2h PHP/5.6.24
Last-Modified: Tue, 09 May 2017 05:59:00 GMT
ETag: “2da8-54f110d53954b”
Accept-Ranges: bytes
Content-Length: 11688
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/octet-stream

圖6 下載服務器響應報文

  1. BOOT程序設計(固件更新)

我們前面完成了嵌入式固件的下載,下面我們就要介紹嵌入式設備的BOOT更新步驟,

BOOT代碼程序主要功能是通過對Flash的操作完成目標區域的數據寫入或者擦除,實現Backup區的數據向APP區的轉移以及轉移完成後的對Flash區的處理。根據我們圖3 嵌入式設備固件更新流程圖,Boot區的代碼程序會檢測Backup區是否有數據,如果有數據先擦除APP區的原有數據然後把Backup區的數據複製到APP區,最後清空Backup區的數據,實現嵌入式設備固件更新的具體代碼程序如下:

[sourcecode language=”c”]bool copy_app(uint32 fw_len, uint32 fw_checksum)
{
uint32 i,nErasedPage;
if (fw_len>0) {
uint32 nPage=FLASH_PagesMask(fw_len);
uint32 checksum=0;
FLASH_Unlock(); //解鎖flash
FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 清除標誌位
//擦除APP區的數據為複製做準備
for (nErasedPage=0; nErasedPage<nPage; nErasedPage++) {
FLASH_ErasePage(ApplicationAddress + 0x400*nErasedPage);
}
for (i=0; i<fw_len; i+=2) {
FLASH_ProgramHalfWord(ApplicationAddress+i, *(uint16*)(AppBackupAddress+i));
checksum+=*(uint16*)(ApplicationAddress+i);
}
//擦除Backup 區內的數據
for (nErasedPage=0; nErasedPage<nPage; nErasedPage++) {
FLASH_ErasePage(AppBackupAddress + 0x400*nErasedPage);
}
FLASH_Lock();
return TRUE;
} else
return FALSE;
}[/sourcecode]

Copy_app()函數實現了固件數據從Backup區向App區的轉移,該函數執行後我們就實現了固件更新。通過APP和BOOT 我們實現了嵌入式設備的雲服務器固件更新。

通過BOOT代碼和APP代碼的協作,我們實現了與固件服務器的通訊,並通過“驗證-下載-更新”等一系列步驟實現了固件的更新。但是和前文的結尾一樣,小熊需要再次提醒各位,“固件升級有風險,務必謹慎再謹慎”,小熊所做的只是一個該系統的簡單雛形,希望能對各位有啟發作用而已,如果需要在批量系統上實現該設想,無疑需要做的工作還有很多很多,在各個操作步驟上都需要做嚴謹的糾錯與冗餘設計。