如何在nRF Connect SDK(NCS)中實現(xiàn)藍牙空中升級?MCUboot和B0兩個Bootloader有什么區(qū)別?MCUboot升級使用的image格式是怎么樣的?什么是SMP協(xié)議?CBOR編碼如何解讀?NCS可不可以進行單bank升級?可不可以把一個nRF5 SDK應用升級到NCS應用?MCUboot拷貝操作中的swap和overwrite有什么區(qū)別?為什么說MCUboot升級永遠都不可能變磚?本文將對以上問題進行闡述。
目錄
1.概述
2. NCS中的Bootloader
2.1 nRF5 SDK Bootloader
2.2 MCUboot
2.3 B0,亦稱nRF Secure Immutable Bootloader(NSIB)
3. DFU協(xié)議
3.1 概述
3.2 SMP DFU協(xié)議
3.2.1 SMP包頭和命令
3.2.2 SMP包payload和CBOR編碼
3.2.3 SMP包詳細解析示例
3.2.4 SMP DFU流程
3.3 nrf dfu協(xié)議
4. NCS DFU升級步驟說明
4.1 SMP DFU升級步驟說明
4.2 nrf_dfu升級步驟說明
4.3 存儲器分區(qū)(多image情況)
5. 移植SMP DFU功能到peripheral_uart(NUS)
6 手機端DFU參考代碼
先講一下DFU和OTA的概念。DFU(Device Firmware Update),就是設(shè)備固件升級的意思,而OTA(Over The Air)是實現(xiàn)DFU的一種方式而已,準確說,OTA的全稱應該是OTA DFU,即通過空中無線方式實現(xiàn)設(shè)備固件升級。只不過大家為了方便起見,直接用OTA來指代固件空中升級(有時候大家也將OTA稱為FOTA,即Firmware OTA,這種稱呼意思更明了一些)。只要是通過無線通信方式實現(xiàn)DFU的,都可以叫OTA,比如4G/WiFi/藍牙/NFC/Zigbee/NB-IoT,他們都支持OTA。DFU除了可以通過無線方式(OTA)進行升級,也可以通過有線方式進行升級,比如通過UART,USB或者SPI通信接口來升級設(shè)備固件。
不管采用OTA方式還是有線通信方式,DFU包括后臺式(background)和非后臺式兩種模式。后臺式DFU,又稱靜默式DFU(Silent DFU),在升級的時候,新固件在后臺悄悄下載,即新固件下載屬于應用程序功能的一部分,在新固件下載過程中,應用可以正常使用,也就是說整個下載過程對用戶來說是無感的,下載完成后,系統(tǒng)再跳到BootLoader程序,由BootLoader完成新老固件拷貝操作,至此整個升級過程結(jié)束。比如智能手機升級Android或者iOS系統(tǒng)都是采用后臺式DFU方式,新系統(tǒng)下載過程中,手機可以正常使用哦。非后臺式DFU,在升級的時候,系統(tǒng)需要先從應用程序跳到BootLoader程序,由BootLoader進行新固件下載工作,下載完成后BootLoader繼續(xù)完成新老固件拷貝操作,至此升級結(jié)束。早先的功能機就是采用非后臺式 DFU來升級操作系統(tǒng)的,即用戶需要先長按某些按鍵進入bootloader模式,然后再進行升級,整個升級過程中手機正常功能都無法使用。
下面再講雙區(qū)(2 Slot)DFU和單區(qū)(1 Slot)DFU,雙區(qū)或者單區(qū)DFU是新固件覆蓋老固件的兩種方式。后臺式DFU必須采用雙區(qū)模式進行升級,即老系統(tǒng)(老固件)和新系統(tǒng)(新固件)各占一塊Slot(存儲區(qū)),假設(shè)老固件放在Slot0中,新固件放在Slot1中,升級的時候,應用程序先把新固件下載到Slot1中,只有當新固件下載完成并校驗成功后,系統(tǒng)才會跳入BootLoader程序,然后擦除老固件所在的Slot0區(qū),并把新固件拷貝到Slot0中,或者把Slot0和Slot1兩者的image進行交換。非后臺式DFU可以采用雙區(qū)也可以采用單區(qū)模式,與后臺式DFU相似,雙區(qū)模式下新老固件各占一塊Slot(老固件為Slot0,新固件為Slot1),升級時,系統(tǒng)先跳入BootLoader程序,然后BootLoader程序把新固件下載到Slot1中,只有新固件下載完成并校驗成功后,才會去擦除老固件所在的Slot0區(qū),并把新固件拷貝到Slot0區(qū)。單區(qū)模式的非后臺式DFU只有一個Slot0,老固件和新固件分享這一個Slot0,升級的時候,進入bootloader程序DFU模式后立馬擦除老固件,然后直接把新固件下載到同一個Slot中,下載完成后校驗新固件的有效性,新固件有效升級完成,否則要求重來。跟非后臺式DFU雙區(qū)模式相比,單區(qū)模式節(jié)省了一個Slot的Flash空間,在系統(tǒng)資源比較緊張的時候,單區(qū)模式是一個不錯的選擇。不管是雙區(qū)模式還是單區(qū)模式,升級過程出現(xiàn)問題后,都可以進行二次升級,都不會出現(xiàn)“變磚”情況。不過雙區(qū)模式有一個好處,如果升級過程中出現(xiàn)問題或者新固件有問題,它還可以選擇之前的老固件老系統(tǒng)繼續(xù)執(zhí)行而不受其影響。而單區(qū)模式碰到這種情況就只能一直待在bootloader中,然后等待二次或者多次升級嘗試,此時設(shè)備的正常功能已無法使用,從用戶使用這個角度來說,你的確可以說此時設(shè)備已經(jīng)“變磚”了。所以說,雖然雙區(qū)模式犧牲了很多存儲空間,但是換來了更好的升級體驗。
可參考下面三個圖來理解上述過程。
如果你是第一次接觸nRF Connect SDK(NCS),那么建議你先看一下這篇文章:開發(fā)你的第一個NCS/Zephyr應用程序,以建立NCS的一些基本知識,然后再往下看以下章節(jié)。
如果你的應用不需要DFU功能,那么Bootloader就可以不要;反之,如果你的應用需要DFU功能,Bootloader就一定需要。Bootloader在其中起到的作用包括:一判斷正常啟動還是DFU升級流程,二啟動并校驗應用image,三升級的時候完成新image和老image的交換或者拷貝工作。進一步說,
啟動向量表可以放在image的最開始處,也可以放在其他地方,這就涉及到image的格式。Image正確的校驗值可以跟image合在一塊存放,也可以單獨放在一個flash page里面。如果image的校驗值是跟image本身合在一塊存放的,這里再次涉及到image的格式。關(guān)于新image和老image存放位置,這就涉及到存儲器分區(qū)問題。Bootloader的實現(xiàn)將直接決定image的格式,以及存儲器的結(jié)構(gòu)劃分。
NCS支持MCUboot,B0和nRF5 Bootloader三種Bootloader,三個Bootloader選其一即可,一般推薦大家使用MCUboot。由于很多讀者對Nordic老的SDK,即nRF5 SDK比較熟悉,我們先以這個nRF5 Bootloader為例來講解他們的Flash分區(qū)以及image格式,然后再講MCUboot和B0,看看他們又是如何分區(qū)和定義image格式的。注意:如果你只對其中某一個具體的Bootloader感興趣,可以跳過其他章節(jié),直接閱讀相關(guān)章節(jié),比如如果你只對MCUboot感興趣,可以只看2.2節(jié)。
nRF5 Bootloader是指nRF5_SDK_17.1.0_ddde560\examples\dfu\secure_bootloader這里面定義的Bootloader,如果你的DFU想使用這個Bootloader,那么nRF5 SDK的存儲區(qū)劃分(雙bank)是下面這樣的:
在nRF Connect SDK(NCS)中,如果也使用nRF5 Bootloader,此時存儲器的分區(qū)跟上面大同小異,我們用NCS中的語言重新組織如下:
當前固件(老固件)在Bank0里面執(zhí)行,新固件接收后直接存放在Bank1,而且程序永遠只執(zhí)行Bank0里面的代碼,Bank1的起始地址是動態(tài)的,其計算公式為: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包傳給目標設(shè)備,如下所示:
這個zip包除了新image本身,還包含一個dat文件,這個dat文件包含新image的大小,SHA256,簽名等信息。
至于升級拷貝,nRF5 Bootloader做法也很簡單,先擦掉Bank0里面的內(nèi)容,然后把Bank1里面的內(nèi)容拷貝到Bank0,然后重新從Bank0啟動,完成整個升級。在拷貝之前,Bootloader會校驗Bank1里面的image完整性,只有校驗通過才會做下一步的拷貝工作,否則退出升級模式。從上可以看出,雖然nRF5 Bootloader會校驗image的完整性,但是如果出現(xiàn)發(fā)版錯誤(打個比方,Win11和Win7都是微軟驗簽,因此完整性校驗都可以通過,但是如果微軟把Win11發(fā)到一臺只能跑Win7的設(shè)備上,那么這臺設(shè)備將無法運行),由于它沒有新image確認操作,也不支持回滾操作,那么升級后系統(tǒng)有可能掛死在一個錯誤的版本里面。
說完了啟動,校驗和升級拷貝,最后說一下如何進入DFU模式。在nRF5 Bootloader里面,通過判斷某些Flag(標志位)來決定要不要進入DFU模式,這些標志位有一個為真,進入DFU模式,否則正常啟動app:
可以看出,整個判斷邏輯還是比較簡單,大家很容易讀懂相關(guān)的源代碼。
nRF5 Bootloader既可以運行在nRF5 SDK中,也可以運行在NCS中。nRF5 Bootloader既支持非后臺式DFU,也支持后臺式DFU,我們做了一個跑在NCS中的后臺式DFU例子:https://github.com/aiminhua/ncs_samples/tree/master/nrf_dfu/ble_intFlash_nrf5_bl。跟nRF5 SDK DFU相比,這個例子有兩個要注意的地方:
從這個例子大家可以體會到,分區(qū)和新image格式只跟Bootloader有關(guān),跟SDK或者DFU協(xié)議無關(guān)。
下面是nRF5 Bootloader啟動的一個示例,供大家參考:
MCUboot位于如下目錄:bootloader/mcuboot/boot/zephyr,在NCS中做DFU的時候,一般都推薦使用MCUboot。MCUboot功能強大,兼容的芯片平臺多,而且是一個久經(jīng)考驗的第三方開源Bootloader。MCUboot把存儲區(qū)劃分為Primary slot和Secondary slot,而且primary slot跟secondary slot兩者大小是一樣的,程序默認在Primary slot中執(zhí)行。有一點需要大家注意,NCS對MCUboot進行了定制,在NCS中,程序只能在Primary slot中執(zhí)行,Secondary slot只是用來存儲新image,而且Secondary slot可以放在內(nèi)部Flash,也可以放在外部Flash,這樣在NCS中,存儲器分區(qū)有如下兩種典型情況:
Secondary slot在內(nèi)部Flash
Secondary slot在外部Flash
注:MCUboot放在0x000000地址。
如前所述,Bootloader有四大功能:啟動image,校驗image,拷貝image以及DFU模式判斷,那么MCUboot是如何完成這4項功能的:
從上可以看出,image的最開始是image header,而不是image啟動向量。Image header里面有一個字段image header size,啟動向量就位于image header size的偏移處,image header一般為0x200大小,一般來說,app的基地址是0xC000,這樣image的啟動向量就在0xC000+0x200=0xC200,MCUboot啟動app的時候就跳轉(zhuǎn)到0xC200這個地址。
2. 校驗image。MCUboot通過讀image的尾信息(tail或者tlv),得到image的SHA256和簽名,從而完成校驗。Image tlv緊跟在image后面,其內(nèi)容示例如下所示:(感興趣的讀者,仔細看一下各個結(jié)構(gòu)體字段定義,并對應image hex進行解讀)
上述示例解讀結(jié)果為:沒有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里,可以看出這種方案是不太方便開發(fā)和調(diào)試的。而MCUboot把app image的SHA256和簽名放在image后面,這樣每次重新編譯一次app image,新的sha256和簽名會自動跟著一起更新,你只需直接下載app而無需去更改Bootloader任何部分,大大方便了開發(fā)和調(diào)試。
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,這樣就實現(xiàn)了A和B的交換。從上可知,實現(xiàn)swap的關(guān)鍵是媒介C的引入,據(jù)此MCUboot支持兩種swap算法:swap_move和swap_scratch,默認采用swap_move。swap_scratch的做法是:在存儲區(qū)中專門劃分一塊scratch區(qū)作為swap媒介,swap的時候,primary slot里面的image先放在scratch區(qū),然后把secondary slot里面的image拷貝到primary slot,最后把scratch區(qū)里面的內(nèi)容拷貝到secondary slot,從而完成一次交換操作,Scratch區(qū)應該比primary或者secondary slot小很多,因此要完成整個image交換,需要循環(huán)執(zhí)行多次上述操作直至整個image(以兩個slot中最大的為準)交換完成。這種算法有兩個弊端:一浪費了scratch區(qū),二由于一次image交換,scratch區(qū)需要執(zhí)行多次擦寫操作,scratch區(qū)的Flash壽命有可能會不夠,為解決上述兩個問題,引入了第二套算法:swap_move,具體做法是:先把primary slot里面整個image向上搬移一個扇區(qū),即先擦掉image size + 1的扇區(qū),然后把image size所在的扇區(qū)內(nèi)容拷貝到image size + 1扇區(qū),然后擦掉image size扇區(qū),并把image size -1所在的扇區(qū)內(nèi)容拷貝到image size扇區(qū),以此循環(huán)往復,直至把整個image向上挪動一個扇區(qū),這樣就為下面的primary slot和secondary slot image交換做好準備。Primary slot和secondary slot image交換的時候,先擦掉primary slot第一個扇區(qū),然后把secondary slot第一個扇區(qū)的內(nèi)容拷貝到primary slot第一個扇區(qū)并擦掉secondary slot第一個扇區(qū),然后把primary slot第二個扇區(qū)內(nèi)容拷貝到secondary slot第一個扇區(qū)并擦掉primary slot第二個扇區(qū),然后把secondary slot第二個扇區(qū)內(nèi)容拷貝到primary slot第二個扇區(qū)并擦掉secondary slot第二個扇區(qū),然后把primary slot第三個扇區(qū)內(nèi)容拷貝到secondary slot第二個扇區(qū)并擦掉primary slot第三個扇區(qū),以此往復,直至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的狀態(tài)組合來決定是否進入DFU模式。在MCUboot中,有一個變量:swap_type,它的取值將決定是否進入DFU模式,而swap_type的值又依賴如下真值表:
swap_type取值
上述的magic,image_ok和copy_done三個字段位于slot最后一個扇區(qū),即slot的最高扇區(qū),他們在扇區(qū)中的排布如下所示(magic字段在扇區(qū)的最高地址):
從上可知,根據(jù)magic,image_ok和copy_done三個變量的不同取值情況,可以得到不同的結(jié)果,即swap_type。我們以State1 表格為例來解讀其中的結(jié)果,State1表格如下:
可以看出,當secondary slot最后一個扇區(qū)的magic字段為Good,即設(shè)置成正確的值,而且image_ok字段不等于1,即為unset狀態(tài),則不管其他變量為什么值(正常情況下,此時其他變量的值都是0xFF),此時swap_type的結(jié)果為:BOOT_SWAP_TYPE_TEST,大家以此類推,就知道State2,State3和State4表格的swap_type結(jié)果是怎么來的。這里有一點需要大家注意的,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種結(jié)果,每種結(jié)果的意義如下所示:
從上我們可以總結(jié)出,為了讓MCUboot進入DFU模式,swap_type結(jié)果必須為BOOT_SWAP_TYPE_TEST或者BOOT_SWAP_TYPE_ PERM,而讓swap_type取值為BOOT_SWAP_TYPE_TEST或者BOOT_SWAP_TYPE_ PERM的關(guān)鍵是讓secondary slot最后一個扇區(qū)的magic字段為0x96f3b83d,這是通過調(diào)用boot_request_upgrade()來實現(xiàn)的,當調(diào)用boot_request_upgrade(false)進入BOOT_SWAP_TYPE_TEST模式,當調(diào)用boot_request_upgrade(true)進入BOOT_SWAP_TYPE_ PERM模式。
State1,State2,State3和State4四個表格是有優(yōu)先級順序的,越往前優(yōu)先級越高,也就是說,如果State1表格匹配成功就不再匹配后面的表格,此時swap_type就是BOOT_SWAP_TYPE_TEST。下面是MCUboot正常啟動的一個示例,可以看出,因為magic,image_ok和copy_done三個變量的取值沒有匹配成功真值表State1,State2和State3,但匹配成功State4表格,所以swap_type的最終結(jié)果是BOOT_SWAP_TYPE_ NONE,即正常啟動app。注:0x3就代表“Unset”(實際取值為0xFF),“Unset”可以看成“Any”一種,因此下述啟動日志表明此時swap_type不匹配State1,State2和State3表格,而匹配State4表格。
很多人會好奇為什么MCUboot使用這么復雜的DFU模式判斷算法?究其根本,還是因為Flash的限制導致的。Flash每次只能擦一個page(擦除時間還比較長),而且壽命又有限,在盡可能少擦Flash的情況下,又要實現(xiàn)上述那么多swap操作,然后有人就想出了上面的算法。
一般來說,一旦你使能MCUboot(CONFIG_BOOTLOADER_MCUBOOT=y),編譯系統(tǒng)會自動幫你生成升級需要的升級文件:app_update.bin或者app_signed.hex(兩者內(nèi)容一模一樣)。當然如果你選擇雙核MCU,那么除了上述應用核的升級文件,編譯系統(tǒng)還會自動生成網(wǎng)絡核的升級文件:net_core_app_update.bin或者net_core_app_signed.hex(兩者內(nèi)容一模一樣)。升級文件示例如下所示:
升級的時候,把相應的升級文件傳給設(shè)備端,設(shè)備端把接收到的升級文件放在secondary slot,待整個image接收完畢,復位進入MCUboot,MCUboot將完成后續(xù)工作直至升級成功。
NSIB(nRF Secure Immutable Bootloader),亦稱B0,位于nrf/samples/bootloader,這個是Nordic自己開發(fā)的一個不可升級的Bootloader。b0把存儲區(qū)劃分成slot0和slot1,并且slot0大小等于slot1大小,s0_image跑在slot0,s1_image跑在slot1,B0根據(jù)s0_image和s1_image的版本號來決定跑哪一個image,如果s0_image的版本號高于或等于s1_image的版本號,那么B0啟動的時候就會跳到s0_image;反之,如果s1_image的版本號高于s0_image的版本號,那么B0啟動的時候就會跳到s1_image。由于s0_image和s1_image都有可能被執(zhí)行,所以s0_image和s1_image必須都放置在內(nèi)部Flash,也就是說slot0和slot1必須都在nRF設(shè)備內(nèi)部Flash中。B0將存儲區(qū)劃分成如下模樣:
如前所述,Bootloader有四大功能:啟動image,校驗image,拷貝image以及DFU模式判斷,那么b0是如何完成這4項功能的:
1. 啟動image。B0通過讀provision區(qū)域信息,得到s0_image和s1_image信息,provision屬于B0的一部分,下面為provision的定義及一個示例:(感興趣的讀者,仔細看一下結(jié)構(gòu)體各個字段定義,并對應image hex進行解讀)
從上面示例可以看出,s0_address為0x9000,0x9000即為s0_image的起始地址,s1_image起始地址可以用同樣道理獲得。得到S0_image或者S1_image的起始地址后,就可以得到兩個image的fw_info,fw_info定義及示例如下所示:
通過fw_info就可以找到boot_address,從而跳轉(zhuǎn)到相應app。
2. 校驗image。B0也支持SHA256或者簽名驗簽,SHA256或者簽名放在image的最后,稱為fw_validation_info,其定義及示例如下所示:
B0通過magic字段找到hash和signature,然后進行校驗。
3. 拷貝image。B0沒有拷貝image的操作,所謂升級,就是執(zhí)行高版本image,具體來說,如果s1_image版本比s0_image版本高,則執(zhí)行s1_image;否則執(zhí)行s0_image。
4. DFU模式進入。B0不存在DFU模式,也就不存在所謂進入DFU模式判斷。每次復位B0都去讀s0_image和s1_image的版本,那個image版本高就執(zhí)行那個image。
基于b0的DFU,有一點需要特別注意,由于S0_image和S1_image兩者的偏移或者啟動向量不一樣,因此即使S0_image和S1_image兩者功能一模一樣,他們的image內(nèi)容也不一樣,這也意味著slot0和slot1對應的升級image是不一樣的。一般來說,手機app或者其他主機并不知道設(shè)備當前正在運行哪個slot里面的image,因此DFU的時候,手機app或其他主機需要先跟設(shè)備溝通,獲知設(shè)備當前正在執(zhí)行哪個image。如果S0_image在運行,就給它傳S1_image(signed_by_b0_s1_image.bin)并放置在slot1中;如果S1_image在運行,就給它傳S0_image(signed_by_b0_s0_image.bin)并放置在slot0中。升級image接收完畢,系統(tǒng)復位,B0自動選擇高版本image執(zhí)行,至此整個升級完成。從上可知,DFU的升級文件必須同時包含signed_by_b0_s0_image.bin 和signed_by_b0_s1_image.bin,實際中我們一般使用如下zip文件:
這里我們做了一個基于b0的DFU例子:https://github.com/aiminhua/ncs_samples/tree/master/nrf_dfu/ble_intFlash_b0,大家感興趣的話,可以自己去看一下(按照里面的readme來操作)。下面是B0正常啟動的一個示例,可以看出B0選擇了slot0里面的s0_image進行裝載,校驗和跳轉(zhuǎn)。
前面說過,為了實現(xiàn)固件升級,需要把新image放在secondary slot(以MCUboot為例),如何把新image傳輸?shù)絪econdary slot?這就是DFU協(xié)議要做的事情,一般來說,DFU協(xié)議需要把image文件分塊一塊一塊傳給設(shè)備端,然后設(shè)備端按照要求將image塊寫入secondary slot,并回復寫入結(jié)果給主機。期間有可能還需要校驗傳輸?shù)膇mage對不對,或者告知每次image塊寫入的偏移地址。最后DFU協(xié)議還有可能涉及一些管理操作,比如image塊寫入的準備工作,讀取設(shè)備狀態(tài),復位設(shè)備等。
這里需要特別強調(diào)一下,DFU協(xié)議是脫離于傳輸層的,也就是說,同樣的DFU協(xié)議可以跑到不同的傳輸層,比如藍牙,WiFi,UDP,USB CDC,UART等,千萬不要把DFU協(xié)議跟特定的傳輸層混為一談。
nRF Connect SDK包含多種DFU協(xié)議,最著名的就是SMP DFU協(xié)議,除此之外,還有其他DFU協(xié)議,比如http_update,hid_configurator,USB DFU class,PCD DFU,以及從nRF5 SDK移植過來的nrf_dfu協(xié)議。不同的應用場景有不同的DFU協(xié)議需求,大家需要根據(jù)自己的情況選擇合適的DFU協(xié)議,就像前述的Bootloader一樣,這些DFU協(xié)議選擇一個適合自己的就可以,不需要全部都要會用。下面著重講一下smp dfu和nrf_dfu兩個dfu協(xié)議。
smp 全稱simple management protocol(簡單管理協(xié)議),它是設(shè)備管理協(xié)議的一種,在NCS中,mcumgr模塊實現(xiàn)了smp協(xié)議,或者說,smp協(xié)議按照mcumgr的要求對相應的傳輸數(shù)據(jù)進行編碼,這樣mcumgr里面注冊的命令組(command group)可以直接對傳輸數(shù)據(jù)進行解析。mcumgr實現(xiàn)的功能比較多,smp DFU只是其中一種,除此之外,它還有很多其他功能,比如shell管理,日志管理等。這里我們只對DFU相關(guān)命令組進行介紹,其他命令組就不在這里講了。
mcumgr里面有兩個命令組跟DFU有關(guān):
smp協(xié)議把數(shù)據(jù)包(packet)分成兩部分:包頭(header)和有效載荷(payload),包頭每一個字節(jié)正好對應如下結(jié)構(gòu)體的每一個字段,即第一個字節(jié)代表nh_op(操作類型),第二個字節(jié)代表nh_flags,第三和四個字節(jié)代表nh_len,第五和六個字節(jié)代表nh_group(命令組編號),第7個字節(jié)代表nh_seq,第8個字節(jié)代表nh_id(命令在該命令組中的編號)
這樣我們就可以通過SMP的包頭找到相應的handler,比如包頭00 00 00 02 00 01 00 00,即對應命令組1的0號命令集的00操作(讀命令),最終找到img_mgmt_state_read這個handler。我們會在3.2.3節(jié)對此示例的解析做詳細說明。
SMP payload采用CBOR編碼,CBOR將一連串二進制數(shù)據(jù)分成多個data item,如下所示:
從上可知,每個data item第一個字節(jié)包含2部分:數(shù)據(jù)類型和數(shù)據(jù)長度,數(shù)據(jù)類型定義如下:
關(guān)于數(shù)據(jù)長度(count)字段,這個有點特殊,它的定義如下:
count字段后面就緊跟著data payload了,count有多大,data payload就有多長,比如count為0x0032,則表示后面0x32個字節(jié)都屬于data payload,至此一個data item結(jié)束,同時意味著另一個data item的開始,以此往復,周而復始。需要大家注意的是,CBOR中的data item可以嵌套另一個data item,也就是說,data item之間是可以有結(jié)構(gòu)的。
比如數(shù)據(jù)payload:64 64 61 74 61,0x64(0b011 00100)表示此data item的數(shù)據(jù)類型為utf-8字符串,長度為4字節(jié),即后面緊跟的64 61 74 61,這4個ASCII碼對應的字符就是:”data”,這樣我們就成功解析出這個payload了。
smp協(xié)議的核心就是通過包頭找到要處理該數(shù)據(jù)包的handler(命令),并把payload打包成一個特定參數(shù)傳給該handler,然后執(zhí)行該handler。
我們現(xiàn)在結(jié)合上面的定義,再看一個實際的smp數(shù)據(jù)包(包含包頭和payload),看看我們最終解析的結(jié)果是什么。
可以看出,nh_op為00,而nh_op定義如下,所以此時為read操作。
nh_group的值為0x0001,目前mcumgr支持的group ID見下圖,所以該數(shù)據(jù)包將觸發(fā)img_mgmt命令組。
nh_id為00,由于nh_group指向 image management group,而img_mgmt命令組定義了如下命令,可以看出00為IMG_MGMT_ID_STATE。
再次結(jié)合下面這個命令或者handler定義列表:
我們現(xiàn)在可以解讀出最終的結(jié)果:00 00 00 02 00 01 00 00 bf ff這個數(shù)據(jù)包將觸發(fā)img_mgmt組里面的IMG_MGMT_ID_STATE集里面的mh_read函數(shù),即img_mgmt_state_read,這個函數(shù)的定義是:
int img_mgmt_state_read(struct mgmt_ctxt *ctxt)
而數(shù)據(jù)包的payload,即bf ff,將作為實參賦給上面的ctxt。我們用CBOR編碼來解析一下bf ff,看看它表示什么意思?bf,即0b101 11111,可以看出,data type為5(表示map類型),count為0x1F(表示未定義長度,通過0xFF劃分data item);ff,根據(jù)前面的描述,此處應該是分隔符,至此一個data item結(jié)束??梢钥闯?,bf ff本身并沒有實際的意義,實際上img_mgmt_state_read也沒有使用輸入?yún)?shù):ctxt,兩者是可以對起來的。
講完smp DFU工作原理,我們再講smp DFU整個工作流程,具體來說,包括如下幾步:
上述有幾個步驟,可以通過發(fā)命令遠程去完成,也可以通過調(diào)用本地API自己去完成,兩種選擇都可以。比如confirm image這一步,你可以等待新image啟動成功,然后重連主機,主機再發(fā)“confirm image”命令,這個時候升級才算真正完成;也可以在新image啟動成功后,在不連主機的情況下,通過調(diào)用前述API:boot_write_img_confirmed()來完成這個確認過程。不管采用那種方法,本質(zhì)上都是調(diào)用boot_write_img_confirmed()來實現(xiàn),不同的是觸發(fā)方式或者時機,發(fā)命令的方式由主機遠程觸發(fā)(SMP DFU就是選擇這種主機遠程發(fā)命令方式),而本地API方式則是設(shè)備自己選擇時機來觸發(fā)(nrf dfu就是選擇這種本地API調(diào)用方式)。
DFU命令說明
當采用UART或者USB傳輸層的時候,上述DFU流程對應的命令如下:
上面每一個命令就是一個request(請求),每一個request就有一個response(響應),通過這種request/response方式,SMP DFU可以安全可靠地完成DFU數(shù)據(jù)傳輸。
藍牙DFU流程解讀
當采用BLE作為傳輸層的時候,上面命令都被手機app打包成二進制數(shù)據(jù)包直接下發(fā)給設(shè)備端,但解析出來之后,你會發(fā)現(xiàn)藍牙DFU流程跟上面說明的流程基本上一模一樣。比如前面的00 00 00 02 00 01 00 00 bf ff,就是手機發(fā)給設(shè)備的第一條DFU命令或者說請求(request)。我們再舉一個例子:上傳image命令(request),它的第一個數(shù)據(jù)包示例如下所示:
從包頭02 00 00 eb 00 01 00 01可以看出,這個數(shù)據(jù)包將觸發(fā)handler:img_mgmt_upload,我們再來看數(shù)據(jù)包payload的前面8個字節(jié):bf 64 64 61 74 61 58 cc,bf表示后面是map數(shù)據(jù),即key/value數(shù)據(jù)對,0x64,表示后面是text string數(shù)據(jù),長度為4,從而得到64這個data item對應的payload為:64 61 74 61,即key=”data”;從0x58開始,就表示value這個data item了,0x58表示這個item為字節(jié)串并且長度為下一個字節(jié):0xcc,也就是說”data”這個key對應的value包含了0xcc個數(shù)據(jù)的字節(jié)流,這樣第一個key/value對解析完畢。然后再解析63 6c 65 6e 1a 00 02 05 a8,0x63,表示此item為text string數(shù)據(jù),長度為3,從而得到payload為6c 65 6e,即key = ”len”;0x1a表示此item為正數(shù),count為后面4個字節(jié),也就是說”len”這個key對應的value為0x000205a8,至此第二個key/value對解析完畢。以此類推,我們后面又可以解析出”sha”和”off”兩個key以及他們各自的value,最后碰到停止符:0xFF,整個map item結(jié)束。前面說過,整個數(shù)據(jù)包的payload會通過參數(shù)傳給img_mgmt_upload作為實參,img_mgmt_upload的函數(shù)聲明為:
img_mgmt_upload(struct mgmt_ctxt *ctxt)
而struct mgmt_ctxt定義如下:
struct mgmt_ctxt {
struct CborEncoder encoder;
struct CborParser parser;
struct CborValue it;
};
實際上,SMP數(shù)據(jù)包payload所在的buffer地址將賦給成員變量it后面的指針(這個指針本身不屬于結(jié)構(gòu)體的一部分,但它緊挨著結(jié)構(gòu)體最后一個元素),這樣我們通過ctxt就可以間接操作SMP數(shù)據(jù)包的payload
請看如下代碼:
rc = cbor_read_object(&ctxt->it, off_attr);
這樣我們就把一個image chunk拷貝到變量:req.img_data,再通過如下代碼調(diào)用Flash訪問API。
img_mgmt_impl_write_image_data(req.off, req.img_data, action.write_bytes, last);
如前所述,每一個request命令都會有一個response,比如上面request命令的response為:
這樣,一個image chunk數(shù)據(jù)就成功寫入到Flash中,不斷循環(huán)這個request和response過程,直至整個image傳送完畢,最后主機還會發(fā)送如下兩條命令以正式結(jié)束整個DFU傳輸過程:
nrf dfu協(xié)議就是nRF5 SDK使用的DFU協(xié)議,相信很多讀者都很熟悉它。nrf dfu協(xié)議定義了兩個角色:controller和target,controller發(fā)request,target回response,一來一往,完成DFU傳輸過程。nrf dfu定義了如下request命令以及他們的response。
Request命令的格式是:Opcode + parameters,Response的格式是:60 + Opcode + parameters,比如編碼:01 02 00 10 00 00,通過上面解析可以知道它是一個創(chuàng)建數(shù)據(jù)對象命令NRF_DFU_OP_OBJECT_CREATE,而這條命令的響應是:60 01 01,可以看出也符合上面的定義。
nrf dfu用到了對象概念,什么叫對象(object)?對象分兩種:command object和data object,其中init包是command對象,而image chunk(image塊)是data對象。
我們可以進一步提煉一下,nrf dfu協(xié)議主要涉及的命令是如下幾個:
我們可以把nrf dfu流程大致歸納為如下幾步:
這里就不再對nrf dfu協(xié)議進行詳細解讀了,有興趣的讀者可以自己查閱Nordic infocenter的相關(guān)章節(jié)介紹,具體鏈接為:https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsdk_nrf5_v17.1.0%2Flib_dfu_transport.html。
在nRF connect SDK中,有一個現(xiàn)成的smp DFU例子,它所在的目錄為:zephyr\samples\subsys\mgmt\mcumgr\smp_svr,這個例子支持多種傳輸層:藍牙,串口,USB CDC,UDP,Shell,F(xiàn)S等,如果使用藍牙作為傳輸層,其升級操作步驟如下所示:
4.修改原始工程,比如廣播名字(CONFIG_BT_DEVICE_NAME="NEW_DFU"放在overlay-bt.conf中),再重新編譯,然后拷貝“build_nrf52840dk_nrf52840/zephyr/app_update.bin”到手機版nRF Connect
5.用手機nRF Connect連接設(shè)備,成功后,點擊右上角的“DFU”圖標,選擇前面的“app_update.bin”文件,然后選擇“Test and Confirm”,DFU開始
6.升級文件傳輸完畢,系統(tǒng)將重啟
7.MCUboot完成swap操作,并跳到新app,廣播將變成“NEW_DFU”
8.手機nRF Connect連接新app,并發(fā)送confirm命令
9.至此整個升級結(jié)束
除了上述的smp_svr例子,我們還做了其他smp例子,這些例子都放在GitHub這里:https://github.com/aiminhua/ncs_samples/tree/master/smp_dfu。請大家仔細閱讀例子里面的readme,并按照readme去操作。
這篇文章:詳解藍牙空中升級(BLE OTA)原理與步驟,詳細闡述了nrf dfu升級步驟說明,雖然文章是以nRF5 SDK為例來敘述的,但其步驟也適用NCS nrf dfu過程。我們在NCS中做了很多nrf dfu例子,他們都放在這里:https://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直接下載鏈接為:https://github.com/NordicSemiconductor/pc-nrfutil/releases,記得把nrfutil.exe所在目錄放在Windows環(huán)境變量中。Python方式安裝nrfutil步驟如下所示:
安裝完成后,在Windows命令行工具輸入:nrfutil version,如果可以正確顯示版本信息,說明安裝已經(jīng)成功
對于Windows用戶,nrfutil運行需要幾個特殊的DLL庫,而這幾個庫有些Windows機器是沒有的,如此,可往:https://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下面內(nèi)容直接覆蓋nrf倉庫目錄
c. 建議大家對照例子里面的readme看一下還有沒有其他準備工作
2) 進入項目目錄:cd nrf_dfu/ble_intFlash
3) 編譯:west build -b nrf52840dk_nrf52840 -d build_nrf52840dk_nrf52840 -p (根據(jù)你自己手上的板子情況,把nrf52840dk_nrf52840換成其他DK,比如nrf5340dk_nrf5340_cpuapp)
4) 燒寫:west flash -d build_nrf52840dk_nrf52840,此時設(shè)備將廣播“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連接設(shè)備,成功后,點擊右上角的“DFU”圖標,選擇前面的“ble_intFlash.zip”文件
8) 升級文件傳輸完畢,系統(tǒng)將重啟
9) MCUboot完成swap操作,并跳到新app,新app自動完成image confirm操作
10) 此時廣播已經(jīng)變成“NEW_DFU”,至此整個升級結(jié)束
https://github.com/aiminhua/ncs_samples/tree/master/nrf_dfu這個目錄下面還有很多其他nrf dfu例子,建議大家可以好好看一下,按照里面的readme文件實際操作一下,相信對MCUboot和nrf dfu理解就會更深入了。
不管是smp dfu還是nrf dfu,都存在secondary slot在內(nèi)部flash還是在外部flash情況,即ble_extFlash和ble_intFlash這兩個例子,兩個例子功能基本上一模一樣,唯一區(qū)別就是secondary slot所在位置,ble_intFlash這個例子secondary slot在內(nèi)部flash,ble_extFlash這個例子secondary slot在外部flash,這兩個例子的main.c文件一模一樣,唯一不同的是conf文件,以及分區(qū)文件partitions.yml。conf文件大家比較容易理解,但是分區(qū)文件大家經(jīng)常困惑,這里再給大家介紹一下,具體可以參考:開發(fā)你的第一個NCS(Zephyr)應用程序。
所謂分區(qū)(Partition),就是對Flash(包括內(nèi)部Flash和外部flash)或者RAM物理區(qū)域進行一個邏輯劃分,人為劃定哪塊區(qū)域干什么工作,比如把MCUboot這個image放在0x0000到0xC000這塊區(qū)域,這種分區(qū)是人為的,所以你可以隨意調(diào)整,比如你把MCUboot放在0x0000到0x10000,當然也是可以的。我們對Flash或者RAM進行分區(qū),目的就是為了把空間利用好,給各個分區(qū)一個ID以便后續(xù)引用,如果代碼里不引用這個分區(qū),那么此分區(qū)只是一個占位符而已,比如app和mcuboot這兩個分區(qū)。
我們先看一下smp_dfu/ble_intFlash這個例子生成的partitions.yml:
從上面可以看出,這個partitions.yml定義了很多分區(qū),比如app,mcuboot,mcuboot_pad,mcuboot_primary等(冒號前面的就是分區(qū)名),而且每一個分區(qū)規(guī)定了它的起始地址,結(jié)束地址,大小,相對位置以及放在什么物理存儲器上,比如app這個分區(qū):
關(guān)于分區(qū)名,只有“app”這個名字是必須有,而且是固定的,代表著主應用程序image;其他分區(qū)名,比如mcuboot,settings_storage,external_flash等,都是隨意定義的,可以修改。比如0x0~0xc000這塊內(nèi)部Flash區(qū),上面取名叫mcuboot,你也可以改成“my_boot”之類的名字,這個也沒關(guān)系的,取名字主要考慮兩點:一是能醒目標識這塊區(qū)域的功能,二是跟代碼里面的引用對起來,比如如下分區(qū)定義,經(jīng)常有人困惑:
第一個“external_flash”是分區(qū)名,第二個“external_flash”是物理存儲器名。作為分區(qū)名的“external_flash”,其實我們可以改成其他名字,以消除某些困惑,之所以使用這個名字,是因為老的littlefs例子里面對外部文件系統(tǒng)所在區(qū)域就稱為“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例子已經(jīng)把這塊區(qū)域重新命名為:littlefs_storage或者storage,所以大家可以把這塊分區(qū)名改為littlefs_storage,如下:
partitions.yml里面使用的region其實是在這個文件:nrf\cmake\partition_manager.cmake定義的,大家可以通過build目錄下的regions.yml文件得知目前定義了幾個物理存儲器:
至于partitions.yml里面使用的placement/span等,這個是用來指定各個分區(qū)的相對位置的,很多人會疑問,既然指定了分區(qū)的起始地址和結(jié)束地址,那還有必要去指定各個分區(qū)的相對位置嗎?這種情況下的確沒必要再指定相對位置了,其實這里弄反了一件事情:partitions.yml里面的地址是placement相對位置定下來之后的結(jié)果。使用placement相對位置,為編譯系統(tǒng)動態(tài)確定各個分區(qū)的位置提供了便利。如果是我們自己來劃分存儲器的分區(qū),我們就可以直接使用絕對地址的方式靜態(tài)指定各個分區(qū)的位置(當然使用placement也是可以的)。
如何人為靜態(tài)指定?答案就是把剛才動態(tài)生成的partitions.yml文件拷貝到項目根目錄下,然后改名為:pm_static.yml,然后再按照自己的需求去修改,比如smp_dfu/ble_extFlash這個例子,如果由系統(tǒng)動態(tài)生成partitions.yml文件,此時mcuboot_secondary分區(qū)所在地址為0x0~0xf0000,而文件系統(tǒng)external_flash或者littlefs_storage分區(qū)所在地址為0xf0000~0x800000,實際上很多客戶喜歡把文件系統(tǒng)放在外部Flash 0x00地址,而把secondary slot放在外部flash最后,據(jù)此可以做如下修改:
這個pm_static.yml文件沒有定義的分區(qū),還是由系統(tǒng)動態(tài)分配。有時為了后續(xù)升級方便,我們會在pm_static.yml文件里面把所有的分區(qū)都按照自己的規(guī)劃重新定義一遍,這樣就不擔心某個image突然變大而導致新的partitions.yml跟老的文件不兼容,從而無法升級。在定義pm_static.yml文件時,有如下規(guī)則必須遵守:
對于flash_primary這個region,由于系統(tǒng)默認認為必須要有一個“app”分區(qū),所以它可以存在而且只能存在一個空隙(gap),這樣系統(tǒng)默認這個gap就是“app”分區(qū)。當然你也可以把flash_primary所有區(qū)域都分好區(qū),包括“app”分區(qū)。
現(xiàn)在我們從零開始,一步一步教大家如何把smp服務添加到peripheral_uart例子中。
peripheral_uart例子所在目錄為:nrf\samples\bluetooth\peripheral_uart,這個例子跟nRF5 SDK里面的nRF5_SDK_17.1.0_ddde560\examples\ble_peripheral\ble_app_uart功能一模一樣,都實現(xiàn)了著名的NUS服務,即藍牙透傳服務。如前所述zephyr\samples\subsys\mgmt\mcumgr\smp_svr這個例子則實現(xiàn)了SMP DFU服務,我們現(xiàn)在把smp藍牙服務移植到peripheral_uart上。
我們仔細查看zephyr\samples\subsys\mgmt\mcumgr\smp_svr這個例子,為了實現(xiàn)SMP DFU,主要修改兩個地方:一是修改prj.conf以包含相應模塊,二是修改main.c的初始化函數(shù)以初始化SMP相關(guān)模塊,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的修改就更簡單,在啟動廣播之前,我們加入如下初始化函數(shù):
smp_bt_register();
os_mgmt_register_group();
img_mgmt_register_group();
就這樣兩步工作,輕輕松松就把SMP DFU服務移植到peripheral_uart上,整個代碼已經(jīng)上傳到https://github.com/aiminhua/ncs_samples/tree/master/smp_dfu/peripheral_uart,大家可以下載下來參考或者測試一下。
從上述例子我們可以看出,在NCS中移植一個例子非常方便,它不需要去添加c文件和頭文件,也不需要去修改編譯選項,還不需要去修改傳統(tǒng)的頭文件進行配置,僅僅修改conf文件和初始化函數(shù),就輕輕松松完成了整個移植,這也是NCS非常大的一個好處。
其實https://github.com/aiminhua/ncs_samples/tree/master/smp_dfu下面包含的例子都同時具備smp和nus兩個服務,并且區(qū)分各種不同情形下的DFU情況,比如secondary slot在外部Flash,通過串口傳輸image等,同時其對peripheral_uart例子進行了小小改動,以更符合某些實際應用場景,建議大家好好看一下,相信對大家理解MCUboot和SMP會幫助不少。
Nordic不僅提供設(shè)備端的DFU參考代碼,同時提供手機端的參考代碼。Nordic分別開發(fā)了Android版和iOS版的DFU庫,大家可以直接拿過來使用,集成到自己的移動端app中,這兩個庫都放在github上,其中smp dfu對應的DFU庫鏈接如下所示:
關(guān)于smp DFU庫如何集成到自己的app,可以參考Nordic如下兩個app:
而nrf dfu對應的DFU庫鏈接如下所示:
Nordic還有一個移動端app:nRF Toolbox,nRF Toolbox是代碼開源的,里面也集成了上面提到的兩種DFU庫(iOS版同時支持SMP DFU和nrf dfu,而Android版僅支持nrf dfu),大家可以參考nRF Toolbox來開發(fā)自己的移動端app。nRF Toolbox源碼也可以在github上找到:
nRF Toolbox軟件界面如下所示: