nRF Connect SDK(NCS)/Zephyr韌體升級詳解 – 重點講述MCUboot和藍牙空中升級

如何在nRF Connect SDK(NCS)中實現藍牙空中升級?MCUboot和B0兩個Bootloader有什麼區別?MCUboot升級使用的image格式是怎麼樣的?什麼是SMP協議?CBOR編碼如何解讀?NCS可不可以進行單bank升級?可不可以把一個nRF5 SDK應用升級到NCS應用?MCUboot拷貝操作中的swap和overwrite有什麼區別?為什麼說MCUboot升級永遠都不可能變磚?本文將對以上問題進行闡述。

目錄

   

1.概述

先講一下DFU和OTA的概念。DFU(Device Firmware Update),就是設備韌體升級的意思,而OTA(Over The Air)是實現DFU的一種方式而已,準確說,OTA的全稱應該是OTA DFU,即通過空中無線方式實現設備韌體升級。只不過大家為了方便起見,直接用OTA來指代韌體空中升級(有時候大家也將OTA稱為FOTA,即Firmware OTA,這種稱呼意思更明了一些)。只要是通過無線通訊方式實現DFU的,都可以叫OTA,比如4G/WiFi/藍牙/NFC/Zigbee/NB-IoT,他們都支援OTA。DFU除了可以通過無線方式(OTA)進行升級,也可以通過有線方式進行升級,比如通過UART,USB或者SPI通訊介面來升級設備韌體。

不管採用OTA方式還是有線通訊方式,DFU包括後台式(background)和非後台式兩種模式。後台式DFU,又稱靜默式DFU(Silent DFU),在升級的時候,新韌體在後台悄悄下載,即新韌體下載屬於應用程式功能的一部分,在新韌體下載過程中,應用可以正常使用,也就是說整個下載過程對用戶來說是無感的,下載完成後,系統再跳到BootLoader程式,由BootLoader完成新老韌體拷貝操作,至此整個升級過程結束。比如智慧手機升級Android或者iOS系統都是採用後台式DFU方式,新系統下載過程中,手機可以正常使用哦。非後台式DFU,在升級的時候,系統需要先從應用程式跳到BootLoader程式,由BootLoader進行新韌體下載工作,下載完成後BootLoader繼續完成新老韌體拷貝操作,至此升級結束。早先的功能機就是採用非後台式 DFU來升級作業系統的,即用戶需要先長按某些按鍵進入bootloader模式,然後再進行升級,整個升級過程中手機正常功能都無法使用。

下面再講雙區(2 Slot)DFU和單區(1 Slot)DFU,雙區或者單區DFU是新韌體覆蓋老韌體的兩種方式。後台式DFU必須採用雙區模式進行升級,即老系統(老韌體)和新系統(新韌體)各佔一塊Slot(存儲區),假設老韌體放在Slot0中,新韌體放在Slot1中,升級的時候,應用程式先把新韌體下載到Slot1中,只有當新韌體下載完成並校驗成功後,系統才會跳入BootLoader程式,然後擦除老韌體所在的Slot0區,並把新韌體拷貝到Slot0中,或者把Slot0和Slot1兩者的image進行交換。非後台式DFU可以採用雙區也可以採用單區模式,與後台式DFU相似,雙區模式下新老韌體各佔一塊Slot(老韌體為Slot0,新韌體為Slot1),升級時,系統先跳入BootLoader程式,然後BootLoader程式把新韌體下載到Slot1中,只有新韌體下載完成並校驗成功後,才會去擦除老韌體所在的Slot0區,並把新韌體拷貝到Slot0區。單區模式的非後台式DFU只有一個Slot0,老韌體和新韌體分享這一個Slot0,升級的時候,進入bootloader程式DFU模式後立馬擦除老韌體,然後直接把新韌體下載到同一個Slot中,下載完成後校驗新韌體的有效性,新韌體有效升級完成,否則要求重來。跟非後台式DFU雙區模式相比,單區模式節省了一個Slot的Flash空間,在系統資源比較緊張的時候,單區模式是一個不錯的選擇。不管是雙區模式還是單區模式,升級過程出現問題後,都可以進行二次升級,都不會出現「變磚」情況。不過雙區模式有一個好處,如果升級過程中出現問題或者新韌體有問題,它還可以選擇之前的老韌體老系統繼續執行而不受其影響。而單區模式碰到這種情況就只能一直待在bootloader中,然後等待二次或者多次升級嘗試,此時設備的正常功能已無法使用,從用戶使用這個角度來說,你的確可以說此時設備已經「變磚」了。所以說,雖然雙區模式犧牲了很多存儲空間,但是換來了更好的升級體驗。

可參考下面三個圖來理解上述過程。

  

如果你是第一次接觸nRF Connect SDK(NCS),那麼建議你先看一下這篇文章:開發你的第一個NCS/Zephyr應用程式,以建立NCS的一些基本知識,然後再往下看以下章節。

2. NCS中的Bootloader

如果你的應用不需要DFU功能,那麼Bootloader就可以不要;反之,如果你的應用需要DFU功能,Bootloader就一定需要。Bootloader在其中起到的作用包括:一判斷正常啟動還是DFU升級流程,二啟動並校驗應用image,三升級的時候完成新image和老image的交換或者拷貝工作。進一步說,

  1. Bootloader首先需要判斷是進入正常應用程式啟動流程還是DFU流程。
  2. 要啟動應用image,Bootloader必須知道啟動image的啟動向量表在哪裡。
  3. 要校驗一個image,Bootloader必須知道這個image正確的校驗值存在哪裡。
  4. 要完成升級,Bootloader必須知道新image所在位置和老image所在位置,並執行一定的拷貝演算法。

啟動向量表可以放在image的最開始處,也可以放在其他地方,這就涉及到image的格式。Image正確的校驗值可以跟image合在一塊存放,也可以單獨放在一個flash page裡面。如果image的校驗值是跟image本身合在一塊存放的,這裡再次涉及到image的格式。關於新image和老image存放位置,這就涉及到存儲器分區問題。Bootloader的實現將直接決定image的格式,以及存儲器的結構劃分。

NCS支援MCUboot,B0和nRF5 Bootloader三種Bootloader,三個Bootloader選其一即可,一般推薦大家使用MCUboot。由於很多讀者對Nordic老的SDK,即nRF5 SDK比較熟悉,我們先以這個nRF5 Bootloader為例來講解他們的Flash分區以及image格式,然後再講MCUboot和B0,看看他們又是如何分區和定義image格式的。注意:如果你只對其中某一個具體的Bootloader感興趣,可以跳過其他章節,直接閱讀相關章節,比如如果你只對MCUboot感興趣,可以只看2.2節。

2.1 nRF5 SDK Bootloader

nRF5 Bootloader是指nRF5_SDK_17.1.0_ddde560\examples\dfu\secure_bootloader這裡面定義的Bootloader,如果你的DFU想使用這個Bootloader,那麼nRF5 SDK的存儲區劃分(雙bank)是下面這樣的:

 

在nRF Connect SDK(NCS)中,如果也使用nRF5 Bootloader,此時存儲器的分區跟上面大同小異,我們用NCS中的語言重新組織如下:

  

