☰
PCA10056 nRF52840 DFU:三段地址、打包与串口/BLE升级排错
2026/10/30 7:18:52 网站建设 项目流程

简介:面向 nRF52840 平台(PCA10056 开发板)的 DFU 升级实践资料包,适合已有一定 Nordic 协议栈与嵌入式基础的开发者,用于打通从 bootloader 配置、固件打包到 DFU 升级验证的完整流程。压缩包共 16 个文件、约 114.35MB,包含 hex 固件与合并文件、批处理脚本、源码与密钥文件,以及 PDF 步骤说明、命令行工具安装包和压缩源码包,类型覆盖较全,便于按升级链路取用。已有 2232 人学习,说明其在同类实践资料中具有一定参考价值。读者可据此获得 DFU 操作步骤、bootloader 相关烧录文件、源码封装过程与可直接复用的批处理命令,减少环境搭建和命令拼接中的试错,适合用于方案验证、教学演示与项目移植前的流程梳理。

1. 三段地址账:为什么 PCA10056 上的 DFU 总卡在“进度条走满不生效”

手上有一块 PCA10056,要把 nRF52840 的 DFU 跑通,最容易出问题的地方通常不是协议栈配置或按键逻辑,而是“烧完 app 点不亮、复位后永远停在 bootloader”——十有八九是 bootloader settings 页没生成,或者应用工程的链接起始地址还停在 0x00000000。nRF52840 DFU 的本质是把一次固件更新拆成三段地址账:SoftDevice 占住低地址不动,Bootloader 站在中间负责收包和搬运,应用跑在 bank 0 上、随时准备被 bank 1 里的新固件替换。三段地址错一位,表现就是进度条走满、重启后版本纹丝不动。下面按 PCA10056 的工程结构走一遍:存储器切分、链接脚本改地址、nrfutil 打包、串口与 BLE 两条传输通道,最后用读回校验确认升级真的落盘。适合已经能编译 SDK 工程、但卡在第一次 DFU 联调的人。

2. 切三个地址:SoftDevice、Bootloader 与 App 在 1MB Flash 里的账本

2.1 先把存储器地图画在纸上,再动工程

nRF52840 有 1MB Flash、256KB RAM,但 DFU 用到的区域是固定的几段,谁都可以改,唯独不能改重叠。常见做法是先在纸上画一条从 0x00000000 到 0x00100000 的横线,然后按顺序切:

区域起始地址(示例)谁来定义说明
MBR + SoftDevice0x00000000SDK 里那份 softdevice hex低位固定,DFU 过程中不搬运
Bootloader0x00027000 附近bootloader 工程的链接脚本起点等于 SoftDevice 的结束地址
DFU Bank 0(当前应用)0x0004D000 附近应用工程链接脚本平时运行的固件放这里
DFU Bank 1(新固件暂存)紧跟 Bank 0bootloader 自动计算与 Bank 0 等长,双区模式核心
MBR 参数页 / Bootloader settings 页Flash 末尾两页SDK 宏定义记录 bank 有效性与固件版本

上表里的地址是“示例值”,千万别抄。正确姿势是编译完 bootloader 后打开 map 文件,搜 FLASH 段的结束地址;或者直接用nrfjprog --memrd读回芯片确认。我在第一次做 PCA10056 的 DFU 时就是照抄了别人博客里的地址,结果 Bootloader 末尾正好压在 Bank 0 的头部,应用一升级就把自己擦掉了,现象是升级完成后设备直接变砖,只能整片擦除重来。

为什么要分双区?因为 DFU 的可靠性完全建立在“旧的还能跑”这个前提上。新固件先被完整写进 Bank 1,写入过程中校验 SHA256,全部通过后才在 settings 页打一个标记,复位后 bootloader 把 Bank 1 搬到 Bank 0。中途掉电、串口拔线、BLE 断连,Bank 0 始终完好,设备还能回到老版本继续用。单区模式(NRF_DFU_SINGLE_BANK_APP_UPDATES置 1)能省下一半空间,代价是写入过程直接覆盖运行区,一旦中断就只能返厂。除非你的应用大到双区放不下,否则不要碰单区。

