IOT-OS之RT-Thread(十七)--- 如何使用HTTP協議實現OTA空中升級

一、Bootloader OTA 原理

隨着物聯網技術的普及,越來越多的嵌入式產品支持網絡訪問能力,嵌入式產品接入網絡,可以方便的從雲端獲得雲計算和人工智能的支持。嵌入式產品不僅可以將複雜的運算過程放到服務器端完成,還可以接受經過訓練的人工智能模型的協調,實現與其它嵌入式產品協同高效配合,提供智能化場景服務的能力。

這些被賦予人工智能支持的嵌入式產品可以稱爲智能硬件,智能硬件爲了不斷優化與其它智能硬件的高效配合,也爲了不斷擴展支持的服務場景,需要具備自我迭代升級的能力。在博文:ARM 代碼燒錄方案與原理詳解中已經介紹過代碼燒錄與升級的各種方案,既然智能硬件具備網絡訪問能力,使用OTA 空中升級技術實現智能硬件Application 代碼的升級迭代更加便捷,一鍵升級的操作對用戶也更加友好。

OTA 空中升級技術需要開發者自己實現Bootloader 代碼,不過主流的IOT 操作系統開發商已經爲我們提供了Bootloader 的開發框架,我麼只需要在此基礎上根據自己需要進行適量修改即可,大大簡化了開發Bootloader 的工作量。RT-Thread 便爲我們提供了通用的Bootloader 的軟件框架,開發者可以通過該Bootloader 直接使用RT-Thread OTA 功能,輕鬆實現對設備端固件的管理、升級與維護。
Bootloader 框架
RT-Thread 提供的Bootloader 軟件框架,底層由Flash 驅動提供ROM 或Flash 分區訪問的能力。博文ARM 代碼燒錄方案與原理詳解中介紹過,OTA 空中升級需要本地提供部分存儲區間,Bootloader 有一個重要功能就是搬移固件代碼,比如升級固件代碼時需要從Download 分區讀取待升級的固件代碼,經校驗通過後,寫入或搬移到Application 分區覆蓋正在使用的固件代碼,這就完成了固件升級過程。

我們在前篇博文:WLAN管理框架 + AP6181(BCM43362) WiFi模塊工程中FAL 分區的基礎上增加bootloader 分區,更新後的分區表如下:

// projects\stm32l475_ota_sample\ports\fal\fal_cfg.h

#define NOR_FLASH_DEV_NAME "W25Q128"
/* partition table */
#define FAL_PART_TABLE                                                                                                  \
{                                                                                                                       \
    {FAL_PART_MAGIC_WROD, "bootloader",     "onchip_flash",                                    0,        64 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,        "app",     "onchip_flash",                            64 * 1024,       448 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,  "easyflash", NOR_FLASH_DEV_NAME,                                    0,       512 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,   "download", NOR_FLASH_DEV_NAME,                           512 * 1024,      1024 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD, "wifi_image", NOR_FLASH_DEV_NAME,                  (512 + 1024) * 1024,       512 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD,       "font", NOR_FLASH_DEV_NAME,            (512 + 1024 + 512) * 1024,  7 * 1024 * 1024, 0}, \
    {FAL_PART_MAGIC_WROD, "filesystem", NOR_FLASH_DEV_NAME, (512 + 1024 + 512 + 7 * 1024) * 1024,  7 * 1024 * 1024, 0}, \
}

上面的分區表中,bootloader 分區和app(application的簡稱)分區位於片上Flash 的Main Flash memory 存儲區間,download 分區位於片外Flash 的W25Q128 上(由於STM32L475 片上Flash 空間只有512KB,需要片外Flash 擴展存儲空間),download 分區用於暫存Application 代碼更新軟件包。片外Flash上的wifi_image 分區用於存儲AP6181 WIFI 模塊的固件代碼,Bootloader 同樣可以將暫存在download 分區內的WIFI 模塊更新固件包搬移到wifi_image 分區內,實現WIFI 模塊固件的升級。

