STM32网络远程升级固件的IAP程序实现与解析 ---附亲测稳定能用的程序

2019-07-20 00:39发布

本帖最后由 lzq12 于 2017-6-27 15:22 编辑

          本文主要对STM32网络升级固件的IAP程序进行解析,也就是在STM32联网的情况下在浏览器上输入指定的IP地址(目前设置为192.168.1.101),然后在浏览器上输入用户名和密码,登陆后可以选择需要升级的bin文件进行固件升级。以下是目前该程序应用的硬件与软件环境:         1.硬件:STM32F407(理论上STM32系列都可以),网卡芯片LAN8720,其他部分参考正点原子的STM32F407探索者开发板。         2.软件:Keil5 ,LWIP1.4.1 主要是基于正点原子STM32F407探索者的第六十章网络通信实验程序与第五十五章串口IAP实验程序(这部分只用到了跳转和烧写FLASH程序)。           说明:该程序与我之前编写的《STM32F407通过SD卡进行程序升级(把bin文件烧写到FLASH的方式)》程序整合起来就可以实现SD卡+网络升级,即可以通过SD卡进行程序升级,如果升级失败自动跳转 去进行网络升级,也可以直接进行网络升级。目前该程序可以应用在项目上,网络升级和SD卡升级均稳定无差错。该程序的网络升级大概需要15秒钟(从点击<上传>到程序升级成功)。         STM32F407通过SD卡进行程序升级(把bin文件烧写到FLASH的方式)》本论坛的链接:http://www.openedv.com/forum.php?mod=viewthread&tid=90835                以下先对网络升级部分的操作步骤进行解说,后面再贴上各个C文件的程序。
一、网络升级的主要实现过程       按住K1键开机,自动进入网络升级模式(进入网络升级模式的方法不一定是要按下K1,可以自己编写条件,比如SD卡升级失败后就进入网络升级模式),然后在浏览器上输入192.168.1.101登陆页面
登陆界面.jpg
        输入用户名和密码(用户名和密码都是admin)之后,点击<登陆>进入文件升级界面,选择升级的bin文件然后点击<上传>进行升级
选择升级文件界面.jpg 更新成功.jpg              本程序已经通过测试,网络升级不会有错乱,程序中会对升级的bin文件的文件名与文件的合法性进行校验,其中网络IP、用户名、密码、文件名均可在程序里自行更改。二、网页界面的HTML代码编写          网页界面不是用他人那种把网页界面转成数组然后存放在程序里调用的方式,用的是直接编写HTML代码的方式,简单直观而且可以任意修改,登陆界面和文件升级界面都可以自己修改,可以看函数http_write_loginweb()和http_write_updatebinweb(),效果如上图的登陆界面和文件升级界面。//用户和密码登陆WEB界面的HTML内容代码填充 voidhttp_write_loginweb(char *pbuff, int *ppos,int error){      *ppos += sprintf(pbuff + *ppos,"<HTML><META HTTP-EQUIV = "Pragma"CONTENT="no-cache">");      http_write_script(pbuff,ppos,error);      *ppos += sprintf(pbuff + *ppos,"<BODY onload=doLoad();> ");      *ppos += sprintf(pbuff + *ppos,"<FORM METHOD=POST ACTION="/checklogin.cgi">");
      //添加WEB界面的内容      *ppos += sprintf(pbuff + *ppos,"<strong>用户名</strong><input type="text"size="20" name="username">");      *ppos += sprintf(pbuff + *ppos,"<strong>密码</strong><input type="password"size="20" name="password">");      *ppos += sprintf(pbuff + *ppos,"<BR><input type="submit" name="login"value="登陆">");
      *ppos += sprintf(pbuff + *ppos,"</FORM></BODY></HTML>");}//选择升级程序WEB界面的HTML内容代码填充void http_write_updatebinweb(char *pbuff, int *ppos,int error){  *ppos += sprintf(pbuff +*ppos, "<HTML><META HTTP-EQUIV = "Pragma"CONTENT="no-cache">");  http_write_script(pbuff,ppos,error);  *ppos += sprintf(pbuff +*ppos, "<BODY onload=doLoad();> ");  *ppos += sprintf(pbuff +*ppos, "<FORM METHOD=POST enctype="multipart/form-data"ACTION="/update.cgi">");
  //添加WEB界面的内容  *ppos += sprintf(pbuff +*ppos, "<TD><input name="upbin"id="upbin" type="file"></TD>");  *ppos += sprintf(pbuff +*ppos, "<TD> <INPUT name="subbin"class="button" id="subbin" type="submit"value="上传"></TD>");  *ppos += sprintf(pbuff +*ppos, "</FORM></BODY></HTML>");}网页界面还可以做得更美观,但这个就要学一些HTML编程了,由于对HTML不是特别熟练,所以对于HTML部分就不做过多解析了。关于网络解析部分,可以直接看httpd.chttpd_cgi_ssi.c三、登陆界面的POST指令解析         网络升级主要用到POST指令,这个需要先了解一下POST指令相关的内容,还有就是CGI的解析,也用到了POST指令下发数据,但和GET的CGI解析方法一样,关于SSI、CGI及GET和POST指令可以看我写的《STM32 网络通信Web Server中 SSI与CGI的应用解析》,本论坛链接:http://www.openedv.com/forum.php?mod=viewthread&tid=104581&extra=。用到CGI的部分主要是在输入用户名和密码之后,点击登陆时,会下发POST指令,这时需要对POST指令进行解析,该POST指令数据通过软件Wireshark来查看,首先打开Wireshark,选择本地连接
