从Application跳转到Bootloader中,可以采用按键(Button)触发,也可以直接用BLE命令,因此后者称为Buttonless。本文介绍Buttonless DFU服务的技术细节和使用方法。

(1)DFU服务

Buttonless DFU是一个自定义服务,它下面仅包含一个特征:

Attribute UUID Property
Secure DFU Service 0xFE59
Buttonless DFU Characteristic 0x8EC90003-F315-4F60-9FB8-838830DAEA50 Write, Indicate

Buttonless DFU特征主要职责是:从Application进入DFU Mode。

DFU Mode指芯片停驻在Bootloader中,准备或正在执行DFU相关动作。

执行这个过程,DFU Controller发送一个Enter DFU Mode的命令,该命令需要返回Response,然后从Application跳转进入Bootloader,进入DFU Mode。

在代码层面,Enter DFU Mode命令向GPREGRET寄存器写入一个标志位,然后重启。Bootloader启动时会检查该标志位。具体代码为:

uint32_t ble_dfu_buttonless_bootloader_start_finalize(void)
{
    err_code = sd_power_gpregret_clr(0, 0xffffffff);
    err_code = sd_power_gpregret_set(0, BOOTLOADER_DFU_START);
    ...
    NVIC_SystemReset();
}

(2)添加DFU服务

SDK 15提供了一个Buttonless的模板工程:ble_app_buttonless_dfu。用户可以基于该模板,开发自己的应用。

为了理解Buttonless服务,这里向ble_app_hrs工程手动添加Buttonless DFU服务。

打开<sdk>\examples\ble_peripheral\ble_app_hrs工程,确保该工程能够正常运行。

添加源文件

在工程中增加一个文件夹nRF_DFU,并添加以下文件:

  • <sdk>\components\ble\ble_services\ble_dfu\ble_dfu.c
  • <sdk>\components\ble\ble_services\ble_dfu\ble_dfu_bonded.c
  • <sdk>\components\ble\ble_services\ble_dfu\ble_dfu_unbonded.c
  • <sdk>\components\libraries\bootloader\dfu\nrf_dfu_svci.c

添加Include目录

在工程配置窗口中找到User Include Directories,添加以下路径:

  • ../../../../../../components/libraries/bootloader
  • ../../../../../../components/libraries/bootloader/ble_dfu
  • ../../../../../../components/libraries/bootloader/dfu
  • ../../../../../../components/libraries/svc

如果使用SEGGER Embedded Studio,需要额外注意这些路径末尾的空格,可能导致不被正确识别。

添加宏开关

在工程配置窗口中找到Preprocessor Definitions,添加下列项:

  • NRF_DFU_SVCI_ENABLED
  • NRF_DFU_TRANSPORT_BLE=1
  • BL_SETTINGS_ACCESS_ONLY

配置sdk_config

在sdk_config.h中,做如下更改:

  • BLE_DFU_ENABLED = 1
  • NRF_SDH_BLE_VS_UUID_COUNT += 1

添加头文件

在main.c中添加以下头文件:

  • #include “nrf_dfu_ble_svci_bond_sharing.h”
  • #include “nrf_svci_async_function.h”
  • #include “nrf_svci_async_handler.h”
  • #include “ble_dfu.h”
  • #include “nrf_power.h”
  • #include “nrf_bootloader_info.h”

添加代码

打开Buttonless示例工程的main.c,将下列函数复制到当前工程的main.c里:

static bool app_shutdown_handler(nrf_pwr_mgmt_evt_t event){};
NRF_PWR_MGMT_HANDLER_REGISTER(app_shutdown_handler, 0);
static void buttonless_dfu_sdh_state_observer(nrf_sdh_state_evt_t state, void * p_context){};
NRF_SDH_STATE_OBSERVER(m_buttonless_dfu_state_obs, 0) = {};
static void ble_dfu_evt_handler(ble_dfu_buttonless_evt_type_t event){};

添加Buttonless DFU服务

在main.c/services_init函数末尾添加DFU服务:

ble_dfu_buttonless_init_t dfus_init = {0};

