组件固件更新 (CFU) 固件实现指南

组件固件更新 (CFU) 是一种协议,也是提交要安装在目标设备上的新固件映像的过程。

注意

CFU 在 Windows 10 版本 2004(Windows 10 2020 年 5 月更新)和更高版本中可用。

向常驻固件提交的 CFU 是文件对,一个文件是产品/服务部分,另一个文件是内容部分。 在将提交内容发送到实现 CFU 进程的固件之前,需要创建每份 CFU 提交内容(每个产品/服务和内容对)。

在 GitHub 上的 CFU 存储库中的示例固件源代码中,CFU 的常规实现不可知的通用代码包含在ComponentFwUpdate.c中。 所有其他文件都是可根据开发人员的唯一实现进行更新或修改的帮助程序文件。

目录

产品/服务部分和内容部分

产品/服务和内容构成了 CFU 架构中的一对文件。

产品/服务部分只是一个 16 字节长的文件,它映射到下面概述的 FWUPDATE_OFFER_COMMAND 结构。

内容部分,要更新的实际固件采用最终用户开发人员规定的格式。 所提供的 CFU 示例代码将 SREC 文件用于固件内容。

产品/服务是一个 16 字节序列。 此产品/服务结构将放入产品/服务文件中。 它本质上是二进制数据,而不是文本,因为产品/服务包含特定含义的位字段。

文件中表示的产品/服务映射到此 C 结构:

typedef struct
{
   struct
   {
       UINT8 segmentNumber;
       UINT8 reserved0 : 6;
       UINT8 forceImmediateReset : 1;
       UINT8 forceIgnoreVersion : 1;
       UINT8 componentId;
       UINT8 token;
   } componentInfo;

   UINT32 version;
   UINT32 hwVariantMask;
   struct
   {
       UINT8 protocolRevision : 4;
       UINT8 bank : 2;
       UINT8 reserved0 : 2;
       UINT8 milestone : 3;
       UINT8 reserved1 : 5;
       UINT16 productId;
   } productInfo;

} FWUPDATE_OFFER_COMMAND;

从低地址到高地址,产品/服务的第一个字节是段号。

  <------- 4 bytes -----------> <-- 8 bytes -->  <-------- 4 bytes --------->
+================================-=============================================+
|  15:0 7:3  2:0  7:6  5:4  3:0   31:0   31:0     7:0  7:0  7:7  6:6  5:0  7:0 |
|  PI | R1 | MS | R0 | BK | PR  | VM   | VN   |   TK | CI | FV | FR | R0 | SN  |
+================================-=============================================+

从高地址到低地址:

Byte(s)    Value
---------------------------------------------------------
15:14   |  (PI)  Product ID is 2 bytes
13      |  (R1)  Reserved1 5-bit register
        |  (MS)  Milestone 3-bit register
12      |  (R2)  Reserved2 2-bit register
        |  (BK)  Bank 2-bit register
        |  (PR)  Protocol Revision  2-bit register
11:8    |  (VM)  Hardware Variant Mask 32-bit register
7:4     |  (VN)  Version 32-bit register
3       |  (TK)  Token 8-bit register
2       |  (CI)  Component ID 8-bit register
1       |  (FV)  Force Ignore Version 1-bit register
        |  (FR)  Force Immediate Reset  1-bit register
        |  (R0)  Reserved0 6-bit register
0       |  (SN)  Segment Number 8-bit register
---------------------------------------------------------

产品/服务注册详细信息

产品 ID。 此 CFU 映像的唯一产品 ID 值可应用于此字段。

UINT16 productID;  

产品/服务内容所表示的固件的里程碑。 里程碑可以是 HW 内部版本的不同版本,例如 EV1 内部版本、EV2 内部版本等。 里程碑定义和值分配由开发人员负责。

UINT8 milestone : 3;

如果固件适用于特定存储区,则 2 位字段支持四个存储区。 存储区寄存器的使用包含在产品/服务的格式中,因为在一些实例中,目标设备使用存储区固件区域。

