天天看點

适用于嵌入式開發的差分更新通用庫

作者:沃愛單片機

1. 什麼是差分/增量更新?

借用網上的介紹:适合嵌入式的差分更新又叫增量更新,顧名思義就是通過差分算法将源版本與目标版本之間差異的部分提取出來制作成差分包,然後在裝置通過還原算法将差異部分在源版本上進行還原進而更新成目标版本的過程。

差分更新方案不僅可以節省MCU内部的資源空間、還可以節省下載下傳流程及下載下傳和更新過程中的功耗。

也就是說,差分更新是拿以前舊裝置内的bin,和目前新版本的bin用某種算法算出他們的差異部分,然後在借助壓縮算法,生産一個極其小的差分包,然後将這個差分包下載下傳到裝置中,裝置在根據解壓算法、差分還原算法,生産一個完整的新版本bin,然後将這個新版本bin刷到執行區執行代碼。

差分更新一般來說,可以極大的減少下載下傳量,特别是對于嵌入式STM32等單片機來說,可以極大的減少維護成本,因為嵌入式裝置的更新維護一般都是空中ota更新,比如藍牙、紅外等,下載下傳速度受到波特率、包長等限制,更新固件包非常的慢,而差分更新可以讓下載下傳的過程極大的縮小。

正常的維護版本,即使改的再多,生成的差分包bin理論上在原bin的5%左右,比如一個300k的bin,改的很多的情況下差分包也不過15k左右,而我實際測試,版本維護平均都在5~10k左右。

2. 差分更新實作原理

差分更新過程:

1、使用舊版本bin檔案和新版本bin檔案制作差分包

2、将差分包下載下傳到裝置内

3、裝置使用差分算法還原出新版本bin

4、裝置将新版本bin進行crc驗證後刷到代碼執行區

5、裝置重新開機并以新版本運作

在過程中有2個關鍵點:

第一:如何使用舊版本bin檔案和新版本bin檔案制作差分包?

該過程我們使用穩定的開源差分算法bsdiff+lzma生成差分包,該算法被大量使用,穩定安全,并且我們已在項目中批量使用,經過長時間的驗證無任何問題。一般來說,該過程都是使用上位機來完成,嵌入式裝置無需關心,我們已經做好了上位機軟體,可以供大家随意使用,稍後會進行介紹。

第二:裝置收到差分包後如何還原出新版本的bin檔案?

該過程就是我們要講解的重點過程,相對應的,嵌入式裝置中,我們依然使用開源差分算法bsdiff+lzma來還原新版本檔案,代碼全開源,并且我已做成了庫、抽象出了極簡的接口,移植起來費不了多少功夫。

基本是市場上所有的單片機如stm32、瑞薩、華大、複旦微等都可以使用,但是有記憶體限制,要求ram至少要10k以上,然後是該庫本身的消耗大概是5k的rom。

3. 關鍵點一:差分包制作過程

對于差分包的制作,我已經開發好了上位機軟體,界面如下圖所示:

适用于嵌入式開發的差分更新通用庫

上位機這邊主要實作使用開源算法bsdiff制作舊版本bin和新版本bin的差分包,然後在使用lzma壓縮算法來壓縮差分包,最終生成一個差分bin,使用方法上位機界面提示的很清楚,最終效果如下圖所示:

适用于嵌入式開發的差分更新通用庫

下載下傳連結:

連結:https://pan.baidu.com/s/1nBpftim8OCUI3i32sP3eOA?pwd=diff

提取碼:diff

4. 關鍵點二:嵌入式裝置中差分算法庫的移植(還原差分包)

4.1. 移植開關算法庫代碼

代碼已開源,位址:

https://gitee.com/qq791314247/mcu_bsdiff_upgrade

整體代碼如下圖所示:

适用于嵌入式開發的差分更新通用庫

如上圖所示,99%的代碼使用者都不用去關心,使用者隻需要提供一個flash寫入接口即可,也就是該庫給定使用者flash位址、資料内容指針、資料内容長度,使用者将該段資料寫入到flash即可,移植起來特别簡單,花不了幾分鐘的功夫,這也是我花大力氣抽象接口的原因。

4.2. 使用該庫的流程

4.2.1. 使用庫的接口