// Initialize the async SVCI interface to bootloader.
err_code = ble_dfu_buttonless_async_svci_init();
APP_ERROR_CHECK(err_code);

dfus_init.evt_handler = ble_dfu_evt_handler;

err_code = ble_dfu_buttonless_init(&dfus_init);
APP_ERROR_CHECK(err_code);    

调整内存地址

按照上面步骤依次走下来,已经可以通过编译。烧录Softdevice和Application,打开串口工具,应该看到日志消息提示内存地址和内存大小需要调整,如下:

<warning> nrf_sdh_ble: Insufficient RAM allocated for the SoftDevice.
<warning> nrf_sdh_ble: Change the RAM start location from 0x20002B90 to 0x20002B98.
<warning> nrf_sdh_ble: Maximum RAM size for application is 0xD468.
<error> nrf_sdh_ble: sd_ble_enable() returned NRF_ERROR_NO_MEM.

在工程配置窗口中找到Section Placement Macro,参照日志调整RAM的起始地址和Size。

重新编译和下载工程。

此时串口工具不再提示内存问题,但是仍然提示Fatal error。这是因为Buttonless DFU服务会检测芯片中的Bootloader,如果我们没有预先烧录Bootloader,就会出现这个错误。

 

至此,为ble_app_hrs工程添加Buttonless DFU服务,代码层面工作全部结束。

(3)Bootloader Settings

直接烧录softdevice、bootloader和application,会发现application并未运行,芯片一直跑在Bootloader中。

芯片启动后先进入Bootloader,检测Bootloader Settings中的数据,如果这些数据指示Flash中有一个有效的Application,则跳转进入Application。Bootloader Settings是Flash中的一段区域,它包含了Application的大小、CRC等数据,执行DFU时也会在这里存储状态信息。

正常执行DFU时,Bootloader自动生成和维护Bootloader Settings信息。而烧录过程不同,需要手动写入。可以根据application.hex生成一个bl_settings.hex,以产生这些数据,然后烧录这个hex。

生成bl_settings.hex的命令为:

nrfutil settings generate 
    --family NRF52 
    --application app.hex 
    --application-version 2 
    --bootloader-version 2 
    --bl-settings-version 1 
    bl_settings.hex

注意–bl-settings-version只能是1,不可以是其他值。从源代码上看它应该是个预留位,没有作用。

通过命令nrfutil settings display bl_settings.hex可以查看Bootloader Settings的内容:

(4)烧录

每次我们修改应用程序代码,都会导致现有的Bootloader Settings信息失效,需要重新生成并烧录bl_settings.hex。可以利用批处理来完成生成、烧录的动作。

第一次烧录程序时,需要烧录softdevice和bootloader:

@echo off

set app=<your app.hex path>

nrfutil settings generate --family NRF52 --application %app% --application-version 2 --bootloader-version 2 --bl-settings-version 1 bl_settings.hex

nrfjprog -e
nrfjprog --program softdevice.hex
nrfjprog --program bootloader.hex
nrfjprog --program bl_settings.hex
nrfjprog --program %app%
nrfjprog --reset

pause

后续再次烧录,可以省略烧录Softdevice和Bootloader的步骤:

@echo off

set app=<your app.hex path>

nrfutil settings generate --family NRF52 --application %app% --application-version 2 --bootloader-version 2 --bl-settings-version 1 bl_settings.hex

nrfjprog --program bl_settings.hex --sectorerase
nrfjprog --program %app% --sectorerase
nrfjprog --reset

pause

nrfjprog命令中使用了--sectorerase参数,以保证只擦除并填写指定区域。

(5)调试

调试时候会频繁的修改代码,如果每次都要重新生成和下载一遍bl_settings.hex,会疯掉。

我们可以在代码中禁止Bootloader检测Bootloader Settings,让它直接跳转进入Application。

打开Bootloader工程main.c,注释掉main.c中的下面两行代码:

ret_val = nrf_bootloader_init(dfu_observer);
APP_ERROR_CHECK(ret_val);