Bootloader 除了提供訪問Flash 分區,在不同分區之間搬移固件代碼的功能外,還提供了固件加解密、固件解壓縮的功能。由於智能硬件是連接Internet 的,這就有可能遭遇網絡攻擊,比如固件升級包被截獲並篡改等,爲了應對網絡攻擊,Bootloader 提供了將固件升級包進行加密認證傳輸的功能(可以參考博文:TLS 1.2/1.3 加密原理)。爲了減少傳輸開銷,同時減少對存儲空間的佔用,Bootloader 提供了將固件升級包進行壓縮傳輸的功能,如果固件更新代碼佔比較小,還可以以差分升級的方式提高效率。OTA 技術中Bootloader 提供的主要功能如下:

  • 固件加密:支持AES-256 加密算法,提高固件下載、存儲安全性;
  • 固件防篡改:使用HMAC(Hash Message Authentication Code,算是哈希摘要算法比如SHA-256 的進階版)校驗固件包的完整性,如果固件被篡改將無法通過完整性校驗,保證了固件傳輸、存儲的安全可靠;
  • 固件壓縮:支持Quicklz 和Fastlz 等壓縮算法,固件經過高效壓縮,可節省傳輸流量,減少Flash 空間佔用,降低下載時間;
  • 差分升級:根據版本差異生成差分包(常採用多bin 文件升級方式,每次只升級其中的少數bin 文件),進一步節省Flash 空間,節省傳輸流量,加快升級速度;
  • 斷電保護:可將升級進度與狀態同步記錄到ROM中,即便遇到意外斷電中止升級過程,也可在上電重啓後從ROM 讀取升級進度和狀態繼續完成升級過程,減少返廠維修概率;
  • 智能還原:支持將出廠固件或前一個穩定版本的固件存儲到recovery 分區,當運行中的固件損壞時,可以將recovery 分區中的代碼搬移到Applicaion 分區,相當於恢復到出廠固件版本或者回退到前一個穩定版本固件,保證設備的可靠運行。

爲了減少Bootloader 代碼的複雜度,將固件升級包下載過程放到Aplication 代碼中完成了,畢竟通過Internet 下載固件升級包需要TCP/IP 協議棧(包括MAC層的LTE、WLAN、WPAN協議棧和應用層的HTTP、FTP協議棧等)的支持,這些網絡協議棧代碼還是挺佔用存儲空間的。

放到Application 代碼中的OTA Downloader 組件也是OTA 空中升級技術的一個重要組成部分,Bootloader 部分主要實現固件升級包的校驗、解壓縮、解密、代碼搬運等功能。OTA 空中升級技術需要的兩大組件:OTA Downloader 和Bootloader 層級框架圖示如下:
RT-OTA 框架
OTA Downloader 組件主要是將固件升級包下載到特定存儲分區,比如片外Flash 的Download 分區,供Bootloader 從該分區讀取、檢驗固件升級包。OTA Downloader 組件可以支持通過USB 通訊協議(藉助Y-modem 組件)從本地PC 下載固件升級包,也支持通過HTTP 協議(藉助http client 組件)從特定服務器下載固件升級包,從固定雲端服務器(藉助RT-Cloud OTA 組件)下載固件升級包實際使用的還是HTTP 協議,只是提供了更便捷友好的交互界面。

OTA Bootloader 組件主要提供了通過FAL 組件訪問Flash 分區的功能,便於從Download 分區讀取固件升級包,同時將固件代碼搬移到目標存儲區間。爲了提高固件升級包傳輸、存儲的安全性,Bootloader 還提供了Tinycrypt 加密功能(使用AES-256 + HMAC-SHA256算法 )。爲了降低傳輸開銷、減小存儲空間佔用,Bootloader 還提供了Quicklz 或Fastlz 解壓縮組件,這些組件都是可選的。

在嵌入式系統方案裏,要完成一次OTA 固件遠端升級,通常需要以下階段:
OTA 固件升級流程

  1. 準備固件升級文件(RT-Thread 使用ota_packager 打包生成 .rbl 格式的固件升級文件),並上傳OTA 固件升級文件到固件託管服務器;
  2. 設備端使用對應的OTA Downloader 組件從固件託管服務器下載OTA 固件升級文件到本地Download 分區;
  3. 新版本固件下載完成後,在適當的時候重啓進入Bootloader;
  4. Bootloader 對本地Download 分區內的OTA 固件升級文件進行解密、解壓縮、校驗等操作(詳細流程可參考下圖),如果校驗通過則將新版本固件代碼搬運到app 分區(如果是WiFi 固件升級文件則搬運到wifi_image 分區);
  5. 升級成功,執行新版本app 固件。

Bootloader OTA 升級流程
RT-Thread 提供的STM32 Bootloader 是閉源的,本文也沒法對其實現原理進行過多介紹。我們可以通過網頁端http://iot.rt-thread.com在線生成的方式獲取,根據自己使用的芯片填寫相關參數,就可以生成自己芯片可用的bootloader.bin 文件,生成過程可參考博文:STM32 通用 Bootloader
爲Pandora開發板生成Bootloader 配置參數
先看硬件配置部分,只支持SPI Flash,並不支持QSPI 通信協議,Pandora 開發板與W25Q128 Flash是通過QSPI 引腳連接的,這裏如果只能配置SPI 引腳的話,就只能使用QSPI 接口的單端SPI 引腳了(可參考博文:SPI + QSPI + HAL),傳輸速率比較慢。再看分區表配置,只能配置app、download、factory 三個分區,無法爲WIFI 模塊更新固件。