對于整個庫的代碼,我們隻需要關心一個接口iap_patch,iap_patch在檔案”user_interface.h”中。

适用于嵌入式開發的差分更新通用庫

該接口介紹也比較清晰,差分包的還原,隻需要調用這一個接口即可。

/**
 * @brief 使用者使用差分更新時唯一需要關心的接口
 * 
 * @param old 裝置中執行區代碼所在的位址,使用者可指定flash執行區的位址,友善算法讀出來目前
 *            運作中的代碼
 * @param oldsize 裝置中執行區代碼的長度,使用者可在差分包bin頭擷取
 * @param patch 裝置中已經下載下傳的差分包所在的flash位址,或者ram位址,隻要能讓算法讀出來即可
 *              注意,下載下傳的差分包自帶image_header_t格式的檔案頭,真正的差分包需要偏
 *              移sizeof(image_header_t)的長度
 * @param patchsize 裝置中已經下載下傳的差分包的長度,使用者可在差分包bin頭擷取
 * @param newfile 新檔案的大小,使用者需填入新版本bin的長度,使用者亦可以差分包bin頭擷取
 * @return int 然後錯誤碼,0成功,1失敗
 */
extern int iap_patch(const uint8_t* old, uint32_t oldsize, const uint8_t* patch,
                     uint32_t patchsize, uint32_t newfile);           

另外,使用該接口還原時所需要的一些資訊可以在差分封包件頭中擷取,上位機在制作差分包時,會自動在差分包的bin頭加上64位元組的檔案頭,以便于告訴嵌入式裝置舊/新版本bin檔案的CRC校驗、長度等資訊。是以使用者在收到差分標頭時,偏移掉這64個位元組的檔案頭的位址才是需要給到iap_patch接口的真正的bin檔案。

檔案頭格式如下代碼,使用者隻需要關心中文注釋的部分,其餘的都是預留的資訊。

/* 差分包制作時自帶的檔案頭資訊,使用者隻需要關心中文注釋的部分 */
 typedef struct image_header
 {
   uint32_t ih_magic; /* Image Header Magic Number */
   uint32_t ih_hcrc;  /* Image Header CRC Checksum 差分包標頭校驗 */
   uint32_t ih_time;  /* Image Creation Timestamp */
   uint32_t ih_size;  /* Image Data Size 差分包的大小 */
   uint32_t ih_load;  /* Data Load Address 上一版本舊檔案的大小 */
   uint32_t ih_ep;   /* Entry Point Address 要更新的新檔案的大小 */
   uint32_t ih_dcrc;  /* Image Data CRC Checksum 新檔案的CRC */
   uint8_t ih_os;   /* Operating System */
   uint8_t ih_arch;  /* CPU architecture */
   uint8_t ih_type;  /* Image Type */
   uint8_t ih_comp;  /* Compression Type */
   uint8_t ih_name[IH_NMLEN]; /* Image Name */
   uint32_t ih_ocrc;  /* Old Image Data CRC Checksum 上一版本舊檔案的CRC */
 } image_header_t; 
 /* 差分包制作時自帶的檔案頭資訊,使用者隻需要關心中文注釋的部分 */           

4.2.2. 接口使用例子

我截取一段我工程中的代碼來講解如何使用該接口還原出新版本bin檔案:

