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.概述

2. NCS中的Bootloader

  2.1 nRF5 SDK Bootloader

  2.2 MCUboot

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

3. DFU协议

  3.1 概述

  3.2 SMP DFU协议

    3.2.1 SMP包头和命令

    3.2.2 SMP包payload和CBOR编码

    3.2.3 SMP包详细解析示例

    3.2.4 SMP DFU流程

  3.3 nrf dfu协议

4. NCS DFU升级步骤说明

  4.1 SMP DFU升级步骤说明

  4.2 nrf_dfu升级步骤说明

  4.3 存储器分区(多image情况)

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

6 手机端DFU参考代码

 

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例子:https://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例子:https://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的相关章节介绍,具体链接为:https://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这里:https://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例子,他们都放在这里: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环境变量中。Python方式安装nrfutil步骤如下所示:

    •  安装Python,下载地址:https://www.python.org/downloads/,安装成功后请确保Windows环境变量包含Python目录 
    • 通过pip安装最新版的nrfutil,即打开Windows命令行工具CMD,输入如下命令:pip install nrfutil,即可以完成nrfutil的安装。

安装完成后,在Windows命令行工具输入:nrfutil version,如果可以正确显示版本信息,说明安装已经成功

对于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下面内容直接覆盖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”,至此整个升级结束

 

https://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上,整个代码已经上传到https://github.com/aiminhua/ncs_samples/tree/master/smp_dfu/peripheral_uart,大家可以下载下来参考或者测试一下。

从上述例子我们可以看出,在NCS中移植一个例子非常方便,它不需要去添加c文件和头文件,也不需要去修改编译选项,还不需要去修改传统的头文件进行配置,仅仅修改conf文件和初始化函数,就轻轻松松完成了整个移植,这也是NCS非常大的一个好处。

其实https://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库链接如下所示:

关于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来开发自己的移动端app。nRF Toolbox源码也可以在github上找到:

nRF Toolbox软件界面如下所示:

 

 

posted on 2022-04-01 11:08  iini  阅读(11599)  评论(5编辑  收藏  举报

导航