在線生成的Bootloader 雖然能夠使用,但擴展性較弱,使用SPI 協議搬運代碼速度較慢,不能訪問W25Q128 Flash 的全部分區。本文我們使用潘多拉STM32L475 開發板光盤資料中提供的bootloader.bin 文件,將Pandora IoT 例程中該文件的路徑複製到我們工程目錄的路徑如下:

// 潘多拉STM32L475 開發板光盤資料中bootloader.bin 文件路徑
.\RT-Thread IoT例程\examples\23_iot_ota_http\bin\bootloader.bin

// bootloader.bin 文件拷貝到我們工程中的目標路徑
.\projects\stm32l475_ota_sample\bin\bootloader.bin

使用“STM32 ST-LINK Utility” 工具分別將我們通過網頁在線生成的rtboot_l4.bin 和從Pandora IoT 例程拷貝來的bootloader.bin 燒錄到Pandora 開發板中,啓動界面對比如下(左圖是rtboot_l4.bin 的啓動界面,右圖是bootloader.bin 的啓動界面):
網頁生成的和Pandora附帶的Bootloader 對比
從上面左右圖對比可以看出,Pandora IoT 例程中的bootloader.bin 針對STM32L475 開發板做了更多的適配修改,可以訪問上文給出的分區表中的全部分區,通過升級速度對比,猜測這個bootloader.bin 也是支持QSPI 通訊協議的。
通過ST-LINK Utility 燒錄bootloader 文件步驟
我們已經有了bootloader.bin,並將其燒錄到Pandora 開發板內的bootloader 分區(該分區起始地址與大小上文已給出,燒錄方法如上圖示),接下來就該準備Application 升級文件了,Application 代碼包含OTA Downloader 組件,下面介紹OTA Downloader 組件的實現原理。

二、HTTP OTA Downloader 實現

物聯網時代,嵌入式產品越來越多的具備網絡訪問能力,這類產品常通過OTA 空中升級技術完成固件版本更新。不管是通過蜂窩移動網、WLAN、WPAN等無線接入方式訪問Internet,還是通過Ethernet 等有線接入方式訪問Internet,主要都是藉助網絡應用層的HTTP 協議獲取固件升級包的(也有通過FTP 協議獲取的,本文使用HTTP 協議)。

RT-Thread 提供的OTA Downloader 組件有兩種固件下載方式:

  • http_ota:通過HTTP 協議獲取固件升級文件,支持通過LTE、WiFi、Bluetooth 等無線網絡下載固件升級文件;
  • ymodem_ota:通過ymodem 協議獲取固件升級文件,實際是通過UART 有線接口下載固件升級文件。

本文主要介紹http_ota 方式下載固件升級文件的原理,由於使用了HTTP 協議,還需要webclient 組件提供HTTP 協議支持。如果讀者不瞭解HTTP 協議,可以先閱讀博文:圖解HTTP + HTTPS + HSTS

我們先在env 環境中執行menuconfig 命令,到“RT-Thread online packages" --> “IoT - internet of things” 菜單下,分別啓用“WebClient ” 組件(啓用文件下載功能)和“ota_downloader” 組件(啓用HTTP/HTTPS OTA,並配置默認的URL ),配置界面如下:
啓用webclient 組件與ota_downloader 組件
HTTP/HTTPS OTA Downloader 實際上就是使用HTTP 協議下載固件升級文件,對比.”\ota_downloader-v1.0.0\src\http_ota.c" 與“.\webclient-v2.1.2\src\webclient_file.c” 文件的實現原理是類似的,只是前者將下載的文件保存到了FAL 存儲分區,後者將下載的文件保存到文件系統中,二者都是使用HTTP/HTTPS 從遠端服務器獲取文件資源的。我們先從HTTP WebClient 的代碼實現開始介紹,HTTP 協議理論部分參考博文:圖解HTTP + HTTPS + HSTS

  • HTTP Session 數據結構描述

WebClient - v2.1.2 只實現了HTTP/1.1 的GET 與POST 方法,對於我們從遠端服務器獲取固件升級文件已經夠用了。HTTP 數據報文主要由請求行/響應行、首部字段、空行、報文主體幾部分構成,其中的報文主體有可能長度很大,不適合放到HTTP Session 結構體內,因此HTTP Session 主要包括請求行/響應行、首部字段、報文主體長度等信息。由於HTTP 是基於TCP 通信的,TCP/IP 協議對上層提供了一組Socket API,HTTP Session 也應包含socket 信息。WebClient 組件給出的HTTP Session 數據結構定義如下:

// projects\stm32l475_ota_sample\packages\webclient-v2.1.2\inc\webclient.h

struct webclient_session
{
    struct webclient_header *header;    /* webclient response header information */
    int socket;
    int resp_status;

    char *host;                         /* server host */
    char *req_url;                      /* HTTP request address*/

    int chunk_sz;
    int chunk_offset;

