nrf52——DFU升级OTA升级方式详解(基于SDK开发例程)
阅读原文时间:2023年07月10日阅读:1

在我们开始前,默认你已经安装好了一些基础工具,如nrfutil,如果你没有安装过请根据官方中文博客去安装好这些基础工具,连接如下:Nordic nRF5 SDK开发环境搭建(nRF51/nRF52芯片平台) - iini - 博客园 (cnblogs.com)

本文只是对整个升级过程进行一次完整操作,以及其中可能出错的地方进行记录,如果你想知道具体原理,那么请你看官方中文博客的两篇文章,如果仅仅只是基于SDK,那么你参考第一篇即可,如果你需要在NCS上进行请参考第二篇,相信你在看了该博客后会对DFU有更深刻的理解,连接如下:详解蓝牙空中升级(BLE OTA)原理与步骤 - iini - 博客园 (cnblogs.com)  ,nRF Connect SDK(NCS)/Zephyr固件升级详解 – 重点讲述MCUboot和蓝牙空中升级 - iini - 博客园 (cnblogs.com)

整体升级流程:

1、生成秘钥

2、生成BootLoader程序(需要算法库和秘钥)

3、生成带DFU——OTA(基于蓝牙方式)的APP工程

4、合成初始的固件、并下载到相应的硬件

5、生成需要更新的APP固件

6、合成升级包固件,一般为ZIP文件。

7、利用手机端的官方APP——nrf connect连接设备并上传升级固件

特别注意,在升级时不同的芯片有许多细节问题,可能导致升级不成功,大部分都已经以错误解决的方式进行了记录(文中标红部分),如果你发现你升级不成功,请先参看这些解决方案,一般都会得到解决,如果在官方SDK没有你使用芯片的BootLoader例程,你可以参考后一篇文章(DFU升级——uart/usb方式),在其中也列举了一些关于DFU时可能遇到的错误,如果在本文中没,可以转到该文章进行参考,说不定 就解决了。

本次测试采用的第官方DK版,是兼容SDK程序的,如果你在测试时是用自己的板子,那么可能由于LED灯按键等引脚的外部电路连接不同,导致程序中相关函数的发回值不正确(在官方例程中基本所有函数都有返回值,并会对返回值进行检查,如果不成功会进入错误中断),当发生错误时,无法正常运行,那么请去注释掉相关点亮LED或者按键判断的函数,在去进行测试。

一、秘钥生成

在开发环境搭建完毕后,新建一个文件夹DFU(任意命名),在DFU中生成秘钥,打开新建文件夹后,按住Shift键2秒左右单击右键选择powershell窗口,运行下面两条命令生成私钥和秘钥(必须保存好,后期升级都要用到)。

  • 私钥生成命令:nrfutil keys generate priv.pem (priv.pem就是私钥)
  • 公钥生成命令:nrfutil keys display --key pk --format code priv.pem --out_file dfu_public_key.c (dfu_public_key.c就是公钥)

运行完毕后生成了如下两个秘钥文件:

二、算法库生成(micro-ecc算法库)——这算法库只需要生成一次就行。

官方的sdk从12.0开始,采取bootloader升级会先验证固件的签名,应用程序和bootloader的验证签名一致才能后续继续升级;反之失败。官方加入密匙验证的校验,需要添加算法校验lib来生成对应的key。

请参看如下连接:详解蓝牙空中升级(BLE OTA)原理与步骤 - iini - 博客园 (cnblogs.com)。在环境正确的前提下,我们打开SDK的如下目录:external\micro-ecc,可以看到如下文件和一个.bat的文件,我们只需要双击运行他就行。会自动生成算法库文件lib文件。

可以看到,这个文件中需要去到github上获取一个算法文件,所以需要保证你的网络良好。

(当然如果你有这个文件那么可以不用去加载,但是要把这文件放到external\micro-ecc目录下,然后可以注释掉如下图红框中的几个指令)

然后就可以运行一下build_all.bat脚本了。

有时候如果你原本使用的是SDK17,但是有需要用到其余版本SDK,如SDK15版本做DFU,这时在执行前面的过程中,发现怎么都不能在相应的工具链目录中生产.lib文件;如下图所示选择keil生成nrf52的.lib文件,在目录:external\micro-ecc\nrf52nf_keil\armgcc 下查看,运行build_all.bat并没有生如图所示.lib文件。