芯片上电后,Bootloader完全不理会DFU Mode,直接进入Application。这样就如同一个没有Bootloader的工程,可以在Application中自由的调试,也无需生成bl_settings.hex。

 

(完)

Bootloader是DFU的基础设施,DFU的基本流程就是在Bootloader中接收新固件数据并写入Flash,接收完毕后重启跳转并运行新固件。

本文介绍Bootloader的相关内容。

(1)编译micro-ecc

micro-ecc是一个面向嵌入式应用的椭圆曲线签名算法(ECDSA)库,Bootloader的加密模块调用了该库,第一次运行需要先编译micro-ecc的静态库文件。

先安装以下工具,并将程序路径加入系统环境变量。

进入文件夹<sdk>\external\micro-ecc,运行build_all.bat文件,将自动下载micro-ecc的源代码,并生成静态库文件。

使用其他版本的GNU Arm Embedded Toolchain,需要修改<sdk>\components\toolchain\gcc\Makefile.windows

(2)使用公钥

使用nrfutil命令行工具,分别生成私钥和公钥:

nrfutil keys generate private_key.pem

nrfutil keys display 
    --key pk 
    --format code private_key.pem 
    --out_file dfu_public_key.c

SDK中的Bootloader工程使用了一个临时公钥,我们需要将其替换成自己的公钥。

(3)工作流程

在DFU的架构中,Bootloader充当DFU Target,手机APP等充当DFU Controller。Controller发出指令(Request),Target给出响应(Response)。

Bootloader实现了一个BLE Server,其中关键Service为:

Attribute UUID Type Property
Secure DFU Service 0xFE59 Service
DFU Control Point 8EC90001-F315-4F60-9FB8-838830DAEA50 Characteristic Notify, Write
DFU Packet 8EC90002-F315-4F60-9FB8-838830DAEA50 Characteristic Notify, Write no Response

DFU Control Point特征用于接收命令(Write)和返回响应(Notify)。

DFU Packet特征用于接收固件数据,它采用Write without Response属性,以加快接收固件速率。同时使用CRC校验保证数据的可靠性。

DFU开始时,Bootloader先接收init packet,接收完后执行验证,验证通过再接收固件数据,接收完后执行验证,验证通过则将新固件复制到Bank 0,重启运行新固件。

(4)签名校验

生成升级包时使用了私钥,私钥利用固件的Hash值生成一个签名,这个签名保存在Init Packet中。

Bootloader工程中包含公钥,利用公钥和Init Packet中的签名可以获得一个Hash值,当固件数据接收完毕,也可以计算得到一个Hash值。这两个Hash值一致则表明固件合法。

(5)工程代码

打开Bootloader工程:<sdk>\examples\dfu\secure_bootloader\pca10040_ble

在绝大多数情况下,不需要修改Bootloader工程代码。少数时候需要使用CMSIS Configurator,编辑sdk_config.h文件:

查看Bootloader的main函数,发现其内容极其简单,核心代码只有两行:

mian() {
    nrf_bootloader_init();
    nrf_bootloader_app_start();
}

nrf_bootloader_init()用来接收新固件的数据,并写入Flash。该函数中有个死循环,在固件接收完之前,不会往后运行,即使断电重启,仍然回到该函数中运行。

nrf_bootloader_app_start()用来跳转进入应用程序。

(6)烧录

如果使用Keil/IAR开发Bootloader,不可以通过IDE直接下载,需要通过nrfjprog命令行工具来下载,命令为:

nrfjprog --program bootloader.hex

 

(完)

前面文章分析了OTA的实现架构、协议和存储问题,提到了一个细节,如果使用双程序更新模式,将Flash空间分成上下两部分,如下图所示,那么最后生成的BIN文件为了放在不同的位置,需要生成两份地址不同文件,然后根据芯片的实际使用情况,选择合适的文件进行烧写。

BLE OTA memory layout

偶然看到一个信息,Nordic做双程升级时候,只需要一个二进制文件,不需要提供两个BIN文件进行选择。Nordic是怎么办到的呢?