1.jpg


           然后在地址栏输入 ip.addr == 192.168.1.101,然后按右边的箭头开始接收数据[size=14.6667px]

2.jpg

          关于解析的过程就是把POST指令找出来,然后执行相应的程序,比如对比用户名和密码等。接下来先看输入用户名和密码,按下<登陆>之后,浏览器下发的POST指令数据:
POST.jpg
           POST指令很明显就是由字符串”POST /XXXXX.cgi”开头,而这个CGI现在是checklogin.cgi,这个可以看函数http_write_loginweb()里面的HTML代码,"<FORMMETHOD=POST ACTION="/checklogin.cgi">",这个HTML的意思是该form表单是通过post指令的方式下发数据,并由checklogin.cgi进行响应(本人HTML也是很菜,目前就这么理解吧)。           开启POST指令需要定义#defineLWIP_HTTPD_SUPPORT_POST   1           然后编写接收和解析POST指令的三个函数:err_thttpd_post_begin(void *connection, const char *uri, const char *http_request,                       u16_t http_request_len, intcontent_len, char *response_uri,                       u16_t response_uri_len,u8_t *post_auto_wnd){#ifLWIP_HTTPD_CGI  int i = 0;#endifstructhttp_state *hs = (struct http_state *)connection; if(!uri || (uri[0] == '')) {    return ERR_ARG; } hs->cgi_handler_index = -1;   // 此变量为本人自己在struct http_state 添加 用于保存CGI handler 索引为-1表示无CGI handler索引 hs->response_file = NULL; // 此变量为本人自己在struct http_state 添加 用于保存 CGI handler 处理完后返回的响应uri.#ifLWIP_HTTPD_CGI  if (g_iNumCGIs && g_pCGIs) {    for (i = 0; i < g_iNumCGIs; i++) {      if (strcmp(uri, g_pCGIs.pcCGIName) ==0) { hs->cgi_handler_index = i; // 找到响应的 CGI handler 将其保存在cgi_handler_index 以便在httpd_post_receive_data中使用         break;       }    }  }  if(i == g_iNumCGIs) {    return ERR_ARG; // 未找到CGI handler   }#endif  return ERR_OK;}#defineLWIP_HTTPD_POST_MAX_PAYLOAD_LEN    512//MEM2_MAX_SIZE//512static char http_post_payload[LWIP_HTTPD_POST_MAX_PAYLOAD_LEN];//主要是存放参数值,如username=admin&password=admin&login=%B5%C7%C2%BDstatic u32_thttp_post_payload_len = 0;err_thttpd_post_receive_data(void *connection, struct pbuf *p){    struct http_state *hs = (struct http_state*)connection;    struct pbuf *q = p;    int count;    u32_t http_post_payload_full_flag = 0;    while(q != NULL)  // 缓存接收的数据至http_post_payload    {          if(http_post_payload_len + q->len<= LWIP_HTTPD_POST_MAX_PAYLOAD_LEN) {          MEMCPY(http_post_payload+http_post_payload_len,q->payload, q->len);          http_post_payload_len += q->len;      }      else { // 缓存溢出 置溢出标志位        http_post_payload_full_flag = 1;        break;      }      q = q->next;    }    pbuf_free(p); // 释放pbuf    if(http_post_payload_full_flag) // 缓存溢出 则丢弃数据    {        http_post_payload_full_flag = 0;        http_post_payload_len = 0;        hs->cgi_handler_index = -1;        hs->response_file = NULL;    }    else if(hs->post_content_len_left == 0){  // POST数据已经接收完毕则处理        if(hs->cgi_handler_index != -1) {            count = extract_uri_parameters(hs,http_post_payload);  // 解析            hs->response_file =g_pCGIs[hs->cgi_handler_index].pfnCGIHandler(hs->cgi_handler_index,count, hs->params,                                            hs->param_vals); // 调用解析函数            http_post_payload_len = 0;        }              else {            hs->response_file = NULL;            http_post_payload_len = 0;        }    }    return ERR_OK;}voidhttpd_post_finished(void *connection, char *response_uri, u16_tresponse_uri_len){      struct http_state *hs = (struct http_state*)connection;    if(hs->response_file != NULL) {        strncpy(response_uri,hs->response_file,response_uri_len); // 拷贝uri 用于给浏览器响应相应的请求    }}             这三个函数有多种写法,基本思路就是把抽离出来后的数据,如username=admin&password=admin&login=%B5%C7%C2%BD,再把相应的数据存起来,比如把username存入hs->params[0]中,把admin存入hs->param_vals[0],具体看函数extract_uri_parameters()