空间怎么核算?一条经验公式:可用应用空间 =(Bootloader settings 页起始地址 − Bank 0 起始地址)÷ 2。1MB 的 nRF52840 上,扣掉 SoftDevice 约 156KB、Bootloader 约 40KB,双区各能分到 380KB 左右。编译完应用看 map 文件里 FLASH 的 used 值,超过这个数,pkg generate时不会报错,但 DFU 传输到最后会因为写入越界而失败,报错还相当隐晦。

2.2 改链接脚本:FLASH ORIGIN 是唯一该动的数

应用工程的链接脚本通常长这样,位于pca10056/s140/armgcc/目录下,文件名以_gcc_nrf52.ld结尾。GCC 工具链下要改的就是 MEMORY 段里那两行:

MEMORY { /* Bank 0 的起点 = Bootloader 结束地址,务必与 bootloader map 文件对齐 */ FLASH (rx) : ORIGIN = 0x0004D000, LENGTH = 0x0005D000 /* RAM 起点保持 0x20000000 不要动,bootloader 跳转前已复位外设与栈 */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00040000 }

ORIGIN填错了会怎样?填小(比如还是 0x00000000),编译能过、烧录能跑,但 bootloader 认为 Bank 0 无效,复位后永远停在 DFU 模式;填大了,中断向量表偏移不对,应用一进main之前就 HardFault。LENGTH要留出 Bank 1 的空间,写满整个剩余区域是最常见的自坑做法。

逻辑说明:链接脚本决定了两件事——中断向量表被放在哪个地址、以及__isr_vector之后第一条指令的入口。Cortex-M4 的向量表在复位时从 0x00000000 读取,bootloader 跳转到应用前会做一次SCB->VTOR重定向和栈指针切换,所以应用必须把自己的向量表链接在ORIGIN上。参数说明:FLASH段的长度建议比实际编译产物多留 8KB 裕量,方便后面加日志或小功能;RAM段在 nRF52840 上就是 0x20000000 起 256KB,不要因为“bootloader 也占 RAM”就去改它,两者不会同时运行。

segmented 工程(SEGGER Embedded Studio)下改法不同:改的是Project -> Options -> Linker -> Section Placement Macros里的FLASH_START和FLASH_SIZE,或者直接改flash_placement.xml里的<MemorySegment>的start属性。两种工具链的地址必须一致,不然团队里一个人用 SES 一个人用 Makefile,编出来的包互相刷不进去,这种“玄学问题”我见过太多次,最后都是地址没对齐。

2.3 用三条命令烧出一个“可被升级”的基线

地址改完、bootloader 编译通过、dfu_public_key.c换成自己的公钥之后,第一次烧录要按下面顺序来。三条命令缺一不可,顺序也不能换:

# 1) 生成 bootloader settings 页:告诉 bootloader 当前 bank0 里有一份有效应用 nrfutil settings generate --family NRF52840 \ --application _build/nrf52840_xxaa.hex \ --application-version 1 \ --bootloader-version 1 \ --bl-settings-version 2 \ _build/settings.hex # 2) 三段合并:softdevice + bootloader + settings,一次写入 mergehex -m s140_nrf52_7.2.0_softdevice.hex \ secure_bootloader.hex \ _build/settings.hex \ -o _build/merged.hex # 3) 整片擦除后写入并校验,最后复位 nrfjprog --program _build/merged.hex --chiperase --verify -f NRF52 nrfjprog --reset -f NRF52

nrfutil settings generate的四个关键参数:--family必须是NRF52840,写错会按别的芯片的页大小生成;--application指向刚编译出来的应用 hex,它只用来读取大小和 CRC,不会复制内容;--application-version是防回滚的基准,后续 DFU 包里的版本号必须大于它;--bl-settings-version 2对应 SDK 15 之后的 settings 结构,老版本工程里填 1,填错会读出乱码版本号。