    int content_length;
    size_t content_remainder;           /* remainder of content length */

    rt_bool_t is_tls;                   /* HTTPS connect */
#ifdef WEBCLIENT_USING_MBED_TLS
    MbedTLSSession *tls_session;        /* mbedtls connect session */
#endif
};

struct  webclient_header
{
    char *buffer;
    size_t length;                      /* content header buffer size */

    size_t size;                        /* maximum support header size */
};

webclient_header 數據結構描述比較簡單,相當於就定義了一段緩存區,用戶將HTTP 的首部字段按ASCII 編碼及固定格式(每個首部字段後面跟回車換行符)拼接到一起即可,HTTP 報文的請求行、響應行、空行也以首部字段的形式保存在webclient_header 結構體中。webclient_session 結構體包含webclient_header 結構體指針、socket、響應狀態碼、請求URL / 服務Host、分塊傳輸chunk_xxx、報文主體長度content_length 等信息,還爲TLS 的支持留下擴展成員tls_session。

webclient 組件除了支持HTTP/1.x,還可以配合MbedTLS 組件進行加密傳輸(也即HTTPS)。由於MbedTLS 佔用存儲資源較大,本文嘗試使用該組件編譯工程時提示存儲空間不足(除去Bootloader 的64KB 空間,留給Application 的只剩448KB,MbedTLS 要佔用超過100KB),再加上本文要用的HTTP 服務器"MyWebServer" 使用HTTPS 功能需要的openssl庫找不到可下載的資源,本文就不使用HTTPS 來傳輸固件升級文件了,僅使用較簡單且佔用資源較少的HTTP/1.1 來傳輸固件升級文件。

  • WebClient 接口函數及調用關係

WebClient 既然是HTTP 協議的一種實現,向上層提供的API 自然是請求和響應,由於響應是對請求的應答,所以上層可以通過一個接口函數webclient_request 向服務器發送請求報文並處理接收到的響應報文(WebClient 組件僅支持HTTP/1.x 的GET 與POST 兩種請求方法),WebClient 組件向上層提供的API — webclient_request 的函數實現代碼如下:

// projects\stm32l475_ota_sample\packages\webclient-v2.1.2\src\webclient.c
/**
 *  send request(GET/POST) to server and get response data.
 *
 * @param URI input server address
 * @param header send header data
 *             = NULL: use default header data, must be GET request
 *            != NULL: user custom header data, GET or POST request
 * @param post_data data sent to the server
 *             = NULL: it is GET request
 *            != NULL: it is POST request
 * @param response response buffer address
 *
 * @return <0: request failed
 *        >=0: response buffer size
 */
int webclient_request(const char *URI, const char *header, const char *post_data, unsigned char **response)
{
    struct webclient_session *session = RT_NULL;
    int rc = WEBCLIENT_OK;
    int totle_length = 0;

    RT_ASSERT(URI);
    if (post_data == RT_NULL && response == RT_NULL)
        return -WEBCLIENT_ERROR;

    if (post_data == RT_NULL)
    {
        /* send get request */
        session = webclient_session_create(WEBCLIENT_HEADER_BUFSZ);
        if (session == RT_NULL)
        {
            rc = -WEBCLIENT_NOMEM;
            goto __exit;
        }

        if (header != RT_NULL)
        {
            char *header_str, *header_ptr;
            int header_line_length;

            for(header_str = (char *)header; (header_ptr = rt_strstr(header_str, "\r\n")) != RT_NULL; )
            {
                header_line_length = header_ptr + rt_strlen("\r\n") - header_str;
                webclient_header_fields_add(session, "%.*s", header_line_length, header_str);
                header_str += header_line_length;
            }
        }

        if (webclient_get(session, URI) != 200)
        {
            rc = -WEBCLIENT_ERROR;
            goto __exit;
        }

        totle_length = webclient_response(session, response);
        if (totle_length <= 0)
        {
            rc = -WEBCLIENT_ERROR;
            goto __exit;
        }
    }
    else
    {
        /* send post request */
        session = webclient_session_create(WEBCLIENT_HEADER_BUFSZ);
        if (session == RT_NULL)
        {
            rc = -WEBCLIENT_NOMEM;
            goto __exit;
        }

        if (header != RT_NULL)
        {
            char *header_str, *header_ptr;
            int header_line_length;

            for(header_str = (char *)header; (header_ptr = rt_strstr(header_str, "\r\n")) != RT_NULL; )
            {
                header_line_length = header_ptr + rt_strlen("\r\n") - header_str;
                webclient_header_fields_add(session, "%.*s", header_line_length, header_str);
                header_str += header_line_length;
            }
        }

        if (rt_strstr(session->header->buffer, "Content-Length") == RT_NULL)
            webclient_header_fields_add(session, "Content-Length: %d\r\n", rt_strlen(post_data));

        if (rt_strstr(session->header->buffer, "Content-Type") == RT_NULL)
            webclient_header_fields_add(session, "Content-Type: application/octet-stream\r\n");

        if (webclient_post(session, URI, post_data) != 200)
        {
            rc = -WEBCLIENT_ERROR;
            goto __exit;
        }

        totle_length = webclient_response(session, response);
        if (totle_length <= 0)
        {
            rc = -WEBCLIENT_ERROR;
            goto __exit;
        }
    }

__exit:
    if (session)
    {
        webclient_close(session);
        session = RT_NULL;
    }

    if (rc < 0)
        return rc;

    return totle_length;
}