#ifdef BSDIFF_UPGRADE
         image_header_t recv_head;
         uint32_t recv_hcrc;  /* 接收到的檔案頭CRC */
         uint32_t calculation_crc;  /* 計算出來的檔案頭CRC */
         uint32_t spi_flash_addr = UPGRADE_PROGRAM_ADDR;
 
         memcpy(&recv_head, (uint8_t *)APPLICATION_A, sizeof(image_header_t));
         recv_hcrc = BigtoLittle32(recv_head.ih_hcrc);
         recv_head.ih_hcrc = 0;
         calculation_crc = crc32((uint8_t *)&recv_head, sizeof(image_header_t));
 
         if (recv_hcrc == calculation_crc)
         {
             recv_head.ih_hcrc = recv_hcrc;
             recv_head.ih_time = BigtoLittle32(recv_head.ih_time);
             recv_head.ih_size = BigtoLittle32(recv_head.ih_size);
             recv_head.ih_dcrc = BigtoLittle32(recv_head.ih_dcrc);
             recv_head.ih_ocrc = BigtoLittle32(recv_head.ih_ocrc);
             /* 差分更新包 */
             recv_head.ih_hcrc = calculation_crc;
             if (crc32((uint8_t *)APPLICATION_RUN, recv_head.ih_load) != recv_head.ih_ocrc)
             {
                 APP_ERR_PRINT("file oldcrc err,calcrc:0X%08X, ih_oldbin_crc:0X%08X,", 
                                           crc32((uint8_t *)APPLICATION_RUN, 
                                           recv_head.ih_load), recv_head.ih_ocrc);
                 goto bsdiff_out;
             }
             RTOS_LOCK();
             disable_task_monitoring(ALL_TASK_RUNFLAG_BIT, true);
             // flash_erase_sector(UPGRADE_PROGRAM_ADDR, UPGRADE_PROGRAM_PAGE);
             recv_hcrc = iap_patch((uint8_t *)APPLICATION_RUN, recv_head.ih_load, 
                                  (uint8_t *)(APPLICATION_A + sizeof(image_header_t)),  
                                    recv_head.ih_size, UPGRADE_PROGRAM_ADDR);
             if (recv_hcrc != recv_head.ih_ep)
             {
                 APP_ERR_PRINT("iap_patch len err.");
                 APP_ERR_PRINT("iap_patch len: %lu, new_len: %lu", recv_hcrc, recv_head.ih_ep);
                 goto bsdiff_out;
             }
             if (erase_program(APPLICATION_A))
             {
                 APP_ERR_PRINT("I erase program failed.");
                 goto bsdiff_out;
             }
 
             current_flash_write_addr = APPLICATION_A;
             for (uint32_t i = 0; i < (recv_head.ih_ep / 1024); i++)
             {
                 xmq25qxx_read(spi_flash_addr, spi_read_cache, 1024);
                 if (xflash_write(current_flash_write_addr, spi_read_cache, 1024))
                 {
                     APP_ERR_PRINT("I write program failed.");
                     goto bsdiff_out;
                 }
                 spi_flash_addr += 1024;
                 current_flash_write_addr += 1024;
                 APP_PRINT("current_flash_write_addr: 0X%08X", current_flash_write_addr);
             }
             if (recv_head.ih_ep % 1024 != 0)
             {
                 memset(spi_read_cache, 0XFF, 1024);
                 xmq25qxx_read(spi_flash_addr, spi_read_cache, recv_head.ih_ep % 1024);
 
                 if (xflash_write(current_flash_write_addr, spi_read_cache, 1024))
                 {
                     APP_ERR_PRINT("I write program failed.");
                     goto bsdiff_out;
                 }
             }
             if (crc32((uint8_t *)APPLICATION_A, recv_head.ih_ep) != recv_head.ih_dcrc)
             {
                 APP_ERR_PRINT("file newcrc err,calcrc:0X%08X, newcrc:0X%08X, len: %lu", 
                                          crc32((uint8_t *)APPLICATION_A, recv_head.ih_ep), 
                                          recv_head.ih_load, recv_head.ih_dcrc);
                 goto bsdiff_out;
             }
             /* 下載下傳成功,開始更新 */
             if (check_bin_file((bin_info_t *)(APPLICATION_A + BIN_INFO_OFFSET))) /* bin檔案非法 */
             {
                 APP_ERR_PRINT("check_bin_file err.");
                 goto bsdiff_out;
             }
             recv_head.ih_dcrc = CRT_CRC16_check(0, (uint8_t *)APPLICATION_A, 
                                                 recv_head.ih_ep);
             readwrite_app_run_bin_info(0, &recv_head.ih_ep, (uint16_t *)&recv_head.ih_dcrc);
             /* 整體校驗成功,确認更新 */
             if (switch_program_stage(STAGE_1))
             {
                 APP_ERR_PRINT("I write switch_program_stage STAGE_0 failed.");  /* 置位更新标志寫失敗 */
                 goto bsdiff_out;
             }
             APP_PRINT("upgrade success.");
   bsdiff_out:
             SYSTEM_RESET();
         }           

繼續閱讀