如果是这种情况,并且产品/服务旨在更新正在使用的存储区,则目标上实现 CFU 的固件可能会拒绝该产品/服务。 否则,目标上实现 CFU 的固件可以根据需要采取其他操作。

如果固件映像的存储区不在最终用户固件的设计中,则忽略此字段是合理的(设置为任何方便的值,但存储区字段中的值是可选的,具体取决于目标固件实现 CFU 的方式)。

UINT8 bank : 2;

使用的 CFU 协议的协议版本为 4 位。

UINT8 protocolRevision : 4;

对应于此固件映像可在其上运行的所有唯一 HW 的位掩码。 例如,产品/服务可能表示它可以在 HW 的 verX 上运行,但不能在 HW 的 verY 上运行。 位定义和值分配由开发人员负责。

UINT32 hwVariantMask;

所提供的固件版本。

UINT32 version;

一个字节令牌,用于标识发出产品/服务的用户特定软件。 这旨在区分可能同时尝试更新同一正在运行的固件的驱动程序和工具。 例如,可以为 CFU 更新驱动程序分配 0xA 令牌,并且可以为开发更新程序工具分配 0xB。 现在,正在运行的固件可以选择根据尝试更新它的进程来接受或忽略命令。

UINT8 token;

要应用固件更新的设备中的组件。

UINT8 componentId;

产品/服务解释标志:如果我们希望原位固件忽略版本不匹配(基于更高版本的较旧版本),请将位设置为强制忽略版本。

UINT8 forceIgnoreVersion: 1;

强制立即重置是用一位断言的。 如果断言该位,主机软件需要原位固件使设备执行重置。 重置的操作特定于平台。 设备的固件可以选择采取措施交换存储区,使新更新的固件成为活动原位固件。 或不。 由固件的实现确定。 预期通常是,如果断言强制立即重置,设备将执行任何必要操作,使固件让更新后的新存储区成为在目标设备上运行的活动固件。

UINT8 forceImmediateReset : 1;

如果产品/服务和内容对的内容部分涉及内容的多个部分。

UINT8 segmentNumber;

处理产品/服务

ProcessCFWUOffer API 接受两个参数:

void ProcessCFWUOffer(FWUPDATE_OFFER_COMMAND* pCommand,
                     FWUPDATE_OFFER_RESPONSE* pResponse)

在此用例中,假定用户软件将数据字节发送到正在运行的固件,则第一条消息是产品/服务消息。

产品/服务消息是上面所述的 16 字节消息(FWUPDATE_OFFER_COMMAND 结构)。

该产品/服务消息是正在运行的固件用于处置产品/服务的数据。

在产品/服务处置过程中,正在运行的固件通过填充 FWUPDATE_OFFER_RESPONSE 结构中的字段来通知发送方。

解释产品/服务

正在运行的固件应在 CFU 进程中跟踪其状态。 它可能已准备就绪/等待接受产品/服务,在 CFU 事务中间,或等待在活动/非活动固件之间交换存储区。

如果正在运行的固件位于 CFU 事务的中间 - 不接受/处理此产品/服务并相应地通知主机。

   if (s_currentOffer.updateInProgress)
   {
       memset(pResponse, 0, sizeof (FWUPDATE_OFFER_RESPONSE));

       pResponse->status = FIRMWARE_UPDATE_OFFER_BUSY;
       pResponse->rejectReasonCode = FIRMWARE_UPDATE_OFFER_BUSY;
       pResponse->token = token;
       return;
   }

产品/服务的组件 ID 字段可用于向正在运行的固件发出信号,指示从正在运行的固件请求特殊操作。 在示例 CFU 代码中,主机使用特殊的产品/服务命令来检索 CFU 引擎的状态 - 正在运行的软件是否能够并准备好接受 CFU 产品/服务。

   else if (componentId == CFU_SPECIAL_OFFER_CMD)
   {
       FWUPDATE_SPECIAL_OFFER_COMMAND* pSpecialCommand =
           (FWUPDATE_SPECIAL_OFFER_COMMAND*)pCommand;
       if (pSpecialCommand->componentInfo.commandCode == CFU_SPECIAL_OFFER_GET_STATUS)
       {
           memset(pResponse, 0, sizeof (FWUPDATE_OFFER_RESPONSE));

           pResponse->status = FIRMWARE_UPDATE_OFFER_COMMAND_READY;
           pResponse->token = token;
           return;
       }
   }