函數webclient_request 有四個參數,分別是請求資源的 URL、首部字段指針 *header、要發送給服務器的POST 請求報文的報文主體數據指針 *post_data、接收到的服務器響應報文的報文主體數據緩衝區地址 *response(也即請求到的資源數據的存儲地址),客戶端採用GET 還是POST 請求方法發送請求報文,取決於第三個參數是否爲空指針。

請求報文中要設置哪些首部字段可以使用接口函數webclient_header_fields_add() 添加相應的字段名稱和字段值, webclient 組件也爲我們提供了幾個默認字段:請求行、Host 字段、User-Agent 字段、空行等,如果我們不設置任何首部字段,將只使用這幾個默認首部字段構造請求報文。

WebClient 組件屬於應用層HTTP 協議,通信依賴於下層的TCP 協議,因此WebClient 向服務器請求資源的過程,底層是由Socket API 實現的。上層webclient_request() 接口函數到底層Socket API 的調用關係如下圖所示:
WebClient API 調用關係
上面只展示了HTTP 協議的接口函數調用關係,由於本文沒有使用mbedtls 組件,就沒有將mbedtls 的接口函數放到上圖中,即便使用mbedtls 組件,調用邏輯也跟上面類似。瞭解了HTTP 協議後,上圖的中間函數理解起來並不難,限於篇幅這裏就不再一一介紹了。

Http_ota_downloader 實現過程跟上圖中的webclient_get_file 函數實現過程類似,二者的區別是前者通過HTTP 協議將文件下載到FAL 存儲分區,後者通過HTTP 協議將文件下載到文件系統中(需要先在一個Block 設備上創建文件系統)。webclient_get_file 函數的實現過程跟前面介紹的webclient_request 函數實現過程類似,由於主要下載文件,相比webclient_request 函數更簡單些,只需要兩個參數,爲方便下文介紹Http_ota_downloader 的實現過程,這裏給出webclient_get_file 函數的實現代碼以供對比(限於篇幅,刪除了部分代碼):

// projects\stm32l475_ota_sample\packages\webclient-v2.1.2\src\webclient_file.c
/**
 * send GET request and store response data into the file.
 *
 * @param URI input server address
 * @param filename store response date to filename
 *
 * @return <0: GET request failed
 *         =0: success
 */
int webclient_get_file(const char* URI, const char* filename)
{
    int fd = -1, rc = WEBCLIENT_OK;
    size_t offset;
    int length, total_length = 0;
    unsigned char *ptr = RT_NULL;
    struct webclient_session* session = RT_NULL;
    int resp_status = 0;

    session = webclient_session_create(WEBCLIENT_HEADER_BUFSZ);
    if(session == RT_NULL)
    ......
    if ((resp_status = webclient_get(session, URI)) != 200)
    ......
    fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0);
    if (fd < 0)
    ......
    ptr = (unsigned char *) web_malloc(WEBCLIENT_RESPONSE_BUFSZ);
    if (ptr == RT_NULL)
    ......
    if (session->content_length < 0)
    {
        while (1)
        {
            length = webclient_read(session, ptr, WEBCLIENT_RESPONSE_BUFSZ);
            if (length > 0)
            {
                write(fd, ptr, length);
                total_length += length;
                LOG_RAW(">");
            }
            else
                break;
        }
    }
    else
    {
        for (offset = 0; offset < (size_t) session->content_length;)
        {
            length = webclient_read(session, ptr,
                    session->content_length - offset > WEBCLIENT_RESPONSE_BUFSZ ?
                            WEBCLIENT_RESPONSE_BUFSZ : session->content_length - offset);

            if (length > 0)
            {
                write(fd, ptr, length);
                total_length += length;
                LOG_RAW(">");
            }
            else
                break;
                
            offset += length;
        }
    }

__exit:
    if (fd >= 0)
        close(fd);
        
    if (session != RT_NULL)
        webclient_close(session);

    if (ptr != RT_NULL)
        web_free(ptr);

    return rc;
}

int wget(int argc, char** argv)
{
    if (argc != 3)
    {
        rt_kprintf("Please using: wget <URI> <filename>\n");
        return -1;
    }
    webclient_get_file(argv[1], argv[2]);
    return 0;
}
MSH_CMD_EXPORT(wget, Get file by URI: wget <URI> <filename>.);