这个时候你可以进行如下几个操作,任选其一就行:

①、在你的cmd命令窗口中定位到external\micro-ecc目录去执行一下build_all.bat中的命令(复制粘贴运行就行),

②、或者在external\micro-ecc中按住键盘shift键后2s点击鼠标右键,在选项框中打开PowerShell窗口运行build_all.bat中的指令。

③、又或者打开build_all.bat脚本,在后面加入pause保持在运行,不要让其运行后就关闭。

运行之后,可以查看到如下报错,找不到相关工具链需要的文件,我测试的版本是缺少7 2018-q2-update文件(各位读者缺少的可能是其余文件),且查看相关报错提示的目录C:/Program Files (x86)/GNU Tools ARM Embedded下没有这个版本。

这个时候我可以去下载提示缺少的GCC编译文件,如果找不到,那么我们可以选择原本有的进行使用,那这种方式怎么做呢?

解决方式:

第一步:在编译生成.lib文件失败的SDK中的components\toolchain\gcc 目录下,有下面几个文件,由于我PC机是windows,所以我打开Makefile.windows。

第二步:更改,可以看到,这个文件确定生成.lib库时使用的gcc版本,我只要更改了和我原本SDK使用相同的版本就行,可以在相同的目录下去查看版本号,或者直接根据你的安装的版本去更改,

完成以上步骤,后直接运行build_all.bat 后在打开external\micro-ecc\nrf52nf_keil\armgcc  可以看到已经生成了.lib的库文件。

三、bootloade程序生成

把第一步生成的dfu_public_key.c替换SDK包examples\dfu路径下的自带公钥,然后打开examples\dfu\secure_bootloader目录,选择你的芯片和想要的升级方式,以及是否包括debug功能的BootLoader程序,并编译运行。芯片和SDK包对照如下表:

在打开的工程中如果编译后有下列错误出现,说明你SDK中没有算法库存在(继续去确定第二点SDK中的算法库是否有生成并存在):

关于这问题,上面的第二点说过算法库是需要生成一次,但是为什么你以前有升级成功,经过一段时间后想要再次跑升级包生成流程还是出现这个问题呢,原因可能是你上使用SDK包和这个SDK包可能不一样了,你下载了新版本的SDK包,或者从新解压过一个SDK包覆盖了原本那个。这时你需要从新去生成算法库文件。

如果编译没有报错,那么我们就找到生成的.hex文件,把它放到DFU文件夹中,重命名一下,可以直接复制粘贴脚本生成相应文件,也可以自己定义,记住更改名字后在执行脚本命令名字要对应。

四、APP程序生成

编译application代码。这一步如果仅仅是进行测试:那么我们可以直接用例程中的带有DFU的蓝牙例程进行测试,但是实际开发中如果有自己的工程,那么就需要去移植DFU功能,移植方式看官方中文博客,路径如下:详解蓝牙空中升级(BLE OTA)原理与步骤 - iini - 博客园 (cnblogs.com)

如果仅仅只是测试那么在SDK中有一个专门的例程是已经添加了DFU蓝牙服务工程,直接编译例程即可,但是在其中只有52810,52811,52832,52840的工程的例程:

SDK根目录 \examples\ble_peripheral\ble_app_buttonless_dfu\pca10040\s132\arm5_no_packs,将生成的hex文件改名为:app.hex   ,放入DFU文件夹。

如你使用的芯片不是这几个型号,那么就没有测试例程,只能自己添加例程,下面我就以52820为例进行例程添加(参资料考:详解蓝牙空中升级(BLE OTA)原理与步骤 - iini - 博客园 (cnblogs.com) )

1)、选择例程:由于一般开发都是基于从机的透传例程进行开发所以我们选项目录examples\ble_peripheral\ble_app_uart下的工程进行添加,这个例程下的板子也是最全的

2)、在NUS(透传例程)添加一个DFU服务

  第一步:.c文件和.h头文件路径 添加

  在工程中新建一个DFU文件,添加四个.c文件(路径:components\ble\ble_services\ble_dfu 和 components\libraries\bootloader\dfu)

  添加如下四个头文件路径:

  

  第二步:添加全局宏定义

DFU_ADD DEBUG BL_SETTINGS_ACCESS_ONLY NRF_DFU_SVCI_ENABLED NRF_DFU_TRANSPORT_BLE=1

  添加后如图所示:

  

