NCS下基于MCUboot的双通道DFU实现:BLE与UART固件升级实战 📅 发布时间:2026/8/19 4:21:58 👁 浏览次数: 1. 项目背景与核心需求为什么要在NCS下做双通道DFU如果你正在用Nordic的nRF52或nRF53系列芯片做产品并且跑的是基于Zephyr RTOS的NCSnRF Connect SDK那么“固件升级”这个功能迟早会摆在你面前。这不是一个“有就更好”的锦上添花而是一个“没有就麻烦”的刚需。想象一下产品已经部署到成百上千个终端你发现了一个必须修复的Bug或者需要增加一个吸引用户的新功能。难道要派人一个个去现场拆机、用J-Link烧录吗显然不现实。这时候无线、远程的固件升级DFU Device Firmware Update能力就成了产品生命线的保障。Nordic的生态提供了强大的DFU支持但在NCS这个相对较新的框架下很多朋友会感到迷茫。官方文档虽然全面但信息分散社区方案五花八门但未必适合你的具体场景。特别是当你的设备同时具备蓝牙和串口比如通过USB转TTL或RS-232时你自然会想能不能让用户自己选择用手机AppBLE还是电脑上位机UART来升级这就是“BLE加UART”双通道DFU的核心价值——提供灵活、可靠的升级路径提升用户体验和产品可维护性。我最近在一个工业传感器项目上就实现了这个需求。设备安装在难以触及的角落通过BLE连接手机进行常规配置和数据查看很方便但当固件包较大超过1MB时BLE的传输速率和稳定性就成了瓶颈。同时设备也预留了一个调试UART接口。于是我们设计了一套方案平时小版本更新用BLE快速便捷大版本更新或BLE连接不稳定时则可以用电脑通过UART进行速度更快、更可靠。这套基于NCS-Zephyr的方案跑下来非常稳定今天我就把其中的核心设计、实操步骤以及我踩过的那些坑毫无保留地分享出来。2. NCS DFU基础架构深度解析MCUboot与镜像管理在动手写代码之前我们必须彻底理解NCS下DFU的基石——MCUboot。它不是Nordic独有的而是一个来自Zephyr社区的、经过工业级验证的通用引导加载程序Bootloader。你可以把它想象成电脑的BIOS但它更智能专门负责验证和启动你的应用程序固件并管理固件升级。2.1 MCUboot的核心工作流程MCUboot遵循“A/B双镜像”或“直接XIP就地执行”的升级策略。在资源相对紧张的嵌入式设备上我们通常使用交换模式Swap Mode。我们以nRF528401MB Flash为例看看Flash是如何被划分的Flash区域起始地址示例大小用途说明MCUboot0x0000 000048KB引导程序本身。负责初始化、镜像验证、升级逻辑。Image-0 (插槽0, Primary)0x0000 C000448KB存放当前正在运行或下次将要运行的固件镜像。Image-1 (插槽1, Secondary)0x0007 C000448KB存放通过DFU接收到的新固件镜像。升级时MCUboot会将此镜像与插槽0的镜像交换。暂存区 (Scratch)0x000F 80004KB一个很小的区域用于在交换镜像时临时存储元数据确保掉电安全。整个升级过程是这样的初始状态设备运行在插槽0的固件App。下载新固件你的App通过BLE或UART将新的.bin或.hex文件写入到插槽1。注意MCUboot和App都“知道”插槽1的地址下载过程由App控制。触发升级下载完成后App需要向MCUboot“打报告”。它通过一个特定的API如boot_request_upgrade或直接设置Flash中的升级标志位告诉MCUboot“插槽1里有个新家伙下次启动你处理一下。”重启与交换设备重启。MCUboot在启动时检查到升级标志开始执行交换操作将插槽0的旧镜像与插槽1的新镜像进行交换实际是交换镜像的“位置”信息并非物理搬运全部数据效率很高。如果交换成功它清除标志然后从新的插槽0即原来的新固件启动。确认与回滚新固件启动后应该尽快调用boot_write_img_confirmed()来“确认”本次升级成功。如果新固件启动失败比如卡死在某个地方无法发出确认MCUboot会在下一次重启时自动执行“回滚”换回之前稳定运行的旧版本。这是MCUboot提供的最重要的安全特性之一。2.2 在NCS中配置MCUboot理解了原理配置就有的放矢了。NCS通过Kconfig和设备树DTS来管理这些配置这比直接修改代码要清晰和安全得多。首先在你的项目目录如app下你需要确保prj.conf文件启用了MCUboot和必要的功能# 启用MCUboot引导程序 CONFIG_BOOTLOADER_MCUBOOTy # 启用对固件镜像的签名验证强烈建议生产环境开启 CONFIG_MCUBOOT_SIGNATURE_KEY_FILE\root-rsa-2048.pem\ # CONFIG_MCUBOOT_ENCRYPTION_KEY_FILE\enc-rsa-2048.pem\ # 如果需要加密可启用 # 启用串口控制台方便MCUboot打印调试信息 CONFIG_SERIALy CONFIG_UART_CONSOLEy CONFIG_CONSOLEy # 启用Flash操作相关的驱动 CONFIG_FLASHy CONFIG_FLASH_PAGE_LAYOUTy CONFIG_STREAM_FLASHy CONFIG_IMG_MANAGERy注意MCUBOOT_SIGNATURE_KEY_FILE指向你的RSA私钥文件。你需要使用imgtoolNCS自带生成一对密钥私钥用于在编译时对固件签名公钥会被编译进MCUboot。MCUboot在启动时会用公钥验证镜像签名只有签名正确的镜像才会被启动这从根本上防止了恶意固件的刷入。其次Flash的分区布局是在设备树中定义的。你通常不需要从头写NCS为Nordic芯片提供了预定义的分区。但你必须理解并检查它。查看ncs_root/zephyr/boards/arm/your_board/your_board.dts文件找到类似下面的部分flash0 { partitions { compatible fixed-partitions; #address-cells 1; #size-cells 1; boot_partition: partition0 { label mcuboot; reg 0x00000000 0x0000C000; // 48KB }; slot0_partition: partitionc000 { label image-0; reg 0x0000C000 0x00070000; // 448KB }; slot1_partition: partition7c000 { label image-1; reg 0x0007C000 0x00070000; // 448KB }; scratch_partition: partitionec000 { label image-scratch; reg 0x000EC000 0x00004000; // 16KB 用作暂存区 }; // ... 可能还有文件系统等其他分区 }; };你的App代码需要通过标签如slot1_partition来获取次级插槽的起始地址和大小以便向正确的位置写入数据。这是连接MCUboot和你的DFU传输逻辑的桥梁。3. 构建双通道传输层BLE DFU与UART DFU的实现MCUboot准备好了它只关心“有没有一个有效的镜像在插槽1里”。至于这个镜像是怎么来的它不管。这就是我们传输层要做的通过BLE或UART把固件文件安全、可靠地搬运到插槽1。3.1 BLE DFU实现基于Nordic的DFU服务最省心的方式是使用Nordic提供的Secure DFU Service。这是一个标准的GATT服务定义了用于控制DFU过程如选择、创建、接收数据的特性和用于传输固件包数据的特性。手机端可以使用Nordic的nRF Connect App进行测试或者集成Nordic提供的移动端SDKnRF-DFU到你的App中。在NCS应用中启用它非常简单# 在prj.conf中 CONFIG_BOOTLOADER_MCUBOOTy CONFIG_MCUBOOT_BOOTLOADER_MODE_DIRECT_XIPn # 确保使用交换模式 CONFIG_IMG_MANAGERy CONFIG_IMG_ERASE_PROGRESSIVELYy # 渐进式擦除避免长时间阻塞 CONFIG_MCUBOOT_IMG_MANAGERy # 启用BLE和必要的GATT服务 CONFIG_BTy CONFIG_BT_PERIPHERALy CONFIG_BT_DEVICE_NAMEMyDFUDevice CONFIG_BT_DFU_SMPy # 这是关键启用通过BLE的DFU服务 CONFIG_BT_DFU_SMP_SECURITY_ENABLEDy # 启用安全连接推荐 CONFIG_BT_GATT_DYNAMIC_DBy编译并烧录后你的设备就会广播并包含DFU服务。用nRF Connect App连接后你会发现一个名为“Secure DFU Service”的服务里面包含“DFU Control Point”、“DFU Packet”等特性。你可以通过App直接选择.bin或.hex文件进行升级。但是这里有一个巨大的“坑”Nordic的BT_DFU_SMP服务其底层默认使用的是MCUmgr协议。MCUmgr是一个设备管理协议它传输的并不是原始的二进制镜像而是一个经过CBOR编码、包含哈希、签名等信息的SMP数据包。这意味着你不能简单地把编译输出的zephyr.bin文件通过这个服务发送。你需要先用imgtool生成一个适合MCUmgr传输的.bin文件或者使用mcumgr命令行工具。# 在NCS环境下的操作 # 1. 首先像往常一样编译你的应用这会生成zephyr.signed.bin已签名 west build -b nrf52840dk_nrf52840 # 2. 使用imgtool生成适合MCUmgr的镜像文件 # 假设你的编译输出在build/zephyr目录 cd build/zephyr imgtool create --align 4 --version 1.2.3 --header-size 32 --slot-size 0x70000 --pad-header zephyr.signed.bin dfu_image.bin生成的dfu_image.bin才是可以通过BLE DFU服务正确传输的文件。手机App如果使用nRF SDK内部也会做类似的处理。如果你尝试直接发送zephyr.signed.bin升级过程很可能会在最后验证失败。3.2 UART DFU实现自定义简单可靠的协议当BLE因为环境、速率或功耗限制不合适时UART就派上用场了。MCUboot本身支持通过串口进行升级MCUboot的串口恢复模式但那通常需要让设备进入一种特殊的引导模式。我们这里要实现的是在应用程序正常运行期间通过一个额外的UART接口接收固件数据。这给了我们最大的灵活性。我们需要自己设计一个简单的、基于UART的DFU协议。它不需要像MCUmgr那么复杂核心目标是可靠地将原始二进制数据写入插槽1的Flash。下面是一个我经过实践验证的简单协议框架协议帧格式为了应对UART的流式特性和可能的数据错乱我们必须设计帧结构。[帧头 0xAA 0x55] [命令字] [数据长度L] [数据...] [校验和]帧头固定的两个字节用于帧同步。命令字定义操作如CMD_START_DFU(0x01),CMD_DATA(0x02),CMD_FINISH(0x03),CMD_RESET(0x04)。数据长度后续数据段的长度。数据有效载荷对于CMD_DATA就是固件数据的片段。校验和简单的字节和校验或CRC8用于验证帧完整性。应用层逻辑上电后UART DFU功能处于监听状态。上位机首先发送CMD_START_DFU帧其中数据段可以包含固件总大小、版本号等。设备端收到后需要擦除整个插槽1分区。这是一个关键且耗时的操作一定要在开始传输数据前完成。上位机将固件文件zephyr.signed.bin分片例如每片256字节依次发送CMD_DATA帧。设备端收到后校验帧然后将数据写入插槽1的当前偏移地址并更新偏移量。传输完成后上位机发送CMD_FINISH帧。设备端计算已写入数据的哈希值如SHA-256并与帧中携带的哈希值对比。如果一致则向MCUboot设置升级标志调用boot_request_upgrade函数。最后上位机发送CMD_RESET设备重启MCUboot执行镜像交换。Zephyr中的关键代码片段#include drivers/flash.h #include storage/flash_map.h #include dfu/mcuboot.h #include sys/crc.h // 获取slot1分区的信息 const struct flash_area *fa; int err flash_area_open(FIXED_PARTITION_ID(slot1_partition), fa); if (err) { /* 处理错误 */ } size_t offset 0; // 当前写入偏移 // 在收到CMD_START_DFU后擦除整个分区 err flash_area_erase(fa, 0, fa-fa_size); // 这是一个阻塞操作时间较长 if (err) { /* 发送错误响应给上位机 */ } // 在收到CMD_DATA帧后写入数据 err flash_area_write(fa, offset, data_buf, data_len); if (err) { /* 发送错误响应 */ } offset data_len; // 在收到CMD_FINISH后验证并设置升级标志 if (hash_matches) { // 注意boot_request_upgrade需要MCUboot的模式支持 int rc boot_request_upgrade(BOOT_UPGRADE_TEST); // 或BOOT_UPGRADE_PERMANENT if (rc 0) { // 发送成功响应 } } // 不要忘记最后关闭flash_area flash_area_close(fa);重要提示flash_area_erase和flash_area_write是阻塞操作在写入Flash期间CPU无法处理其他任务包括响应UART中断。这会导致数据丢失。解决方案使用双缓冲开辟两个缓冲区A和B。当UART DMA或中断填满缓冲区A时启动一个线程如k_work将A的数据写入Flash同时UART继续向缓冲区B填充数据。控制帧速率上位机在发送下一帧数据前必须等待设备端的“ACK”响应。设备端在完成Flash写入后才发送ACK。启用流控如果硬件支持RTS/CTS使用硬件流控是最可靠的方式。4. 双通道协同与状态机设计现在我们有BLE和UART两个升级通道它们不能同时工作否则会争抢Flash资源导致数据错乱。我们需要一个全局的DFU状态机来管理。一个清晰的状态机设计如下IDLE正常应用运行状态两个通道都监听但未激活。BLE_DFU_ACTIVEBLE连接进入DFU模式开始接收SMP包。此时应锁定资源UART DFU收到任何启动命令都应返回“忙”错误。UART_DFU_ACTIVEUART收到有效的CMD_START_DFU帧进入该状态。应暂停或拒绝新的BLE DFU连接请求。PROCESSING固件数据接收完成正在计算哈希、设置升级标志等。此状态任何新的传输请求都应被拒绝。PENDING_RESET升级标志已设置等待设备重启。可以通知用户“升级成功设备即将重启”。这个状态机可以用一个全局变量dfu_state来实现所有相关的函数在操作前都必须检查状态。例如在UART的解析函数中if (dfu_state ! IDLE dfu_state ! UART_DFU_ACTIVE) { send_uart_response(STATUS_BUSY); return; } switch(cmd) { case CMD_START_DFU: if (dfu_state IDLE) { dfu_state UART_DFU_ACTIVE; // ... 初始化Flash操作 } break; case CMD_DATA: if (dfu_state UART_DFU_ACTIVE) { // ... 处理数据 } break; // ... }同时在BLE DFU服务启动的回调函数中也要将状态设置为BLE_DFU_ACTIVE。这样两个通道就实现了互斥访问保证了升级过程的安全。5. 实战中的“坑”与优化策略理论走通只是第一步实际调试中会遇到各种问题。下面是我总结的几个关键点和优化建议5.1 Flash操作阻塞系统与看门狗复位这是最常遇到的问题。无论是BLE还是UART DFU向Flash写入几KB的数据都可能需要几十毫秒。在这期间如果看门狗WDT没有被喂食系统就会复位。解决方案分片与小块写入不要一次性写入整个帧的数据。将每帧数据再分成更小的块如64字节进行写入在每小块写入间隙喂狗。使用线程和信号量将Flash写入操作放在一个低优先级的后台线程中。主线程或中断服务程序收到数据后将数据放入队列并释放一个信号量。后台线程等待信号量然后从队列取出数据写入Flash。在后台线程的循环中定期喂狗。调整看门狗超时时间在prj.conf中适当增加看门狗的超时时间但这不是根本解决办法。CONFIG_WDT_NRFXy CONFIG_WDT_NRF_TIMEOUT8000 # 超时时间设为8秒5.2 电源稳定性与掉电保护升级过程中掉电可能导致Flash中的数据处于不一致状态最坏情况是设备“变砖”。MCUboot的交换机制和暂存区设计已经提供了很好的掉电保护。但为了更安全在UART协议中增加断点续传CMD_START_DFU帧可以携带一个“起始偏移量”参数。如果设备在升级中意外复位重新连接后它可以向上位机报告当前已写入的偏移量。上位机可以从该偏移量处继续发送剩余数据而无需重新擦除和传输整个文件。这需要设备在写入Flash时定期将当前偏移量保存到非易失性存储如Flash的另一个小分区或FRAM中。使用CONFIG_IMG_ERASE_PROGRESSIVELY这个配置项会让MCUboot在交换时按需擦除页而不是一开始就擦除整个目标区域可以缩短交换过程中的“危险窗口期”。5.3 镜像验证失败问题排查升级后重启MCUboot日志如果使能了串口输出报错“Image not valid”或“Signature failed”。排查步骤检查签名密钥确保你编译MCUboot时使用的公钥root-rsa-2048.pem.pub和编译应用时使用的私钥是配对的。一个常见的错误是修改了密钥文件但忘记清理build目录重新编译MCUboot导致MCUboot里的公钥还是旧的。检查Flash写入完整性在UART DFU中确保每一帧数据都正确写入了Flash。可以在CMD_FINISH时从Flash中重新读取整个插槽1的数据计算哈希并与上位机发送的哈希对比。也可以在代码中开启调试将每个写入块的地址和数据和校验打印出来。检查镜像头使用imgtool的info命令检查你生成的dfu_image.bin或zephyr.signed.bin文件。imgtool info zephyr.signed.bin检查镜像大小是否超过插槽1的分区大小检查版本号等信息是否正确。检查分区地址这是最隐蔽的坑确保你的App在写入Flash时使用的slot1_partition的地址与MCUboot认为的slot1_partition地址完全一致。它们都来自设备树文件。务必检查你的应用项目是否使用了正确的设备树覆盖overlay文件没有错误地覆盖分区表。5.4 性能优化提升UART DFU速度UART的波特率比如921600是理论上限实际速度受限于Flash写入速度、协议开销和流控。增大数据帧长度在保证可靠性的前提下将每帧的数据段长度从256字节提高到512甚至1024字节可以减少协议头尾的开销比例。启用DMA如果MCU的UART支持DMA务必在Zephyr中启用它。这可以极大减少CPU中断负载让CPU有更多时间处理Flash写入和喂狗。CONFIG_UART_ASYNC_APIy CONFIG_UART_0_ASYNCy CONFIG_UART_0_NRF_HW_ASYNCy CONFIG_UART_0_NRF_HW_ASYNC_TIMER2 # 指定使用的Timer实例优化Flash写入flash_area_write函数内部会处理跨页写入。但如果你能保证每次写入的数据块都是页大小如4KB的整数倍并且地址对齐效率会最高。不过这通常需要在上位机端做分片对齐。6. 完整开发、测试与生产流程最后我们把所有环节串起来形成一个可重复的流程。环境搭建与密钥生成# 安装NCS和工具链略 # 生成签名密钥对 cd your_project imgtool keygen -k root-rsa-2048.pem -t rsa-2048 # 这会生成私钥root-rsa-2048.pem和公钥root-rsa-2048.pem.pub编译MCUboot# 进入MCUboot目录NCS中通常位于bootloader/mcuboot west build -b nrf52840dk_nrf52840 bootloader/mcuboot/boot/zephyr west flash # 将MCUboot烧录到设备编译并生成应用镜像# 进入你的应用目录 west build -b nrf52840dk_nrf52840 # 编译后在build/zephyr下会生成zephyr.signed.bin已签名 # 如果需要用于UART DFU这个文件可以直接用。 # 如果需要用于BLE DFU (MCUmgr)需要转换 cd build/zephyr imgtool create --align 4 --version 1.2.3 --header-size 32 --slot-size 0x70000 --pad-header zephyr.signed.bin dfu_image.bin测试BLE DFU使用nRF Connect App连接设备。进入DFU服务选择dfu_image.bin文件进行升级。观察App日志和设备串口日志如果MCUboot开启了CONFIG_MCUBOOT_SERIAL。测试UART DFU编写一个简单的Python上位机脚本使用pyserial库按照你定义的协议发送zephyr.signed.bin文件。脚本应包含帧封装、校验和计算、流控等待等待设备ACK和断点续传逻辑。通过串口助手观察设备打印的调试信息。生产部署产线首先烧录MCUboot。然后通过UART高速、可靠烧录第一个版本的应用程序包含完整的双通道DFU功能。此后设备在客户端可以通过BLE或UART进行任意次数的升级。关键务必保管好你的私钥root-rsa-2048.pem泄露意味着任何人都可以为你的设备签名固件。公钥则被编译进MCUboot无需保密。实现NCS下的双通道DFU就像为你的设备安装了一个永不停机的“软件加油站”。BLE提供了随时随地、无接触的升级便利性而UART则作为高速、稳定的后备通道确保了在最恶劣通信环境下升级的可行性。整个方案的核心在于理解MCUboot的分区管理和升级逻辑然后围绕它构建可靠的数据传输层。过程中最耗费时间的往往是调试Flash写入、协议同步和状态管理这些细节。希望我分享的这些具体步骤和踩坑经验能帮你更快地打通这条关键路径。当你第一次通过手机App成功让设备在几十秒内完成功能更新时那种成就感会让你觉得所有的调试都是值得的。