内存布局

Nordic芯片的Flash布局图如下:

Nordic_DFU_Memory_Layout

Flash地址从下向上增长,最底层的MBR(Master Boot Record)区域存放中断向量表的位置。芯片中的Bootloader、SoftDevice和Application都具有自己的向量表,MBR的寄存器UICR.NRFFW[1]指示系统该选择哪一个中断向量表。(参考

SoftDevice区域存放协议栈。

Application区域存放用户固件程序。

APP Data区域存放用户希望永久保存的数据,比如设备地址等。这块区域在升级过程中也可以保持不被擦写。

DFU Bootloader区域存放Bootloader程序,用以执行升级过程。

更新方案

Nordic提供了三种更新方案,分别是:(参考

  1. 双程DFU
  2. 单程DFU
  3. 更新BootLoader和SoftDevice

1. 双程DFU

双程DFU方案的操作流程如下:

Dual-Bank DFU memory layout

从左侧图中可以看到Application区域空间分成了两部分:Blank 0和Blank 1。

假如当前的程序放在Blank 0区域,那么新的程序将被复制到Blank 1区域,如中间的图。

大多数芯片厂的DFU过程到这里就结束了,只要通过Bootloader引导MCU进入到新的程序地址即可。这样必然会产生本文开头提到的问题,在升级程序的时候,需要确定旧的程序放置位置。由于生成的BIN文件包含了起始地址信息,这意味着要新的程序要准备两份功能相同、地址不同的BIN文件,按照实际情况选择合适的文件烧写空白区域。

Nordic使用了一个小技巧,即将新的程序(Blank 1)复制到旧的程序空间(Blank 0),这样就是的程序基地址永远在Blank 0,从而解决了上述困难。

由于Flash赋值是一个耗时操作,DFU做起来一定会很慢。仔细观察,会发现双程DFU至少需要执行两次擦除和两次烧写,分别是:

  • 烧写Blank 1
  • 擦除Blank 0
  • 烧写Blank 0
  • 擦除Blank 1

疑问:如果在执行右侧图的Flash赋值过程中途,发生了断电,该怎么办?

我猜测应该先将Blank 1的内容复制到Blank 0,当复制完毕校验通过,再擦除Blank 1,结束DFU过程。这样即使在复制中途断电,Blank 1的内容仍然有效,重启后可以重新进行复制操作。读源码应该可以确认这一点。

2. 单程DFU

单程DFU的操作流程如下:

Single-Blank DFU memory layout

单程DFU就是现将Application区域清空,再将新的程序写入到Application空间的基地址。

这种做法好处是充分利用Application的空间,而且只需要擦除一次,烧写一次,操作时间相对于双程DFU一定会有大幅度降低。

单程DFU的缺点是不能在线更新,因为用户程序需要被擦除,所以一定需要断开连接和重启。

3. BootLoader和SoftDevice

升级BootLoader和SoftDevice的操作流程如下:

BootLoader and SoftDevice DFU memory layout

这个图乍一看比较复杂。

(1)擦除旧用户程序

(2)将新的Bootloader和SoftDevice写入Application区域

(3)将新的Bootloader和SoftDevice复制到Bootloader和SoftDevice区域

So easy? ——不是的!

并不是每个厂商的芯片都能实现这个功能,因为第(2)步到第(3)步之间,MCU要擦除Bootloader区域,有些BLE厂商的MCU,Bootloader程序是唯一的启动器,Bootloader程序自身是不可以被擦除的,否则MCU无法正常启动。Nordic的芯片存在MBR,利用MBR引导MCU跳转到Bootloader或其他地方。

程序在第(2)步结束时设置MBR的寄存器,让MCU下次启动时候不直接跳转到Bootloader区域,而是跳转到Application区域,执行一个复制操作,将新Bootloader程序和SoftDevice程序复制到各自区域,然后再设置MBR寄存器,让MCU下次重启时候直接跳转到Bootloader区域。

虽然仅仅增加了几行Flash来存放MBR信息,但是对MCU的更新提供了极大的灵活性。

(完)