其實FOTA(Firmware Over-The-Air)技術,已經是一個相對比較成熟的技術了,不管是硬件還是軟件都已經很完善了,就比如我們的手機系統更新,帶的手環或者智能手表的更新,又或者是智能音響的升級,相信大家現在在各種電子產品中都會見到固件更新,這種固件更新大家都可以理解為是FOTA,只不過叫法不是很統一。那么它有什么地方值得我們學習呢?其實最重要的一點就是,假如我們是做的真正的產品,當你賣出去了很多產品,但是這時候你發現產品有Bug,那怎么辦呢?這時候就需要我們通過FOTA來對產品進行固件升級,這樣就可以完美的解決掉Bug了,那么假如沒有FOTA技術,難不成退貨?難不成去用戶家里升級?這都是不現實的。
呃呃,閑話少說,我們還是開始正題,FOTA其實就是在樂鑫給的Demo中有,但是很多人都不是很注重樂鑫給的Demo,或者官方給的文檔,遇到問題的時候總是在尋求他人的幫助,但是這些問題自己多讀幾遍文檔啥的都可以完全解決,再者說別人說的也不一定正確,所以還是建議大家多讀文檔,多看官方給的Demo。
那么先給大家一個本篇文章必看的文檔鏈接,看一下官方對FOTA更權威的講解,而不是聽我這~
https://www.espressif.com/sites/default/files/documentation/99c-esp8266_fota_upgrade_cn.pdf
大家可以跟着第二章利用樂鑫的雲平台進行一次FOTA升級測試,當然這一種方式比較繁瑣的,工程也比較大,閱讀起來還是有一定的難度,那么大家就可以再看一下第三章的升級原理,仔細閱讀一下,你就可以自己寫了~
我們先來看一下Flash布局,幫助我們更好的理解,這里為了方便和大家一起分析,就把文檔中重要的部分截圖展示,還是建議大家去仔細閱讀一下的:
可以看到圖中,我用紅框圈出來了兩部分,這兩部分就是FOTA當中我們的初始化固件(user1.bin)和升級固件(user2.bin)在flash中的存儲位置,也就是說user2.bin是我們通過FOTA無線熱更新升級的,但是這里面最重要的一個部分我並沒有圈出來,大家猜一下是哪一部分呢?相信大家肯定都知道,那就是最前面的boot信息部分,也就是我們常說的bootloader部分,那么這一部分到底有什么作用呢?
是的,這問題明顯文檔中也提到了,它的作用就是決定運行user1.bin部分還是user2.bin部分,你把整個user2.bin的固件升級完成了,但是一重啟還是運行user1.bin部分,那是不是就沒啥用了呢?所以第一次燒寫的時候千萬不要忘記燒寫boot_Vxxx.bin。
在前面我講大家可以跟着第二章利用樂鑫的雲平台進行一次測試,那么我們自己做不利用樂鑫的雲平台,是不是需要一個服務器呢?回答:是的。假如沒有呢?那我們還有很多方式可以實現,比如本地搭建,缺點是只能局域網內升級,說白了,我們只需要將固件放在一個可以通過url訪問的位置,並且可以下載就可以完成我們的FOTA升級了。
這一點樂鑫也有提我們利用自定義服務器的要求:
如果你對web這塊比較熟悉,那么就可以很容易的實現了,不熟悉也沒關系,我們還有萬能的python,利用python的web框架,我們就可以很簡單的實現一個web server了,那么這里我們寫一個最簡單的,只提供下載固件的API就可以了,最核心的功能實現了就可以,話不多說,看代碼?
1 import os 2 from werkzeug.utils import secure_filename 3 from flask import Flask,request,send_from_directory 4 5 6 app = Flask(__name__) 7 8 @app.route('/api/download_firmware',methods=['GET']) 9 def download(): 10 filename = request.args.get('firmware_bin') 11 12 print("\n") 13 print("[IAMLIUBO] file name: " + filename) 14 print("[IAMLIUBO] file size: " + str(os.path.getsize('bin/upgrade/'+filename))) 15 print("\n") 16 17 if os.path.isfile(os.path.join('bin/upgrade', filename)): 18 return send_from_directory('bin/upgrade',filename,as_attachment=True) 19 20 if __name__ == '__main__': 21 app.run(host='0.0.0.0',debug=True)
這里我們只實現了一個最簡單的web server,並且對外提供固件下載功能,這個腳本直接放在工程目錄下運行就可以了,FOTA生成的固件都在“bin/upgrade”文件夾下,所以你只要正確編譯好了固件是不會有問題的,需要說明的是Flask框架默認端口是5000。
那么我們再來看一下設備端的代碼,其實非常簡單,這些代碼是我從樂鑫給的Demo中分離出來的,沒有用的全部刪掉了,先上代碼給大家看一下:
1 LOCAL void ICACHE_FLASH_ATTR 2 tcp_client_connect_cb(void *arg) 3 { 4 struct espconn *pespconn = arg; 5 struct upgrade_server_info *server = NULL; 6 7 INFO("[IAMLIUBO] tcp client connect cb\n"); 8 if (wifi_get_opmode() == STATIONAP_MODE ) { 9 wifi_set_opmode(STATION_MODE); 10 } 11 12 espconn_regist_recvcb(pespconn, tcp_client_recv_cb); 13 espconn_regist_sentcb(pespconn, tcp_client_sent_cb); 14 15 server = (struct upgrade_server_info *)os_zalloc(sizeof(struct upgrade_server_info)); 16 17 server->upgrade_version[15] = '\0'; 18 19 INFO("[IMALIUBO] fota upgarde start\r\n"); 20 fota_upgrade_begin(pespconn, server); 21 } 22 LOCAL void ICACHE_FLASH_ATTR 23 fota_upgrade_rsp(void *arg) 24 { 25 struct upgrade_server_info *server = arg; 26 struct espconn *pespconn = server->pespconn; 27 28 if (server->upgrade_flag == true) { 29 INFO("[IAMLIUBO] fota upgarde successfully\n"); 30 system_upgrade_reboot(); 31 } else { 32 INFO("[IAMLIUBO] fota upgrade failed\n"); 33 } 34 35 os_free(server->url); 36 server->url = NULL; 37 os_free(server); 38 server = NULL; 39 } 40 41 42 LOCAL void ICACHE_FLASH_ATTR 43 fota_upgrade_begin(struct espconn *pespconn, struct upgrade_server_info *server) 44 { 45 uint8 user_bin[30] = {0}; 46 47 server->pespconn = pespconn; 48 49 os_memcpy(server->ip, pespconn->proto.tcp->remote_ip, 4); 50 51 server->port = OTA_SERVER_PORT; 52 53 54 server->check_cb = fota_upgrade_rsp; 55 server->check_times = 120000; 56 57 if (server->url == NULL) { 58 server->url = (uint8 *)os_zalloc(512); 59 } 60 61 if (system_upgrade_userbin_check() == UPGRADE_FW_BIN1) { 62 os_memcpy(user_bin, "user2.4096.new.6.bin", 21); 63 } else if (system_upgrade_userbin_check() == UPGRADE_FW_BIN2) { 64 os_memcpy(user_bin, "user1.4096.new.6.bin", 21); 65 } 66 67 os_sprintf(server->url, "GET /api/download_firmware?firmware_bin=%s HTTP/1.0\r\nHost: "IPSTR":%d\r\n"pheadbuffer"", 68 user_bin,IP2STR(server->ip)); 69 70 INFO("[IAMLIUBO] %s\n",server->url); 71 72 73 if (system_upgrade_start(server) == false) { 74 INFO("[IAMLIUBO] upgrade is already started\n"); 75 } 76 } 77 78 79 void ICACHE_FLASH_ATTR 80 fota_init(void) 81 { 82 user_conn.proto.tcp = &user_tcp; 83 user_conn.type = ESPCONN_TCP; 84 user_conn.state = ESPCONN_NONE; 85 86 uint32 ota_server_ip = ipaddr_addr(OTA_SERVER_IP); 87 88 os_memcpy(user_conn.proto.tcp->remote_ip, &ota_server_ip, 4); 89 user_conn.proto.tcp->local_port = espconn_port(); 90 91 user_conn.proto.tcp->remote_port = OTA_SERVER_PORT; 92 93 espconn_regist_connectcb(&user_conn, tcp_client_connect_cb); 94 espconn_regist_reconcb(&user_conn, tcp_client_recon_cb); 95 96 espconn_connect(&user_conn); 97 }
主要的就這幾個函數吧,第一步是初始化,就是我們先跟服務器建立TCP連接,可能有人會問為什么還要建立TCP連接呢?這就涉及到網絡知識了,大家可以自行百度學習一下哦~建立TCP連接成功后我們就開始升級了,可以看到我們在連接成功回調函數里調用了fota_upgrade_begin()函數,在這里我們還聲明了一個結構體,用於保存升級信息,包括服務器IP,端口,升級前版本信息,此次升級版本信息,固件的URL地址等等,不過我們這里只是簡單的去實現,所以很多並沒有用到,但這不影響我們使用。
開始升級后,我們需要先檢查當前的userbin,然后去拼裝我們的URL,如果你有別的版本命名方式,就需要根據自己的命名方式去拼裝URL了。
1 if (system_upgrade_userbin_check() == UPGRADE_FW_BIN1) { 2 os_memcpy(user_bin, "user2.4096.new.6.bin", 21); 3 } else if (system_upgrade_userbin_check() == UPGRADE_FW_BIN2) { 4 os_memcpy(user_bin, "user1.4096.new.6.bin", 21); 5 } 6 7 os_sprintf(server->url, "GET /api/download_firmware?firmware_bin=%s HTTP/1.0\r\nHost: "IPSTR":%d\r\n"pheadbuffer"", 8 user_bin,IP2STR(server->ip));
升級完成后,應該做什么呢?當然是重啟了,我們在回調函數中檢查升級是否成功,如果成功,我們就重啟運行新的固件:
1 if (server->upgrade_flag == true) { 2 INFO("[IAMLIUBO] fota upgarde successfully\n"); 3 system_upgrade_reboot(); 4 } else { 5 INFO("[IAMLIUBO] fota upgrade failed\n"); 6 }
至此,就完成了,是不是很簡單呢?大家可以直接拉取我的例程運行,如果你不做改動,你會發現會不停的在兩個固件中互相拉取重啟運行,而且你也可以不用用串口下載程序了,直接通過FOTA下載程序,簡單方便又快捷~
快來測試一下吧,附上Github倉庫地址:
https://github.com/imliubo/makingfunxyz-esp8266
歡迎大家去我的倉庫點個star,有問題或者Bug可以提交issues,我看到后會第一時間回復,如果您對我的代碼有改進意見,也歡迎fork后提交PR,我會及時采納大家的意見。