mergehex -m后面跟的多个 hex 按地址自动拼接,重叠会直接报错,这其实是好事——它帮你做了地址冲突检查。最后--chiperase会擦掉整片 Flash 包括 settings 页,所以必须一次性把 settings.hex 也写进去;如果先单独烧 softdevice + bootloader、再单独烧 settings,中间那次复位会让 bootloader 找不到有效应用,直接进 DFU 广播模式。

烧完这一遍,板子上电应该直接跑应用,按键、串口、BLE 都正常。此时才算有了一个“可以被升级”的基线。验证方法很简单:把板子断电重启三次,如果每次都进应用而不是停在 DFU,说明 settings 页没问题。

3. 打包链路:nrfutil 里七个参数决定升级包能不能被接受

3.1 私钥、公钥与那个丢了就得重烧 bootloader 的 .pem

Secure DFU 的信任链只有一条:bootloader 编译时嵌进去的公钥,必须和打包时用的私钥成对。所以第一步不是写代码,是生成密钥对:

# 生成私钥,这是整个升级链的根,丢了就只能重烧 bootloader nrfutil keys generate priv.pem # 导出公钥为 C 源码,替换 bootloader 工程里的 dfu_public_key.c nrfutil keys display --key pk --format code priv.pem --out_file dfu_public_key.c

生成的dfu_public_key.c里是一个uint8_t pk[64]数组,把它拷进examples/dfu/secure_bootloader/pca10056_ble/对应位置,覆盖 SDK 自带的示例公钥,然后重新编译 bootloader。逻辑说明:bootloader 在接收 init packet 时会用这个公钥验证签名,签名对不上直接拒绝,连传输阶段都不会进。参数说明:--format code输出 C 数组,--format hex输出十六进制字符串,后者适合贴进产线工具或文档。

血泪经验是:priv.pem一定要进密码库或者至少进 CI 的 secret 里,绝对不能只放在某个人的笔记本上。丢了之后所有在外的设备都无法再升级到用新密钥签名的固件,因为它们烧的是旧 bootloader,只认旧公钥。唯一的补救办法是让用户走一次“重烧 bootloader”的流程,相当于返厂。

3.2 pkg generate 的参数逐个对账

打包命令看着短,但每个参数错了都表现为“传输成功、重启无效”。完整写法:

nrfutil pkg generate \ --hw-version 52 \ --sd-req 0x00B6 \ --application _build/nrf52840_xxaa.hex \ --application-version 2 \ --key-file priv.pem \ _build/app_dfu.zip

逐条解释:

参数取值作用与填错后果
--hw-version52nRF52840 的硬件版本号。写错 bootloader 直接拒收,报Invalid hash或Not supported
--sd-reqSoftDevice ID要求设备上已存在的 SoftDevice 版本。可用nrfutil pkg generate --help查表,或从 SDK 的 softdevice 头文件里取
--application应用 hex待升级固件本体,会被转成 bin 并计算 SHA256
--application-version单调递增整数必须大于设备当前版本,否则 bootloader 判定为回滚并拒绝
--key-file私钥路径签名用,与 bootloader 内公钥必须配对
输出app_dfu.zip扩展名随意,但后续所有 DFU 命令都吃这个包

--sd-req是最容易出错的一个。如果你不确定当前 SoftDevice 的 ID,两个办法:一是直接跑nrfutil pkg generate --help,多数版本会打印支持的 ID 列表;二是打开 SDK 里对应 softdevice 目录下的头文件,找SD_...相关的版本宏。填0xFFFE表示不校验 SoftDevice 版本,只在自己调试、确定设备上装的就是那一版 SoftDevice 时用,量产包千万别这么干,否则用户拿到一个跨版本升级包会直接把协议栈搞坏。

--application-version也要认真对待。很多团队的做法是把它接到 git tag 上,git describe --tags之类的结果转成整数。手工改版本号迟早会忘,一旦发出去的包版本号比设备上的低,用户端就是“升级成功但版本没变”,排查半天才发现是防回滚在起作用。

3.3 拆开 zip 看 manifest.json 与 init packet

打包完成后,别急着推给设备,先自己看一眼内容:

nrfutil pkg display _build/app_dfu.zip