當前韌體(老韌體)在Bank0裡面執行,新韌體接收後直接存放在Bank1,而且程式永遠只執行Bank0裡面的程式碼,Bank1的起始地址是動態的,其計算公式為:Bank0起始地址 + Bank0 image大小。由於nRF5 Bootloader跳到Bank0的時候,直接跳到一個固定地址(0x1000),因此它不需要專門去找新image的啟動向量,換句話說,如果使用nRF5 Bootloader的話,新image就是應用程式碼編譯後的樣子,不需要添加任何的頭或者尾資訊。如果這樣的話,image的SHA256或者簽名校驗怎麼做?在nRF5 Bootloader中,把正確的SHA256或者簽名放在settings page裡面,這樣image就真得不需要任何頭或者尾資訊,當需要校驗image的時候,從settings page中取出標準值,然後進行校驗。那這些標準的SHA256或者簽名怎麼從遠程傳過來呢?答案是init包,所以nRF5 Bootloader升級的時候,需要把一個zip包傳給目標設備,如下所示:

  

這個zip包除了新image本身,還包含一個dat文件,這個dat文件包含新image的大小,SHA256,簽名等資訊。

至於升級拷貝,nRF5 Bootloader做法也很簡單,先擦掉Bank0裡面的內容,然後把Bank1裡面的內容拷貝到Bank0,然後重新從Bank0啟動,完成整個升級。在拷貝之前,Bootloader會校驗Bank1裡面的image完整性,只有校驗通過才會做下一步的拷貝工作,否則退出升級模式。從上可以看出,雖然nRF5 Bootloader會校驗image的完整性,但是如果出現發版錯誤(打個比方,Win11和Win7都是微軟驗簽,因此完整性校驗都可以通過,但是如果微軟把Win11發到一台只能跑Win7的設備上,那麼這台設備將無法運行),由於它沒有新image確認操作,也不支援回滾操作,那麼升級後系統有可能掛死在一個錯誤的版本裡面。

說完了啟動,校驗和升級拷貝,最後說一下如何進入DFU模式。在nRF5 Bootloader裡面,通過判斷某些Flag(標誌位)來決定要不要進入DFU模式,這些標誌位有一個為真,進入DFU模式,否則正常啟動app:

  • 特定按鍵是否按下
  • 保持暫存器GPREGRET1是否為0xB1
  • Settings page裡面當前bank是否為Bank1
  • 上次DFU過程是否還在進行中
  • 應用程式校驗是否通過

可以看出,整個判斷邏輯還是比較簡單,大家很容易讀懂相關的源程式碼。

nRF5 Bootloader既可以運行在nRF5 SDK中,也可以運行在NCS中。nRF5 Bootloader既支援非後台式DFU,也支援後台式DFU,我們做了一個跑在NCS中的後台式DFU例子://github.com/aiminhua/ncs_samples/tree/master/nrf_dfu/ble_intFlash_nrf5_bl。跟nRF5 SDK DFU相比,這個例子有兩個要注意的地方:

  • 我們是通過把Settings page裡面的當前bank設置為Bank1來觸發DFU模式的
  • 由於是後台式DFU,我們只把DFU進度資訊保存在RAM裡面,沒有將其保存在Settings page這個Flash頁面中。

從這個例子大家可以體會到,分區和新image格式只跟Bootloader有關,跟SDK或者DFU協議無關。

下面是nRF5 Bootloader啟動的一個示例,供大家參考:

  

2.2 MCUboot

MCUboot位於如下目錄:bootloader/mcuboot/boot/zephyr,在NCS中做DFU的時候,一般都推薦使用MCUboot。MCUboot功能強大,兼容的晶片平台多,而且是一個久經考驗的第三方開源Bootloader。MCUboot把存儲區劃分為Primary slot和Secondary slot,而且primary slot跟secondary slot兩者大小是一樣的,程式默認在Primary slot中執行。有一點需要大家注意,NCS對MCUboot進行了訂製,在NCS中,程式只能在Primary slot中執行,Secondary slot只是用來存儲新image,而且Secondary slot可以放在內部Flash,也可以放在外部Flash,這樣在NCS中,存儲器分區有如下兩種典型情況:

 

Secondary slot在內部Flash

 

Secondary slot在外部Flash

註:MCUboot放在0x000000地址。

如前所述,Bootloader有四大功能:啟動image,校驗image,拷貝image以及DFU模式判斷,那麼MCUboot是如何完成這4項功能的:

  1. 啟動image。MCUboot通過讀image的頭資訊(header),得到啟動向量,然後跳到啟動向量,完成啟動。Image header資訊如下:(感興趣的讀者,仔細看一下各個結構體欄位定義,並對應image hex進行解讀) 

從上可以看出,image的最開始是image header,而不是image啟動向量。Image header裡面有一個欄位image header size,啟動向量就位於image header size的偏移處,image header一般為0x200大小,一般來說,app的基地址是0xC000,這樣image的啟動向量就在0xC000+0x200=0xC200,MCUboot啟動app的時候就跳轉到0xC200這個地址。

2. 校驗image。MCUboot通過讀image的尾資訊(tail或者tlv),得到image的SHA256和簽名,從而完成校驗。Image tlv緊跟在image後面,其內容示例如下所示:(感興趣的讀者,仔細看一下各個結構體欄位定義,並對應image hex進行解讀)

 

上述示例解讀結果為:沒有IMAGE_TLV_PROT_INFO_MAGIC,只有普通的IMAGE_TLV_INFO_MAGIC,IMAGE_TLV_INFO_MAGIC總共有3個tag:IMAGE_TLV_SHA256 (0x10), IMAGE_TLV_KEYHASH(0x01),以及IMAGE_TLV_ECDSA256(0x22)。

nRF5 Bootloader把app image的SHA256和簽名放在settings page里,這樣每次重新編譯一次app image,還需要重新生成一個settings page,然後把兩者一起合併燒到晶片里,這樣Bootloader才能通過image完整性校驗而跳到app;如果只把新編譯的app image燒到晶片里,此時image完整性校驗將失敗而導致程式一直死在Bootloader里,可以看出這種方案是不太方便開發和調試的。而MCUboot把app image的SHA256和簽名放在image後面,這樣每次重新編譯一次app image,新的sha256和簽名會自動跟著一起更新,你只需直接下載app而無需去更改Bootloader任何部分,大大方便了開發和調試。

