ARTICLE DETAIL

资讯详情

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

FUS体系架构拆解:M4应用通过IPCC通道触发STM32MP1固件升级

FUS体系架构拆解:M4应用通过IPCC通道触发STM32MP1固件升级 2. FUS体系架构拆解M4、FUS服务与IPCC各自的职责2.1 先厘清FUS到底管哪一段固件做STM32MP1系列平台开发的同学一定绕不开一个东西叫FUSFirmware Upgrade System。很多刚接触MP1的人容易把它跟MCU领域的IAP、OTA混淆其实两者完全不是一个层面的东西。FUS在STM32MP1上主要负责的是安全启动链里那些关键固件的升级包括FSBL、SSBL、OP-TEE以及M4侧的协处理器固件。它并不负责Linux内核和根文件系统的升级那一层通常由SWUpdate或者RAUC来做。从标题“FUS Firmware upgrade by M4 user application (using IPCC channel)”大概能猜到这个项目的场景需要让M4核心上的用户应用程序借助IPCC通道主动去触发FUS升级流程。这在很多产品里都是刚需——比如M4负责实时控制A7侧跑Linux做人机交互当现场需要升级M4固件时不希望为了这点事重启整个系统或者现场根本没有A7侧的命令行入口。这时候让M4应用自己发起FUS升级请求是一条比较优雅的路径。STM32MP1的FUS本质上是一套在Cortex-A7安全世界里运行的固件服务它跟硬件中的OTPOne-Time Programmable fuse、TAMPER、RNG等模块协同工作。FUS的“Fuse”和“Firewall”这两个概念经常被混淆记住一句话就够FUS的“F”虽然是Firmware但它管的是固件的认证、解密和烧写真正的落盘操作是往受保护的flash分区里写数据。整个流程涉及签名验证、解密、防回滚这是它区别于普通IAP的核心点。2.2 M4用户应用在FUS流程里的角色边界标题里的关键词“by M4 user application”值得仔细品味。在ST官方的参考设计里FUS升级通常由A7侧的Linux用户空间程序或U-Boot发起通过调用OP-TEE提供的FUS服务接口完成。M4应用直接参与FUS流程在官方参考文档里并不算最常见的设计但它是一种完全可行的扩展用法。M4在这个体系里承担的角色不是“执行者”而是“发起者”。FUS服务的实际执行者是Cortex-A7安全侧OP-TEE里的FUS固件M4只是通过IPCC向A7侧发出一条“升级请求”消息后续的认证、解密、烧写动作全部由A7侧的FUS服务完成。这件事必须想清楚否则容易在设计时走偏——M4端不需要也不应该直接操作flash、OTP或者签名校验它只需要把升级意图和固件数据正确地传给A7侧即可。这就像公司里的项目审批流程M4是提需求的业务部门它写申请单、递交材料但审批和拨款发生在更高层级的部门。如果业务部门自己跑到财务系统里改数据那整个安全体系就形同虚设了。2.3 IPCC通道为什么是M4与FUS服务通信的最优解IPCCInter-Processor Communication Controller是STM32MP1上专门用于核间通信的硬件模块它设计得比较巧妙支持双核双向通信每个方向都有独立的通道和中断不需要额外的软件协议栈底层就是一组寄存器和共享内存。为什么选IPCC而不是共享内存裸搞、UART或者Mailbox三个理由硬件层安全隔离IPCC的通道配置受ETZPCExtended TrustZone Protection Controller管控M4和A7侧的访问权限可以在启动时分配。用IPCC传输FUS相关指令天然就能按安全策略隔离不用自己造轮子。低延迟中断唤醒IPCC的doorbell机制可以在接收方休眠时唤醒对方这对M4侧的低功耗场景非常重要——平时M4可以睡大觉收到升级请求时直接被中断唤醒。与OpenAMP无缝衔接ST的官方核间通信框架OpenAMP底层就是基于IPCC的如果用OpenAMP做M4与A7的常规通信那么FUS升级走同一通道不需要额外维护一套物理链路逻辑上非常干净。不过IPCC只是“传输管道”不是“协议层”。实际传递FUS命令、固件块数据、校验结果这些信息需要在IPCC之上定义一套应用层协议。这就是标题里“using IPCC channel”背后真正的工作量所在。3. 协议设计与交互流程从命令帧到固件块传输3.1 FUS命令集与消息帧结构定义在动手写代码之前首先要定义M4侧与A7侧FUS服务之间的消息协议。FUS服务本身有一套官方命令码比如查询版本、解锁、写固件、校验、提交等。M4侧应用要把这些命令封装成IPCC消息帧发送给A7侧。我采用的帧结构参考了ST官方FUS协议并做了一些适合IPCC传输的裁剪字段长度说明sync2字节固定魔数0xA5F1用于校验帧同步cmd_id2字节FUS命令码如0x01查询版本、0x02解锁、0x03写块、0x04提交payload_len2字节载荷长度小端序payloadN字节具体参数或固件数据crc324字节对整个帧的CRC32校验这里有个容易踩的坑IPCC消息长度有限制不能把一个完整的固件包一帧传过去。通常单帧IPCC载荷建议不要超过512字节更稳妥的是256字节左右。固件包要拆成很多块逐块发送A7侧收到后写入临时缓冲区等全部接收完再做整体校验和提交。这个设计跟TCP/IP的分包思路类似——先建立“逻辑连接”然后持续传输数据块最后收尾提交。区别在于没有滑动窗口和拥塞控制因为IPCC是片上总线不存在丢包问题只要确保帧同步和CRC校验即可。3.2 M4侧触发FUS升级的完整时序整个升级过程被设计成以下几个阶段每个阶段M4和A7侧都有明确的状态机管理阶段一握手与版本查询M4上电后如果检测到需要升级的标志比如flash里有个marker、或者收到了A7的升级指令首先发一条FUS_CMD_GET_VERSION给A7侧。A7侧FUS服务返回当前固件版本号和FUS服务自身的状态。这一阶段的主要作用是确认FUS服务活着、链路正常同时了解当前版本用于后续的防回滚判断。阶段二解锁与权限确认正式升级前A7侧FUS服务会检查M4发来的升级请求是否有权限。这里涉及两个层面的校验一是IPCC通道本身的访问权限ETZPC配置层面二是FUS业务逻辑层面的校验比如是否处于允许升级的模式、目标固件分区是否可写。如果没通过校验A7侧会返回错误码M4侧应用需要根据错误码做相应处理。阶段三分块传输固件M4侧将固件包拆成若干个数据块逐块封装成FUS_CMD_WRITE消息发送给A7侧。A7侧每收到一块写入RAM中的暂存区同时计算累计的哈希值。每块发送完M4等待A7返回ACK再发下一块。这个“发一块等一帧”的停等方式虽然效率不高但胜在实现简单且可靠。阶段四校验与提交全部数据块发送完后M4发FUS_CMD_COMMIT。A7侧收到后对暂存数据的哈希和签名做最终验证验证通过才真正写入目标分区并更新相关OTP fuse或版本信息。如果校验失败A7侧返回明确的错误码M4侧可以决定重试或放弃。这里要特别强调M4侧发送的固件数据本身必须是已签名、已加密的。FUS服务只负责验证和烧写不负责在运行时临时签名。签名和加密在构建阶段通过ST提供的工具链完成私钥永远留在安全的构建环境里绝不进入产品设备。3.3 双核状态机与超时处理双核通信最怕的就是“两边状态对不上”——M4等A7的ACK等到超时A7却在等M4的下一个数据块结果两边都死等。为了避免这个问题必须定义一套严谨的状态机和超时机制。M4侧的状态机我是这样设计的IDLE空闲等待升级触发条件HANDSHAKE已发送版本查询等待响应UNLOCK已发送解锁请求等待授权结果TRANSFER正在发送数据块等待每块ACKCOMMIT已发送提交指令等待最终结果ERROR异常状态记录错误码并停止传输每个状态都对应一个超时定时器。IPCC是片上通信正常延迟是微秒级所以M4侧的超时时间设成500ms已经很宽裕了。如果超过1秒还没有响应基本可以判定A7侧出问题了这时候M4应该主动放弃本次升级并报告错误而不是无限期等待。实际调试中遇到最多的问题是A7侧FUS服务在OP-TEE里执行I/O操作时耗时比预期长很多尤其是大块数据写入flash时。如果M4侧超时设得太短会出现A7明明还在正常处理M4却已经因为超时中断了通信的情况。我的经验是把数据块ACK的超时设得比“最坏I/O时间”多一个数量级比如块写入最坏需要200ms超时就设2秒。4. 关键代码实现M4侧发送逻辑与A7侧FUS服务对接4.1 M4侧基于OpenAMP的IPCC发送核心代码在STM32MP1的M4侧工程里使用OpenAMP框架来操作IPCC是最高效的方式。OpenAMP把底层的IPCC寄存器和中断封装成了virtio设备M4侧只需要通过rpmsg通道收发消息不用直接操作IPCC寄存器。M4侧发送FUS消息的核心代码大致如下#include openamp.h #include rpmsg.h #define FUS_RPMSG_SERVICE_NAME fus_service #define FUS_MAX_PAYLOAD_SIZE 256 static struct rpmsg_endpoint fus_ep; static struct rpmsg_device *fus_rdev; int fus_send_message(uint16_t cmd_id, uint8_t *payload, uint16_t payload_len) { uint8_t tx_buf[FUS_MAX_PAYLOAD_SIZE]; fus_frame_t *frame (fus_frame_t *)tx_buf; /* 构建帧 */ frame-sync 0xA5F1; frame-cmd_id cmd_id; frame-payload_len payload_len; if (payload_len 0 payload ! NULL) { memcpy(frame-payload, payload, payload_len); } frame-crc32 crc32_compute(tx_buf, sizeof(fus_frame_header_t) payload_len); uint32_t msg_len sizeof(fus_frame_header_t) payload_len; int ret rpmsg_send(fus_ep, tx_buf, msg_len); if (ret 0) { /* 发送失败记录错误码 */ fus_log_error(rpmsg_send failed: %d, ret); return -1; } return 0; }这里要注意rpmsg_send的缓冲区大小限制。OpenAMP的rproc_virtio设备默认缓冲区大小是512字节扣掉rpmsg头后实际可用载荷约496字节。我把FUS帧的最大载荷限制在256字节就是为了在缓冲区边界内留足余量。4.2 M4侧升级流程的接收回调与状态管理光能发送还不够M4侧还得能接收A7侧返回的ACK和错误码。OpenAMP使用回调机制注册接收回调后一旦A7侧有消息返回M4会被中断唤醒并执行回调函数。static void fus_ep_cb(struct rpmsg_endpoint *ep, void *data, size_t len, uint32_t src, void *priv) { fus_frame_t *resp (fus_frame_t *)data; /* 校验返回帧的CRC */ if (!fus_frame_validate(resp, len)) { fus_transition_state(FUS_STATE_ERROR); fus_error_code FUS_ERR_CRC_MISMATCH; return; } switch (resp-cmd_id) { case FUS_RSP_VERSION: fus_handle_version_response(resp); break; case FUS_RSP_UNLOCK: fus_handle_unlock_response(resp); break; case FUS_RSP_WRITE_ACK: fus_handle_write_ack(resp); break; case FUS_RSP_COMMIT: fus_handle_commit_response(resp); break; default: fus_transition_state(FUS_STATE_ERROR); fus_error_code FUS_ERR_UNKNOWN_RESPONSE; break; } }状态机的迁移逻辑非常适合用switch-case来实现但要注意回调函数运行在中断上下文不能在里面做耗时操作。我的做法是回调只做状态记录和数据拷贝真正的业务逻辑放在主循环里轮询状态标志避免在中断里长时间占用CPU。4.3 A7侧FUS服务对接的关键点A7侧的代码不在本文范围内但M4侧应用工程师必须了解A7侧FUS服务的工作方式否则联调时连协议都对不上。A7侧的FUS服务一般运行在OP-TEE的TATrusted Application里通过GIC中断监听IPCC事件。收到M4消息后TA会调用底层FUS驱动把数据写入安全存储区域。A7侧最需要注意的是安全状态切换。FUS服务在解锁阶段可能涉及TEE和Normal World的切换如果在IPC回调里直接做这种操作很容易触发内核调度器的问题。所以A7侧的常见做法是先收到消息、快速应答然后把实际FUS操作交给工作队列或专用线程不让IPC回调阻塞太久。我联调时遇到过A7侧返回“timeout”而M4侧还在等ACK的情况最后定位到原因是A7侧TA里用了阻塞式flash写操作耗时超过预期。解决方案是在M4侧把写块ACK的超时时间从500ms放宽到2秒并且让A7侧每收到一个块就立即先回一个“已收到”的应答等flash写完了再回“已落盘”的应答。这种“两段式ACK”配合使用能有效避免长I/O操作导致的超时误判。5. 签名验证、安全边界与防回滚机制5.1 FUS升级为什么必须做签名验证如果只是普通的MCU IAPCRC校验加长度检查基本就够用了。但FUS升级的是安全启动链里的核心固件一旦被篡改整个设备的信任根就崩了。所以FUS对固件包的签名验证是强制性的不是可选项。ST的签名方案用的是ECDSA非对称签名。构建阶段用私钥对固件包签名运行时FUS服务用公钥验证签名。公钥被烧录在OTP fuse或者安全flash区域不可被覆盖。这就意味着私钥泄露 设备安全彻底失效OTP fuse被改 变砖风险极高签名算法本身ECDSA P-256目前仍然被认为是安全的在M4侧设计上整包固件的哈希校验可以放在A7侧做也建议放在A7侧。M4侧哪怕只做一次CRC32校验都能提前拦截很大一部分“传输出错”的问题省得把坏数据都发给A7侧浪费时间。5.2 防回滚机制与版本号管理防回滚是FUS体系里相对隐蔽但非常重要的设计。攻击者如果拿到了一个旧版本的合法固件可以利用已知漏洞进行攻击。FUS通过维护版本号来防止这种情况每个固件包都带有版本号FUS服务在烧写前会检查新版本号是否大于当前版本号小于等于则拒绝写入。这里的版本号有两个层面固件包内的版本号构建时编入用于FUS服务做业务判断OTP fuse里的版本号烧写成功后更新用于系统重启后的确认M4侧在发起升级时应该先查询A7侧的FUS服务返回的当前版本号与本地固件包的版本号做对比版本不匹配就直接终止升级不要等到A7侧拒绝后才处理。这样可以减少一次无效的固件传输。5.3 安全边界与异常降级策略设计FUS升级时还要想清楚一个问题如果升级到一半失败了系统怎么办这是一个特别现实的工程问题尤其当M4是系统主控时升级失败可能导致整机无法启动。我推荐的策略是分级降级第一阶段M4先升级A7侧的FUS服务自身如果FUS版本也需要升级第二阶段M4在A7的配合下把新M4固件写入临时分区第三阶段验证新M4固件签名完整后切换到新固件失败处理保留旧固件在active分区新固件在inactive分区启动时如果检测到新固件异常自动回滚到旧固件这种A/B双分区方案在FUS体系里是可行的因为FUS本身支持多个固件槽位。代价是flash占用翻倍但换来的安全性提升是值得的——尤其是在现场无法人工干预的设备上自动回滚能力可能比升级本身还重要。6. 常见问题排查与调试技巧6.1 IPCC通信链路相关故障现象可能原因排查方法M4发送消息后A7无响应IPCC通道权限未配置检查ETZPC配置确认M4侧的IPCC写通道已被授权A7收不到M4的消息rpmsg设备名不匹配确认A7侧TA注册的service name与M4侧的FUS_RPMSG_SERVICE_NAME一致通信偶尔失败帧同步字被误判检查是否在消息前添加了不必要的字节或者rx端是否做了对齐处理CRC频繁校验失败载荷长度字段错误重点检查小端字节序ARM双核通常都是小端但DMA搬运时可能引入额外字节遇到IPCC通信问题我调试的第一步永远是停掉所有业务逻辑只做回环测试。让A7每收到一条消息就原样返回M4侧确认收到的内容与发送内容一致。回环通过后再逐步添加FUS业务逻辑这样能快速缩小问题范围。6.2 FUS协议交互中的状态不一致问题M4侧状态机处于TRANSFER状态时A7侧却因为解锁失败自动进入了ERROR状态这种场景在联调初期很常见。原因多半是M4侧在上电后没有正确完成解锁流程就开始发数据块。排查思路很简单在M4侧每个状态迁移点加调试打印同时在A7侧FUS TA的每个入口也加日志。两边日志一对比就能看出谁先偏离了预期流程。严格遵循“先握手、再解锁、后传数据、最后提交”的顺序不要试图省略任何一个状态。6.3 超时参数调优与重试策略前文提到IPCC链路的物理延迟极低但Flash I/O可能很慢。超时参数不能一刀切要分场景设置版本查询500ms解锁响应1s可能涉及安全世界切换写块ACK2s考虑flash写入的最坏时间提交确认5s可能涉及大批量元数据更新重试策略方面我建议每个数据块最多重发3次。重试3次仍然失败则终止整个升级流程并进入ERROR状态。这时候M4侧要做的不是盲目再次尝试而是上报错误码等待上位机或者A7侧下发明确的“重新升级”指令。6.4 实战心得M4控制台调试与固件包预检最后分享两个实战经验。第一个是调试控制台一定要在早期就做好。在M4侧保留一个简便的shell/日志输出口比如UART转USB把FUS状态机的每一次迁移、每一帧消息的收发、每一个错误码都打印出来。这比任何调试器都好用尤其是设备在客户现场部署后通过日志能快速定位是M4侧发起的请求有问题还是A7侧处理有异常。第二个是批量生产前的固件包预检脚本。我用Python写了一个小工具在烧录前自动检查固件包的签名有效性、版本号完整性、包大小与目标分区容量的匹配度。这看起来是个小事但确实救回过好几次试产——因为构建服务器上的签名环境偶尔会被改动生成出来的固件包实际上在设备上根本通不过FUS认证。与其在产线上浪费时间不如在构建阶段就把这些检查自动化。FUS升级链路虽然链路长、环节多但只要把协议定清楚、状态机管好、超时和重试策略设计合理M4侧作为升级发起方的方案完全可控。这套设计在STM32MP1上稳定跑过多个量产项目希望这些经验能帮少走一些弯路。
返回列表