nRF52840 DFU固件升级:分区、打包与三通道排错
简介面向nRF52840开发者的DFU升级实践资料包基于PCA10056开发板整理适合已具备一定Nordic SDK与蓝牙基础、需要完成固件空中升级的嵌入式工程师。内容围绕DFU流程组织包含升级步骤说明文档、bootloader相关十六进制文件、SoftDevice与应用固件合并文件、公私钥及C源码并配有封装好的批处理脚本和命令行工具安装程序可减少手工拼接与烧录环节的试错成本。压缩包共16个文件以hex、bat、zip、pdf、exe、key和c源码为主整体约114.35MB结构上便于按文档、固件、脚本和源码分块查阅。已有2232人学习适合用于搭建DFU验证环境、理解bootloader配置、密钥生成与固件合并打包也可作为项目升级流程的参考素材。1. 手里只有一块 PCA10056nRF52840 DFU 到底在升级什么手上这块 PCA10056 插上 USB 跑个例程很轻松但真到产品阶段用户不可能拿着调试器去升级固件——nRF52840 DFU 解决的就是这件事让设备自己通过蓝牙、串口或 USB 收下新固件再把它搬进 Flash。第一次做的人多半卡在打包hex 明明能烧转成 DFU 包却提示对象非法或者包升级到 100% 成功复位后却停在 bootloader 里再也跑不回应用。这块板子的 1MB Flash 里挤着 MBR、SoftDevice、应用和 bootloader 四段内容DFU 每次只动其中一段而决定成败的是地址表、密钥、版本号这三样东西任何一样对不上就翻车。适合已经把 nRF5 SDK 工程跑起来、想把升级通道打通的人。2. 先把地址算清楚四段镜像在 1 MB Flash 里怎么排做 DFU 最容易犯的错是上来就抄命令生成 zip。命令本身不长但每个参数背后都挂着一个地址或一个 ID抄错一位后面所有调试都是白费力气。所以这一章先把地图画出来再谈打包。2.1 分区表一张表把 MBR、SoftDevice、应用和 bootloader 钉死nRF52840 的 Flash 从 0x00000000 开始四段镜像按顺序往上排。下面这张表是我在 S140 v7 系列 SoftDevice 上量出来的换 SoftDevice 版本必须重新算别直接照抄区域起始地址大小说明MBR0x000000000x1000固化引导负责查找 bootloader 和跳转SoftDevice0x000010000x26000协议栈本体版本不同大小不同应用0x00027000视工程而定应用工程链接脚本里的 FLASH_START应用可增长区0x00027000 size余量DFU 双 bank 时要留出同等大小Bootloader0x000F80000x6000DFU 服务、搬运逻辑、校验逻辑MBR 参数页0x000FE0000x1000记录 bootloader 是否存在及入口Bootloader settings0x000FF0000x1000应用 CRC、应用大小、版本号判断依据只有两个bootloader 工程里的flash_placement.xml和应用工程链接脚本里的FLASH_START。这两个值必须和上表一致否则 DFU 写完数据后跳转过去就是一片空白表现出来就是“升级成功但不开机”。同理RAM 起始地址也随 SoftDevice 变化改了 Flash 布局却没改 RAM会在协议栈初始化时直接挂死这种问题用调试器单步都很难看出来属于典型的玄学现场。还有一点DFU 的双 bank 机制要求 bank1 的容量不小于 bank0也就是应用的可用空间实际上只有一半。有人把应用做到接近 bootloader 起始地址编译能过、直接烧写也能跑一旦走 DFU 就报资源不足。把应用大小控制在 bootloader 起始地址之前一半以内是最省心的做法。2.2 密钥与 init packet验签这一关是怎么过的Secure DFU 的“Secure”全部压在一对公私钥上。私钥留在你电脑里用来给升级包签名公钥编译进 bootloader设备端拿它验签。公钥默认放在 bootloader 工程的dfu_public_key.c里出厂前必须换成自己的否则谁都能给你设备刷固件。# 生成私钥一整条产品线只生成一次丢了就只能靠调试器救砖 nrfutil keys generate private.pem # 导出公钥 C 数组覆盖 bootloader 工程里的 dfu_public_key.c nrfutil keys display --key pk --format code private.pem --out_file dfu_public_key.c # 确认私钥内容确实写到了当前目录而不是临时路径 nrfutil keys display --key sk --format hex private.pem这三条命令里第一条是根后两条都是派生物。private.pem一旦被覆盖已经出货的设备会拒绝所有新包因为设备里烧的是旧公钥验签必然失败。这是我在项目里见过最贵的教训之一现在团队里统一要求私钥进密码库dfu_public_key.c进版本库两边做交叉核对。每个 DFU 包里除了固件本体还带一个 init packet里面装着固件版本号、硬件版本、SoftDevice 依赖 ID 和固件哈希最后整体签名。bootloader 收到包之后的校验顺序大致是签名 → 硬件版本 → SoftDevice 版本 → 固件版本不能低于当前除非强制→ 哈希。任何一环不过包都不会被写进 bank1。--sd-req这个参数就是 SoftDevice 依赖 ID。它要求包里的应用必须运行在指定的 SoftDevice 上值写错了设备会直接判定固件不匹配。这个值没有推算公式只能查 SDK 文档里的 SoftDevice firmware ID 表按你实际使用的版本对号入座如果应用压根不跑协议栈传0x00可以让 bootloader 跳过这项校验。2.3 打包实操从 hex 到 DFU 包的四条命令工作目录里通常有四类文件应用编译出来的app.hex、bootloader 的bootloader.hex、SoftDevice 的s140_*.hex以及你自己生成的settings.hex和private.pem。先把应用打成一个可下发的 zip# 应用升级包。hw-version 52 是 nRF52840 的硬件版本号 # application-version 必须大于设备里现有的值否则会被版本校验挡下 nrfutil pkg generate \ --hw-version 52 \ --sd-req 0x00B6 \ --application app.hex \ --application-version 2 \ --key-file private.pem \ app_v2_dfu.zip再生成 bootloader settings 页。这一步很多教程一句带过但它决定了设备复位后到底跑应用还是停在 bootloader# settings.hex 里记录应用大小、应用 CRC32、应用版本、bootloader 版本 nrfutil settings generate \ --family NRF52840 \ --application app.hex \ --application-version 2 \ --bootloader-version 1 \ --bl-settings-version 1 \ settings.hex注意--application app.hex必须是最终要烧进去的那一份。换了应用却没重新生成 settingsCRC 就对不上设备会认为应用无效。最后把几段拼成一张整包 hex产线和实验室都用它避免漏烧# 先合并 bootloader 侧三件套 mergehex -m s140_nrf52_softdevice.hex bootloader.hex settings.hex -o bootloader_all.hex # 再和应用合并成一张出厂整包 mergehex -m bootloader_all.hex app.hex -o full_production.hex合并顺序不影响结果但合并的内容影响结果full_production.hex里如果少了 settings 页第一次上电设备就会停在 DFU 模式等升级。生成的 zip 包里只有应用不包含 bootloader 和 SoftDevice这一点必须记牢——DFU 升级的是应用不是整片 Flash。3. 烧写与首次启动bootloader、settings 页和应用 CRC 的配合打包完成只是准备真正的第一次成功启动要靠烧写顺序和 settings 页的配合。这一章按“烧什么、怎么烧、怎么确认烧对了”三步走。3.1 烧写顺序与整包烧写四段镜像之间是有依赖的MBR 通过 MBR 参数页找 bootloaderbootloader 通过自己的链接地址找 SoftDevice 和应用应用通过链接地址找 SoftDevice。所以最稳的方式不是分开烧而是一次烧整包# 整片擦除 烧写 校验一条命令搞定 nrfjprog -f NRF52 --program full_production.hex --chiperase --verify --reset--chiperase会先擦掉整片 Flash包括 MBR 参数页和 settings 页然后按 hex 里的地址逐段写入。这条命令比“先擦后烧再复位”的手工流程可靠得多原因是擦除和写入之间如果断电MBR 参数页会处于半写状态设备可能既不进 bootloader 也不进应用只能重新接线救。如果非要分段烧顺序建议是 SoftDevice → bootloader → settings → 应用全部烧完再复位。中途不要复位因为复位会触发 MBR 扫描此时参数页还没写好扫描结果不可预期。我一般会在脚本里加一条--reset放在整串命令最后中间过程不加。3.2 settings 页里到底存了什么settings 页就 4KB但它是 bootloader 判断“应用是否可信”的唯一依据。核心字段是几个bank0 的应用大小、bank0 的 CRC32、应用版本号、bootloader 版本号双 bank 场景下还有 bank1 的对应字段。bootloader 每次启动都会重新算一遍应用区的 CRC32和 settings 页里的值比对一致才跳转。这解释了两类高频故障。第一类直接烧了app.hex但没烧settings.hex或者 settings 是用旧版应用生成的CRC 对不上设备会老老实实停在 DFU 模式。第二类用调试器单独烧应用做调试烧完发现设备不进应用了——因为调试器不会自动更新 settings 页。做应用调试时要么每次重生成 settings要么干脆在调试配置里把 settings 一起带上。提示--bl-settings-version选 1 还是 2 取决于 bootloader 的编译配置两者结构不同。版本不匹配时 bootloader 会判定 settings 无效表现同样是停在 DFU 模式排查时先确认这个参数。3.3 用指示灯和日志确认已经进入 DFU 模式设备有三种方式进入 bootloader 的 DFU 状态上电时应用无效、按键强制进入、应用里通过 buttonless 服务主动跳转。前两种不依赖应用代码调试阶段优先用按键。PCA10056 上默认把 Button 4 配成 DFU 入口。按住它再按复位松开后设备不再运行应用而是进入 DFU 广播和串口监听。此时板载指示灯会呈现固定的闪烁节奏广播名默认是DfuTarg用手机的 BLE 调试工具扫一下就能看到。如果灯不闪、广播也扫不到八成是 bootloader 根本没烧进去或者 MBR 参数页里没有登记它。更直接的判断方式是串口日志。bootloader 工程编译时把日志打开接上串口就能看到类似“应用校验失败”“进入 DFU 等待”这类输出比盯着灯猜快得多。buttonless 方式则要额外注意应用里需要注册 DFU 服务并在触发时切换到 bootloader多数实现还要求设备已经和主机完成绑定否则跳转请求会被拒绝。这一块配置项分散在应用和 bootloader 两边是踩坑密度最高的地方之一。4. 三条升级通道BLE、UART、USB CDC 的参数与实测差异同一份 DFU 包可以走蓝牙、串口或 USB 送进去三条通道的体验差别很大。选型时别只看速率还要看现场有没有物理接触条件、bootloader 里有没有编译对应传输层。4.1 BLE 通道拿开发板当桥先烧桥接固件用命令行走 BLE DFU 有个前提你的电脑没有蓝牙协议栈直连能力得让一块开发板充当桥。这块桥板要烧专用的桥接固件烧完之后它通过 USB 虚拟串口接收命令通过蓝牙把数据转发给目标设备。# -p 是桥板枚举出来的串口-n 是目标设备的广播名 nrfutil dfu ble -pkg app_v2_dfu.zip -p COM5 -n MyDevice参数里最容易搞错的是-p它指的是桥板的串口不是目标设备的。目标设备只通过-n指定的广播名被识别。如果现场有多台同名设备命令会连到随机一台所以量产环境一定要给每台设备写入唯一名称。速率方面默认 ATT MTU 是 23 字节单包有效载荷只有 20 字节实际吞吐大概 12 KB/s一个 200KB 的应用要跑两三分钟。协商到更大的 MTU 后能提升到 78 KB/s但能不能协商成功取决于手机或桥接端属于不可控因素。另一个隐性限制是连接间隔和从机延迟如果应用在跑低功耗广播参数DFU 期间可能频繁断连。直接用手机升级更简单装个支持 DFU 服务的调试工具选中 zip 包下发即可。缺点是排错信息少失败了只给你一个错误码得回来对着日志猜。4.2 UART 通道只管波特率和流控串口通道的参数最少出问题的点也最少是实验室阶段最推荐的调试通道。PCA10056 上默认的 UART 引脚是 P0.06 作 TXD、P0.08 作 RXD波特率 1152008 数据位、无校验、1 停止位。# 串口 DFU端口填目标设备自己枚举的串口 nrfutil dfu serial -pkg app_v2_dfu.zip -p COM7 -b 115200-b必须和 bootloader 里配置的波特率一致。想提速可以改成 230400 或 460800但两边都得改只改一边会得到一堆乱码和超时。流控默认关闭长距离或高波特率场景建议在 bootloader 里打开硬件流控否则丢包会导致重传反而更慢。还有一个容易忽略的点串口通道要求 bootloader 编译时启用了 UART 传输。如果 bootloader 是默认配置只开了 BLE串口这边发什么都没反应现象是命令一直阻塞在“等待响应”。4.3 USB CDC 通道换个 USB 口就换了个串口nRF52840 自带全速 USB 控制器PCA10056 上直接插即可。这里的 DFU 走的是 CDC ACM 类也就是虚拟串口不是标准 DFU 类所以你在设备管理器里看到的是“USB 串行设备”不是“DFU 设备”。# USB CDC DFU端口号每次插拔都可能变 nrfutil dfu usb-serial -pkg app_v2_dfu.zip -p COM9三个实测注意点。第一端口号会变插不同的 USB 口、换不同的线枚举出来的 COM 号都不一样脚本里不要写死。第二Windows 需要 CDC 驱动新系统自带老系统可能要手动装。第三也是最多人栽的有些 USB 线只有电源线没有数据线插上去设备能亮灯但主机完全枚举不到串口。这种问题不看设备管理器根本发现不了。速率上 USB 通道最快实测能到几十 KB/s一个几百 KB 的应用几秒钟就完。代价是 bootloader 必须编译 USB 传输层而这会额外占用几 KB Flash在小容量型号上要掂量。4.4 三条通道怎么选通道典型速率前置条件适用场景BLE18 KB/sbootloader 编 BLE 传输命令行方式还需桥板已出货设备、无物理接口UART510 KB/sbootloader 编 UART 传输引脚与波特率对齐实验室调试、产线工装USB CDC数十 KB/sbootloader 编 USB 传输数据线合规有 USB 口的成品、快速迭代我的习惯是开发阶段全走 UART因为报错信息最完整产线走 USB 或调试器整包烧写现场升级才用 BLE。5. 排错现场五类 DFU 失败的定位顺序DFU 的报错信息很不友好一个错误码背后可能是五个原因。下面这五条是我实际遇到频率最高的按“现象 → 原因 → 解决”写照着顺序排查能省不少时间。5.1 包被拒先看版本号和依赖 ID现象打包命令执行成功下发到 0.x% 就中断设备返回对象非法或操作失败手机端只显示一句“升级失败”。原因最常见的是--application-version没比设备里现有的大。bootloader 默认拒绝同版本和低版本防止误刷回滚。其次是--sd-req与设备里 SoftDevice 的实际 ID 不一致init packet 里的依赖项直接对不上。解决先从设备侧读出当前应用版本串口日志或调试器读 settings 页把版本号加一重打再把--sd-req对着 SDK 文档的 SoftDevice firmware ID 表核对一遍。两个都确认无误还失败就检查包是不是用另一把私钥签的——公钥和私钥不是一对验签必然不过。5.2 升级成功但起不来settings 页和链接地址现象进度条走到 100%设备复位后既不广播也不响应串口指示灯保持某个固定状态。原因两个方向。一是 settings 页里的应用 CRC 与真正写入的应用不匹配bootloader 计算后判定应用无效停在 DFU 模式。二是应用工程的FLASH_START与分区表不一致DFU 把固件写到了 A 地址bootloader 却从 B 地址跳转。解决先确认应用 hex 的起始地址与分区表一致再确认 settings.hex 是用同一份 app.hex 生成的。改完任何一处都要重新生成、重新烧写不要图省事复用旧文件。5.3 卡在 0%连接被别人占着现象BLE 通道命令发出后一直停在“正在连接”或者连上了但进度不动。原因目标设备已经被手机或另一台主机连着广播也停了或者应用里的 buttonless 跳转没生效设备压根没进 DFU 广播命令行方式下还可能是桥板没烧桥接固件。解决断开所有已连接的中心设备按住 DFU 按键复位强制进入 bootloader用扫描工具确认能看到DfuTarg广播。桥板方案先确认桥板固件烧写正确、串口能通。5.4 找不到串口线和驱动现象nrfutil dfu usb-serial报端口不存在或者设备管理器里端口一闪就消失。原因充电线没有数据线芯bootloader 没编译 USB 传输层主机缺少 CDC 驱动或者设备根本没进入 DFU 模式USB 外设没被初始化。解决换一根确认能传数据的线观察设备管理器是否有新设备出现确认 bootloader 编译配置里 USB 传输已打开必要时手动指定驱动。端口一闪就消失通常是供电或枚举失败优先怀疑线材。5.5 升级中途断电别急着换包现象升级到一半掉电或被人拔了线重新上电后设备不进应用也不正常广播。原因双 bank 机制下bank1 处于写了一部分的状态bootloader 判定升级未完成会停在 DFU 模式等待续传。解决重新下发同一个包不要重新打包。DFU 支持断点续传校验通过后才会切换 bank。如果换了包哈希变了之前写的部分全部作废等于从头再来。6. 批量与回滚把 DFU 包纳入产线脚本的一条底线单台调通之后真正的麻烦才刚开始一批设备要升级版本要能追溯出了问题要能说清是哪个包。我现在的做法是把打包和下发全部脚本化包名里带版本号目录里带校验文件。#!/usr/bin/env bash set -euo pipefail VER${1:?用法: ./flash_all.sh 2} PKGdist/app_v${VER}_dfu.zip # 包不存在就直接退出避免拿旧包刷出一批不一致的设备 [ -f ${PKG} ] || { echo 缺少 ${PKG}; exit 1; } for port in /dev/ttyACM*; do echo 正在升级 ${port} # 单台失败不中断整批只记录回头单独处理 nrfutil dfu serial -pkg ${PKG} -p ${port} -b 115200 || echo 失败: ${port} done脚本里两个细节值得说。set -euo pipefail让未定义变量和管道错误都能暴露出来循环里对单台失败做兜底是为了避免一台设备没接好导致后面全部跳过。包本身也要留指纹我通常在出包脚本末尾追加一段import hashlib, pathlib # 为每个升级包生成 sha256 摘要和版本号一起写进发布记录 for pkg in sorted(pathlib.Path(dist).glob(*_dfu.zip)): digest hashlib.sha256(pkg.read_bytes()).hexdigest() print(f{pkg.name}\t{digest[:16]}\t{pkg.stat().st_size})版本号、依赖 ID、摘要三样对上才算一个可追溯的包。回滚这件事得提前想清楚。bootloader 默认拒绝低版本固件真要退回旧版只能把版本号抬高再打一个“假新版本”包这会污染版本序列后续排查会被自己坑。比较稳妥的做法是出厂时预留一个功能最简的恢复应用任何异常都能刷回去。还有一个边界bootloader 自身也能通过 DFU 升级但那属于另一条路径包的类型不同失败之后设备连 DFU 都进不去只能靠调试器。所以 bootloader 的改动我从来不放进常规升级流程一律走有线烧写。从那以后我每次出包都会强制走一遍“版本号 依赖 ID 摘要”的三项对账再让测试同事碰硬件。希望帮到你。本文还有配套的精品资源点击获取