ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

nRF52833串口单区升级实战:MCUboot配置与安全固件更新指南

2026/8/19 12:34:39 拓冰建站 浏览量
nRF52833串口单区升级实战:MCUboot配置与安全固件更新指南 1. 项目背景与核心需求解析最近在搞一个基于nRF52833的低功耗传感器项目设备部署在野外需要通过串口进行固件升级。客户的要求很明确升级过程要可靠不能因为断电变砖同时存储空间要省着用最好别搞双备份那种“奢侈”的方案。这不Nordic的nRF Connect SDKNCS和MCUboot这套组合拳就进入了视野。但说实话官方文档对于“串口单区升级”这个具体场景的指引就像乐高说明书里缺了几页关键步骤散落在各个角落需要自己动手拼凑。所谓“单区升级”是相对于传统的“交换升级”而言的。在交换升级中Flash被划分为两个同样大小的“槽位”主槽和副槽。新固件先下载到副槽验证成功后再与主槽交换位置。这固然安全但代价是Flash空间直接翻倍。对于nRF52833这种Flash资源512KB并不算特别宽裕的芯片或者对成本极其敏感的应用这有点难以接受。单区升级的精髓在于新固件直接覆盖在旧固件之上只在Flash中保留一个固件镜像区域大大节省了空间。但这就带来了一个核心挑战如何确保在覆盖写入的过程中即使突然断电设备也不会启动一个半截子的、损坏的固件导致彻底“变砖”这就是MCUboot的价值所在。它作为一个小巧、安全的引导加载程序驻留在Flash开头的一小块受保护区域。在单区升级模式下它的工作流程可以概括为1. 通过串口接收新固件并临时写入到Flash中旧固件后面的空闲区域或特定暂存区。2. 对新固件进行验签如果使能了签名和完整性校验。3. 校验通过后再执行一个“原地覆盖”的操作将旧固件区域替换为新固件。这个过程听起来简单但要让MCUboot配合NCS和nRF52833的串口跑起来从工程配置到烧录脚本每一步都有不少细节需要注意一不留神就会卡在“握手失败”或者“镜像无效”的坑里。2. 工程环境搭建与关键配置剖析工欲善其事必先利其器。玩转NCS和nRF52833第一步就是搭建一个靠谱的开发环境。我强烈建议使用NCS的Toolchain Manager来安装一切它能帮你处理好编译器、工具链和Python依赖的版本避免“我的电脑可以你的电脑不行”的玄学问题。安装好后用west命令来管理项目是标准操作。创建一个新的应用程序比如叫my_uart_dfu基础结构就有了。但要让MCUboot支持串口单区升级核心在于对几个关键配置文件的修改这就像给设备设定“基因”。2.1 引导程序MCUboot的配置首先我们需要配置MCUboot本身。在NCS中MCUboot是作为一个Zephyr的“子模块”存在的。我们主要修改child_image/mcuboot.conf这个文件。# child_image/mcuboot.conf # 启用串口恢复模式DFU。这是通过串口接收新固件的大门。 CONFIG_BOOT_SERIALy # 指定用于DFU的串口设备。在nRF52833上通常使用UART0。 CONFIG_BOOT_SERIAL_UARTy CONFIG_BOOT_SERIAL_UART_DEV_NAMEUART_0 # 单区升级的核心开关必须启用。 CONFIG_SINGLE_IMAGE_DFUy # 启用对镜像的完整性校验SHA256和签名验证如果使用。 CONFIG_BOOT_VALIDATE_SLOT0y CONFIG_MCUBOOT_SIGN_RSAy CONFIG_MCUBOOT_SIGN_RSA_LEN2048 # 或3072根据你的密钥对来 # 为了提高串口传输的可靠性可以启用串口流控如果需要硬件支持。 # CONFIG_BOOT_SERIAL_CDC_ACMn # 如果是USB CDC则设为y我们这里是UART保持n # CONFIG_UART_LINE_CTRLy # 如果需要流控启用此项 # 调试信息输出初期排查问题时非常有用量产时可以关闭。 CONFIG_BOOT_SERIAL_WAIT_FOR_DFUy CONFIG_LOGy CONFIG_MCUBOOT_LOG_LEVEL_DBGy这里有几个关键点CONFIG_SINGLE_IMAGE_DFUy这是灵魂配置。它告诉MCUboot“我们只有一个固件槽位新固件来了你得想办法安全地覆盖上去。”CONFIG_BOOT_SERIAL_UART_DEV_NAME必须和你应用程序中实际使用的串口设备名匹配。在nRF52833的DTS设备树中UART0通常就是这个名。如果不确定可以去检查build/zephyr/include/generated/devicetree_unfixed.h文件。签名验证对于严肃的产品强烈建议启用。你需要用imgtool.py生成密钥对并在编译MCUboot时指定公钥。这能防止恶意固件被刷入。如果只是内部测试可以先关闭签名CONFIG_MCUBOOT_SIGN_RSAn但一定要明白其中的安全风险。2.2 主应用程序的配置接下来需要修改主应用程序的配置文件通常是prj.conf。# prj.conf # 启用串口控制台MCUboot的DFU模式信息会从这里输出方便调试。 CONFIG_SERIALy CONFIG_UART_CONSOLEy CONFIG_CONSOLEy CONFIG_CONSOLE_SUBSYSy CONFIG_CONSOLE_GETCHARy # 确保串口驱动正确初始化。对于nRF52833通常使用UARTE带EasyDMA的UART驱动。 CONFIG_UART_0_NRF_UARTEy # 配置串口引脚。这通常在板级定义文件.dts或overlay文件里做更规范 # 但也可以在prj.conf里覆盖例如对于nRF52833 DK # CONFIG_UART_0_TX_PIN6 # CONFIG_UART_0_RX_PIN8 # CONFIG_UART_0_RTS_PIN5 # CONFIG_UART_0_CTS_PIN7 # 调整Flash布局以适应MCUboot。MCUboot会占用Flash起始的一部分空间。 CONFIG_FLASH_LOAD_OFFSET0x1000 # MCUboot通常占用0x0-0x10000应用从0x10000开始 CONFIG_FLASH_LOAD_SIZE0x70000 # 应用程序可用的空间大小 # 如果应用程序中也需要使用串口进行通信非DFU注意避免和MCUboot的DFU串口冲突。 # 通常MCUboot DFU使用一个固定的串口应用可以使用另一个。特别注意Flash偏移量CONFIG_FLASH_LOAD_OFFSET必须和MCUboot所占用的空间大小严格对应。你需要查看编译MCUboot后生成的build/mcuboot/zephyr/zephyr.dts文件找到slot0-partition的起始地址。假设MCUboot编译出来大小是0xC000那么你的应用偏移量就应该是0xC000。上面的0x1000只是一个示例实际值一定要核对设置错误会导致应用程序被烧写到错误的位置无法运行。2.3 设备树DTSOverlay配置为了更清晰地管理硬件资源特别是引脚分配最佳实践是使用设备树Overlay文件。在你的应用目录下创建boards/nrf52833dk_nrf52833.overlay以nRF52833 DK为例。// boards/nrf52833dk_nrf52833.overlay uart0 { status okay; current-speed 115200; tx-pin 6; rx-pin 8; // rts-pin 5; // 如果需要硬件流控 // cts-pin 7; }; / { chosen { // 指定MCUboot串口恢复模式使用的串口设备 zephyr,boot-serial uart0; // 指定控制台输出串口可以和boot-serial是同一个 zephyr,console uart0; }; };这个Overlay文件明确地启用并配置了uart0的引脚和波特率这里用了115200你也可以用9600或其他。通过zephyr,boot-serial属性告诉MCUboot“你的串口DFU功能请使用这个uart0节点。”通过zephyr,console属性告诉系统“控制台输出也走这个串口。”这样配置后MCUboot在进入DFU模式时就会乖乖地监听uart0等待我们发送新固件。3. 编译与烧录生成可引导的完整镜像配置好之后就是编译环节。这里有个顺序问题必须先编译MCUboot引导程序再编译你的应用程序。# 在项目根目录下 # 1. 编译MCUboot引导程序 west build -b nrf52833dk_nrf52833 bootloader/mcuboot/boot/zephyr -p # 编译成功后生成的MCUboot镜像在 # build/zephyr/zephyr.hex 或 zephyr.bin # 我们记下它的实际大小比如是 0xC000 字节。 # 2. 编译主应用程序并指定正确的Flash偏移量 # 假设我们确认MCUboot大小为0xC000 west build -b nrf52833dk_nrf52833 -- -DCONFIG_FLASH_LOAD_OFFSET0xC000 # 应用程序镜像在build/zephyr/zephyr.bin但是这样得到的是两个独立的镜像mcuboot.bin和app.bin。我们需要把它们合并成一个可以直接烧录到Flash起始地址的“完整镜像”。NCS提供了mergehex工具来做这件事。# 合并镜像。假设mcuboot.bin在./mcuboot_build目录app.bin在当前build目录 nrfjprog --family nrf52 --eraseall mergehex --merge ./mcuboot_build/zephyr/zephyr.hex ./build/zephyr/zephyr.hex --output combined.hex nrfjprog --family nrf52 --program combined.hex --reset更常见的做法是使用west flash命令并配置一个.west/config文件或使用--hex-file参数让它自动处理合并和烧录。不过手动操作一遍能让你更清楚底层发生了什么。关键在于合并后的hex文件其内容布局必须和我们在配置中定义的Flash布局MCUboot区 应用区完全一致。烧录完成后重启设备。如果配置了串口控制台你应该能看到MCUboot的启动信息以及应用程序的启动日志。MCUboot在启动时会先检查应用程序镜像的有效性如果使能了验证然后跳转到应用程序运行。4. 串口DFU升级实操全流程与排坑指南现在设备里已经运行着带有MCUboot和我们的应用程序的固件了。接下来就是重头戏如何通过串口给它推送一个新版本的应用程序固件。4.1 升级工具链准备MCUboot的串口DFU协议需要配合一个主机端的工具来发送固件。最常用的就是mcumgr。它是一个功能强大的管理工具可以通过多种方式串口、BLE、USB与运行MCUboot的设备通信。首先安装mcumgr# 使用Go安装推荐 go install github.com/apache/mynewt-mcumgr-cli/mcumgrlatest # 安装后确保Go的bin目录在PATH环境变量中。 # 或者通过系统包管理器如Ubuntu # sudo apt update # sudo apt install mcumgr4.2 进入DFU模式与握手默认情况下MCUboot在开机时会先尝试进入串口DFU模式等待几秒钟时间可配置CONFIG_BOOT_SERIAL_WAIT_FOR_DFU。如果在这段时间内没有收到有效的DFU命令它就会跳转到应用程序。所以升级的第一步是让设备进入DFU模式。有两种方法上电自动进入设备一上电在启动日志出现后立即开始操作mcumgr。通过应用程序触发在你的应用程序代码中可以调用一个特定的函数如boot_request_upgrade或者检测一个GPIO按键来主动让系统复位并进入MCUboot的DFU模式。这对于产品化功能更友好。假设我们使用上电自动进入的方式。打开一个串口终端如picocom,minicom或Windows的Putty、SecureCRT连接到nRF52833的串口波特率设为115200。给设备上电你会看到类似如下的输出*** Booting Zephyr OS build v3.4.99-ncs1 *** [MCUBOOT] [INF] main: Starting bootloader [MCUBOOT] [INF] main: Primary image: magicunset, swap_type0x1, copy_done0x3, image_ok0x3 [MCUBOOT] [INF] main: Boot source: none [MCUBOOT] [INF] main: Swap type: none [MCUBOOT] [INF] main: Bootloader chainload address offset: 0xc000 [MCUBOOT] [INF] main: Jumping to the first image slot如果很快跳过了[MCUBOOT]的信息并进入了你的应用说明等待时间太短。可以尝试在MCUboot配置中增加CONFIG_BOOT_SERIAL_DELAY的数值或者更可靠的方法是在MCUboot启动日志出现的一瞬间通过mcumgr发送一个echo命令来“抓住”它。打开另一个终端使用mcumgr连接串口mcumgr --conntype serial --connstring dev/dev/ttyACM0,baud115200 echo请将/dev/ttyACM0替换为你电脑上实际的串口设备号Windows上是COMx。如果连接成功会返回一个响应。这表示MCUboot正处于DFU模式并准备接收命令。4.3 镜像上传与固件升级连接成功后就可以上传我们新编译好的应用程序镜像zephyr.signed.bin或zephyr.bin取决于是否签名了。mcumgr --conntype serial --connstring dev/dev/ttyACM0,baud115200 image upload build/zephyr/zephyr.bin这个命令会启动上传过程。mcumgr会将bin文件分块通过串口协议发送给设备端的MCUboot。MCUboot接收后会将其写入到Flash中预先留出的“暂存区域”。在单区升级模式下这个“暂存区域”通常就是应用程序区域后面的一块空闲空间或者是应用程序区域本身但采用了一种“擦除-写入”的原地更新策略。上传过程中你可以在串口终端看到MCUBoot的进度输出。上传完成后需要告诉MCUboot“新镜像传完了你检查一下如果没问题就把它标记为下次启动的候选。”mcumgr --conntype serial --connstring dev/dev/ttyACM0,baud115200 image list这个命令会列出当前设备上的所有镜像通常会有两个当前运行的和刚上传的。刚上传的镜像状态可能是pending等待确认。然后我们测试新镜像mcumgr --conntype serial --connstring dev/dev/ttyACM0,baud115200 image test hash-of-new-imagehash-of-new-image是image list命令输出中新镜像对应的哈希值一长串字符串。这个命令会让MCUboot将新镜像标记为“测试”状态并复位设备。设备会使用新镜像启动。如果启动成功并且运行一段时间后你认为它稳定就需要确认它否则MCUboot在下一次启动时会回滚到旧镜像。确认镜像使升级永久生效# 如果设备还在新镜像运行并且连接的是应用程序的串口非DFU模式可能需要先复位进入DFU模式再操作。 mcumgr --conntype serial --connstring dev/dev/ttyACM0,baud115200 image confirm执行confirm后MCUboot会将新镜像标记为“已确认”旧镜像的存储空间会被回收。至此一次完整的串口单区升级就完成了。4.4 实战排坑与高频问题这个过程看似清晰但实际中你会遇到各种“坑”。下面是我踩过的一些以及解决办法“握手失败”或“无响应”问题执行mcumgr echo或upload时超时没有任何反应。排查串口设备号不对Windows的COMx和Linux的/dev/ttyXXX要确认无误。设备管理器或ls /dev/tty*查看。波特率不匹配确保mcumgr命令中的baud参数和MCUboot配置current-speed以及串口终端软件的波特率三者完全一致。115200和9600是常用值。流控问题如果你的硬件连接了RTS/CTS流控线但在软件MCUboot配置和mcumgr命令中没有启用会导致数据堵塞。最简单的办法是在硬件上不连接流控线并在软件配置中禁用流控rts-pin和cts-pin不设置mcumgr命令中也不加rtscts1之类的参数。MCUboot等待超时设备已经启动到应用程序了。尝试在设备上电瞬间立刻发送mcumgr echo命令或者修改应用程序添加一个触发进入DFU模式的机制如长按某个键。权限问题Linux/Mac当前用户没有读写串口设备的权限。使用sudo或将自己加入dialout组sudo usermod -a -G dialout $USER然后注销重登。“Image 校验失败”或“签名错误”问题上传镜像后MCUboot报告镜像无效。排查Flash偏移量错误这是最常见的原因。你编译应用程序时指定的CONFIG_FLASH_LOAD_OFFSET必须和MCUboot实际占用的空间大小完全一致。差一个字节都不行。务必检查MCUboot编译输出的.hex文件大小或者查看其生成的zephyr.dts中slot0-partition的start地址。签名密钥不匹配如果你启用了签名那么用于编译MCUboot的公钥必须和用于签名应用程序镜像的私钥是配对的一对。用imgtool.py重新检查密钥对并确保在编译MCUboot和签名应用时使用的是正确的密钥文件。镜像格式问题mcumgr上传的应该是原始的.bin文件或者是已签名的signed.bin文件。不要上传.hex或.elf文件。确保你上传的是build/zephyr/zephyr.bin未签名或build/zephyr/zephyr.signed.bin已签名。升级后设备“变砖”问题升级操作特别是confirm之后设备无法启动串口也无输出。排查与挽救单区升级的风险这正是单区升级的最大风险。在覆盖写入过程中断电可能导致镜像损坏。MCUboot的机制已经尽力保证原子性但极端情况仍可能发生。最后的防线——串口恢复幸运的是MCUboot的串口恢复模式CONFIG_BOOT_SERIALy通常是独立于主镜像的。即使主镜像损坏只要MCUboot区域本身没有被破坏在每次启动时它仍然会尝试进入串口DFU模式等待几秒钟。这时你可以重新通过mcumgr连接并上传一个已知是好的镜像。这就是为什么一定要确保CONFIG_BOOT_SERIALy并且串口引脚配置正确。使用J-Link救砖如果串口恢复也失败了还有最后一招——使用J-Link仿真器通过SWD接口完全擦除Flash然后重新烧录合并后的完整combined.hex文件。这是物理层面的恢复。传输速度慢或传输中断问题上传几KB的固件需要很长时间或者中途断连。优化提高波特率在硬件和线材质量允许的情况下将波特率从9600提高到115200甚至921600速度会有质的飞跃。调整mcumgr超时和重试mcumgr命令可以添加--timeout和--retry参数适应不稳定的串口连接。检查硬件连接USB转串口线的质量、杜邦线的接触是否良好都会影响高速传输的稳定性。对于长距离或工业环境考虑使用RS-485而不是简单的TTL UART。5. 进阶考量与生产部署建议当你的原型机成功跑通串口单区升级后就需要考虑如何将它产品化使其更健壮、更易用。5.1 安全性加固签名与加密对于任何可能面临物理接触或网络攻击的设备固件签名是必须的。MCUboot支持RSA、ECDSA等多种签名算法。生成密钥对使用imgtool.py keygen生成。编译带公钥的MCUboot在MCUboot配置中指定公钥文件路径CONFIG_MCUBOOT_SIGNATURE_KEY_FILE。签名应用程序在编译后使用imgtool.py sign对zephyr.bin进行签名生成zephyr.signed.bin。上传签名后的镜像mcumgr上传的必须是signed.bin文件。更进一步如果固件需要保密可以启用加密CONFIG_MCUBOOT_ENC_IMAGESy。MCUboot支持使用AES-128/256对镜像进行加密确保即使Flash被读取也无法获得原始固件代码。5.2 升级流程优化从手动到自动在实验室里用mcumgr命令行操作没问题但给现场设备或客户升级时需要一个更友好的方式。开发上位机工具使用Python的pyserial库和intelhex库参考MCUboot的串口协议自己编写一个带图形界面的升级工具。这样可以集成进度条、日志显示、多设备批量升级等功能。集成到应用程序在应用程序中实现一个简单的串口命令解析器。当收到特定的升级触发命令如#UPDATE#时应用程序主动调用系统复位函数并设置一个标志位存储在非易失性存储如Flash或RTC备份寄存器中告诉MCUboot下次启动直接进入DFU模式。这样就不需要用户卡着时间点去操作了。状态反馈与回滚升级工具应该能查询设备当前运行的镜像版本、哈希值。并且在测试阶段image test后如果新固件运行不稳定工具应能支持用户手动触发回滚操作而不是被动等待MCUboot超时回滚。5.3 资源与稳定性权衡单区升级虽然省空间但也有其局限性没有真正的“回滚”在交换升级中旧镜像完好无损地保存在另一个槽位回滚是瞬间的。在单区升级中所谓的“回滚”其实是在新镜像启动失败后MCUboot重新尝试引导旧镜像但旧镜像所在的Flash区域可能已经在升级过程中被部分擦写。MCUboot通过一些元数据image_ok标志来管理状态但其可靠性依赖于完整的升级过程。如果断电发生在覆盖旧镜像的关键时刻回滚可能失效。升级过程耗时单区升级需要在Flash上原地擦除再写入这个过程比交换升级中写入空闲槽位要慢且在此期间设备不可用变砖风险窗口期。对于大固件需要仔细评估这个时间窗口是否可接受。存储空间管理你需要精确计算MCUboot、应用程序、以及升级过程中可能需要的临时缓冲区所占用的Flash空间。使用west build -t rom_report命令可以生成详细的内存占用报告务必确保所有部分不会溢出。因此在决定采用单区升级前务必问自己我的设备Flash空间真的紧张到必须省下一个镜像的空间吗产品的可靠性和升级成功率与节省下来的几十KB Flash成本哪个更重要对于许多应用nRF52833的512KB Flash在采用交换升级后依然足够这可能是更稳妥的选择。单区升级更像是一种在资源极端受限情况下的高级技巧需要开发者对底层有更深的理解和更细致的测试。