
1. 项目概述为什么FOTA是嵌入式开发的“必修课”在嵌入式项目里最让人头疼的场景之一莫过于设备已经部署到现场却发现了一个必须修复的Bug或者需要增加一个新功能。传统的做法是派人去现场用JTAG或者串口线一个一个地烧录成本高、效率低对于部署在偏远地区或数量庞大的设备群来说这几乎是个不可能完成的任务。固件空中升级FOTA技术就是为了解决这个痛点而生的。它让设备能够通过网络或通信接口远程、安全地更新自身固件是实现产品“可运营、可维护”能力的基石。这次我们要拆解的是基于德州仪器F29H85x这款高性能微控制器的UART FOTA实现方案。你可能会有疑问现在都202X年了为什么还用UART这种“古老”的接口做FOTA原因很实际可靠、简单、通用。在复杂的工业电磁环境或对成本极其敏感的消费类产品中UART的硬件简单性和通信可靠性往往是首选。而F29H85x作为TI C2000系列的新成员集成了硬件安全模块HSM和独特的A/B Bank闪存架构让基于UART的FOTA在安全性和可靠性上达到了新的高度。简单来说这个方案的核心是设备里永远存着两套完整的固件A区和B区一套正在运行另一套用于接收和验证新固件。升级时新固件通过UART传到空闲区验证通过后一次重启就能完成“分区切换”让新固件接管控制权。整个过程就像给飞行中的飞机更换引擎要求绝对平稳、安全、不间断。接下来我会结合自己实际调试这个例程的经验带你从设计思路到代码细节彻底搞懂它。2. 核心机制深度解析A/B分区与HSM如何保驾护航2.1 A/B分区交换机制实现“无感”升级的基石F29H85x的FOTA功能其硬件基础是它的A/B Bank闪存交换机制。你可以把CPU的闪存地址空间想象成两个“视图”活动视图和更新视图。物理上有两组闪存Bank例如FLC1.B0/B1和FLC1.B2/B3它们可以被动态地映射到这两个视图上。活动区当前CPU正在执行的代码所在地址空间。更新区或称非活动区专门用于接收和存储新固件的地址空间。关键在于这个映射关系不是固定的而是由一个叫做BANKMGMTBank Management的特殊闪存区域来管理的。这个区域存放着决定哪个物理Bank是“活动”状态的关键信息。这里有个至关重要的细节Bank Mode。F29H85x支持多种Bank Mode但只有Bank Mode 1仅CPU1支持交换和Bank Mode 3CPU1和CPU3均支持交换才启用A/B交换功能。如果你的应用只跑在CPU1上用Mode 1就够了如果是双核应用CPU1CPU3则必须配置为Mode 3。这个模式在芯片出厂时或第一次通过CCS编程时就需要设定好之后FOTA流程会基于此模式工作。BANKMGMT区域里最重要的三个字段是BANK_STATUS标识该Bank对例如B0/B1作为一个对的内容是否有效。有效值是一个固定的魔数0x55555555_55555555。如果两个Bank都无效设备就无法从闪存启动。BANK_UPDATE_CTR一个64位的递减计数器用于标识固件版本的新旧。数值越小版本越新。当两个Bank都有效时BootROM会比较它们的计数器选择数值更小的即版本更新的作为活动Bank。BANKMODE仅存在于CPU1的BANKMGMT区域定义了设备当前的Bank Mode在启动时被加载到SSU_GEN_REGS.BANKMODE寄存器。触发交换的逻辑当我们在“更新区”成功写入新固件后需要让系统在下次重启时切换到新固件。操作就是去修改“更新区”对应的BANKMGMT区域。具体是将其BANK_UPDATE_CTR设置为一个比当前“活动区”BANKMGMT中的计数器更小的值通常是减1。同时确保其BANK_STATUS有效且BANKMODE字段正确。设备复位后BootROM会重新比较计数器发现“更新区”的版本更“新”于是执行交换将原先的“更新区”映射为“活动区”系统也就自然启动了新固件。实操心得Bank Mode配置的坑我曾经在调试双核FOTA时死活无法触发CPU3的交换最后发现是Bank Mode设成了Mode 1。Mode 1下CPU3根本没有分配闪存空间自然无法进行FOTA。所以在项目初期务必根据你的应用核数通过CCS的Flash插件或调用Fapi_issueProgBankMode()API正确配置Bank Mode。这个操作需要擦写闪存配置完成后必须重启芯片才能生效。2.2 HSM安全模块为固件加上“数字指纹”如果说A/B分区解决了升级的“连续性”问题那么HSM解决的就是“可信性”问题。在物联网时代固件被篡改的风险极大。F29H85x的HSM是一个独立的协处理器专门负责密码学运算和安全启动。在HS-SE高安全-安全使能设备状态下HSM会深度参与FOTA流程认证HSM会使用预置的客户密钥验证即将写入闪存的新固件镜像的签名基于X.509证书。只有签名验证通过的固件才会被放行。编程在非安全状态HS-FS下由CPU1直接调用Flash API编程。在HS-SE状态下编程动作本身也由HSM来执行。CPU1只是把接收到的固件数据块传给HSMHSM在验证后亲自写入闪存。这实现了“隔离执行”即使CPU1的代码被攻破攻击者也无法直接写入闪存。完整性检查全部固件写入后HSM还会对整个更新区的固件进行一次完整性校验确保在传输和编程过程中没有发生位翻转等错误。安全状态迁移新的F29H85x芯片默认是HS-FS状态此时HSM使用TI默认密钥安全启动未强制开启JTAG调试口也是开放的方便初期开发。当你准备量产时需要通过KeyWriter工具将自己的密钥注入HSM并将设备转换为HS-SE状态。一旦转为HS-SEJTAG口默认关闭安全启动强制开启任何未经签名的固件都将无法运行。这个转换是不可逆的务必在充分测试后进行。注意事项开发与量产的安全流程强烈建议在HS-FS状态下完成所有的功能开发、调试和FOTA逻辑测试。因为此时JTAG可用调试方便。等所有逻辑都验证无误后再在最后的量产固件上启用签名并将设备转为HS-SE状态。TI的TIFS SDK提供了完整的密钥管理和签名工具链mcu_rom_image_gen.py等需要集成到你的构建流程中。3. 工程实现详解从双工程结构到无缝跳转TI提供的示例采用了“引导程序应用工程”的双工程结构。这种结构非常清晰将底层的、通用的FOTA引擎与上层的、业务特定的应用代码分离。3.1 工程结构剖析SBL与App的分工Flash-Based UART SBL工程这是FOTA的核心引擎。它常驻在CPU1闪存起始的一段空间例如0x10001000开始的前83KB。它的职责包括设备初始化时钟、GPIO、外设。监听UART命令解析主机发送的升级指令。执行固件接收、校验如果使能安全和编程到“更新区”的完整流程。管理BANKMGMT区域在升级完成后为下一次重启配置好交换条件。提供明确的入口点供应用程序跳转回来触发升级。FOTA_Example_Application工程这是一个示例应用比如一个简单的LED闪烁程序。它的关键在于与SBL的协同它被链接到SBL之后的高地址空间例如0x10020000与SBL共存于闪存中。它在自己的主循环中依然监听UART。当收到特定的FOTA命令如DFU_CPU1时它不是自己处理而是直接跳转到SEL工程中预留的特定函数入口地址。跳转后CPU的控制权交还给SBL由SBL接管后续的所有升级操作。而此时应用的中断服务程序如那个定时器触发的LED闪烁并不会被禁用SBL会在处理UART数据包的间隙继续响应这些中断。这就实现了“后台静默升级”用户可能只会看到LED闪烁稍微慢了一点但功能并未中断。3.2 链接与后构建步骤让两个工程合二为一让两个独立的工程编译后能合并成一个完整的、可一次烧录的镜像是工程配置的关键。这里主要依靠链接器命令文件和后构建步骤。链接器配置 在SBL工程的.cmd文件里你需要仔细规划内存布局。必须为SBL代码、App代码以及SBL内部用于跳转的“桩函数”分配好互不重叠的地址空间。例如MEMORY { ... CPU1_FLASH_RP0 : origin 0x10001000, length 0x014000 /* 83KB for SBL */ CPU1_APP : origin 0x10020000, length 0x060000 /* 留给应用程序的空间 */ ... } SECTIONS { ... /* SBL的代码段放在CPU1_FLASH_RP0 */ .text : CPU1_FLASH_RP0 /* 一个特殊的输出段比如叫.fota_app它本身是空的但占据了CPU1_APP的空间 */ .fota_app : CPU1_APP, type DSECT /* DSECT类型表示该段初始为空 */ ... }同时在SBL的C源文件中你需要使用#pragma或__attribute__来定义一个函数并将其定位到这个空的.fota_app段这个函数就是应用跳转回来的入口。后构建步骤 这是自动化合并的魔法所在。流程如下编译App首先编译FOTA_Example_Application生成一个.out文件。转换为二进制使用armofd和armhex工具或TI的objcopy将.out文件中的代码/数据段提取出来生成一个纯净的二进制文件firmware.bin。编译SBL编译SBL工程生成SBL的.out文件。注意此时SBL的.fota_app段是空的。合并二进制使用objcopy工具将上一步生成的firmware.bin文件作为数据更新到SBL的.out文件的.fota_app段中。命令类似于ti-cgt-arm.objcopy --update-section .fota_appfirmware.bin sbl_with_empty_app.out sbl_with_app.out安全签名HS-SE必需如果是安全启动需要使用TI的mcu_rom_image_gen.py脚本对合并后的sbl_with_app.out或对应的.bin进行签名生成X.509证书并将证书再更新到镜像的特定证书段。生成最终镜像最后将处理后的.out文件再次转换为可供烧录的.bin或.hex文件。经过这些步骤你得到的就是一个包含了SBL引导程序和用户应用程序的单一镜像文件可以直接烧录到设备的“活动区”。踩坑记录地址对齐与跳转指令在实现从App跳转回SBL时我最初直接使用函数指针调用结果发生了硬件错误。原因是C函数调用会使用BL等指令这些指令有相对跳转的范围限制。更可靠的做法是将SBL中的入口点函数地址定义为绝对地址常量在App中使用汇编指令BX或直接设置PC寄存器来跳转。TI的示例中使用的CPU_jumpToAddr((uint32_t)ENTRY_POINT_ADDR)函数其内部很可能就是这样的汇编跳转。务必确保你跳转的地址正是SBL中那个特殊输出段的起始地址。4. 完整FOTA操作流程与主机端工具4.1 设备端工作流程以HS-FS状态为例让我们跟随一次完整的非安全FOTA升级看看代码是如何流动的上电/复位设备从Flash启动BootROM根据BANKMGMT信息找到有效的SBL并开始执行。SBL初始化SBL初始化系统时钟、GPIO、UART等外设。它会读取Data Flash中存储的“上一次成功升级的Bank Mode”并与当前硬件Bank Mode比较。如果相同启动一个5秒的“升级等待窗口”通过UART打印提示信息等待主机发送升级命令。如果不同说明Bank Mode被更改过例如从Mode 1换到了Mode 3则进入无限等待升级状态不尝试启动旧应用。这是一种安全策略强制要求在新模式下进行首次升级。决策点如果在超时前收到升级命令SBL进入固件接收与编程循环。如果超时未收到命令SBL通过函数指针跳转到应用程序的入口地址即0x10020000将控制权交给用户App。应用程序运行用户App如LED闪烁开始运行并保持UART中断使能。主循环可能也在监听串口。运行时触发升级设备在运行中收到主机发来的DFU_CPU1命令。App的中断服务程序收到此命令设置一个标志位。App的主循环检测到这个标志位立刻跳转回SEL中指定的FOTA处理函数入口点。后台升级SBL重新获得控制权开始通过UART接收新的固件数据包同时仍然响应App配置的定时器中断LED继续闪烁。它调用Flash API将接收到的数据编程到“非活动”的Flash Bank中。更新Bank管理信息固件编程完毕后SBL擦写“非活动区”对应的BANKMGMT区域将其BANK_UPDATE_CTR设置为比当前活动区更小的值。返回与复位SBL将当前Bank Mode记录到Data Flash用于下次启动比较然后可以返回App或者直接触发一个软件复位。更常见的做法是通知主机“升级成功请重启设备”由用户或主机命令触发硬件复位。重启与切换设备复位。BootROM再次读取BANKMGMT发现原先的“非活动区”计数器值更小于是执行Bank Swap。新的固件被映射到活动地址空间系统启动后运行的就是刚刚升级的新版本了。4.2 主机端通信协议与工具TI的示例配套了一个Python脚本作为主机端编程工具。理解其通信协议对于调试和二次开发至关重要。协议通常是简单的基于字节包的请求-响应模型。一个典型的命令包可能包含同步头如0x08 0x19用于标识帧开始。命令字如0xA0代表PING0xA1代表DFU_CPU1开始CPU1固件升级。数据长度后续数据段的长度。数据载荷具体的参数或固件数据。校验和CRC8或累加和用于验证数据完整性。主机端升级脚本的核心步骤连接与握手打开串口发送PING命令等待设备回复特定的响应如PONG确认通信链路及设备状态正常。发送升级命令发送DFU_CPU1或DFU_CPU3命令告诉设备“准备好我要开始发送CPU1/CPU3的固件了”。发送固件数据将待升级的.bin文件分拆成多个固定大小的数据包如512字节依次发送。每发送一个包等待设备回复“ACK”确认再发送下一个。如果收到“NACK”则重发该包。验证与结束发送完所有数据包后发送一个“结束传输”命令。设备会进行内部校验在HS-SE状态下会要求HSM做完整性检查并回复最终成功或失败的状态。触发重启主机发送设备复位命令或提示用户手动重启设备以完成Bank Swap。调试技巧自己写个简单的主机测试工具在开发初期我强烈建议你基于Python的pyserial库自己编写一个精简版的主机工具。不一定需要像TI示例那样功能完整但至少要能完成连接、握手、发送一个简单的固件包比如一个只有几条指令的小程序。这能帮助你快速隔离问题是设备端UART驱动有问题是命令解析出错还是Flash编程逻辑有bug自己写的工具添加调试日志、单步控制都会方便得多。5. 开发与调试中的常见问题与解决方案在实际动手实现这个FOTA框架时你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方法总结出来。5.1 链接地址冲突与内存布局错误问题现象程序编译正常但烧录后运行要么直接跑飞要么在跳转到App或跳回SBL时发生硬件错误。排查思路检查.map文件这是最重要的调试文件。分别打开SBL工程和App工程编译生成的.map文件。确认SBL的代码、数据段是否完全位于你规划的区域如0x10001000到0x1001AFFF。确认App的代码、数据段是否完全位于你规划的区域如0x10020000开始并且与SBL的区域没有任何重叠。特别检查那个用于跳转的“桩函数”如CPU1_FOTA_ENTRY的绝对地址是否与App代码中跳转时使用的地址完全一致。检查链接器命令文件确保MEMORY定义的长度足够且SECTIONS分配没有越界。注意DSECT类型段不占用实际的二进制空间但它声明了这块地址已被占用链接器不会将其他内容放进去。检查后构建脚本确认合并二进制文件的命令是否正确。使用fromelf或objdump工具查看最终生成的合并后的.out或.bin文件用十六进制编辑器查看关键地址如App起始地址0x10020000处是否有正确的代码通常是第一条指令的机器码。解决方案仔细核对并统一SBL和App工程中的内存地址定义。最好使用一个公共的头文件来定义这些绝对地址如// fota_memory_map.h #define SBL_BASE_ADDR 0x10001000 #define APP_BASE_ADDR 0x10020000 #define JUMP_TO_FOTA_ENTRY (SBL_BASE_ADDR 0x5000) // 假设入口点在SBL内偏移0x5000处确保SBL的.cmd文件、App的.cmd文件以及双方源代码中跳转用的地址都引用自同一个头文件。5.2 Flash编程失败或校验错误问题现象主机显示发送成功但设备端报告编程失败或重启后新固件未运行。排查思路Flash驱动与算法确保你使用的Flash APIFapi_xxx系列函数与你的芯片型号和Flash Bank完全兼容。F29H85x的Flash控制器可能与老型号C2000不同。擦除操作在编程前是否对目标Flash Sector执行了擦除Flash只能将bit从1写成0擦除是将整个Sector置1。编程前必须擦除。地址对齐Flash编程通常有最小写入单位如128-bit。确保你发送的数据包大小和起始地址是对齐的。数据缓冲区Flash编程函数通常需要一个RAM中的源数据缓冲区。确保这个缓冲区地址是正确且可访问的。如果是在中断服务程序中调用注意栈空间是否足够。干扰中断在擦除/编程Flash期间这是一个相对耗时的操作必须禁止全局中断吗查阅芯片手册。有些芯片要求有些则允许。F29H85x的示例中在编程期间似乎没有禁用所有中断但为了绝对安全在调用Fapi_issueAsyncProgrammingWithStatus()等函数期间可以考虑临时提升中断优先级或屏蔽相关中断。电源稳定性Flash编程对电源电压非常敏感。在调试阶段尤其是使用调试器供电时确保电源纹波在合理范围内。不稳定的电源会导致编程数据错误。解决方案在Flash编程的关键函数周围添加详细的日志输出通过UART记录每一步的返回状态Fapi_StatusType。TI的Flash API通常会返回非常具体的错误码如Fapi_Error_FlashRegionsNotAligned等根据错误码能快速定位问题。5.3 双核CPU3FOTA的特殊问题问题现象CPU1的FOTA正常但CPU3的固件升级后不运行。排查思路Bank Mode确认这是最常见的原因。必须将芯片设置为Bank Mode 3CPU3才有自己独立的、可交换的Flash Bank。CPU3启动流程在Bank Mode 3下CPU3的代码存放在哪里它的入口点是什么在SBL中你需要先将CPU3从复位状态释放通过配置CPUSYS相关寄存器然后才能将固件编程到CPU3的Flash区域。TI示例中take_cpu3_out_of_reset()函数就是干这个的。独立的BANKMGMTCPU3有自己的BANKMGMT区域在FRI-2中。在触发CPU3的Bank Swap时你需要修改的是CPU3对应的BANKMGMT区域而不是CPU1的。命令包中的目标地址需要正确指向CPU3的更新区域。内存隔离确保CPU1和CPU3的代码、数据地址空间没有冲突。仔细规划两者的链接文件。5.4 从HS-FS切换到HS-SE状态后的异常问题现象在HS-FS下调试正常的FOTA流程切换到HS-SE状态后升级失败甚至设备“变砖”无法启动。排查思路与预防签名证书HS-SE状态下所有在Flash中执行的代码包括SBL和App都必须经过正确的签名。确保你的后构建步骤正确调用了mcu_rom_image_gen.py并使用了与注入HSM的密钥对应的签名密钥。证书注入生成的X.509证书必须被正确编程到Flash镜像的特定证书区域。这个区域在链接文件中定义通常是.certs段。后构建脚本需要将生成的证书二进制文件更新到这个段。调试接口转为HS-SE后JTAG可能被禁用。这意味着你无法再通过CCS连接芯片进行调试。在转换前务必确认你的SBL和App在HS-FS下已完全稳定并且UART日志输出功能是可靠的这是你后续在HS-SE状态下唯一的调试窗口。回退方案在HS-SE状态下如果新固件签名错误或启动失败设备会卡在BootROM。设计一个“安全恢复模式”至关重要。例如可以通过一个特定的GPIO引脚电平让BootROM跳过安全启动验证从UART接收一个急救程序。这需要在项目初期就和硬件同事一起规划。实现一个稳定可靠的FOTA功能是嵌入式产品迈向成熟的关键一步。F29H85x的方案提供了从硬件安全模块到灵活内存架构的强力支持。整个过程中最耗费时间的往往不是核心逻辑的编写而是对内存布局、链接脚本、构建后处理以及安全流程这些“周边工程”的细致把握。多花时间理解.map文件用简单的测试用例验证每一个环节比如先实现一个只能接收和回显数据的UART SBL再逐步增加Flash编程、Bank管理、安全签名等复杂功能稳扎稳打最终你得到的将不仅仅是一个升级功能而是对整个嵌入式系统启动链、内存管理和安全机制的深刻理解。