3. Image拷貝。MCUboot支援多種image拷貝動作,確切說是image swap(交換)操作,即把secondary slot裡面的image交換到Primary slot,如何swap呢?總體上分swap和overwrite兩種。Overwrite跟上面的nRF5 Bootloader一樣,即先擦除primary slot裡面的老image,然後把secondary slot裡面的新image拷貝到primary slot,完成整個升級過程。Swap就是把primary slot和secondary slot裡面的image進行交換,即primary slot裡面的image搬移到secondary slot,secondary slot裡面的image搬移到primary slot。欲swap A和B,我們需引入一個媒介:C,演算法是C=A;A=B;B=C,這樣就實現了A和B的交換。從上可知,實現swap的關鍵是媒介C的引入,據此MCUboot支援兩種swap演算法:swap_move和swap_scratch,默認採用swap_move。swap_scratch的做法是:在存儲區中專門劃分一塊scratch區作為swap媒介,swap的時候,primary slot裡面的image先放在scratch區,然後把secondary slot裡面的image拷貝到primary slot,最後把scratch區裡面的內容拷貝到secondary slot,從而完成一次交換操作,Scratch區應該比primary或者secondary slot小很多,因此要完成整個image交換,需要循環執行多次上述操作直至整個image(以兩個slot中最大的為準)交換完成。這種演算法有兩個弊端:一浪費了scratch區,二由於一次image交換,scratch區需要執行多次擦寫操作,scratch區的Flash壽命有可能會不夠,為解決上述兩個問題,引入了第二套演算法:swap_move,具體做法是:先把primary slot裡面整個image向上搬移一個扇區,即先擦掉image size + 1的扇區,然後把image size所在的扇區內容拷貝到image size + 1扇區,然後擦掉image size扇區,並把image size -1所在的扇區內容拷貝到image size扇區,以此循環往複,直至把整個image向上挪動一個扇區,這樣就為下面的primary slot和secondary slot image交換做好準備。Primary slot和secondary slot image交換的時候,先擦掉primary slot第一個扇區,然後把secondary slot第一個扇區的內容拷貝到primary slot第一個扇區並擦掉secondary slot第一個扇區,然後把primary slot第二個扇區內容拷貝到secondary slot第一個扇區並擦掉primary slot第二個扇區,然後把secondary slot第二個扇區內容拷貝到primary slot第二個扇區並擦掉secondary slot第二個扇區,然後把primary slot第三個扇區內容拷貝到secondary slot第二個扇區並擦掉primary slot第三個扇區,以此往複,直至primary slot或者secondary slot兩者中最大的那個image size拷貝完成,整個image swap流程宣告完成。從上面演算法描述大家可以感覺出,swap操作是比較耗時的,但是它安全,支援回滾操作。如果大家不需要這個回滾操作的話(就像nRF5 SDK那樣),那麼大家可以選擇overwrite模式(打開#define MCUBOOT_OVERWRITE_ONLY)以加快MCUboot拷貝速度。

4. 是否進入DFU模式。nRF5 Bootloader通過判斷某些標誌位以此決定是否進入DFU模式,與此簡單判斷不同,MCUboot是通過primary slot和secondary slot的狀態組合來決定是否進入DFU模式。在MCUboot中,有一個變數:swap_type,它的取值將決定是否進入DFU模式,而swap_type的值又依賴如下真值表:

 

swap_type取值

上述的magic,image_ok和copy_done三個欄位位於slot最後一個扇區,即slot的最高扇區,他們在扇區中的排布如下所示(magic欄位在扇區的最高地址):

 

從上可知,根據magic,image_ok和copy_done三個變數的不同取值情況,可以得到不同的結果,即swap_type。我們以State1 表格為例來解讀其中的結果,State1表格如下:

 

可以看出,當secondary slot最後一個扇區的magic欄位為Good,即設置成正確的值,而且image_ok欄位不等於1,即為unset狀態,則不管其他變數為什麼值(正常情況下,此時其他變數的值都是0xFF),此時swap_type的結果為:BOOT_SWAP_TYPE_TEST,大家以此類推,就知道State2,State3和State4表格的swap_type結果是怎麼來的。這裡有一點需要大家注意的,magic欄位在Flash中只有兩種正常取值:全FF和0x96f3b83d,而image_ok和copy_done在Flash中也只有兩種正常取值:全FF和0x01,而表格中所謂的「Good」,「Any」,「Unset」,「0x01」,是對上述兩種取值的泛化,比如magic欄位等於0x96f3b83d,就叫「Good」;image_ok等於0xFF,就叫「Unset」或者「Any」(當然「Any」意味著0x55等其他非法值也可以兼容)。swap_type總共有6種結果,每種結果的意義如下所示:

    1. BOOT_SWAP_TYPE_TEST。MCUboot將進入DFU模式,而且為test目的的DFU。跟下面的BOOT_SWAP_TYPE_ PERM模式相比,BOOT_SWAP_TYPE_TEST的DFU過程與之一模一樣,也就是說BOOT_SWAP_TYPE_TEST就是進行正常的真正DFU,只不過DFU完成後,MCUboot跳到新app,這個時候新app必須把secondary slot裡面的image_ok欄位寫為1,即調用boot_write_img_confirmed()這個API來完成,否則再次複位進入MCUboot的時候,MCUboot會認為新image有問題(沒有確認),從而執行回滾操作,重新把老image換到primary slot,然後繼續跑老image(此時升級應該算失敗)。
    2. BOOT_SWAP_TYPE_ PERM。如前所述,BOOT_SWAP_TYPE_ PERM跟BOOT_SWAP_TYPE_TEST DFU過程一模一樣,唯一區別的是,一旦設為PERM(永久)模式,哪怕新image沒有去寫image_ok欄位,再次複位進入MCUboot,MCUboot也不會去執行回滾操作,而強制認為升級已成功。
    3. BOOT_SWAP_TYPE_ REVERT,回滾操作。前述的回滾操作,swap_type就是BOOT_SWAP_TYPE_ REVERT。一旦檢測到BOOT_SWAP_TYPE_ REVERT,MCUboot將進行回滾操作。
    4. BOOT_SWAP_TYPE_ NONE。正常啟動模式,MCUboot將直接跳到app,而不是進入DFU模式。
    5. BOOT_SWAP_TYPE_ FAIL。當MCUboot校驗primary slot裡面的image失敗時,就會報BOOT_SWAP_TYPE_ FAIL,此時程式將死在MCUboot裡面。
    6. BOOT_SWAP_TYPE_ PANIC。當MCUboot啟動過程中出現了致命錯誤,就會報BOOT_SWAP_TYPE_ PANIC,此時程式將死在MCUboot裡面。

從上我們可以總結出,為了讓MCUboot進入DFU模式,swap_type結果必須為BOOT_SWAP_TYPE_TEST或者BOOT_SWAP_TYPE_ PERM,而讓swap_type取值為BOOT_SWAP_TYPE_TEST或者BOOT_SWAP_TYPE_ PERM的關鍵是讓secondary slot最後一個扇區的magic欄位為0x96f3b83d,這是通過調用boot_request_upgrade()來實現的,當調用boot_request_upgrade(false)進入BOOT_SWAP_TYPE_TEST模式,當調用boot_request_upgrade(true)進入BOOT_SWAP_TYPE_ PERM模式。

State1,State2,State3和State4四個表格是有優先順序順序的,越往前優先順序越高,也就是說,如果State1表格匹配成功就不再匹配後面的表格,此時swap_type就是BOOT_SWAP_TYPE_TEST。下面是MCUboot正常啟動的一個示例,可以看出,因為magic,image_ok和copy_done三個變數的取值沒有匹配成功真值表State1,State2和State3,但匹配成功State4表格,所以swap_type的最終結果是BOOT_SWAP_TYPE_ NONE,即正常啟動app。註:0x3就代表「Unset」(實際取值為0xFF),「Unset」可以看成「Any」一種,因此下述啟動日誌表明此時swap_type不匹配State1,State2和State3表格,而匹配State4表格。

 

很多人會好奇為什麼MCUboot使用這麼複雜的DFU模式判斷演算法?究其根本,還是因為Flash的限制導致的。Flash每次只能擦一個page(擦除時間還比較長),而且壽命又有限,在儘可能少擦Flash的情況下,又要實現上述那麼多swap操作,然後有人就想出了上面的演算法。

一般來說,一旦你使能MCUboot(CONFIG_BOOTLOADER_MCUBOOT=y),編譯系統會自動幫你生成升級需要的升級文件:app_update.bin或者app_signed.hex(兩者內容一模一樣)。當然如果你選擇雙核MCU,那麼除了上述應用核的升級文件,編譯系統還會自動生成網路核的升級文件:net_core_app_update.bin或者net_core_app_signed.hex(兩者內容一模一樣)。升級文件示例如下所示:

 

升級的時候,把相應的升級文件傳給設備端,設備端把接收到的升級文件放在secondary slot,待整個image接收完畢,複位進入MCUboot,MCUboot將完成後續工作直至升級成功。 

2.3 B0,亦稱nRF Secure Immutable Bootloader(NSIB)

NSIB(nRF Secure Immutable Bootloader),亦稱B0,位於nrf/samples/bootloader,這個是Nordic自己開發的一個不可升級的Bootloader。b0把存儲區劃分成slot0和slot1,並且slot0大小等於slot1大小,s0_image跑在slot0,s1_image跑在slot1,B0根據s0_image和s1_image的版本號來決定跑哪一個image,如果s0_image的版本號高於或等於s1_image的版本號,那麼B0啟動的時候就會跳到s0_image;反之,如果s1_image的版本號高於s0_image的版本號,那麼B0啟動的時候就會跳到s1_image。由於s0_image和s1_image都有可能被執行,所以s0_image和s1_image必須都放置在內部Flash,也就是說slot0和slot1必須都在nRF設備內部Flash中。B0將存儲區劃分成如下模樣:

 

如前所述,Bootloader有四大功能:啟動image,校驗image,拷貝image以及DFU模式判斷,那麼b0是如何完成這4項功能的:

1. 啟動image。B0通過讀provision區域資訊,得到s0_image和s1_image資訊,provision屬於B0的一部分,下面為provision的定義及一個示例:(感興趣的讀者,仔細看一下結構體各個欄位定義,並對應image hex進行解讀) 

從上面示例可以看出,s0_address為0x9000,0x9000即為s0_image的起始地址,s1_image起始地址可以用同樣道理獲得。得到S0_image或者S1_image的起始地址後,就可以得到兩個image的fw_info,fw_info定義及示例如下所示:

 

通過fw_info就可以找到boot_address,從而跳轉到相應app。

2. 校驗image。B0也支援SHA256或者簽名驗簽,SHA256或者簽名放在image的最後,稱為fw_validation_info,其定義及示例如下所示:

 

B0通過magic欄位找到hash和signature,然後進行校驗。

3. 拷貝image。B0沒有拷貝image的操作,所謂升級,就是執行高版本image,具體來說,如果s1_image版本比s0_image版本高,則執行s1_image;否則執行s0_image。

4. DFU模式進入。B0不存在DFU模式,也就不存在所謂進入DFU模式判斷。每次複位B0都去讀s0_image和s1_image的版本,那個image版本高就執行那個image。

基於b0的DFU,有一點需要特別注意,由於S0_image和S1_image兩者的偏移或者啟動向量不一樣,因此即使S0_image和S1_image兩者功能一模一樣,他們的image內容也不一樣,這也意味著slot0和slot1對應的升級image是不一樣的。一般來說,手機app或者其他主機並不知道設備當前正在運行哪個slot裡面的image,因此DFU的時候,手機app或其他主機需要先跟設備溝通,獲知設備當前正在執行哪個image。如果S0_image在運行,就給它傳S1_image(signed_by_b0_s1_image.bin)並放置在slot1中;如果S1_image在運行,就給它傳S0_image(signed_by_b0_s0_image.bin)並放置在slot0中。升級image接收完畢,系統複位,B0自動選擇高版本image執行,至此整個升級完成。從上可知,DFU的升級文件必須同時包含signed_by_b0_s0_image.bin 和signed_by_b0_s1_image.bin,實際中我們一般使用如下zip文件:

 

這裡我們做了一個基於b0的DFU例子://github.com/aiminhua/ncs_samples/tree/master/nrf_dfu/ble_intFlash_b0,大家感興趣的話,可以自己去看一下(按照裡面的readme來操作)。下面是B0正常啟動的一個示例,可以看出B0選擇了slot0裡面的s0_image進行裝載,校驗和跳轉。 

 

3. DFU協議

3.1 概述

前面說過,為了實現韌體升級,需要把新image放在secondary slot(以MCUboot為例),如何把新image傳輸到secondary slot?這就是DFU協議要做的事情,一般來說,DFU協議需要把image文件分塊一塊一塊傳給設備端,然後設備端按照要求將image塊寫入secondary slot,並回複寫入結果給主機。期間有可能還需要校驗傳輸的image對不對,或者告知每次image塊寫入的偏移地址。最後DFU協議還有可能涉及一些管理操作,比如image塊寫入的準備工作,讀取設備狀態,複位設備等。

這裡需要特彆強調一下,DFU協議是脫離於傳輸層的,也就是說,同樣的DFU協議可以跑到不同的傳輸層,比如藍牙,WiFi,UDP,USB CDC,UART等,千萬不要把DFU協議跟特定的傳輸層混為一談。

nRF Connect SDK包含多種DFU協議,最著名的就是SMP DFU協議,除此之外,還有其他DFU協議,比如http_update,hid_configurator,USB DFU class,PCD DFU,以及從nRF5 SDK移植過來的nrf_dfu協議。不同的應用場景有不同的DFU協議需求,大家需要根據自己的情況選擇合適的DFU協議,就像前述的Bootloader一樣,這些DFU協議選擇一個適合自己的就可以,不需要全部都要會用。下面著重講一下smp dfu和nrf_dfu兩個dfu協議。

3.2 SMP DFU協議

smp 全稱simple management protocol(簡單管理協議),它是設備管理協議的一種,在NCS中,mcumgr模組實現了smp協議,或者說,smp協議按照mcumgr的要求對相應的傳輸數據進行編碼,這樣mcumgr裡面註冊的命令組(command group)可以直接對傳輸數據進行解析。mcumgr實現的功能比較多,smp DFU只是其中一種,除此之外,它還有很多其他功能,比如shell管理,日誌管理等。這裡我們只對DFU相關命令組進行介紹,其他命令組就不在這裡講了。

3.2.1 SMP包頭和命令

mcumgr裡面有兩個命令組跟DFU有關:

  • img_mgmt,即image管理命令組,該命令組又具體包括3個命令集4個具體命令,詳細定義如下: 

  • os_mgmt,即OS管理命令組,該命令組又具體包括3個命令集4個具體命令,詳細定義如下:(實際上,DFU只用到了os_mgmt_reset這個命令) 

 

smp協議把數據包(packet)分成兩部分:包頭(header)和有效載荷(payload),包頭每一個位元組正好對應如下結構體的每一個欄位,即第一個位元組代表nh_op(操作類型),第二個位元組代表nh_flags,第三和四個位元組代表nh_len,第五和六個位元組代表nh_group(命令組編號),第7個位元組代表nh_seq,第8個位元組代表nh_id(命令在該命令組中的編號)。

 

這樣我們就可以通過SMP的包頭找到相應的handler,比如包頭00 00 00 02 00 01 00 00,即對應命令組1的0號命令集的00操作(讀命令),最終找到img_mgmt_state_read這個handler。我們會在3.2.3節對此示例的解析做詳細說明。

3.2.2 SMP包payload和CBOR編碼

SMP payload採用CBOR編碼,CBOR將一連串二進位數據分成多個data item,如下所示: 

從上可知,每個data item第一個位元組包含2部分:數據類型和數據長度,數據類型定義如下:

  • 0,正數
  • 1,負數
  • 2,位元組串(byte string)
  • 3,UTF-8字元串(text string)
  • 4,數組
  • 5,map(又稱字典)
  • 6,tag(這個用得少)
  • 7,浮點數或者特殊類型,其中特殊類型將short count 20–23定義為 false, true, null和undefined

關於數據長度(count)欄位,這個有點特殊,它的定義如下:

  • 如果長度為0–23,則直接用short count的5 bits來表示,從第2個位元組開始表示data payload
  • 如果short count為24(0x18),則表示第2個位元組代表長度,從第3個位元組開始表示data payload
  • 如果short count為25(0x19),則表示第2和第3個位元組合起來表示長度,從第4個位元組開始表示data payload
  • 如果short count為26(0x1A),則表示第2,第3,第4和第5個位元組合起來表示長度,從第6個位元組開始表示data payload
  • 如果short count為27(0x1B),則表示第2至第9個位元組合起來表示長度,從第10個位元組開始表示data payload
  • 如果short count為31(0x1F),則表示長度為未定義,從第2個位元組開始表示data payload,直到遇到停止符:0xFF

count欄位後面就緊跟著data payload了,count有多大,data payload就有多長,比如count為0x0032,則表示後面0x32個位元組都屬於data payload,至此一個data item結束,同時意味著另一個data item的開始,以此往複,周而復始。需要大家注意的是,CBOR中的data item可以嵌套另一個data item,也就是說,data item之間是可以有結構的。

比如數據payload:64 64 61 74 61,0x64(0b011 00100)表示此data item的數據類型為utf-8字元串,長度為4位元組,即後面緊跟的64 61 74 61,這4個ASCII碼對應的字元就是:」data」,這樣我們就成功解析出這個payload了。

3.2.3 SMP包詳細解析示例

smp協議的核心就是通過包頭找到要處理該數據包的handler(命令),並把payload打包成一個特定參數傳給該handler,然後執行該handler

我們現在結合上面的定義,再看一個實際的smp數據包(包含包頭和payload),看看我們最終解析的結果是什麼。

  • 00 00 00 02 00 01 00 00 bf ff

可以看出,nh_op為00,而nh_op定義如下,所以此時為read操作。

 

nh_group的值為0x0001,目前mcumgr支援的group ID見下圖,所以該數據包將觸發img_mgmt命令組。

 

nh_id為00,由於nh_group指向 image management group,而img_mgmt命令組定義了如下命令,可以看出00為IMG_MGMT_ID_STATE。

 

再次結合下面這個命令或者handler定義列表:

 

我們現在可以解讀出最終的結果:00 00 00 02 00 01 00 00  bf ff這個數據包將觸發img_mgmt組裡面的IMG_MGMT_ID_STATE集裡面的mh_read函數,即img_mgmt_state_read,這個函數的定義是:

int img_mgmt_state_read(struct mgmt_ctxt *ctxt)

而數據包的payload,即bf ff,將作為實參賦給上面的ctxt。我們用CBOR編碼來解析一下bf ff,看看它表示什麼意思?bf,即0b101 11111,可以看出,data type為5(表示map類型),count為0x1F(表示未定義長度,通過0xFF劃分data item);ff,根據前面的描述,此處應該是分隔符,至此一個data item結束。可以看出,bf ff本身並沒有實際的意義,實際上img_mgmt_state_read也沒有使用輸入參數:ctxt,兩者是可以對起來的。

3.2.4 SMP DFU流程

講完smp DFU工作原理,我們再講smp DFU整個工作流程,具體來說,包括如下幾步:

  1. 簽名升級image。註:app_update.bin已經是簽過名的image
  2. 上傳image,即把app_update.bin傳送到目標設備
  3. 列出image以獲得image的hash值
  4. 測試image,即寫magic欄位,以讓MCUboot進入DFU模式
  5. 複位設備,以重新進入MCUboot,從而MCUboot進入DFU模式,並執行相應的swap操作,並完成兩個slot image之間的交換或者拷貝動作
  6. Confirm image,即新image啟動成功後,對其image_ok欄位進行置1操作

上述有幾個步驟,可以通過發命令遠程去完成,也可以通過調用本地API自己去完成,兩種選擇都可以。比如confirm image這一步,你可以等待新image啟動成功,然後重連主機,主機再發「confirm image」命令,這個時候升級才算真正完成;也可以在新image啟動成功後,在不連主機的情況下,通過調用前述API:boot_write_img_confirmed()來完成這個確認過程。不管採用那種方法,本質上都是調用boot_write_img_confirmed()來實現,不同的是觸發方式或者時機,發命令的方式由主機遠程觸發(SMP DFU就是選擇這種主機遠程發命令方式),而本地API方式則是設備自己選擇時機來觸發(nrf dfu就是選擇這種本地API調用方式)。

DFU命令說明

當採用UART或者USB傳輸層的時候,上述DFU流程對應的命令如下:

  1. mcumgr conn add myCOM type=”serial” connstring=”dev=COM13,baud=115200,mtu=256″     (Note: change the COM if needed)
  2.   mcumgr -c myCOM image upload app_update.bin
  3.  mcumgr -c myCOM image list
  4.  mcumgr -c myCOM image test <hash of slot-1 image>
  5.   mcumgr -c myCOM reset
  6.  mcumgr -c myCOM image confirm

上面每一個命令就是一個request(請求)每一個request就有一個response(響應),通過這種request/response方式,SMP DFU可以安全可靠地完成DFU數據傳輸。

藍牙DFU流程解讀

當採用BLE作為傳輸層的時候,上面命令都被手機app打包成二進位數據包直接下發給設備端,但解析出來之後,你會發現藍牙DFU流程跟上面說明的流程基本上一模一樣。比如前面的00 00 00 02 00 01 00 00  bf ff,就是手機發給設備的第一條DFU命令或者說請求(request)。我們再舉一個例子:上傳image命令(request),它的第一個數據包示例如下所示:

 

從包頭02 00 00 eb 00 01 00 01可以看出,這個數據包將觸發handler:img_mgmt_upload,我們再來看數據包payload的前面8個位元組:bf 64 64 61 74 61 58 cc,bf表示後面是map數據,即key/value數據對,0x64,表示後面是text string數據,長度為4,從而得到64這個data item對應的payload為:64 61 74 61,即key=」data」;從0x58開始,就表示value這個data item了,0x58表示這個item為位元組串並且長度為下一個位元組:0xcc,也就是說」data」這個key對應的value包含了0xcc個數據的位元組流,這樣第一個key/value對解析完畢。然後再解析63 6c 65 6e 1a 00 02 05 a8,0x63,表示此item為text string數據,長度為3,從而得到payload為6c 65 6e,即key = 」len」;0x1a表示此item為正數,count為後面4個位元組,也就是說」len」這個key對應的value為0x000205a8,至此第二個key/value對解析完畢。以此類推,我們後面又可以解析出」sha」和」off」兩個key以及他們各自的value,最後碰到停止符:0xFF,整個map item結束。前面說過,整個數據包的payload會通過參數傳給img_mgmt_upload作為實參,img_mgmt_upload的函數聲明為:

img_mgmt_upload(struct mgmt_ctxt *ctxt)

而struct mgmt_ctxt定義如下:

struct mgmt_ctxt {

    struct CborEncoder encoder;

    struct CborParser parser;

    struct CborValue it;

};

實際上,SMP數據包payload所在的buffer地址將賦給成員變數it後面的指針(這個指針本身不屬於結構體的一部分,但它緊挨著結構體最後一個元素),這樣我們通過ctxt就可以間接操作SMP數據包的payload,請看如下程式碼: 

rc = cbor_read_object(&ctxt->it, off_attr);

這樣我們就把一個image chunk拷貝到變數:req.img_data,再通過如下程式碼調用Flash訪問API。

img_mgmt_impl_write_image_data(req.off, req.img_data, action.write_bytes, last);

如前所述,每一個request命令都會有一個response,比如上面request命令的response為:

  • 03 00 00 0d 00 01 00 01 bf 62 72 63 00 63 6f 66 66 19 09 40 ff

這樣,一個image chunk數據就成功寫入到Flash中,不斷循環這個request和response過程,直至整個image傳送完畢,最後主機還會發送如下兩條命令以正式結束整個DFU傳輸過程:

  • 02 00 00 32 00 01 00 00 BF 67 63 6F 6E 66 69 72 6D F4 64 68 61 73 68 58 20 47 7C C8 4B 52 27 23 03 DA 27 41 F1 1D 38 46 0F 11 AE DB 5E 75 A2 D3 25 0C 6E DE EF 15 84 24 49 FF,大家可以仿照上面的做法來解析一下這個數據包,它解析的結果是:調用img_mgmt_state_write,並寫入magic欄位,同時將swap類型設為BOOT_SWAP_TYPE_TEST
  • 02 00 00 02 00 00 00 05 BF FF,這個包解析的結果是:調用os_mgmt_reset,對設備進行複位

3.3 nrf dfu協議

nrf dfu協議就是nRF5 SDK使用的DFU協議,相信很多讀者都很熟悉它。nrf dfu協議定義了兩個角色:controller和target,controller發request,target回response,一來一往,完成DFU傳輸過程。nrf dfu定義了如下request命令以及他們的response。

Request命令的格式是:Opcode + parameters,Response的格式是:60 + Opcode + parameters,比如編碼:01 02 00 10 00 00,通過上面解析可以知道它是一個創建數據對象命令NRF_DFU_OP_OBJECT_CREATE,而這條命令的響應是:60 01 01,可以看出也符合上面的定義。

nrf dfu用到了對象概念,什麼叫對象(object)?對象分兩種:command object和data object,其中init包是command對象,而image chunk(image塊)是data對象。

我們可以進一步提煉一下,nrf dfu協議主要涉及的命令是如下幾個:

  • 選擇對象(NRF_DFU_OP_OBJECT_SELECT),用來選擇init包或者image包
  • 創建對象(NRF_DFU_OP_OBJECT_CREATE),用來創建init包或者一個image 4kB塊
  • 寫對象(NRF_DFU_OP_OBJECT_WRITE),即傳輸實際數據。由於藍牙將命令和數據分成兩個不同characteristic,寫對象其實就是寫數據,是一個專門的characteristic:packet characteristic,因此發送寫對象命令時,就沒有必要加上Opcode,而是直接把數據寫到packet characteristic上。由於串口只有一個RX線,因此通過串口DFU的時候,寫對象命令還是有Opcode的。
  • 獲取對象的CRC(NRF_DFU_OP_CRC_GET),用來獲取前面init包或者4kB image塊的CRC值
  • 執行對象(NRF_DFU_OP_OBJECT_EXECUTE),即把數據真正寫入Flash中

我們可以把nrf dfu流程大致歸納為如下幾步:

  1. 選擇init對象
  2. 創建init對象
  3. 執行init對象
  4. 選擇image data對象
  5. 創建第一個4kB data對象
  6. 寫對象,即設備(target)循環接收主機發過來的image chunk,直至4kB
  7. 計算4kB image塊的CRC,並返回給主機(controller)以供其校驗
  8. 執行4kB image塊對象,即將其寫入到Flash中
  9. 循環往複,直至整個image寫入完畢
  10. 寫DFU標誌,並複位設備
  11. 複位後進入Bootloader DFU模式,Bootloader完成後續的拷貝工作,至此整個DFU過程宣告結束

這裡就不再對nrf dfu協議進行詳細解讀了,有興趣的讀者可以自己查閱Nordic infocenter的相關章節介紹,具體鏈接為://infocenter.nordicsemi.com/index.jsp?topic=%2Fsdk_nrf5_v17.1.0%2Flib_dfu_transport.html

4. NCS DFU升級步驟說明

4.1 SMP DFU升級步驟說明

在nRF connect SDK中,有一個現成的smp DFU例子,它所在的目錄為:zephyr\samples\subsys\mgmt\mcumgr\smp_svr,這個例子支援多種傳輸層:藍牙,串口,USB CDC,UDP,Shell,FS等,如果使用藍牙作為傳輸層,其升級操作步驟如下所示:

  1. 進入項目目錄:cd zephyr\samples\subsys\mgmt\mcumgr\smp_svr
  2. 編譯:west build -b nrf52840dk_nrf52840 -d build_nrf52840dk_nrf52840 -p — -DOVERLAY_CONFIG=”overlay-bt.conf”(根據你自己手上的板子情況,把nrf52840dk_nrf52840換成其他DK,比如nrf5340dk_nrf5340_cpuapp)
  3. 燒寫:west flash -d build_nrf52840dk_nrf52840,此時設備將廣播「Zephyr」

 

4.修改原始工程,比如廣播名字(CONFIG_BT_DEVICE_NAME=”NEW_DFU”放在overlay-bt.conf中),再重新編譯,然後拷貝「build_nrf52840dk_nrf52840/zephyr/app_update.bin」到手機版nRF Connect

 

5.用手機nRF Connect連接設備,成功後,點擊右上角的「DFU」圖標,選擇前面的「app_update.bin」文件,然後選擇「Test and Confirm」,DFU開始

 

 

 

6.升級文件傳輸完畢,系統將重啟

 

7.MCUboot完成swap操作,並跳到新app,廣播將變成「NEW_DFU」

 

 

8.手機nRF Connect連接新app,並發送confirm命令

9.至此整個升級結束

除了上述的smp_svr例子,我們還做了其他smp例子,這些例子都放在GitHub這裡://github.com/aiminhua/ncs_samples/tree/master/smp_dfu。請大家仔細閱讀例子裡面的readme,並按照readme去操作。

4.2 nrf_dfu升級步驟說明

這篇文章:詳解藍牙空中升級(BLE OTA)原理與步驟,詳細闡述了nrf dfu升級步驟說明,雖然文章是以nRF5 SDK為例來敘述的,但其步驟也適用NCS nrf dfu過程。我們在NCS中做了很多nrf dfu例子,他們都放在這裡://github.com/aiminhua/ncs_samples/tree/master/nrf_dfu,我們以nrf_dfu/ble_intFlash為例來簡要闡述nrf dfu升級步驟,以幫助大家理解整個DFU過程:

1)      準備。