最后,如果存在存储区交换挂起,则会进行检查。 存储区交换是指固件保留信息,即是否仍在从正在运行的活动应用程序切换到新下载映像的过程中。

如何以及在哪里执行存储区切换是嵌入式固件的实现特定任务。 CFU 协议和进程允许在执行 CFU 的远程用户应用程序与正在运行的原位固件之间交换信息。

   else if (s_bankSwapPending)
   {
       memset(pResponse, 0, sizeof (FWUPDATE_OFFER_RESPONSE));

       pResponse->status = FIRMWARE_UPDATE_OFFER_REJECT;
       pResponse->rejectReasonCode = FIRMWARE_UPDATE_OFFER_SWAP_PENDING;
       pResponse->token = token;
       return;
   }

最后,如果正在运行的固件的状态不繁忙,componentId 不是特殊命令,并且没有挂起的存储区交换 - 我们就可以处理此产品/服务。

处理产品/服务涉及(但并不限于)下述四个步骤:

步骤 1 - 检查存储区

在产品/服务中检查正在运行的应用程序的存储区。 它们是相同的还是不同的?

如果相同,则拒绝产品/服务(我们不想覆盖正在运行/活动的映像)。

否则继续。

步骤 2 - 检查 hwVariantMask

正在运行的固件会根据正在运行它的 HW 检查产品/服务中的 hwVariantMask。 这样,如果产品/服务对目标无效,则嵌入式固件可以拒绝该产品/服务。 (例如,如果正在运行的固件位于旧 HW 内部版本上,并且新提供的固件适用于较新的 HW 内部版本 - 则正在运行的固件应拒绝此产品/服务)

如果无效,则拒绝此产品/服务。

否则继续。

步骤 3 - 检查固件版本

检查所提供的固件内容的版本是否早于当前应用程序固件的版本。

由用户实现决定如何检查哪个固件版本高于另一个固件版本,以及是否允许使用产品/服务中的“forceIgnoreVersion”字段。 典型的固件开发将允许在产品开发期间和在固件的调试版本中使用“forceIgnoreVersion”字段,但不允许在产品/发布固件中使用该字段(不允许在新固件基础上更新较旧的固件)。

如果此检查失败,则拒绝产品/服务。

否则继续。

步骤 4 - 接受产品/服务

产品/服务很好。 接受包含响应的产品/服务,该响应根据固件将消息和状态返回到远程用户应用程序的方式而定制。 所谓的“响应”是数据(演示头文件中所示的打包数据结构),此数据通过设备的适当方式写给用户应用程序。

处理内容

内容的处理通常是一个多步骤过程。 多个步骤是指固件在部分(也称为数据“块”)中接受固件映像的功能。 一次性将整个映像发送到嵌入式固件并非总是可行的,因此,期望 CFU 协议的实现和过程接受小片段中的内容是实际可行的。

此讨论在描述 CFU 内容的过程时使用假设。

内容处理的状态机涉及三种状态。

  1. 处理第一个块的状态。

  2. 处理最后一个块的状态。

  3. 处理第一个和最后一个块之间的任何块的状态。

内容命令的结构

与产品/服务一样,内容具有一个结构,其中包含演示中 CFU 算法使用的字段。

typedef struct
{
   UINT8 flags;
   UINT8 length;
   UINT16 sequenceNumber;
   UINT32 address;
   UINT8 pData[MAX_UINT8];
} FWUPDATE_CONTENT_COMMAND;