3)、 修改sdk_config.h文件

  第一步:使能DFU模块,就是修改sdk_config.h中的宏定义  

#define BLE_DFU_ENABLED 1
#define NRF_DFU_BLE_BUTTONLESS_SUPPORTS_BONDS 0

  第二步:由于我们需要添加一个服务,所以ATT table size也要变大,同时增加一个DUF的UUID,原本只有NUS服务,所以UUID为1,增加一个变为2

#define NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE 1600
#define NRF_SDH_BLE_VS_UUID_COUNT 2

  添加修改如下图:绿色框处为修改后的值。

  

  第三步:预留足够的RAM空间给蓝牙服务建立运行

  由于我们添加了一个服务,增大了ATT table size的大小,那么原本预留个蓝牙服务建立运行的RAM空间就不够啦,所以我们要修改APP在RAM的运行起始地址(增大),在RAM前面流程足够的    RAM空间给到蓝牙服务进行缓冲,同时需要注意的是每一块芯片的RAM是不同的,需要在设置时需要根据芯片的RAM大小去对应Size的参数,如下为nrf52820的修改

  

  由于nrf52820的RAM为8000在图中红框柱部分相加为8000,如果正在实际使用中RTT报错为存储空间不够(报错打印为4)如4.3所示的错误,那也请来调整第二步和第三步的大小。很大概率就解决了。

4)、代码添加

  第一步:头文件添加,在main.c中加入头文件

#ifdef DUF_ADD

#include "ble_dfu.h"
#include "nrf_bootloader_info.h"
#include "nrf_power.h"
#endif

  第二步:然后在services_init()中添加ble dfu服务

#ifdef DUF_ADD
ble_dfu_buttonless_init_t dfus_init = {0};
dfus_init.evt_handler = ble_dfu_evt_handler;
err_code = ble_dfu_buttonless_init(&dfus_init);
APP_ERROR_CHECK(err_code);
#endif

  添加后如下图所示,(ble_dfu_evt_handler为DFU回调函数)

  

  并在函数前添加如下代码(在DFU回调中你还可以加入一些你需要的其余处理):