a. 安裝PC版nrfutil。nrfutil安裝有兩種方式,一種是直接下載exe文件,一種是以Python的方式進行安裝。nrfutil.exe直接下載鏈接為://github.com/NordicSemiconductor/pc-nrfutil/releases記得把nrfutil.exe所在目錄放在Windows環境變數中。Python方式安裝nrfutil步驟如下所示:

    •  安裝Python,下載地址://www.python.org/downloads/,安裝成功後請確保Windows環境變數包含Python目錄 
    • 通過pip安裝最新版的nrfutil,即打開Windows命令行工具CMD,輸入如下命令:pip install nrfutil,即可以完成nrfutil的安裝。

安裝完成後,在Windows命令行工具輸入:nrfutil version,如果可以正確顯示版本資訊,說明安裝已經成功

對於Windows用戶,nrfutil運行需要幾個特殊的DLL庫,而這幾個庫有些Windows機器是沒有的,如此,可往://www.microsoft.com/en-us/download/details.aspx?id=40784下載

b. 進入nrf_dfu/ble_intFlash/sdk_change目錄,選擇你的SDK版本,比如ncs_v1.8.0,把nrf_dfu/ble_intFlash/sdk_change/ncs_v1.8.x下面內容直接覆蓋nrf倉庫目錄