内容命令的结构比产品/服务结构更简单。 内容定义为要写入内存的字节序列。 内容的报头是此结构的字段:

  1. UINT8 flags 表示内容“块”是第一个、最后一个还是其他。

  2. UINT8 length 标记 pData 字段的长度。 在 CFU 的演示代码中,pData 的大小限制为 255 字节。 其他实现可能会改变“块”的最大大小。

  3. UINT16 sequenceNumber 标记要作为内容提交的块的索引计数器。

  4. UINT32 address 块的地址偏移量。 在此版本的 CFU 演示中,实现已预定义有关每个应用区域的物理地址的信息。 例如,两个存储区固件实现可能让 App1 以地址 0x9000 开头,App2 以地址 0xA0000 开头。 因此,根据固件映像的准备方式 (S-Records),SREC 中的地址可能是物理地址或偏移量。 在任何情况下,都需要在准备内容和 CFU 内容处理的具体实现例程之间达成共识,以确定在内存中写入块的位置的真实物理地址。 固件开发人员应采用最佳做法,并为每个内容博客的有效地址范围执行检查。 例如,CFU 代码演示了一项检查,用于查明 App1(适用于 0x9000)是否可能具有与 App2 重叠的地址等。

  5. UINT8 pData[MAX_UINT8] - 这是固件映像块的原始字节。 在用户应用程序中小心,只将 length 字节放入内容块的完整字节流中。

根据所提供代码中的 CFU 演示,内容结构中没有使用位字段。

第一个块

第一个块会启动固件内容的下载。 正在运行的固件会尝试将块写入非易失性内存。 当然,内容“块”包含有关应在内存中写入块的位置、要写入的数据量和其他字段的信息。

每个 componentID 目标设备都是不同的,并且有多个方法将数据保存到内存中。 例如,一个 componentId 可能需要写入内部闪存,另一个 componentId 可能会写入外部 SPI 闪存,或者另一个组件可能会利用另一个 IC 的 I2C 协议来更新其映像。 本文档随附的演示重点介绍了如何使用一个名为 ICompFwUpdateBspWrite 的函数,每个唯一固件都必须通过了解其设计目标的基础非易失性内存 I/O 函数实现该函数。

除第一个或最后一个以外的任何其他块

当用户应用程序提供另一个块时,接受新块的过程会继续,在消息中再次显示应写入块的地址、包含的字节数以及其他字段的元数据。

原位固件会像第一个块方案一样处理这种情况。

但请注意,每当系统无法捕获块并将其保存到内存中时,就由原位固件来响应故障代码。

最后一个块

仅当原位固件需要执行任务来验证刚刚写入内存的映像时,最后一个块才会提出质询。

首先,最后一个块写入内存。

然后,与最后一个块中的 CRC 字段相比,至少应在已写入内存的数据(从第一个到最后一个块)之间做出 CRC 检查。 每个实现固件都应了解如何获取已下载映像的 CRC。

请记住,执行 CRC 检查确实需要时间。 与为产品/服务和块提交执行 CFU 的正常流程不同。 最后一个块提交(如果包括 CRC 检查)将涉及一定延迟,只是因为 CRC 检查可能正在检查大内存区域。 根据目标设备和其他因素,这可能不是问题。

重要

传入映像的 CRC 检查是可选的,可以注释掉。但是,应制定最佳做法,至少应采用此检查。 强烈建议在 CFU 进程中的此时执行其他操作,以确保已下载映像的完整性。 其中一些操作可能包括验证映像的“已签名”部分和/或检查证书信任链或其他确保安全固件映像的最佳做法。 这些由固件开发人员负责。

在最后一个块之后清理

写入最后一个块并且 CRC 检查已完成后,如果验证的任何部分失败,固件可能会以失败做出响应。

否则,预期固件中的 CFU 进程将以成功状态进行响应。

强制重置检查

产品/服务中的强制重置标志用于确定目标 MCU 是否应进行重置(用户定义的重置)。

通常,当强制重置时,目的是使 MCU 执行重置,以便使应用存储区切换。 更新永久性变量以表示在重置时启动进入的固件映像由固件开发人员负责。