ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于nRF Connect SDK的MCUboot与蓝牙DFU固件升级实战指南

基于nRF Connect SDK的MCUboot与蓝牙DFU固件升级实战指南 1. 项目概述为什么固件升级是嵌入式产品的“生命线”在嵌入式产品特别是基于蓝牙等无线连接的物联网设备开发中固件升级功能早已不是“锦上添花”而是“生死攸关”的必备能力。想象一下你的智能手环上市后发现了一个导致耗电剧增的Bug或者你的智能门锁需要增加一个新的开锁协议如果没有可靠的固件升级机制唯一的办法可能就是召回产品这对任何公司都是灾难性的。因此实现一套安全、可靠、用户无感的固件升级方案是产品从原型走向市场、并具备长期生命力的核心保障。nRF Connect SDK (NCS) 作为 Nordic Semiconductor 官方推出的、基于 Zephyr RTOS 的开发框架为 Nordic nRF52/nRF53/nRF91 系列芯片提供了强大的蓝牙、Thread、Matter 等无线协议栈支持。而在 NCS 的生态中MCUboot和蓝牙空中升级正是构建这套升级体系的两大基石。MCUboot 负责底层固件的安全引导、验证和切换而蓝牙空中升级则提供了无线更新的通道。很多人刚开始接触时容易把这两者混为一谈或者认为配置好了 MCUboot 就自动拥有了空中升级能力这其实是一个常见的误解。本文将从一个一线开发者的视角深入拆解在 NCS/Zephyr 环境下如何将 MCUboot 与蓝牙空中升级有机结合构建一个完整的、可投入生产的固件升级方案并重点分享那些官方文档不会写的配置细节和实战踩坑经验。2. MCUboot 深度解析不只是个 BootloaderMCUboot 是一个开源的、安全的第二级 Bootloader它已经成为 Zephyr 乃至整个嵌入式 IoT 领域的事实标准。在 NCS 中它被深度集成但其工作原理和配置选项依然有大量值得深究的细节。2.1 MCUboot 的核心工作流程与安全机制MCUboot 的核心职责是确保设备每次启动时运行的固件是经过验证的、完整的、且未被篡改的。它通常采用“A/B 双分区”或“交换升级”策略。以最经典的 A/B 分区又称“直接 XIP”模式为例其内存布局和流程如下分区布局Flash 被划分为几个关键区域。Bootloader 分区存放 MCUboot 自身代码。主应用程序分区 (slot0)设备正常运行时使用的固件。次应用程序分区 (slot1)用于存放通过空中升级或其他方式下载的新固件。暂存分区 (scratch)在某些升级模式如交换升级中用作临时交换区。其他分区可能包括文件系统、设置存储区等。启动验证流程设备上电后首先运行 MCUboot。MCUboot 检查slot0中的固件。它会使用预置的公钥或哈希对固件的签名进行验证。如果验证通过则跳转到slot0执行启动成功。如果slot0验证失败例如固件损坏MCUboot 会尝试检查slot1。如果slot1中有已验证的固件MCUboot 可能会尝试将其复制到slot0并启动实现故障恢复。升级流程新固件通过蓝牙被下载并写入slot1。下载完成后应用程序或升级服务会设置一个特定的标志通常是一个bootloader库中的函数调用或写入一个特定的 Flash 区域告知 MCUbootslot1中有待升级的镜像。设备下一次重启时MCUboot 会检测到这个标志。它会验证slot1中的新固件。验证成功后MCUboot 执行固件切换操作。在“直接 XIP”模式下这可能仅仅是更新一个指向活动分区的指针在“交换升级”模式下则需要将slot0和slot1的内容进行交换。切换成功后MCUboot 清除升级标志并跳转到新的活动固件现在是原来的slot1运行。注意这里最容易出错的地方是升级标志的管理。标志必须在固件完全、正确写入slot1后才能设置。如果在下载中途发生断电而标志已被设置MCUboot 在下次启动时可能会尝试验证一个不完整的镜像导致启动失败。因此标志的设置必须是升级过程的最后一步原子操作。2.2 在 NCS 中配置 MCUboot关键 Kconfig 与 DTS 修改NCS 通过 Kconfig 和 Device Tree (DTS) 来配置 MCUboot这是与裸机开发或其它 SDK 最大的不同也是灵活性的来源。首先启用 MCUboot。在你的项目目录如app下的prj.conf文件中必须添加CONFIG_BOOTLOADER_MCUBOOTy这一行会引入一整套 MCUboot 相关的默认配置。其次调整分区表。这是最核心的一步。NCS 使用 DTS 来定义内存分区。你需要修改boards/目录下对应你开发板的.overlay文件例如boards/arm/thingy53_nrf5340/thingy53_nrf5340_cpuapp.overlay或者在你的项目根目录创建app.overlay。关键是要重新划分 Flash为slot0,slot1和bootloader分配空间。一个典型的修改示例如下以 nRF52840 为例需根据实际 Flash 大小调整/ { chosen { zephyr,code-partition slot0_partition; }; soc { flash-controller4001e000 { flash0: flash0 { partitions { compatible fixed-partitions; #address-cells 1; #size-cells 1; /* Bootloader 分区大小需足够存放 MCUboot */ boot_partition: partition0 { label mcuboot; reg 0x00000000 0x0000C000; /* 48KB */ }; /* 存储升级标志、交换状态等信息 */ storage_partition: partitionc000 { label storage; reg 0x0000C000 0x00004000; /* 16KB */ }; /* 主应用程序分区 (slot0) */ slot0_partition: partition10000 { label image-0; reg 0x00010000 0x00060000; /* 384KB */ }; /* 次应用程序分区 (slot1) */ slot1_partition: partition70000 { label image-1; reg 0x00070000 0x00060000; /* 384KB */ }; /* 暂存分区 (scratch)交换升级模式需要 */ scratch_partition: partitiond0000 { label image-scratch; reg 0x000d0000 0x00020000; /* 128KB */ }; /* 后续可以是文件系统分区等 */ storage_partition: partitionf0000 { label storage; reg 0x000f0000 0x00008000; }; }; }; }; }; };关键点解析zephyr,code-partition这个chosen节点告诉 Zephyr 编译系统你的应用程序链接时应该以哪个分区为基准。在启用 MCUboot 后这应该指向slot0_partition。分区大小boot_partition的大小必须足够容纳你编译出的 MCUboot 镜像mcuboot.hex。你可以先给一个估计值如 48KB编译后通过west build -t rom_report查看mcuboot的实际大小来调整。slot0和slot1的大小必须一致且要能放下你的应用程序。地址对齐分区的起始地址和大小最好与 Flash 的擦除扇区如 4KB对齐避免后续操作复杂化。最后关键的 Kconfig 选项。在child_image/mcuboot.conf文件中如果没有则创建你可以对 MCUBoot 镜像本身进行配置# 启用对硬件加密加速的支持如果芯片支持 CONFIG_BOOT_USE_MBEDTLSy CONFIG_MBEDTLS_AES_Cy CONFIG_MBEDTLS_CTR_DRBG_Cy # 选择升级模式直接 XIP 还是交换升级 CONFIG_BOOT_SWAP_USING_MOVEy # 使用直接 XIP (Move) 模式 # CONFIG_BOOT_SWAP_USING_SCRATCHy # 使用交换升级 (Scratch) 模式 # 启用串口日志输出调试时非常有用 CONFIG_BOOT_SERIAL_CDC_ACMy CONFIG_BOOT_SERIAL_UARTy CONFIG_MCUBOOT_SERIALy # 签名算法必须与生成固件签名时使用的算法一致 CONFIG_BOOT_SIGNATURE_TYPE_RSAy CONFIG_BOOT_SIGNATURE_TYPE_RSA_LEN2048 # 或者使用 ECDSA # CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256y经验之谈对于资源相对紧张的 nRF52 系列我强烈推荐使用BOOT_SWAP_USING_MOVE直接 XIP模式。交换升级模式需要额外的scratch分区并且在切换时会进行全分区拷贝耗时更长断电风险窗口也更大。直接 XIP 模式只是更新一个指向活动 slot 的指针速度快可靠性更高。3. 蓝牙空中升级的实现DFU 服务与协议栈集成MCUboot 准备好了“房间”分区和“保安”验证蓝牙空中升级则是负责把“新住户”新固件安全送进房间的“快递员”。在 NCS 中这个“快递员”主要是通过蓝牙 LE 的 DFU 服务来实现的。3.1 NCS 中的 DFU 服务选择与集成NCS 提供了两种主流的 DFU 服务实现你需要根据产品形态和协议要求进行选择Nordic 的 SMP (Simple Management Protocol) 服务路径zephyr/subsys/mgmt/mcumgr/smp_bt。特点这是 MCUboot 项目官方推荐的配套协议与 MCUboot 集成度最高。它基于自定义的 GATT 服务使用 CBOR 编码格式传输命令和数据。功能强大不仅支持固件升级还支持文件系统管理、日志收集、设置读写等通过mcumgr命令行工具。启用方法在应用的prj.conf中添加CONFIG_MCUMGRy CONFIG_MCUMGR_SMP_BTy CONFIG_MCUMGR_CMD_IMG_MGMTy CONFIG_MCUMGR_CMD_OS_MGMTy CONFIG_MCUMGR_BUF_SIZE1024优点标准化功能全面有成熟的桌面端工具 (mcumgr) 和手机端库支持。缺点协议稍重需要额外的 GATT 服务可能增加功耗和代码体积。Zephyr 自带的 DFU 服务路径zephyr/subsys/bluetooth/services/ots配合dfu示例。特点这是一个更轻量级的实现有时直接被称为BT DFU。它利用蓝牙 Object Transfer Service (OTS) 来传输固件二进制文件。客户端如手机App将固件文件作为一个“对象”写入设备的 OTS 服务。启用方法通常需要参考samples/dfu示例进行配置。优点相对轻量直接利用标准 OTS 服务概念简单。缺点功能相对单一主要是文件传输升级逻辑如触发 MCUboot需要开发者自己实现更多控制逻辑。我的选择建议对于大多数需要投入量产的产品我推荐使用 SMP 服务。原因有三第一它与 MCUboot 是“原配”状态同步如下载进度、升级确认更完善第二mcumgr工具链成熟调试和自动化测试都非常方便第三除了升级其额外的设备管理功能在产品后期运维中可能会派上大用场。3.2 集成 SMP DFU 服务的详细步骤与代码剖析假设你选择 SMP 服务以下是将其集成到现有蓝牙应用中的关键步骤。第一步配置项目。如前所述在prj.conf中启用 SMP 相关配置。此外为了支持固件镜像管理还必须启用 Flash 操作和镜像管理命令CONFIG_FLASHy CONFIG_FLASH_MAPy CONFIG_STREAM_FLASHy CONFIG_IMG_MANAGERy第二步初始化 SMP 传输层。在你的主应用程序中通常是main.c需要在蓝牙连接就绪后初始化 SMP。一个典型的模式是#include zephyr/mgmt/mcumgr/smp_bt.h void main(void) { int err; /* 初始化你的硬件和外设 ... */ /* 初始化蓝牙 */ err bt_enable(NULL); if (err) { printk(Bluetooth init failed (err %d)\n, err); return; } /* 广播配置 ... */ start_advertising(); /* 初始化 SMP over Bluetooth */ smp_bt_register(); /* 你的主循环 ... */ while (1) { k_sleep(K_SECONDS(1)); } }smp_bt_register()这个函数会向 Zephyr 的 MCUmgr 子系统注册蓝牙传输层之后当手机端的mcumgr工具连接上并发送命令时就能被正确处理。第三步处理升级确认。这是确保升级体验流畅的关键。当新固件通过 SMP 完全下载到slot1后MCUboot 的镜像管理模块会将其标记为“待定”。设备需要重启才能应用。但直接重启用户体验不好。更好的做法是让应用程序在收到升级完成的指令后通知用户例如通过 LED 闪烁或手机 App 提示然后等待用户确认或在空闲时自动重启。 你可以监听MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK或MGMT_EVT_OP_IMG_MGMT_STATE等事件或者更简单点在mcumgr命令行执行image confirm后设备会在下次启动时固定使用新镜像。应用程序可以在升级流程的最后主动调用boot_request_upgrade()并安排重启。#include zephyr/dfu/mcuboot.h /* 在某个处理升级完成的函数中 */ if (升级文件已验证且写入完成) { /* 告知 MCUbootslot1 中的镜像是有效的请求升级 */ int ret boot_request_upgrade(BOOT_UPGRADE_PERMANENT); if (ret 0) { printk(升级已确认将在下次重启后生效。\n); /* 可以在这里通知用户然后延迟重启 */ k_work_schedule(reboot_work, K_SECONDS(5)); } }第四步编译与签名。这是另一个大坑。为了 MCUboot 能验证你的固件应用程序镜像必须经过签名。NCS 使用imgtool.py来完成这项工作。你需要在编译后对生成的zephyr.hex合并了应用和 SoftDevice进行签名。 通常这通过 CMake 自动完成。但你必须提供签名密钥。最安全的方式是在构建时生成或指定密钥。# 生成一对新的 RSA-2048 密钥对 west build -b your_board -- -DMCUBOOT_SIGNATURE_KEY_FILE\path/to/your-rsa-2048.pem\构建系统会自动调用imgtool对输出进行签名。请务必保管好你的私钥 (*.pem)丢失后将无法生成能被现有 Bootloader 验证的新固件。4. 实战全流程从编译到手机端升级测试让我们串联起所有步骤完成一次从零开始的完整空中升级实战。4.1 环境准备与镜像构建准备密钥在项目根目录创建一个keys文件夹并生成密钥。mkdir -p keys cd keys # 使用 imgtool 生成 RSA 密钥 python3 -m imgtool keygen -k rsa-2048.pem -t rsa-2048配置项目确保你的prj.conf、app.overlay和child_image/mcuboot.conf都已按前述章节正确配置。编译 BootloaderMCUboot 需要单独编译。# 在项目根目录 west build -b nrf52840dk_nrf52840 -p auto -d build/mcuboot -- -DOVERLAY_CONFIGoverlay-bt.conf -DBOOTLOADER_MCUBOOT1 -DMCUBOOT_SIGNATURE_KEY_FILE\$(pwd)/keys/rsa-2048.pem\ west flash -d build/mcuboot注意overlay-bt.conf需要你创建里面包含CONFIG_BOOT_SERIAL_UARTy等调试配置。编译并签名应用程序west build -b nrf52840dk_nrf52840 -p auto -- -DMCUBOOT_SIGNATURE_KEY_FILE\$(pwd)/keys/rsa-2048.pem\ # 生成的已签名合并镜像在 build/zephyr/merged.hex west flash --hex-file build/zephyr/merged.hex4.2 使用 mcumgr 命令行工具进行升级测试这是开发阶段最常用的测试方法。首先在电脑上安装mcumgr。# macOS brew install mcumgr # Linux (Go 环境) go install github.com/apache/mynewt-mcumgr-cli/mcumgrlatest连接设备确保设备在广播并且 SMP 服务已初始化。使用mcumgr扫描并连接。mcumgr conn add bt typeble connstringpeer_nameYourDeviceName # 或者使用 peer_id mcumgr conn show mcumgr -c bt conn查看当前镜像状态mcumgr -c bt image list这会显示slot0和slot1的哈希、版本等信息。上传新固件mcumgr -c bt image upload build/zephyr/app_update.binapp_update.bin是专门用于升级的二进制格式通常由imgtool从.hex文件转换而来或者由构建系统直接生成在build/zephyr/下寻找zephyr.signed.bin或类似文件。上传过程会有进度显示。确认并测试新镜像# 再次查看状态会看到 slot1 中有一个 pending 的镜像 mcumgr -c bt image list # 设置新镜像为下次启动的镜像 mcumgr -c bt image confirm # 重启设备 mcumgr -c bt reset设备重启后MCUboot 会验证并引导到新的固件。再次连接并使用image list查看会发现新镜像已经运行在slot0并且被标记为“已确认”。4.3 手机端 App 集成升级功能对于最终产品你需要一个手机 AppiOS/Android来发起升级。核心是集成一个支持 SMP 协议的蓝牙库。Android可以使用 Nordic 官方提供的Android BLE Library其中包含了Dfu模块。或者使用开源库如mcumgr-ios的 Android 移植版。你需要实现发现并连接设备。发现SMP服务UUID:8D53DC1D-1DB7-4CD3-868B-8A527460AA84及其特征。将imgtool生成的.bin文件分块通过SMP特征写入。监听通知特征解析mcumgr响应处理进度和错误。发送image confirm和reset命令。iOS可以使用 Nordic 的iOS DFU Library它同样支持 SMP。集成步骤与 Android 类似。由于 iOS 对后台蓝牙操作限制更严需要特别注意状态保存和重连逻辑。实战心得在手机端实现时最大的挑战是传输稳定性。蓝牙连接可能中断手机可能进入后台。你必须实现健壮的重试机制、断点续传SMP 协议本身支持和进度持久化。一个简单的策略是将固件文件分成若干小块如 512 字节每成功写入一块就在本地记录进度。如果传输中断重新连接后先查询设备端镜像状态对比本地进度然后从断点处继续发送。5. 生产部署与版本管理进阶考量当你的产品从实验室走向生产线固件升级方案需要考虑更多工程化因素。5.1 生成可工厂烧录的完整镜像生产线烧录的不是我们开发时分开的 Bootloader 和 App而是一个包含 Bootloader 和已签名应用程序的单一 HEX 文件。在 NCS 中可以使用west命令生成它west build -b your_board -- -DMCUBOOT_SIGNATURE_KEY_FILE\keys/rsa-2048.pem\然后在build目录下你需要将mcuboot.hex和已签名的应用镜像merged.hex合并。可以使用nrfjprog或mergehex工具mergehex -m build/mcuboot/zephyr/zephyr.hex build/zephyr/merged.hex -o combined_factory_image.hex这个combined_factory_image.hex就是交给生产线的文件。务必在烧录后测试设备能否正常启动并进入应用程序。5.2 版本管理与回滚策略版本号在prj.conf中设置CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION格式如1.2.3。这个版本号会被编译进镜像头部mcumgr image list可以查看。建议将版本号与你的 Git Tag 关联。自动版本递增可以在 CMakeLists.txt 中通过脚本自动生成版本号避免手动修改出错。回滚策略MCUboot 支持回滚。如果新固件启动失败比如连续启动失败 N 次MCUboot 可以自动回滚到上一个已知良好的版本。通过配置CONFIG_BOOT_UPGRADE_ONLY可以禁用回滚仅允许升级CONFIG_BOOT_SWAP_USING_MOVE和CONFIG_BOOT_IMG_ERASE_PROGRESSIVELY等选项会影响回滚行为。对于高可靠性要求的产品建议启用回滚并仔细测试断电场景。5.3 安全强化防回滚与硬件加密防回滚为了防止攻击者用旧版本固件可能存在已知漏洞替换新版本需要启用防回滚。在child_image/mcuboot.conf中设置CONFIG_BOOT_UPGRADE_ONLYy并配合版本号检查。MCUboot 会拒绝烧写版本号不大于当前运行版本的固件。硬件加密如果使用 nRF5340 或 nRF9160 等带有 CryptoCell 的芯片务必在mcuboot.conf中启用CONFIG_BOOT_USE_MBEDTLS和CONFIG_CC3XX_BACKENDy等选项让签名验证和加密操作由硬件加速大幅提升安全性和速度。6. 深度排坑那些我踩过的“坑”与解决方案即使按照指南一步步操作在实际项目中依然会遇到各种诡异问题。这里分享几个让我耗费大量调试时间的典型案例。6.1 分区表配置错误导致的启动失败现象编译一切正常烧录后设备毫无反应连 Bootloader 的串口日志都没有。排查首先检查 MCUboot 是否被正确烧录。使用nrfjprog --readregs查看 PC 指针或者用调试器单步。如果 MCUboot 已运行但无法跳转十有八九是分区表问题。使用west build -t rom_report和west build -t ram_report查看编译出的应用程序实际占用的 ROM 和 RAM 大小。对比app.overlay中定义的slot0分区大小。常见错误是应用程序实际大小超过了slot0分区定义的大小。MCUboot 验证镜像时会检查镜像头部声明的尺寸是否超出分区边界如果超出则直接判定为无效。另一个隐蔽的坑是地址不对齐。确保分区的起始地址是 Flash 页大小如 0x1000的整数倍。非对齐的分区虽然可能编译通过但在 MCUboot 执行擦写操作时会导致硬件错误。解决方案仔细计算分区大小留足余量通常为应用程序大小的 120%。使用rom_report的输出作为依据。确保地址对齐。6.2 签名密钥不匹配导致验证失败现象空中升级上传固件成功但重启后设备无法启动或者通过mcumgr image list看到新镜像状态为 “pending” 且哈希值旁边有 “invalid” 标记。排查确认你用于签名应用程序的私钥rsa-2048.pem与编译MCUboot时通过-DMCUBOOT_SIGNATURE_KEY_FILE指定的公钥是配对的。一个极易出错的地方你可能有多个项目或测试分支不小心混用了不同的密钥对。或者构建脚本中指向的密钥文件路径错误。检查 MCUboot 的编译输出确认它包含了正确的公钥。可以尝试用imgtool验证imgtool verify -k keys/rsa-2048.pem build/zephyr/zephyr.signed.bin解决方案建立严格的密钥管理流程。为每个产品线使用独立的密钥对并在 CI/CD 系统中妥善保管私钥。在项目的README.md中明确记录使用的是哪对密钥。构建脚本中采用绝对路径或通过环境变量引用密钥。6.3 蓝牙 MTU 与 SMP 缓冲区大小不足现象升级小固件正常但升级大固件100KB时传输极其缓慢甚至中途失败。排查蓝牙 ATT_MTU 决定了单次数据传输的最大容量。默认可能是 23 字节减去协议开销后有效载荷很小。SMP 协议本身的缓冲区大小也有限制由CONFIG_MCUMGR_BUF_SIZE定义。如果单个 SMP 帧超过这个大小会被拆包效率降低。使用btmon或手机端的 BLE 调试工具观察实际传输的包大小。解决方案协商更大的 MTU在应用程序中启用并请求更大的 MTU。CONFIG_BT_GATT_AUTO_UPDATE_MTUy /* 或者在连接后调用 bt_gatt_exchange_mtu() */增大 SMP 缓冲区在prj.conf中适当增加CONFIG_MCUMGR_BUF_SIZE例如 2048但要注意 RAM 消耗。优化手机端确保手机端 BLE 库也请求了大的 MTU并且以合适的块大小如MCUMGR_BUF_SIZE - 协议头发送数据。6.4 升级后应用程序无法连接蓝牙现象升级完成设备重启新固件运行正常但手机 App 再也搜不到或连不上设备了。排查最可能的原因是蓝牙地址或配对信息丢失。在 nRF 芯片中蓝牙识别信息和配对绑定信息通常存储在 Flash 的一个保留区域与应用程序分区独立。如果你的分区表重新布局不小心覆盖了 Flash 中存储蓝牙信息的位置就会导致此问题。在 nRF5 SDK 中这通常是UICR或 Flash 末尾的某个区域。在 NCS/Zephyr 中这由settings子系统管理其存储位置由 DTS 中的storage分区定义。检查你的app.overlay确保为settings子系统或任何你用来存储持久化信息的分区分配了独立且固定的空间并且在升级前后这个分区的地址没有变化。解决方案在 DTS 中明确定义一个专用于settings的分区并确保它在 MCUboot 的slot0/slot1分区之外不会被升级过程擦写。例如flash0 { partitions { ... /* 专门用于存储蓝牙配对等设置信息 */ settings_storage: partitionf8000 { label settings_storage; reg 0x000f8000 0x00008000; }; }; };然后在prj.conf中配置CONFIG_SETTINGS_NVSy和CONFIG_NVS_STORAGE_PARTITIONsettings_storage。这样无论应用程序如何升级蓝牙配对信息都能得到保留。
返回列表