输出里会列出Package ID、Application version、HW version、SD requirement、Init packet contents等字段。包里实际上就三样东西:manifest.json描述包结构、一个.bin固件本体、一个.datinit packet。init packet 里塞的是固件的元信息和签名,bootloader 在接收第一个字节前就把它校验完,不合格直接掐断连接——所以“连上了但立刻断开”往往是包本身的问题,不是传输链路的问题。

这个顺序很关键:先校验元信息、再传固件、最后校验整包哈希。好处是坏包不会浪费你几十秒的传输时间;坏处是报错信息很靠前,容易被误判成“连接失败”。我一般会把pkg display的输出直接贴到发布记录里,出问题时对照一眼就知道当时打的是什么包。

还有一个常被忽略的点:pkg generate输出的 zip 里,bin 文件的命名和 manifest 里的引用是对应的,不要手工解压改文件名再压回去。改完之后 manifest 里的 CRC 对不上,传输到 90% 多才失败,白白浪费时间。

4. 触发与传输:按键进 DFU、串口推包、BLE 空口升级

4.1 GPREGRET + 复位:应用侧最省事的入口

设备已经在跑应用,怎么让它乖乖跳进 bootloader 的 DFU 模式?最通用的做法是写 GPREGRET 寄存器再复位:

#include "nrf_power.h" #include "nrf_delay.h" #include "nrf_gpio.h" #define DFU_MAGIC_VALUE 0xB1 /* 必须与 bootloader 里检查的魔数一致 */ #define DFU_BUTTON_PIN 11 /* PCA10056 上的 Button 1 */ static void dfu_enter_request(void) { /* GPREGRET 在复位后不会被清零,bootloader 启动时读它决定去留 */ nrf_power_gpregret_set(0, DFU_MAGIC_VALUE); nrf_delay_ms(10); /* 等寄存器写入稳定,避免偶发读到旧值 */ NVIC_SystemReset(); } /* 在主循环里轮询按键,低电平有效(板载按键上拉) */ void dfu_button_poll(void) { if (nrf_gpio_pin_read(DFU_BUTTON_PIN) == 0) { nrf_delay_ms(50); /* 简易消抖 */ if (nrf_gpio_pin_read(DFU_BUTTON_PIN) == 0) { dfu_enter_request(); } } }

逻辑说明:nrf_power_gpregret_set(0, x)把值写进 GPREGRET0,这个寄存器在软复位和看门狗复位后都保留,只有掉电才丢。bootloader 的nrf_dfu_enter_check()会读它,命中魔数就停在 DFU 模式不再跳应用。参数说明:魔数 0xB1 是 SDK 里的约定值,改它就必须同步改 bootloader 源码重新编译;nrf_delay_ms(10)那行不能省,我在某块板子上遇到过不加延时偶发失效的情况,属于典型的时序问题。

bootloader 侧还需要在sdk_config.h里把NRF_BL_DFU_ENTER_METHOD_GPREGRET置 1,否则它根本不看这个寄存器。另外NRF_BL_DFU_ENTER_METHOD_BUTTON是另一条路径:bootloader 自己检测按键长按,好处是应用挂了也能进 DFU,坏处是按键引脚在 bootloader 和应用里都得配置一遍。

4.2 串口 DFU 的引脚、波特率与流控

串口是最适合产线和实验室的通道,一条命令推包:

# 通过串口推送 DFU 包,-fc 0 表示关闭硬件流控 nrfutil dfu serial -pkg _build/app_dfu.zip -p COM3 -b 115200 -fc 0

参数说明:-p是串口号,Linux 下是/dev/ttyUSB0这种;-b是波特率,必须和 bootloader 里 UART 初始化配置一致;-fc 0关闭 RTS/CTS 流控。最后这个参数是翻车高发区——PCA10056 默认的 UART 引脚是 P0.06/P0.08,如果 bootloader 里没开流控而主机端开了,握手阶段就会一直卡住,nrfutil那边显示“等待设备响应”,实际上数据一个字节都没发出去。