Webclient 組件還爲webclient_get_file 函數導出了一個MSH 命令,我們在工程中添加webclient 組件後,可以待網絡連接成功後,使用wget 命令從某個URL 下載一個文件到本地filesystem 分區(本文基於前一篇博文的工程:AP6181(BCM43362) WiFi模塊驅動移植,在該工程中已經爲filesystem 分區創建了一個elmfat 文件系統),如果能順利從遠端服務器下載一個文件到本地,並且該文件是可以正常訪問的,說明webclient 組件的添加和配置沒有問題。
wget 示例結果

  • HTTP_ota_downloader 實現原理

HTTP_ota_downloader 組件的核心就是 http_ota_fw_download 函數,前面也說了該函數的實現過程與webclient_get_file 函數類似,二者最大的不同就是訪問Flash 存儲分區的方式不同,前面已經展示了webclient_get_file 函數的實現代碼,下面展示http_ota_fw_download 函數的實現代碼如下:

// projects\stm32l475_ota_sample\packages\ota_downloader-v1.0.0\src\http_ota.c

#define HTTP_OTA_URL              PKG_HTTP_OTA_URL

static int http_ota_fw_download(const char* uri)
{
    int ret = 0, resp_status;
    int file_size = 0, length, total_length = 0;
    rt_uint8_t *buffer_read = RT_NULL;
    struct webclient_session* session = RT_NULL;
    const struct fal_partition * dl_part = RT_NULL;

    /* create webclient session and set header response size */
    session = webclient_session_create(GET_HEADER_BUFSZ);
    if (!session)
    ......
    /* send GET request by default header */
    if ((resp_status = webclient_get(session, uri)) != 200)
    ......
    file_size = webclient_content_length_get(session);
    rt_kprintf("http file_size:%d\n",file_size);
    if (file_size <= 0)
    ......
    /* Get download partition information and erase download partition data */
    if ((dl_part = fal_partition_find("download")) == RT_NULL)
    {
        LOG_E("Firmware download failed! Partition (%s) find error!", "download");
        ret = -RT_ERROR;
        goto __exit;
    }

    if (fal_partition_erase(dl_part, 0, file_size) < 0)
    {
        LOG_E("Firmware download failed! Partition (%s) erase error!", dl_part->name);
        ret = -RT_ERROR;
        goto __exit;
    }

    buffer_read = web_malloc(HTTP_OTA_BUFF_LEN);
    if (buffer_read == RT_NULL)
    ......
    memset(buffer_read, 0x00, HTTP_OTA_BUFF_LEN);
    LOG_I("OTA file size is (%d)", file_size);

    do
    {
        length = webclient_read(session, buffer_read, file_size - total_length > HTTP_OTA_BUFF_LEN ?
                            HTTP_OTA_BUFF_LEN : file_size - total_length);   
        if (length > 0)
        {
            /* Write the data to the corresponding partition address */
            if (fal_partition_write(dl_part, total_length, buffer_read, length) < 0)
            {
                LOG_E("Firmware download failed! Partition (%s) write data error!", dl_part->name);
                ret = -RT_ERROR;
                goto __exit;
            }
            total_length += length;

            print_progress(total_length, file_size);
        }
        else
        {
            LOG_E("Exit: server return err (%d)!", length);
            ret = -RT_ERROR;
            goto __exit;
        }

    } while(total_length != file_size);

    ret = RT_EOK;

    if (total_length == file_size)
    {
        if (session != RT_NULL)
            webclient_close(session);
        if (buffer_read != RT_NULL)
            web_free(buffer_read);

        LOG_I("Download firmware to flash success.");
        LOG_I("System now will restart...");

        rt_thread_delay(rt_tick_from_millisecond(5));

        /* Reset the device, Start new firmware */
        extern void rt_hw_cpu_reset(void);
        rt_hw_cpu_reset();
    }

__exit:
    if (session != RT_NULL)
        webclient_close(session);
    if (buffer_read != RT_NULL)
        web_free(buffer_read);

    return ret;
}

void http_ota(uint8_t argc, char **argv)
{
    if (argc < 2)
    {
        rt_kprintf("using uri: " HTTP_OTA_URL "\n");
        http_ota_fw_download(HTTP_OTA_URL);
    }
    else
        http_ota_fw_download(argv[1]);
}
MSH_CMD_EXPORT(http_ota, Use HTTP to download the firmware);

對比http_ota_fw_download 函數與webclient_get_file 函數的實現代碼也可以看出其中的相似性,調用webclient 組件接口函數的過程基本一致,http_ota_fw_download 函數直接將下載的固件升級文件存儲到FAL 的download 分區,不需要爲該分區創建一個文件系統,相當於這個分區對客戶是隱藏的,既節省了文件系統管理的開銷,又能防止存儲在download 分區中的固件被用戶破壞。