c. 建議大家對照例子裡面的readme看一下還有沒有其他準備工作

2)      進入項目目錄:cd nrf_dfu/ble_intFlash

3)      編譯:west build -b nrf52840dk_nrf52840 -d build_nrf52840dk_nrf52840 -p (根據你自己手上的板子情況,把nrf52840dk_nrf52840換成其他DK,比如nrf5340dk_nrf5340_cpuapp)

4)      燒寫:west flash -d build_nrf52840dk_nrf52840,此時設備將廣播「Nordic_DFU」

 

5)      修改原始工程,比如廣播名字(CONFIG_BT_DEVICE_NAME=”NEW_DFU”),再重新編譯,然後拷貝「build_nrf52840dk_nrf52840/zephyr/ app_signed.hex」到update目錄

 

6)      雙擊update目錄中的zip_generate.bat,將生成ble_intFlash.zip,將ble_intFlash.zip拷貝到手機nRF Connect中

 

7)      用手機nRF Connect連接設備,成功後,點擊右上角的「DFU」圖標,選擇前面的「ble_intFlash.zip」文件

  

8)      升級文件傳輸完畢,系統將重啟

 

9)      MCUboot完成swap操作,並跳到新app,新app自動完成image confirm操作

 

10)   此時廣播已經變成「NEW_DFU」,至此整個升級結束

 