波特率能不能往上提?可以,但要看 bootloader 里的配置是否同步改。串口 DFU 的实际吞吐受限于 UART 速率和 Flash 写入速度,115200 下推一个 300KB 的包大约需要 30 秒。想快就提到 921600,前提是 USB 转串口模块和线材撑得住,劣质线材在高速下丢字节,表现是传输到某一百分比后校验失败重试。

引脚映射也要提前确认。bootloader 工程里的 UART 引脚是独立的,和应用里的配置互不影响。如果你的板子把串口接到了别的引脚,只改应用没用,必须改 bootloader 的 UART 初始化并重烧。这一点我在第一次做定制板时踩过:应用串口日志正常,DFU 就是连不上,查了两天才发现是两套配置。

进入 DFU 模式后再执行上面那条命令。如果提示找不到设备,先确认板子确实在 DFU 模式(复位后不进应用、有对应广播或串口无输出),再确认串口号没选错——接了调试器又接了串口线的时候,枚举出三四个 COM 口是常事。

4.3 BLE DFU 的广播名、服务 UUID 与三分钟窗口

BLE 通道适合已经出货、没有物理接口的设备。进入 bootloader 后,设备会以自己的 MAC 地址为基础广播,名字通常是固定的 DFU 标识,DFU 服务 UUID 是 0xFE59(Secure DFU Service)。手机端装一个通用的 BLE DFU 工具,或者用命令行工具推包,二选一。

命令行方式大致是这样:

# 通过 BLE 推送,-ic 指定目标设备地址(从扫描结果里拿) nrfutil dfu ble -pkg _build/app_dfu.zip -ic "AA:BB:CC:DD:EE:FF" -n "DfuTarg"

-n是目标设备广播名,-ic是地址。BLE DFU 的传输速率远低于串口,300KB 的包通常要几分钟,而且对连接稳定性敏感——手机离得远一点、旁边 Wi-Fi 干扰大一点,就可能中途断开重来。

这里有个必须知道的机制:bootloader 里有个不活动超时(NRF_BL_DFU_INACTIVITY_TIMEOUT_MS,默认 3 分钟左右)。进入 DFU 模式后如果这段时间内没有开始传输,bootloader 会主动复位回应用。所以调试时不要慢悠悠地扫设备,进 DFU 之后尽快发起连接。反过来,如果传输中途断连,超时计时会重新开始,设备会留在 DFU 模式等你重连,双区模式下老固件始终完好,重来一次就行。

还有一点:BLE DFU 期间广播名和 MAC 地址的关系是“地址末字节 +1”,某些扫描工具会显示成另一个设备,别以为撞上了别人的板子。多设备同时进 DFU 时,靠地址区分,别靠名字——名字全是一样的。

5. 五个翻车现场:从打包到重启的排查清单

5.1 传输 100%、重启后版本没变

现象:nrfutil显示传输完成、校验通过,设备复位后连上去读版本号,还是旧的。

原因:九成是--application-version没有大于设备当前版本,bootloader 的防回滚逻辑直接丢弃了 Bank 1 的内容,但传输阶段不会报错,因为协议只负责把包送达。另一种可能是打包用的私钥和 bootloader 里的公钥不配对,不过这种情况通常在校验签名阶段就失败了,不会走到 100%。

解决:先执行nrfutil pkg display确认包里的版本号,再对比设备当前版本。版本号必须严格大于,不能等于。研发阶段建议在构建脚本里自动递增,不要手写。

5.2 上电永远停在 DFU 模式,进不去应用

现象:烧完 softdevice + bootloader + 应用,复位后不进应用,串口无输出或者一直广播 DFU 名。

原因:settings 页缺失或者内容不匹配。最常见的是只mergehex了 softdevice 和 bootloader,把settings.hex忘了;其次是settings.hex是在改地址之前生成的,里面的应用大小和 CRC 对不上新固件。

解决:重新执行nrfutil settings generate,参数里的--application指向当前实际烧进去的那份 hex,然后重新合并、整片擦除重烧。判断依据很简单:整片擦除后如果设备停在 DFU 模式,说明 settings 页确实是空的;如果擦除重烧后正常,说明之前就是漏了这一步。

