PCA10056 nRF52840 DFU:三段地址、打包与串口/BLE升级排错
简介面向 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 NRF52nrfutil 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 的 .pemSecure 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 是 0xFE59Secure 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 generatekeys子命令的参数也有变化。解决先跑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 读回 → 冷启动复测”这条链一步不省。有次赶进度跳过了读回结果发出去的包版本号比样机低客户那边升级完毫无变化排查了一整天才定位到防回滚。希望帮到你。本文还有配套的精品资源点击获取