需要注意的一點是,在往download 分區寫入數據前,需要先將其擦除,也即將該存儲分區的所有位都寫爲1,因爲Flash 編程原理都是隻能將1寫爲0,而不能將0寫成1。http_ota_fw_download 函數爲了讓用戶能直觀感受到下載進度,還通過print_progress 函數增加了打印下載進度的功能。

當文件下載完成後,http_ota_fw_download 函數會在最後調用rt_hw_cpu_reset 函數讓MCU 重啓復位,開始執行bootloader 代碼,bootloader 檢查download 分區內有固件升級文件,且校驗通過後,會將download 分區內的固件代碼搬移到app 分區,完成固件版本升級,最後再跳轉到app 分區執行更新後的Application 代碼。

Ota_downloader 組件也爲http_ota_fw_download 函數導出了一個MSH 命令,我們可以使用http_ota 命令完成從遠端服務器下載固件升級文件到本地FAL download 分區的任務。在啓用ota_downloader 組件時可以設置一個默認的URL(固件升級文件所在遠端託管服務器的URL),如果想換個下載源URL,只需要使用http_ota <URL>,也即在命令後加一個URL 參數即可。

三、Bootloader OTA示例

到這裏Bootloader 代碼已經準備好了,OTA Downloader 模塊也已經添加進Application 了,可以繼續第一部分介紹的OTA 固件遠端升級方案了嗎?再回顧下博文:ARM 代碼燒錄方案與原理詳解中介紹的IAP 燒錄方案,由於Application 代碼前面要爲Bootloader 代碼預留存儲空間,也即Application 代碼存儲在Main Flash memory 區間起始位置向後偏移一段距離處,需要重新設置中斷向量表偏移地址,也即重新設置 VTOR 寄存器的值,同時修改Application 工程的ROM 區間地址參數。

本文爲bootloader 分配了64KB 的存儲空間,app 分區的起始地址爲0x0801 0000,區間大小爲448KB(也即0x70000 字節)。首先我們需要將Application 工程的中斷向量表映射到app 分區起始位置也即0x0801 0000 處,該任務可以通過設置VTOR 中斷向量表偏移寄存器完成,VTOR 寄存器的結構如下圖示:
VTOR寄存器結構
從上圖可知,Cortex-M4 的VTOR 寄存器bit31:7 有效,我們可以定義一個VTOR 掩碼NVIC_VTOR_MASK,將有效位置1,無效位置0,得到掩碼NVIC_VTOR_MASK值爲0xFFFFFF80。我們再將要設置的目標偏移地址0x0801 0000 與該掩碼進行位與運算,即可得到VTOR 寄存器的值。設置VTOR 寄存器的代碼如下:

// projects\stm32l475_ota_sample\applications\main.c

/* 將中斷向量表起始地址重新設置爲 app 分區的起始地址 */
static int ota_app_vtor_reconfig(void)
{
    #define NVIC_VTOR_MASK   0xFFFFFF80
    #define RT_APP_PART_ADDR 0x08010000
    /* 根據應用設置向量表 */
    SCB->VTOR = RT_APP_PART_ADDR & NVIC_VTOR_MASK;

    return 0;
}
INIT_BOARD_EXPORT(ota_app_vtor_reconfig);

重新設置VTOR 寄存器的函數ota_app_vtor_reconfig 被自動初始化組件調用,INIT_BOARD_EXPORT 說明該函數是最早被初始化的,此時調度器還未啓動。重新設置中斷向量表後,系統開始啓動並進入main 函數,按照正確的中斷向量表響應系統異常與中斷服務。

前一篇博文中主要介紹WIFI 模塊的移植和使用,main 函數設計的較複雜,本文中對其簡化,只對WIFI 模塊進行必要的初始化配置,連接WIFI 的操作交由用戶通過“wifi join [SSID] [PASSWORD]”命令完成,這裏配置了WIFI 自動連接功能,已經連接過的WIFI 在MCU 重啓後會自動連接。

既然本工程主要爲了驗證版本升級,我們定義一個軟件版本APP_VERSION,在main 函數中打印當前的軟件版本,後續升級版本時,我們同步更新版本號,就可以通過當前軟件版本號來判斷是否升級成功了,添加打印當前軟件版本信息後的main 函數代碼如下:

// projects\stm32l475_ota_sample\applications\main.c

#define APP_VERSION  "1.0.0"

int main(void)
{
    /* 初始化 wlan 自動連接配置 */
    wlan_autoconnect_init();
    /* 使能 wlan 自動連接功能 */
    rt_wlan_config_autoreconnect(RT_TRUE);

    /* 打印當前軟件版本信息 */
    LOG_I("The current version of APP firmware is %s\n", APP_VERSION);

    return 0;
}