四、文件升级界面的POST指令解析          对下发升级文件数据的POST指令进行解析这部分用不到上面的三个函数,所以是在函数http_recv()里面直接对POST指令进行接收和解析。先看文件升级界面的HTML代码,该代码是函数http_write_updatebinweb()实现。          在这个HTML中,FORM表单的代码和其他的不一样,这个要注意:<FORMMETHOD=POST enctype="multipart/form-data"ACTION="/update.cgi">其实就是添加了enctype="multipart/form-data"编码方式,这个主要是用于下发文件数据。          在选择好文件之后,点击<上传>,浏览器就开始下发POST指令以及升级文件的数据。          需要注意的是,#define PBUF_POOL_BUFSIZE       1600这个大小不能太小,因为一帧数据最大是1514,小于1514会导致接收到的数据出现错误。            POST指令数据的解析可以分为三个步骤:            第一步是接收到下发数据的POST指令,POST指令的数据开头为"POST/update.cgi",后面跟上一堆数据,这个可以通过软件Wireshark来查看[size=14.6667px]

升级的第一帧POST指令.jpg

          找到这个POST指令头之后,接着找"boundary="这个字符串,这个字符串后面跟着39个字符”---------------------------------7e17132213f8”(长度和字符有可能是变化的),后面的39个字符是用来判断下发的文件数据的截止位置(我们叫它B),我们把这些字符串先叫boundarystring。接下来还要找"Content-Length: "这个字符串,这个字符串后面的数据是指接下来还要接收的数据量,也就是总的数据量:361774。当然这个总数据量不等于升级文件的数据量,所以才需要boundarystring来判断。            接下来是找"octet-stream"字符串,这个字符串后面跟着的就是升级文件的真正数据了,所以这个是用来判断接收文件数据的起始位置,我们叫它A
[size=14.6667px]

升级的第二帧POST指令.jpg

            那么第一步可以说完成了,我们需要找到真正的升级文件的数据可以理解为找到A和B之间的数据,并且把这些数据收集起来。
升级的最后一帧POST指令.jpg


         所以第二步就是把文件数据收集起来放到数组mem2base[],这个数组是存于外部SRAM中。         第三步就是把数组mem2base[]里面的数据进行校验,然后把数据烧写入FLASH中。         以上三步的实现过程可以看httpd.c里面的函数http_recv()
           按照以上的思路,只要把SD卡升级的部分加上,那就可以实现SD卡+网络升级,具体的工程文件就不贴上来了,就贴几个主要的C文件。感兴趣的朋友还可以对W25Q128这个FLASH芯片进行系统文件升级,原理是一样,16M的文件升级也是稳定无误。目前经过测试这个网络升级程序能够稳定运行,15秒左右就能升级完毕,而且对W25Q128升级16M系统文件也完全没有问题(程序上稍加修改就可以),2分钟左右就能升级完成。
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
该问题目前已经被作者或者管理员关闭, 无法添加新回复
45条回答
lg75
1楼-- · 2019-07-22 01:45
 精彩回答 2  元偷偷看……
muestudio
2楼-- · 2019-07-22 03:20
谢谢分享~~~~~~~~~~有空慢慢学习~~~~
hanzixi_angel
3楼-- · 2019-07-22 05:14
lg75 发表于 2018-3-5 17:20
设备在内网,服务器推送,端口映射?

设备在内网外网无所谓  只要能连上外网就行   http嘛  推送是通过mqtt
lg75
4楼-- · 2019-07-22 09:07
 精彩回答 2  元偷偷看……
hanzixi_angel
5楼-- · 2019-07-22 13:43
lg75 发表于 2018-3-7 08:36
学无止境,请教一下,如果是服务器主动“推送”,那发起升级请求的应该是服务器吧,所以我才会问端口映射 ...

主动方在于服务器   设备端只是接收推送并执行升级  至于端口映射问题都是服务端解决的 具体解决方案不太清楚  可以用花生壳自己映射一下   我们是企业服务器  用的也是第三方的
white199494
6楼-- · 2019-07-22 14:51
楼主想问一下跳转的过程有关闭所有外部中断吗?我只有关闭所有外部中断才可以正常跳转,但是这样的话APP里面所有外部中断都打不开了???

一周热门 更多>