#ifdef DUF_ADD
1 static void disconnect(uint16_t conn_handle, void * p_context)
2 {
3 UNUSED_PARAMETER(p_context);
4
5 ret_code_t err_code = sd_ble_gap_disconnect(conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
6 if (err_code != NRF_SUCCESS)
7 {
8 NRF_LOG_WARNING("Failed to disconnect connection. Connection handle: %d Error: %d", conn_handle, err_code);
9 }
10 else
11 {
12 NRF_LOG_DEBUG("Disconnected connection handle %d", conn_handle);
13 }
14 }
15
16 static void advertising_config_get(ble_adv_modes_config_t * p_config)
17 {
18 memset(p_config, 0, sizeof(ble_adv_modes_config_t));
19
20 p_config->ble_adv_fast_enabled = true;
21 p_config->ble_adv_fast_interval = APP_ADV_INTERVAL;
22 p_config->ble_adv_fast_timeout = APP_ADV_DURATION;
23 }
24
25 static void ble_dfu_evt_handler(ble_dfu_buttonless_evt_type_t event)
26 {
27 switch (event)
28 {
29 case BLE_DFU_EVT_BOOTLOADER_ENTER_PREPARE:
30 {
31 NRF_LOG_INFO("Device is preparing to enter bootloader mode.");
32
33 // Prevent device from advertising on disconnect.
34 ble_adv_modes_config_t config;
35 advertising_config_get(&config);
36 config.ble_adv_on_disconnect_disabled = true;
37 ble_advertising_modes_config_set(&m_advertising, &config);
38
39 // Disconnect all other bonded devices that currently are connected.
40 // This is required to receive a service changed indication
41 // on bootup after a successful (or aborted) Device Firmware Update.
42 uint32_t conn_count = ble_conn_state_for_each_connected(disconnect, NULL);
43 NRF_LOG_INFO("Disconnected %d links.", conn_count);
44 break;
45 }
46
47 case BLE_DFU_EVT_BOOTLOADER_ENTER:
48 // YOUR_JOB: Write app-specific unwritten data to FLASH, control finalization of this
49 // by delaying reset by reporting false in app_shutdown_handler
50 NRF_LOG_INFO("Device will enter bootloader mode.");
51 break;
52
53 case BLE_DFU_EVT_BOOTLOADER_ENTER_FAILED:
54 NRF_LOG_ERROR("Request to enter bootloader mode failed asynchroneously.");
55 // YOUR_JOB: Take corrective measures to resolve the issue
56 // like calling APP_ERROR_CHECK to reset the device.
57 break;
58
59 case BLE_DFU_EVT_RESPONSE_SEND_ERROR:
60 NRF_LOG_ERROR("Request to send a response to client failed.");
61 // YOUR_JOB: Take corrective measures to resolve the issue
62 // like calling APP_ERROR_CHECK to reset the device.
63 APP_ERROR_CHECK(false);
64 break;
65
66 default:
67 NRF_LOG_ERROR("Unknown event from ble_dfu_buttonless.");
68 break;
69 }
70 }
#endif

  在main()函数的最前面加入修改BootLoader广播名字的代码,(由于iOS DFU的时候默认就会去改广播名字,为了兼容iOS):

#if DFU_ADD
uint32_t err_code;
err_code = ble_dfu_buttonless_async_svci_init();
APP_ERROR_CHECK(err_code);
#endif

  添加完成,我们编译生成hex文件,将生成的hex文件改名为:app.hex   ,放入DFU文件夹。

实际工程添加中,你可能会出现出现无法运行的情况,那是由于我以52820为例添加的DFU例程,但是如果你采用其余芯片,可能导致留给协议栈的RAM空间并不够,所以你在发现第6步烧写原始固件后无法找到蓝牙广播,那请打开你的RTT,看是否有提示你RAM空间不够的错误,如果有该提示,请按照4.2中的方式去调整RAM的大小,如提示有2BA8,增大到2BB8。特别注意,如果没有RTT打印,请多次连接一下,我们的官方例程都是自带RTT打印的,如果没有,可能没有连接正确。

五、生成BootLoader settings page

采用官方博客中说的版本2方式生成settings.hex文件。命令如下:(如果文件中没有按照脚本中的方式命名,请根据自己生成的文件名进行更改)

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

六、原始固件烧写

这一步就是我们一个项目开发完成后,把要写到出厂产品的中固件烧写到芯片中,后期如果有需求需要去做APP升级,那么就可以通过DFU的方式升级了。

如中文博客中的方式一样运行后续的命令:

将上文生成的3个hex文件和softdevice hex文件merge成一个文件,然后通过nrfjprog或者nRF Connect桌面版进行烧写,相关命令如后续所示:

6.1.1、复制一个协议栈文件(softdevice hex)到DFU文件目录下,我们可以打开SDK如下目录:components\softdevice 这里有各种版本的协议栈,当然我上面编译的APP工程协议栈栈为112的6.1.1版本,如下图所示,那么就打开找到相同名的hex文件复制到DFU目录下。

这时我们的DFU目录下有了以下这些文件:

开始合并hex文件,脚本命令:

mergehex --merge bootloader.hex settings.hex --output bl_temp.hex
mergehex --merge bl_temp.hex app.hex s112_nrf52_6.1.1_softdevice.hex --output whole.hex

烧写hex文件命令(以nrfjprog为例),也可以用nRF connect(如果没有nRF connect请参看中文博客开发环境搭建篇进行环境搭建):

nrfjprog --eraseall -f NRF52
nrfjprog --program whole.hex --verify -f NRF52
nrfjprog --reset -f NRF52

在使用一些版本的SDK进行DFU升级时,如果在合成安装包后,在烧写阶段出现如下错误(以nrf52810为例):

命令行方式:数据位域有效区域外,或者有效区域没有数据。

J-flash方式:不符合所选闪存扇区或不符合程序目标

然后查看生成的whole.hex文件,发现BootLoader的数据在地址0007 F000开始的地址,如下图所示,但是对于有些芯片(如nrf52810)flash只到60000,显然对于该芯片这个地址已经超过了FLASH的是存储容量,所以在烧写的时候会报错,那是因为合成的最终HEX固件已经超过了flash的容量。

问题解决:

在生成settings.hex文件时,更改一下生成命令:

原来的生成命令如下(执行后,默认开始地址为nrf52832的地址),也就是0x0007F000:

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

但是我们工程中BootLoader的地址为0x0002F000(对于nrf52810)

针对于这个问题,只要修改一下生成BootLoader文件的指令即可:如下为nrf52810的,那如果是其余系列的芯片有相关问题,解决方式一样,在生成BootLoader时把默认的NRF52,改为NRF52xxx即可。

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

命令执行后可以看到执行情况如下,开始地址已经变为和工程中一样适用于nrf52810的地址,然后在去下载就不会报错了。

针对以上这类问题,如你发现在下载时使用6.2所列出的烧写命令居然不能进行烧写,那么你可以尝试把nrf52改为nrf52xxx(对应的芯片型号)进行测试,说不定就烧写成功了。

特别注意:针对该问题总结一下替换命令:

setting.hex生成命令

对应芯片型号

NRF51

nrf51系列芯片

NRF52810

nrf52810

NRF52QFAB

nrf52820

NRF52

nrf52832、nrf52833

NRF52840

nrf52840

如果你自己打板,在经过上面的步骤后发现固件无法运行,手机设备搜索不到APP中设置的名字,只能搜索到BootLoader中的名字,那说明程序没有正确跳转到APP端,这样就算升级后,也依然无法跳转APP执行,这是由于BootLoader程序中选择了需要按键确认跳转的方式,而你自己的硬件并没有设计该按键,导致程序一开始执行,就由于GPIO口电平不正确,错误判断直接进去到DFU模式(程序运行在BootLoader中),无法正确跳转到APP,去执行正确的升级流程。

解决方式:在编译BootLoader时,更改如下宏定义——取消勾选,这样就可以正确跳转APP,进行升级了。

6.4 下载固件到芯片后可以用nRF connect手机版搜索到我们的设备,连接后有如图所示的界面,可以看到右上角有一个DFU的图标,说明我们的原始固件具有了DUF(OTA方式)功能。

七、固件升级

如果要进行新固件的升级,那么就需要准备好一个.zip升级包,然后在利用手机APP进行升级。那这个.zip如何进行生成呢?下面我们就来一步一步的的进行,

7.1.1、生成新的APP程序

因为我这是做测试,就只改变一下设备的名字,把名字改为“Nordic_DFU_NEW”,然后编译工程,找到新生成的HEX文件复制到我们的DFU文件夹,重命名为app_new.hex;

7.1.2、确定协议栈版本号

在SDK的components\softdevice目录下,有各个版本协议栈的资料,我们根据自己使用的版本来选择确定值,我使用的是s112的版本,所以在如下目录下components\softdevice\s112\doc有一个PDF文档,我们打开该文档,可以看到如图所示的协议栈版本号:

所以在如下命令中我使用的协议栈版本号,要设置为00B8,所以在开发自己的DFU例程时,要根据使用的协议栈去确定生成.zip 升级包命令中的协助栈版本号是什么,如果出错,你 会发现,不管怎么升级,你的新固件都是无法升级成功并运行的。

7.1.3、升级包合成

升级包生成命令如下,请修改版本号为你使用的协议栈版本号:

nrfutil pkg generate --application app_new.hex --application-version 2 --hw-version 52 --sd-req 0x00B8 --key-file priv.pem app_s112_new.zip

运行命令后你会发现在DFU文件夹中生成了一个app_s112_new.zip压缩包,这就是我们要通过手机APP发给我们设备的升级包

先把7.1步骤生成的固件包上传个手机,然后打开手机APP,我使用的是nordic官方APP(nrf connect),然后连接好设备然后点击DFU的升级标识。

然后在接下来的界面选择ZIP升级方式,然后点击OK。

然后在手机中找到发个手机的ZIP升级压缩包并确定:

接下来你会看到我们的升级界面如下,这里注意一点,升级过程中会从新进行连接,连接后为BootLoader的名称,既如图显示的DFUTARG,请切换为原来的蓝牙NORDIC——UART才能看到升级包传输界面:

升级完成后,复位一下设备,我们重新在手机APP上搜索一下,可以发现蓝牙广播名已经变为我们新固件的名字了:

到此,升级完毕!

7.3、错误解决

有些时候会出现这种情况,在1到6的过程都没有问题,但是还是升级失败,那可能是你APP版本太低,那请去nordic官网下载最新的APP重新安装一下,之后在去重复升级流程。