※已刊登在“无线电”07月刊上手把手教你如何实现自动固件更新
—— 嵌入式篇
作者:常席正,魏文龙
我们在上期 “手把手教你如何实现自动固件更新——服务器篇”那篇文章中介绍了通过云服务器更新固件的方法,并着重介绍了服务器端的前期准备以及软件设计。这次小熊和大家分享下嵌入式端的软件设计。相对于服务器端的软件设计,嵌入式软件设计需要更为严谨,因为固件升级出错的后果会非常严重,因为这个功能一般使用在批量的设备上,而固件是控制系统的核心软件,因此固件更新出错的话,会造成设备大面积瘫痪。所以严重性不言而喻。
本期我们来介绍一下客户端的具体实现过程,如图1所示,根据我们的自动固件更新协议,在与更新服务器建立连接后,服务器会要求客户端进行一系列验证,嵌入式设备在通过验证后,更新服务器会告知此嵌入式设备的最新固件信息,嵌入式设备根据这些信息下载并更新固件
图1 自动固件更新协议
“下载并更新固件”几个字囊括了我们所要执行的所有步骤。我们将按照以下步骤分别介绍。
1.下载准备–对Flash进行分区
为了实现‘下载并更新固件’我们先要做一些准备工作,我们把MCU的Flash分为三个区分别为BOOT区,APP区和Backup区,如图2所示
图2内存空间分配
了解了空间分配之后,我们再来看一下我们这个演示中各部分的主要功能:
- BOOT区:
- 清空APP区,为新APP写入做准备;
- 把暂存在Backup区的新版本程序拷贝到APP区;
- APP区
APP区是应用程序运行区域,实现正常的网络连接,并更新固件。
- 配置网络参数;
- 在线固件升级;
每次上电都会从Boot区引导,若判断上层APP区载入程序是否成功,成功则直接从Boot区跳转到APP区,正常运行主程序。
- Backup区
- 从服务器接收并备份需要更新的新应用程序,也就是固件存储区域。
备注:由于备区的大小为112K,所以意味着APP的大小最大为112K ;
- 程序流程设计
我们完成了对闪存的分区规划后,就要设计我们程序的流程,图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)步:
- APP 程序设计(固件验证与下载)
APP代码程序初始化网络配置参数,实现嵌入式设备与服务器的正常连接,下载固件到备区,图4描述的为嵌入式设备与服务器的通信过程。
图4服务器-嵌入式设备通信过程示意图
固件服务器-嵌入式设备的通信过程大致分为三步:
- 连接:嵌入式设备分配socket并连接到服务器。
- 通信:连接建立后。服务器在接收到来自嵌入式设备的请求后发送应答。
- 关闭:请求/应答完成后关闭连接。
我们APP区的函数主要做的就是下载固件,在程序里我们是通过w5500_version()和w5500_update()两个函数来实现的,w5500_version()用来验证当前的版本号与服务器上的版本号是否相同,如果当前版本号小于服务器上的版本号就进行更新。
01 void w5500_version(void)
02 {
03 uint8 recv_buffer[2048];
04 uint8 version[10];
05 switch (getSn_SR(W5500_UPDATE)) {
06 case SOCK_ESTABLISHED:
07 if (getSn_IR(W5500_UPDATE) & Sn_IR_CON) {
08 setSn_IR(W5500_UPDATE, Sn_IR_CON);
09 }
10 send(W5500_UPDATE,(const uint8 *)postH,sizeof(postH));//发送验证
11 Delay_ms(5000);
12 if ((len = getSn_RX_RSR(W5500_UPDATE)) > 0) {
13 len = recv(W5500_UPDATE, (uint8*)recv_buffer, len); //接收数据
14 if (strstr((char*)recv_buffer,”\”error\””)) { //报文内包含error,就结束函数
15 printf(“upload error\r\n”);
16 return;
17 }
18 printf(“%s\r\n”,recv_buffer);//打印服务器响应报文
19 mid((char*)recv_buffer,”\”version\”:”,”,”,(char*)version);//可以获取路径
20 /*********读取版本号************/
21 if (strncmp(ver_num,version,7)<0) {
22 update_flag=1;
23 mid((char*)recv_buffer,”\”http://W5500.com/fw_update/upload/”,”\”,”,(char*)bin_name);//可以获取路径
24 snprintf(post_msg,sizeof(post_msg),
25 “POST /fw_update/upload/%s HTTP/1.1\r\n”\
26 “Host:w5500.com\r\n”\ “Accept:image/gif,image/x-xbitmap,image/jpeg,image/pjpeg,*/*\r\n”\
27 “Pragma:no-cache\r\n”\
28 “Accept-Encoding: gzip,deflate\r\n”\
29 “Connection:keep-alive\r\n”\
30 “\r\n”,bin_name);
31 printf(“The version is %s\r\n”,ver_num);
32 } else {
33 printf(“The version is %s\r\n”,version);
34 printf(“The version is no need to update\r\n”);
35 return;
36 }
37 }
38 close(W5500_UPDATE);
39 break;
40 case SOCK_CLOSE_WAIT:
41 break;
42 case SOCK_CLOSED:
43 socket(W5500_UPDATE,Sn_MR_TCP,30000,Sn_MR_ND);
44 break;
45 case SOCK_INIT:
46 connect(W5500_UPDATE, server_ip ,server_port);
47 break;
48 }
49 }
上述函数中W5500先与固件服务器建立TCP Socket 连接,然后通过send函数发送固件查询报文“postH”,该报文主要功能是把W5500的MAC地址发送给服务器。
01 char postH[]= {
02 “POST /fw_update/2.php HTTP1.1\r\n”\
03 “Host:w5500.com\r\n”\
04 “Accept:image/gif,image/x-xbitmap,image/jpeg,image/pjpeg,*/*\r\n”\
05 “User-Agent: Mozilla/4.0 (compatible;MSTE 5.5;Windows 98)\r\n”\
06 “Content-Length:21\r\n”\
07 “Content-Type:application/x-www-form-urlencoded\r\n”\
08 “Cache-Control:no-Cache\r\n”\
09 “Connection:close\r\n”\
10 “\r\n”\
11 “mac=00:08:DC:11:12:13″\
12 };
服务器端会根据传送的MAC地址检查设备是否注册以及是否有相关类型的固件。如果验证不通过固件服务器就会向嵌入式设备发送error信息。如果验证通过服务器会向嵌入式设备回复如图5所示的报文,报文中包含最新的版本号、下载路径、固件大小、文件Hash校验值等4个关键信息:{“version”:”X.X.X”,”path”:”…”,”size”:”XX”,”hash”:”…”},客户端收到这4个关键信息后,先提取版本号与当前版本号比较如果比当前版本号新则置位更新标志位并且拼接下载固件的报文。
图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()进行固件的下载,具体代码如下。
void Firmware_download(void)
{
if(getSn_IR(W5500_UPDATE) & 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)) > 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(); //擦除flash内的数据
if((tmplen%2)!=0)//如果是奇数个数据
{
flg=1;
tail=Buffer[len-1];//保留最后一字节的数据
}
for(i=0;i<tmplen-1;i=i+2) //半字写入
{
data = *(p+4+i+1);
data=(data<<8) + (*(p+4+i));
FLASH_ProgramHalfWord(flashdest, data);
recv_count++;
flashdest += 2;
}
rxlen = tmplen;
tmplen=0;
}else if(rxlen>0)
{
memset(Buffer,0xff,2048);
tmplen = getSn_RX_RSR(W5500_UPDATE);
if(tmplen>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 = data_len +2;
}
for(i = 0; i<data_len-1; i+=2)
{
data = Buffer[i+1];
data =(data<<8) + Buffer[i];
FLASH_ProgramHalfWord(flashdest, data);
flashdest += 2;
recv_count++;
}
rxlen += tmplen;
}
}
}
FLASH_Lock();
update_flag=2;
}
}
在Firmware_download()函数中,向服务器发送请求下载固件的报文post_msg,服务器在收到报文以后,会向嵌入式设备发如图5所示的下载送响应报文,并开始传送固件,我们通过下载响应报文中Content-Length获取待传送固件的长度content_len然后根据文件长度去获取数据并将数据写入flash的Backup区。通过以上的函数操作,我们已经从服务器成功的获取了固件文件即完成我们的APP下载的步骤。
图6 下载服务器响应报文
- BOOT程序设计(固件更新)
我们前面完成了嵌入式固件的下载,下面我们就要介绍嵌入式设备的BOOT更新步骤,
BOOT代码程序主要功能是通过对Flash的操作完成目标区域的数据写入或者擦除,实现Backup区的数据向APP区的转移以及转移完成后的对Flash区的处理。根据我们图3 嵌入式设备固件更新流程图,Boot区的代码程序会检测Backup区是否有数据,如果有数据先擦除APP区的原有数据然后把Backup区的数据复制到APP区,最后清空Backup区的数据,实现嵌入式设备固件更新的具体代码程序如下:
01 bool copy_app(uint32 fw_len, uint32 fw_checksum)
02 {
03 uint32 i,nErasedPage;
04 if (fw_len>0) {
05 uint32 nPage=FLASH_PagesMask(fw_len);
06 uint32 checksum=0;
07 FLASH_Unlock(); //解锁flash
08 FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 清除标志位
09 //擦除APP区的数据为复制做准备
10 for (nErasedPage=0; nErasedPage<nPage; nErasedPage++) {
11 FLASH_ErasePage(ApplicationAddress + 0x400*nErasedPage);
12 }
13 for (i=0; i<fw_len; i+=2) {
14 FLASH_ProgramHalfWord(ApplicationAddress+i, *(uint16*)(AppBackupAddress+i));
15 checksum+=*(uint16*)(ApplicationAddress+i);
16 }
17 //擦除Backup 区内的数据
18 for (nErasedPage=0; nErasedPage<nPage; nErasedPage++) {
19 FLASH_ErasePage(AppBackupAddress + 0x400*nErasedPage);
20 }
21 FLASH_Lock();
22 return TRUE;
23 } else
24 return FALSE;
25 }
Copy_app()函数实现了固件数据从Backup区向App区的转移,该函数执行后我们就实现了固件更新。通过APP和BOOT 我们实现了嵌入式设备的云服务器固件更新。
通过BOOT代码和APP代码的协作,我们实现了与固件服务器的通讯,并通过“验证-下载-更新”等一系列步骤实现了固件的更新。但是和前文的结尾一样,小熊需要再次提醒各位,“固件升级有风险,务必谨慎再谨慎”,小熊所做的只是一个该系统的简单雏形,希望能对各位有启发作用而已,如果需要在批量系统上实现该设想,无疑需要做的工作还有很多很多,在各个操作步骤上都需要做严谨的纠错与冗余设计。