//github.com/aiminhua/ncs_samples/tree/master/nrf_dfu這個目錄下面還有很多其他nrf dfu例子,建議大家可以好好看一下,按照裡面的readme文件實際操作一下,相信對MCUboot和nrf dfu理解就會更深入了。

4.3 存儲器分區(多image情況)

不管是smp dfu還是nrf dfu,都存在secondary slot在內部flash還是在外部flash情況,即ble_extFlash和ble_intFlash這兩個例子,兩個例子功能基本上一模一樣,唯一區別就是secondary slot所在位置,ble_intFlash這個例子secondary slot在內部flash,ble_extFlash這個例子secondary slot在外部flash,這兩個例子的main.c文件一模一樣,唯一不同的是conf文件,以及分區文件partitions.yml。conf文件大家比較容易理解,但是分區文件大家經常困惑,這裡再給大家介紹一下,具體可以參考:開發你的第一個NCS(Zephyr)應用程式

所謂分區(Partition),就是對Flash(包括內部Flash和外部flash)或者RAM物理區域進行一個邏輯劃分,人為劃定哪塊區域幹什麼工作,比如把MCUboot這個image放在0x0000到0xC000這塊區域,這種分區是人為的,所以你可以隨意調整,比如你把MCUboot放在0x0000到0x10000,當然也是可以的。我們對Flash或者RAM進行分區,目的就是為了把空間利用好,給各個分區一個ID以便後續引用,如果程式碼里不引用這個分區,那麼此分區只是一個佔位符而已,比如app和mcuboot這兩個分區。