5.3 串口 DFU 卡在连接阶段,一直没有进度

现象:命令执行后长时间无输出,或提示等待设备响应,最终超时。

原因:三个高发点——串口号选错、波特率与 bootloader 配置不一致、硬件流控开关不匹配。还有一个隐蔽的:bootloader 根本没进 DFU 模式,设备还在跑应用,应用的串口当然不响应 DFU 协议。

解决:先用串口助手以同样波特率打开端口,确认设备在 DFU 模式下有响应;再检查-fc参数与 bootloader 的流控配置是否一致;最后核对 UART 引脚定义,定制板上尤其容易错。

5.4 改了 FLASH ORIGIN,顺手把 RAM ORIGIN 也改了

现象:应用能烧进去,但一上电就 HardFault,或者运行几秒后死机。

原因:nRF52840 上 bootloader 和应用不会同时运行,bootloader 跳转前已经复位了外设和栈指针,应用的 RAM 起点保持 0x20000000 即可。把 RAM ORIGIN 往后挪,会导致启动文件里的栈顶初始化和链接脚本不一致,向量表里的初始栈指针指向非法区域。

解决:RAM 段只改 LENGTH 不改 ORIGIN,除非你非常确定自己在做什么。改完用调试器挂上去看SCB->VTOR和 MSP 的值,一眼就能看出问题。

5.5 nrfutil 版本不同,命令直接不认

现象:照着某篇教程敲命令,提示unknown command或参数不存在,比如dfu genpkg找不到。

原因:nrfutil 从 5.x 到 6.x 有一次比较大的命令重构,旧版的nrfutil dfu genpkg在新版里变成了nrfutil pkg generate,keys子命令的参数也有变化。

解决:先跑nrfutil --version看清版本,再跑nrfutil pkg generate --help看当前版本的参数列表,以本机帮助为准。团队里统一版本,最好把版本号写进构建脚本的注释里,避免“我这能跑你那不能跑”。

6. 出厂前的最后一道闸:用版本台账和读回校验确认升级生效

DFU 跑通一次不算完,能稳定复现才算。我在产线环境里的固定动作是三步:出包、核对、读回。

第一步,出包之后立刻nrfutil pkg display,把输出里的Application version、HW version、SD requirement三个字段和发布单逐字比对。这一步挡掉的是“打错包”这类低级但高频的事故——版本号少加一、hw-version 用了别的芯片的值、sd-req 填了一个设备上根本不存在的 SoftDevice ID,全都在这三行里暴露。

第二步,升级完成后不要只看界面上的“成功”提示,用调试器把 settings 页读回来:

# 读回整片 Flash 到本地文件,便于离线比对 nrfjprog --readcode _build/readback.hex -f NRF52 # 用 nrfutil 解析 settings 页,确认应用版本与 CRC nrfutil settings display _build/settings_readback.hex

--readcode把整片 1MB 读出来只要几秒,读回来的 hex 里末尾几页就是 settings 区。用nrfutil settings display解析它,能看到当前生效的 application version、bootloader version 和 bank 有效性标志。这一步是黑匣子:只要版本号对得上、bank 标记为有效,就说明新固件真的落到了 Bank 0,而不是“看起来升级成功”。

第三步,做一次断电复测。升级完成后拔掉电源,等十秒再上电,确认设备仍进应用、版本仍是新的。有些问题只在冷启动时暴露,比如 settings 页写入时掉电导致半写状态,热复位看不出来,冷启动就变砖。量产出厂前,这个动作要抽检到每一批。

版本台账我一般用一张表维护:包名、application version、bootloader version、sd-req、签名私钥指纹、构建时间。看起来是笨办法,但当你手上同时有开发板、客户样机、产线烧录器三种状态时,这张表是唯一能对得上账的东西。

从那以后我每次出 DFU 包,都强制走一遍“pkg display 截图 → 升级 → readcode 读回 → 冷启动复测”这条链,一步不省。有次赶进度跳过了读回,结果发出去的包版本号比样机低,客户那边升级完毫无变化,排查了一整天才定位到防回滚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询