修改完中斷向量表偏移地址,還需要修改Application 工程ROM 的地址參數,包括board.h 和linker_scripts 文件中的地址參數配置,本文使用的IDE 工具爲Keil MDK,對應的linker_scripts 文件是link.sct,這兩個文件修改ROM 地址參數如下:

// projects\stm32l475_ota_sample\board\board.h
......
#define STM32_FLASH_START_ADRESS       ((uint32_t)0x08010000)
#define STM32_FLASH_SIZE               (448 * 1024)
#define STM32_FLASH_END_ADDRESS        ((uint32_t)(STM32_FLASH_START_ADRESS + STM32_FLASH_SIZE))
......

// projects\stm32l475_ota_sample\board\linker_scripts\link.sct

LR_IROM1 0x08010000 0x00070000  {    ; load region size_region
  ER_IROM1 0x08010000 0x00070000  {  ; load address = execution address
  ......
  }
  ......
}

修改完board.h 和link.sct 文件中的ROM 參數配置,當然也需要修改Keil MDK Options 中對應的ROM 參數配置,我們可以在模板文件template.uvprojx 中按下圖所示修改ROM 參數配置,以後通過scons --target=mdk 命令重新生成工程時就不需要再次修改ROM 參數了。
Keil MDK 修改ROM 地址參數配置
到這裏Application 工程就準備好了,我們在env 環境中執行scons --target=mdk5 命令生成MDK 工程,打開工程文件project.uvprojx 編譯無報錯,將代碼燒錄到Pandora 開發板中。本文第一部分已經將bootloader.bin 文件通過“STM32 ST-LINK Utility” 工具燒錄到Pandora 開發板中了,由於Application 工程(也即本文中的stm32l475_ota_sample 工程)已經重新配置了ROM 起始地址與區間大小參數,通過Keil MDK 工具正好將Application 工程代碼燒錄到app 分區(也即Main Flash memory 區間中bootloader 代碼後面)。Bootloader 與application 代碼燒錄後,Pandora 開發板bootloader 和application 的啓動界面分別如下:
stm32l475_ota_sample 工程執行結果
Pandora 開發板easyflash 分區已存儲WIFI 熱點參數,自動連接WIFI 生效,使用第二部分介紹的wget 命令下載百度首頁到本地文件系統成功,說明本工程新增的webclient 組件工作正常。

下面開始按照第一部分介紹的OTA 固件遠端升級方案繼續進行,首先是使用rt_ota_packaging_tool 打包生成固件更新文件,將工程中的APP_VERSION 宏定義修改爲“2.0.0”,使用Keil MDK 重新編譯工程,生成bin格式的工程文件rtthread.bin。使用rt_ota_packaging_tool 工具將文件rtthread.bin 打包爲文件rtthread.rblrt_ota_packaging_tool 工具的配置界面如下:
OTA 固件打包配置參數
選擇Keil MDK 編譯生成的工程文件rtthread.bin,配置壓縮算法、加密算法、加密密鑰、加密初始化向量IV、固件分區名、固件版本等參數即可打包爲bootloader 可解析的升級文件rtthread.rbl(默認與文件rtthread.bin 在相同目錄下)。本文以升級application 代碼作爲示例,所以固件分區名填寫app 分區,該bootloader 也支持升級其它分區的代碼,比如升級WIFI 模塊的固件wifi_image.rbl 時固件分區名填寫wifi_image 分區即可。

接下來將生成的固件更新文件rtthread.rbl 上傳到託管服務器,本文使用MyWebServer工具作爲託管服務器,執行MyWebServer.exe程序,在服務目錄項選擇生成的固件更新文件rtthread.rbl,配置IP 地址爲你使用的PC 的IP地址(可通過ipconfig /all命令查看),HTTP 端口號爲80,啓動Start 運行MyWebServer託管服務器:
將升級文件託管到MyWebServer 服務器
在finsh 控制檯執行命令http_ota "http://192.168.43.145:80/rtthread.rbl"即可啓動Application 工程中的OTA_Downloader 模塊,開始從託管服務器(也即上面啓動的MyWebServer服務器)下載固件升級文件rtthread.rbl到download 分區,下載完成後MCU 重啓復位開始執行bootloader 程序。Bootloader 程序對本地Download 分區內的OTA 固件升級文件rtthread.rbl 進行解密、解壓縮、校驗等操作,如果校驗通過則將新版本固件代碼搬運到app 分區,代碼搬運完成後跳轉到新版本的application 代碼開始執行,整個過程如下圖所示:
OTA 升級執行結果
執行http_ota命令前的軟件版本號爲“1.0.0”,執行http_ota命令並完成固件升級後,finsh 顯示的軟件版本號爲“2.0.0”,說明已成功完成OTA 固件升級過程。

本示例工程源碼下載地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_ota_sample

更多文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章