我們先看一下smp_dfu/ble_intFlash這個例子生成的partitions.yml:

 

從上面可以看出,這個partitions.yml定義了很多分區,比如app,mcuboot,mcuboot_pad,mcuboot_primary等(冒號前面的就是分區名),而且每一個分區規定了它的起始地址,結束地址,大小,相對位置以及放在什麼物理存儲器上,比如app這個分區:

 

關於分區名,只有「app」這個名字是必須有,而且是固定的,代表著主應用程式image;其他分區名,比如mcuboot,settings_storage,external_flash等,都是隨意定義的,可以修改。比如0x0~0xc000這塊內部Flash區,上面取名叫mcuboot,你也可以改成「my_boot」之類的名字,這個也沒關係的,取名字主要考慮兩點:一是能醒目標識這塊區域的功能,二是跟程式碼裡面的引用對起來,比如如下分區定義,經常有人困惑:

 

第一個「external_flash」是分區名,第二個「external_flash」是物理存儲器名。作為分區名的「external_flash」,其實我們可以改成其他名字,以消除某些困惑,之所以使用這個名字,是因為老的littlefs例子裡面對外部文件系統所在區域就稱為「external_flash」,程式碼如下所示: 

FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(external_flash);
static struct fs_mount_t fs_mnt = {
    .type = FS_LITTLEFS,
    .fs_data = &external_flash,
    .storage_dev = (void *)FLASH_AREA_ID(external_flash),
    .mnt_point = "/lfs",
};

實際上最新的littlefs例子已經把這塊區域重新命名為:littlefs_storage或者storage,所以大家可以把這塊分區名改為littlefs_storage,如下:

 

partitions.yml裡面使用的region其實是在這個文件:nrf\cmake\partition_manager.cmake定義的,大家可以通過build目錄下的regions.yml文件得知目前定義了幾個物理存儲器:

 

至於partitions.yml裡面使用的placement/span等,這個是用來指定各個分區的相對位置的,很多人會疑問,既然指定了分區的起始地址和結束地址,那還有必要去指定各個分區的相對位置嗎?這種情況下的確沒必要再指定相對位置了,其實這裡弄反了一件事情:partitions.yml裡面的地址是placement相對位置定下來之後的結果。使用placement相對位置,為編譯系統動態確定各個分區的位置提供了便利。如果是我們自己來劃分存儲器的分區,我們就可以直接使用絕對地址的方式靜態指定各個分區的位置(當然使用placement也是可以的)。

如何人為靜態指定?答案就是把剛才動態生成的partitions.yml文件拷貝到項目根目錄下,然後改名為:pm_static.yml,然後再按照自己的需求去修改,比如smp_dfu/ble_extFlash這個例子,如果由系統動態生成partitions.yml文件,此時mcuboot_secondary分區所在地址為0x0~0xf0000,而文件系統external_flash或者littlefs_storage分區所在地址為0xf0000~0x800000,實際上很多客戶喜歡把文件系統放在外部Flash 0x00地址,而把secondary slot放在外部flash最後,據此可以做如下修改:

 

這個pm_static.yml文件沒有定義的分區,還是由系統動態分配。有時為了後續升級方便,我們會在pm_static.yml文件裡面把所有的分區都按照自己的規劃重新定義一遍,這樣就不擔心某個image突然變大而導致新的partitions.yml跟老的文件不兼容,從而無法升級。在定義pm_static.yml文件時,有如下規則必須遵守:

  • mcuboot_primary大小必須等於mcuboot_secondary,而且CONFIG_BOOT_MAX_IMG_SECTORS最好也等於他們大小/4096
  • 如果使用了一個region(flash_primary這個region除外),那麼這個region每一塊區域都要屬於一個分區名字,不能出現某塊區域沒有分區名字情況。比如上面重新定義了external_flash region,根據regions.yml文件定義,external_flash總共有8Mbytes,那麼這8Mbytes都必須有一個分區名字,而我們定義的littlefs_storage和mcuboot_secondary兩個分區的確包含了全部8MB區域。如果我們定義littlefs_storage所在區域為0x0~0x700000,而mcuboot_secondary所在區域為0x710000~0x800000,那麼系統就會報錯,因為這裡還有一個空隙(gap):0x700000~0x710000是沒有取分區名字的。解決這個問題有兩個辦法:一個就是上面的方法把0x700000~0x710000划到littlefs_storage分區,一個就是給這塊區域專門取一個名字,比如:my_unused_area(見下面示意),也是可以解決問題的。

 

對於flash_primary這個region,由於系統默認認為必須要有一個「app」分區,所以它可以存在而且只能存在一個空隙(gap),這樣系統默認這個gap就是「app」分區。當然你也可以把flash_primary所有區域都分好區,包括「app」分區。

  • regions.yml文件裡面各個存儲器的物理大小必須符合實際,這個通過修改dts文件來保證的。這裡面最容易出錯的就是external_flash,external_flash的大小在regions.yml文件裡面是以位元組為單位(在kconfig文件裡面也是以位元組為單位的),但是external_flash對應的設備樹,比如MX25R64,它在dts文件裡面是以bit為單位的,所以當大家使用其他外部Flash的時候,請仔細檢查這些size對不對
  • settings_storage,即settings使用的分區,大家可以將分區名改成:storage,這是其一,其二settings系統最終使用的最大flash區域大小是由CONFIG_PM_PARTITION_SIZE_SETTINGS_STORAGE決定,而不是settings_storage分區本身大小決定,所以建議大家把CONFIG_PM_PARTITION_SIZE_SETTINGS_STORAGE的值設為settings_storage分區大小。
  • 至於RAM分區,道理也是一樣的。這裡需要注意的是,RAM各個分區的大小大家可以直接到dts文件裡面去調整,而無需在pm_static.yml文件裡面調整。當然,大家在pm_static.yml裡面調整也是可以的,殊途同歸,達到目的就好了。對於nRF52系列,只有一個sram_primary分區,這個沒什麼好講的;對於nRF53系列,除了sram_primary這個分區,它還有rpmsg_nrf53_sram分區以及pcd_sram分區,其中rpmsg_nrf53_sram是用來藍牙協議棧host和controller之間進行雙核通訊的,而pcd_sram是用來升級網路核image的。

5. 移植SMP DFU功能到peripheral_uart(NUS)

現在我們從零開始,一步一步教大家如何把smp服務添加到peripheral_uart例子中。

peripheral_uart例子所在目錄為:nrf\samples\bluetooth\peripheral_uart,這個例子跟nRF5 SDK裡面的nRF5_SDK_17.1.0_ddde560\examples\ble_peripheral\ble_app_uart功能一模一樣,都實現了著名的NUS服務,即藍牙透傳服務。如前所述zephyr\samples\subsys\mgmt\mcumgr\smp_svr這個例子則實現了SMP DFU服務,我們現在把smp藍牙服務移植到peripheral_uart上。

我們仔細查看zephyr\samples\subsys\mgmt\mcumgr\smp_svr這個例子,為了實現SMP DFU,主要修改兩個地方:一是修改prj.conf以包含相應模組,二是修改main.c的初始化函數以初始化SMP相關模組,prj.conf主要修改點如下:

CONFIG_BOOTLOADER_MCUBOOT=y
CONFIG_MCUMGR=y
CONFIG_MCUMGR_CMD_IMG_MGMT=y
CONFIG_MCUMGR_CMD_OS_MGMT=y
CONFIG_BT_L2CAP_TX_MTU=252
CONFIG_BT_BUF_ACL_RX_SIZE=256
CONFIG_MCUMGR_SMP_BT=y
CONFIG_MCUMGR_SMP_BT_AUTHEN=n
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2304
CONFIG_MAIN_STACK_SIZE=2048

我們把上述config加在nrf\samples\bluetooth\peripheral_uart\prj.conf文件最後,這樣prj.conf就改完了。

main.c的修改就更簡單,在啟動廣播之前,我們加入如下初始化函數:

            smp_bt_register();

            os_mgmt_register_group();

            img_mgmt_register_group();

就這樣兩步工作,輕輕鬆鬆就把SMP DFU服務移植到peripheral_uart上,整個程式碼已經上傳到//github.com/aiminhua/ncs_samples/tree/master/smp_dfu/peripheral_uart,大家可以下載下來參考或者測試一下。

從上述例子我們可以看出,在NCS中移植一個例子非常方便,它不需要去添加c文件和頭文件,也不需要去修改編譯選項,還不需要去修改傳統的頭文件進行配置,僅僅修改conf文件和初始化函數,就輕輕鬆鬆完成了整個移植,這也是NCS非常大的一個好處。

其實//github.com/aiminhua/ncs_samples/tree/master/smp_dfu下面包含的例子都同時具備smp和nus兩個服務,並且區分各種不同情形下的DFU情況,比如secondary slot在外部Flash,通過串口傳輸image等,同時其對peripheral_uart例子進行了小小改動,以更符合某些實際應用場景,建議大家好好看一下,相信對大家理解MCUboot和SMP會幫助不少。

6 手機端DFU參考程式碼

Nordic不僅提供設備端的DFU參考程式碼,同時提供手機端的參考程式碼。Nordic分別開發了Android版和iOS版的DFU庫,大家可以直接拿過來使用,集成到自己的移動端app中,這兩個庫都放在github上,其中smp dfu對應的DFU庫鏈接如下所示:

而nrf dfu對應的DFU庫鏈接如下所示:

Nordic還提供了一個移動端app:nRF Toolbox,nRF Toolbox是程式碼開源的,裡面也集成了上面提到的兩種DFU庫(iOS版同時支援SMP DFU和nrf dfu,而Android版僅支援nrf dfu),大家可以參考nRF Toolbox來開發自己的移動端app。nRF Toolbox源碼也可以在github上找到:

nRF Toolbox軟體介面如下所示: