安全启动和安全固件更新(SBSFU)9:UserApp -- 用户应用与YMODEM升级

第9篇:UserApp -- 用户应用与YMODEM升级详解

本文基于 X-CUBE-SBSFU v2.6.2,硬件平台 NUCLEO-G474RE,加密方案 ECC384 + AES256-CBC + SHA384。

本文面向零基础读者,将逐行解析 UserApp 工程的每一个模块。


1. UserApp 的角色定位

在 SBSFU 安全启动框架中,整个 Flash 分为三大区域:SE Core(安全引擎核心)SBSFU(安全引导程序)UserApp(用户应用程序)。UserApp 占据了最大的一块 Flash 空间(约 448KB),它承担着双重身份:

身份 说明
用户业务逻辑 你的产品代码 -- 传感器采集、算法运算、通信协议、人机交互等
固件更新客户端 通过 UART 串口与上位机通信,接收新固件并通过 YMODEM 协议写入 Download Slot

UserApp 不是孤立运行的。它通过与 SE(Secure Engine)的接口函数,安全地访问受保护的功能 -- 比如查询下载槽状态、标记新固件就绪等。这些接口函数由 SBSFU 工程导出,以 se_interface_application.o 的形式被 UserApp 链接。


2. UserApp 的启动流程

SBSFU 在完成当前固件签名验证后,通过以下指令跳转到 UserApp:

复制代码
SBSFU 验证通过 → 加载 Active Slot 的 MSP 和 Reset_Handler → 跳转执行
    ↓
SystemInit():芯片级初始化(FPU、向量表偏移)
    ↓
main():用户程序入口(main.c line 64)
    ↓
HAL_Init():HAL 库初始化
    ↓
HAL_RCC_DeInit():恢复 SBSFU 配置的时钟
    ↓
SystemClock_Config():重新配置系统时钟为 150MHz
    ↓
FLASH_If_Init():初始化 Flash 接口
    ↓
BSP_LED_Init(LED_GREEN):初始化绿色 LED,闪烁一次
    ↓
COM_Init():初始化 UART 通信(115200-8-N-1)
    ↓
BUTTON_INIT():初始化用户按钮
    ↓
printf 欢迎信息 → FW_APP_Run() 主循环

关键代码片段(main.c):

c 复制代码
int main(void)
{
  uint32_t i = 0U;
  pUserAppId = (uint8_t *)&UserAppId;

  HAL_Init();
  HAL_RCC_DeInit();           // 关键:解除 SBSFU 的时钟配置
  SystemClock_Config();        // 独立配置 150MHz 时钟
  FLASH_If_Init();
  BSP_LED_Init(LED_GREEN);

  // 绿色 LED 闪烁一次,表示 UserApp 已启动
  for (i = 0U; i < USER_APP_NBLINKS; i++)
  {
    BSP_LED_Toggle(LED_GREEN);
    HAL_Delay(100U);
    // ... 共 4 次翻转 = 2 个完整周期
  }

  // 如果 SBSFU 配置了 IWDG,UserApp 必须及时喂狗
  WRITE_REG(IWDG->KR, IWDG_KEY_RELOAD);

  COM_Init();
  BUTTON_INIT();

  printf("\r\n==============================================================");
  printf("\r\n=              (C) COPYRIGHT 2017 STMicroelectronics         =");
  printf("\r\n=                          User App #%c                       =", *pUserAppId);
  printf("\r\n==============================================================");
  printf("\r\n\r\n");

  FW_APP_Run();     // 进入主循环

  while (1U) {}
}

注意几个关键点:

  • HAL_RCC_DeInit():SBSFU 在运行期间可能配置了不同于最终应用需求的时钟。UserApp 必须重置 RCC 并重新配置。
  • WRITE_REG(IWDG->KR, IWDG_KEY_RELOAD):由于 SBSFU 可能已使能独立看门狗(IWDG),UserApp 的第一个操作就是喂狗,否则 16 秒后将触发看门狗复位。
  • UserAppId 是一个有趣的常量('A'),它用指针引用而不是直接使用,使得修改这个字符不会触发完整编译的重新链接。在差分更新演示中特别有用。

3. 主菜单功能详解

FW_APP_Run() 是 UserApp 的主循环,它通过串口终端显示一个菜单并等待用户按键。菜单结构如下:

c 复制代码
void FW_APP_PrintMainMenu(void)
{
  printf("\r\n=================== Main Menu ============================\r\n\n");
  printf("  Download a new Fw Image ------------------------------- 1\r\n\n");
  printf("  Test Protections -------------------------------------- 2\r\n\n");
  printf("  Test SE User Code ------------------------------------- 3\r\n\n");
  printf("  Multiple download ------------------------------------- 4\r\n\n");
  printf("  Validate a FW Image------------------------------------ 5\r\n\n");
  printf("  Selection :\r\n\n");
}

选项 1:下载新固件(Download a new Fw Image)

按下 1 键后,调用 FW_UPDATE_Run()。这个函数做了以下事情:

  1. 打印 "New Fw Download" 横幅
  2. 调用 SFU_APP_GetDownloadAreaInfo(SLOT_DWL_1, &fw_image_dwl_area) 获取下载槽信息(起始地址、最大容量)
  3. 调用 FW_UPDATE_DownloadNewFirmware() 启动 YMODEM 接收
  4. 下载成功后读取固件头,调用 SFU_APP_InstallAtNextReset() 标记安装请求
  5. 调用 NVIC_SystemReset() 触发系统复位

选项 2:测试安全保护(Test Protections)

按下 2 键后,进入 TEST_PROTECTIONS_RunMenu(),显示测试子菜单:

复制代码
=================== Test Menu ============================
  Test : CORRUPT ACTIVE IMAGE --------------------------- 1
  Test Protection: Secure User memory ------------------- 2
  Test Protection: IWDG --------------------------------- 3
  Test Protection: TAMPER ------------------------------- 4
  Test Protection: Secure Header ------------------------ 5
  Previous Menu ----------------------------------------- x

这是整个 SBSFU 安全体系的关键验证手段,将在第 8 节详细展开。

选项 3:调用 SE 用户服务(Test SE User Code)

按下 3 键后,进入 SE_USER_CODE_RunMenu(),试图通过 SE 接口查询各 Active Slot 的固件信息。如果安全用户内存保护处于使能状态,UserApp 对这些函数的调用将被 MPU 阻止,导致硬故障异常。这是一个通过实际测试验证安全保护是否生效的子程序。

选项 4:多镜像功能(Multiple download)

在双镜像(2_Images)配置中,由于只有 1 个 Download Slot,这个选项输出:

复制代码
"  -- !!Only 1 download area configured - feature not available!!"

功能代码实际存在(FW_UPDATE_MULTIPLE_RunMenu()),代码中遍历 SFU_NB_MAX_DWL_AREA 检查是否有多个下载槽,但当前配置的 SLOT_DWL_2_STARTSLOT_DWL_3_START 均为 0x00000000(见 mapping_fwimg.h)。

选项 5:固件验证(Validate a FW Image)

按下 5 键后,调用 FW_VALIDATE_RunMenu()。这个功能受 ENABLE_IMAGE_STATE_HANDLING 编译开关控制,默认关闭。当默认关闭时,输出:

复制代码
"  Feature not supported !"

当启用时,新固件安装后处于 SELF_TEST 状态,用户程序中的自检代码确认一切正常后,通过这个菜单项显式调用验证,将固件状态设为 VALID。若超时未验证(或被标记为 INVALID),下次复位时 SBSFU 将触发回滚。


4. 串口通信模块(com.c)详解

com.c 是 UserApp 与外界通信的唯一通道,它的架构非常简洁:

4.1 UART 初始化

c 复制代码
HAL_StatusTypeDef COM_Init(void)
{
  UartHandle.Instance = COM_UART;               // 通常是 LPUART1 或 USART1
  UartHandle.Init.BaudRate = 115200U;           // 115200 波特率
  UartHandle.Init.WordLength = UART_WORDLENGTH_8B;  // 8 位数据
  UartHandle.Init.StopBits = UART_STOPBITS_1;       // 1 位停止位
  UartHandle.Init.Parity = UART_PARITY_NONE;        // 无校验
  UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;  // 无硬件流控
  UartHandle.Init.Mode = UART_MODE_RX | UART_MODE_TX;
  UartHandle.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_RXOVERRUNDISABLE_INIT;
  UartHandle.AdvancedInit.OverrunDisable = UART_ADVFEATURE_OVERRUN_DISABLE;

  return HAL_UART_Init(&UartHandle);
}

4.2 printf 重定向

com.c 实现了 __write()fputc()/iar_fputc()/__io_putchar() 函数(根据编译器选择),将 C 标准库的 printf 输出重定向到 UART:

c 复制代码
// Keil 版本
PUTCHAR_PROTOTYPE  // 即 int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&UartHandle, (uint8_t *)&ch, 1U, 0xFFFFU);
  return ch;
}

4.3 收发函数

c 复制代码
// 发送
HAL_StatusTypeDef COM_Transmit(uint8_t *Data, uint16_t uDataLength, uint32_t uTimeout)
{
  return HAL_UART_Transmit(&UartHandle, (uint8_t *)Data, uDataLength, uTimeout);
}

// 接收
HAL_StatusTypeDef COM_Receive(uint8_t *Data, uint16_t uDataLength, uint32_t uTimeout)
{
  return HAL_UART_Receive(&UartHandle, (uint8_t *)Data, uDataLength, uTimeout);
}

// 清空接收缓冲
HAL_StatusTypeDef COM_Flush(void)
{
  __HAL_UART_FLUSH_DRREGISTER(&UartHandle);
  return HAL_OK;
}

4.4 与 VCOM 的关系

NUCLEO-G474RE 板载的 ST-LINK/V2-1 调试器内部集成了 USB 转串口(Virtual COM Port)功能。硬件上:

  • STM32G474RE 的 UART TX/RX 引脚连接到 ST-LINK 的虚拟串口
  • 用户电脑通过 Micro-USB 连接开发板后,在设备管理器中会看到 "STMicroelectronics STLink Virtual COM Port"
  • COM_Init() 初始化的是 MCU 端的 UART 外设,不需要关心 VCOM 的 USB 端协议

5. YMODEM 协议详解(ymodem.c)

5.1 YMODEM 是什么

YMODEM 是 1980 年代由 Chuck Forsberg 开发的经典文件传输协议,是 XMODEM 的增强版。它的核心特点:

  • 数据包大小可变:128 字节或 1024 字节
  • CRC-16 校验:使用 CCITT 多项式(0x1021)
  • 批量传输:一次可传输多个文件
  • 文件名和大小在第一个数据包中发送

5.2 协议数据包格式

复制代码
┌──────────┬─────────┬──────────┬────────────┬─────────────┬────────────┬──────────────┬───────────┐
│  SOH/STX │ 序号    │ 反码序号  │ 数据[0]     │ ...        │ 数据[n]    │ CRC 高字节   │ CRC 低字节 │
│  1 byte  │ 1 byte  │ 1 byte   │            │            │            │ 1 byte       │ 1 byte    │
└──────────┴─────────┴──────────┴────────────┴─────────────┴────────────┴──────────────┴───────────┘
│<─ 头部 ─>│<────────── 包序号验证 ──────────>│<──────── 数据区 (128/1024 bytes) ─────>│<─ CRC16 ─>│

控制字符定义(ymodem.h):

c 复制代码
#define SOH   0x01   // 128 字节数据包起始标志
#define STX   0x02   // 1024 字节数据包起始标志
#define EOT   0x04   // 传输结束
#define ACK   0x06   // 确认(接收方→发送方)
#define NAK   0x15   // 否认(接收方→发送方)
#define CA    0x18   // 连续两个 CA 表示中止传输
#define CRC16 0x43   // 'C',请求 CRC-16 校验模式
#define RB    0x72   // 启动序列(rb + 0x0D)

5.3 完整传输流程

复制代码
步骤1: 接收方发送 'C' (0x43) ──────────────────→ 发送方
       含义:我准备好了,请使用 CRC-16 模式
       
步骤2: 发送方发送 rb (0x72 0x62 0x0D) ────────→ 接收方
       含义:启动 YMODEM 传输会话

步骤3: 发送方发送 Packet #0 (SOH + 0x00 + 0xFF + 文件名 + 文件大小) ─→ 接收方
       格式:SOH 00 FF "filename.sfb\0 filesize "
       例如:SOH 00 FF "UserApp.sfb\0 123456 "
       
步骤4: 接收方验证文件信息 → 发送 ACK → 发送 CRC16('C')
       接收方调用 Ymodem_HeaderPktRxCpltCallback(uFileSize)

步骤5: 发送方发送 Packet #1 (STX + 0x01 + 0xFE + 1024字节数据 + CRC16)
步骤6: 接收方验证 CRC → 发送 ACK
       ...
步骤N: 发送方发送最后一个数据包
步骤N+1: 接收方发送 ACK

步骤N+2: 发送方发送 EOT (0x04) ────────→ 接收方
         含义:文件传输完成
         
步骤N+3: 接收方发送 ACK ────────→ 发送方

步骤N+4: 发送方发送空 Packet #0 (SOH 00 FF 全零数据) ─→ 接收方
         含义:批次结束(YMODEM 支持批量传输多个文件)
         
步骤N+5: 接收方发送 ACK
         传输结束

5.4 Ymodem_Receive() 函数解析

这是 ymodem.c 的核心函数(line 225-385),实现了一个双循环的状态机:

复制代码
外层 while (session_done == 0U)
│   代表一次完整的 YMODEM 会话
│
├── 内层 while (file_done == 0U)
│   │   代表单个文件传输
│   │
│   ├── ReceivePacket()  ← 接收一个数据包
│   │   ├── 识别包头:SOH/STX/EOT/CA/ABORT/RB
│   │   ├── 读取完整数据包
│   │   ├── 验证包序号(PACKET_NUMBER == ~PACKET_CNUMBER)
│   │   └── CRC-16 校验
│   │
│   └── 根据包类型处理:
│       ├── Packet #0:提取文件名和文件大小
│       ├── Data Packet:写入 Flash
│       ├── EOT:文件传输结束
│       └── 空 Packet #0:会话结束
│
└── 错误处理:连续 5 次错误 → 中止

关键的数据包接收逻辑(ReceivePacket 函数):

c 复制代码
static HAL_StatusTypeDef ReceivePacket(uint8_t *pData, uint32_t *puLength, uint32_t uTimeout)
{
  // ... 接收第一个字节,判断包类型
  switch (char1)
  {
    case SOH:  packet_size = PACKET_SIZE;      break;  // 128 字节
    case STX:  packet_size = PACKET_1K_SIZE;    break;  // 1024 字节
    case EOT:                                    break;  // 传输结束
    case CA:   /* 连续两个 CA 中止 */            break;
    case ABORT1: case ABORT2: status = HAL_BUSY; break;  // 用户中止
    case RB:   /* 启动序列 */                    break;
    default:   status = HAL_ERROR;               break;
  }

  if (packet_size >= PACKET_SIZE)  // 128 或 1024
  {
    // 接收包序号(1B) + 反码序号(1B) + 数据(n B) + CRC16(2B)
    COM_Receive(&pData[PACKET_NUMBER_INDEX], packet_size + PACKET_OVERHEAD_SIZE, uTimeout);

    // 序号验证
    if (pData[PACKET_NUMBER_INDEX] != ((pData[PACKET_CNUMBER_INDEX]) ^ 0xFF))
    {
      packet_size = 0U;  // 序号错误,重置
    }
    else
    {
      // CRC-16 校验
      crc = pData[packet_size + PACKET_DATA_INDEX] << 8U;
      crc += pData[packet_size + PACKET_DATA_INDEX + 1U];
      if (HAL_CRC_Calculate(&CrcHandle, (uint32_t *)&pData[PACKET_DATA_INDEX], packet_size) != crc)
      {
        packet_size = 0U;  // CRC 错误
      }
    }
  }
  *puLength = packet_size;
  return status;
}

CRC 计算通过 STM32G474RE 的硬件 CRC 外设完成,使用 CRC-16-CCITT 多项式 0x1021

c 复制代码
void Ymodem_Init(void)
{
  __HAL_RCC_CRC_CLK_ENABLE();
  CrcHandle.Instance = CRC;
  CrcHandle.Init.GeneratingPolynomial = 0x1021U;   // CCITT 多项式
  CrcHandle.Init.CRCLength = CRC_POLYLENGTH_16B;   // 16 位 CRC
  CrcHandle.Init.InitValue = 0U;                   // 初始值 = 0
  CrcHandle.InputDataFormat = CRC_INPUTDATA_FORMAT_BYTES;
  HAL_CRC_Init(&CrcHandle);
}

5.5 MINICOM_YMODEM 编译开关

ymodem.h 中定义了一个条件编译开关:

c 复制代码
// #define MINICOM_YMODEM  // 默认被注释

Teraterm (Windows)和 Minicom(Linux)两种终端软件的 YMODEM 实现有微妙差异:

差异项 Teraterm (默认) Minicom
数据包大小 1024 字节(STX 头) 128 字节(SOH 头)
包头部长度 3 字节 6 字节
数据区偏移 PACKET_DATA_INDEX = 4 PACKET_DATA_INDEX = 7
接收方式 一次接收完整包 逐字节接收

Linux 用户需要取消 MINICOM_YMODEM 的注释重新编译。包头部结构的差异表现为:

c 复制代码
// Teraterm 包结构(PACKET_HEADER_SIZE=3):
// [unused][0x01/0x02][序号][反码][数据...][CRC高][CRC低]

// Minicom 包结构(PACKET_HEADER_SIZE=6):  
// [unused][0x01/0x02][0x00][0xFF/seq#][0x00][序号][反码][数据...][CRC高][CRC低]

6. 固件下载触发流程

6.1 入口:FW_UPDATE_Run()(fw_update_app.c)

c 复制代码
void FW_UPDATE_Run(void)
{
  HAL_StatusTypeDef ret = HAL_ERROR;
  uint8_t fw_header_dwl_slot[SE_FW_HEADER_TOT_LEN];
  SFU_FwImageFlashTypeDef fw_image_dwl_area;

  printf("\r\n================ New Fw Download =========================\r\n\n");

  // Step 1: 获取下载槽信息
  SFU_APP_GetDownloadAreaInfo(SLOT_DWL_1, &fw_image_dwl_area);
  // 返回 fw_image_dwl_area.DownloadAddr = 0x08048000
  // 返回 fw_image_dwl_area.MaxSizeInBytes = 216 * 1024

  // Step 2: 启动下载
  ret = FW_UPDATE_DownloadNewFirmware(&fw_image_dwl_area);

  if (HAL_OK == ret)
  {
    // Step 3: 读取刚下载到 DWL 槽中的固件头
    FLASH_If_Read(fw_header_dwl_slot, (void *)fw_image_dwl_area.DownloadAddr, SE_FW_HEADER_TOT_LEN);

    // Step 4: 请求下次复位时安装
    SFU_APP_InstallAtNextReset((uint8_t *)fw_header_dwl_slot);

    // Step 5: 复位系统
    printf("  -- Image correctly downloaded - reboot\r\n\n");
    HAL_Delay(1000U);
    NVIC_SystemReset();
  }
}

6.2 下载执行:FW_UPDATE_DownloadNewFirmware()

c 复制代码
static HAL_StatusTypeDef FW_UPDATE_DownloadNewFirmware(SFU_FwImageFlashTypeDef *pFwImageDwlArea)
{
  HAL_StatusTypeDef ret = HAL_ERROR;
  COM_StatusTypeDef e_result;
  uint32_t u_fw_size;
  YMODEM_CallbacksTypeDef ymodemCb = {Ymodem_HeaderPktRxCpltCallback, Ymodem_DataPktRxCpltCallback};

  printf("  -- Send Firmware \r\n\n");
  WRITE_REG(IWDG->KR, IWDG_KEY_RELOAD);   // 喂狗

  // Step 1: 擦除下载槽整片区域
  printf("  -- -- Erasing download area ...\r\n\n");
  ret = FLASH_If_Erase_Size((void *)(pFwImageDwlArea->DownloadAddr), pFwImageDwlArea->MaxSizeInBytes);

  if (ret == HAL_OK)
  {
    printf("  -- -- File> Transfer> YMODEM> Send ");

    // Step 2: 初始化 YMODEM(配置 CRC 硬件)
    Ymodem_Init();

    // Step 3: 开始 YMODEM 接收循环
    e_result = Ymodem_Receive(&u_fw_size, pFwImageDwlArea->DownloadAddr, &ymodemCb);

    if (e_result == COM_OK)
    {
      printf("  -- -- Programming Completed Successfully!\r\n\n");
      printf("  -- -- Bytes: %d\r\n\n", u_fw_size);
      ret = HAL_OK;
    }
  }
  return ret;
}

6.3 回调函数:Ymodem_HeaderPktRxCpltCallback()

当接收到 Packet #0(文件名包)时被调用,计算预期接收的块数:

c 复制代码
HAL_StatusTypeDef Ymodem_HeaderPktRxCpltCallback(uint32_t uFileSize)
{
  m_uFileSizeYmodem = 0U;
  m_uPacketsReceived = 0U;
  m_uNbrBlocksYmodem = 0U;

  m_uFileSizeYmodem = uFileSize;

#ifndef MINICOM_YMODEM
  // Teraterm 发 1KB 包:向上取整计算块数
  m_uNbrBlocksYmodem = (m_uFileSizeYmodem + (PACKET_1K_SIZE - 1U)) / PACKET_1K_SIZE;
#else
  // Minicom 发 128B 包
  m_uNbrBlocksYmodem = (m_uFileSizeYmodem + (PACKET_SIZE - 1U)) / PACKET_SIZE;
#endif

  HAL_Delay(1000U);  // YMODEM 协议要求的延迟
  return HAL_OK;
}

6.4 回调函数:Ymodem_DataPktRxCpltCallback()

这是最复杂的回调,负责将收到的数据包写入 Flash。核心逻辑:

c 复制代码
// Teraterm 版本(非 MINICOM_YMODEM)
HAL_StatusTypeDef Ymodem_DataPktRxCpltCallback(uint8_t *pData, uint32_t uFlashDestination, uint32_t uSize)
{
  static uint32_t m_uDwlImgStart = 0U;    // 下载起始地址
  static uint32_t m_uDwlImgEnd = 0U;      // 下载结束地址
  static uint32_t m_uDwlImgCurrent = 0U;  // 当前写入位置

  m_uPacketsReceived++;

  // 处理第一个包(包含固件头)
  if (m_uPacketsReceived == 1)
  {
    m_uDwlImgStart = uFlashDestination;
    m_uDwlImgCurrent = uFlashDestination;

    // 计算下载结束地址 = 起始地址 + 差分固件大小 + (差分偏移 % Swap大小) + 头部大小
    m_uDwlImgEnd = uFlashDestination
                   + ((SE_FwRawHeaderTypeDef *)pData)->PartialFwSize
                   + (((SE_FwRawHeaderTypeDef *)pData)->PartialFwOffset % SLOT_SIZE(SLOT_SWAP))
                   + SFU_IMG_IMAGE_OFFSET;
  }

  // 固件头可能跨越两个 YMODEM 包:需要特殊处理
  if ((m_uDwlImgCurrent < (m_uDwlImgStart + SFU_IMG_IMAGE_OFFSET)) &&
      ((m_uDwlImgCurrent + uSize) >= (m_uDwlImgStart + SFU_IMG_IMAGE_OFFSET)))
  {
    // 先写入头部部分
    uLength = SFU_IMG_IMAGE_OFFSET % PACKET_1K_SIZE;
    FLASH_If_Write((void *)m_uDwlImgCurrent, pData, uLength);

    // 读回完整头部,获取 PartialFwOffset 以计算偏移
    FLASH_If_Read((uint8_t *)&fw_header_dwl, (void *)m_uDwlImgStart, SE_FW_HEADER_TOT_LEN);
    m_uDwlImgCurrent += uLength + fw_header_dwl.PartialFwOffset % SLOT_SIZE(SLOT_SWAP);
    uSize -= uLength;
    pData += uLength;
  }

  // 数据 8 字节对齐后写入 Flash
  if (uSize != 0U)
  {
    // 调整大小到 FLASH_IF_MIN_WRITE_LEN 的倍数
    if (uSize % FLASH_IF_MIN_WRITE_LEN != 0U)
    {
      uOldSize = uSize;
      uSize += (FLASH_IF_MIN_WRITE_LEN - (uSize % FLASH_IF_MIN_WRITE_LEN));
      while (uOldSize < uSize) { pData[uOldSize] = 0xFF; uOldSize++; }
    }

    FLASH_If_Write((void *)m_uDwlImgCurrent, pData, uSize);
    m_uDwlImgCurrent += uSize;
  }
  return e_ret_status;
}

7. Flash 接口(flash_if.c)

UserApp 对 Flash 的所有读写操作都通过 flash_if.c 中的统一接口完成。它设计为支持内部 Flash外部 Flash(如 QSPI Flash)的抽象层。

7.1 路由机制

所有函数都根据地址范围判断操作内部还是外部 Flash:

c 复制代码
#define EXTERNAL_FLASH_ADDRESS  0x90000000U

HAL_StatusTypeDef FLASH_If_Write(void *pDestination, const void *pSource, uint32_t uLength)
{
  if ((uint32_t)pDestination < EXTERNAL_FLASH_ADDRESS)
    return FLASH_INT_If_Write(pDestination, pSource, uLength);  // 内部 Flash
  else
    return FLASH_EXT_If_Write(pDestination, pSource, uLength);  // 外部 Flash
}

对于 NUCLEO-G474RE,没有外部 Flash,所以 FLASH_EXT_* 函数全部返回 HAL_ERROR

7.2 擦除操作

c 复制代码
HAL_StatusTypeDef FLASH_INT_If_Erase_Size(void *pStart, uint32_t uLength)
{
  first_page = GetPage(uStart);
  nb_pages = GetPage(uStart + uLength - 1U) - first_page + 1U;

  do
  {
    chunk_nb_pages = (nb_pages >= NB_PAGE_SECTOR_PER_ERASE) ? NB_PAGE_SECTOR_PER_ERASE : nb_pages;
    // 每次擦除最多 2 页,避免阻塞太长(喂狗)
    HAL_FLASHEx_Erase(&x_erase_init, &page_error);
    WRITE_REG(IWDG->KR, IWDG_KEY_RELOAD);  // 擦除间隙喂狗
    nb_pages -= chunk_nb_pages;
  } while (nb_pages > 0);
}

注意:擦除是分批次进行的(每次最多 2 页),每批次之间喂狗,避免长时间擦除触发看门狗复位。

7.3 写入操作

c 复制代码
HAL_StatusTypeDef FLASH_INT_If_Write(void *pDestination, const void *pSource, uint32_t uLength)
{
  HAL_FLASH_Unlock();

  // 按双字(8 字节)循环写入
  for (uint32_t i = 0U; i < uLength; i += 8U)
  {
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, (uint32_t)pDestination, *((uint64_t *)(pdata + i)));

    // 立即验证写入内容
    if (*(uint64_t *)pDestination != *(uint64_t *)(pdata + i))
    {
      e_ret_status = HAL_ERROR;
      break;  // 写入验证失败
    }
    pDestination = (void *)((uint32_t)pDestination + 8U);
  }

  HAL_FLASH_Lock();
  return e_ret_status;
}

关键约束:

  • G4 系列 Flash 按双字(8 字节)编程
  • 每次写入后立即回读验证,确保数据正确
  • 写入前后需要 HAL_FLASH_Unlock() / HAL_FLASH_Lock() 保护

7.4 读取操作

内部 Flash 读取非常简单,就是 memcpy

c 复制代码
HAL_StatusTypeDef FLASH_INT_If_Read(void *pDestination, const void *pSource, uint32_t uLength)
{
  memcpy(pDestination, pSource, uLength);
  return HAL_OK;
}

8. 安全保护测试(test_protections.c)

这是 UserApp 中最有教育意义的模块。它通过一系列破坏性测试验证 SBSFU 安全机制是否真正生效。

8.1 Test 1:Corrupt Active Image(损坏固件镜像)

c 复制代码
static void TEST_PROTECTIONS_RunCORRUPT(uint32_t slot_number)
{
  // 在活动槽固件头之后写入全零,破坏固件内容
  ret = FLASH_If_Write(
    (void *)(TEST_PROTECTIONS_CORRUPT_IMAGE_FLASH_ADDRESS(slot_number)),
    (void *)&pattern,
    TEST_PROTECTIONS_CORRUPT_IMAGE_FLASH_SIZE  // 32 字节
  );
  // 注意:头部被保留以支持反回滚检查

  NVIC_SystemReset();  // 触发复位
  // 下次启动时 SBSFU 的签名验证将失败 → 进入 Local Loader 模式
}

地址计算宏:

c 复制代码
#define TEST_PROTECTIONS_CORRUPT_IMAGE_FLASH_ADDRESS(A)  \
  ((uint32_t)(SlotStartAdd[A] + SFU_IMG_IMAGE_OFFSET))

写入点跳过了固件头(保留头部),只损坏固件内容区域。复位后 SBSFU 重新计算签名,发现签名不匹配,拒绝执行被损坏的固件。

8.2 Test 2:Secure User Memory(安全用户内存)

c 复制代码
static void TEST_PROTECTIONS_RunSecUserMem_CODE(void)
{
  // 尝试读取 SE Core 密钥区的代码
  SE_ReadKey = (void (*)(unsigned char *))(
    (unsigned char *)(TEST_PROTECTIONS_SE_ISOLATED_CODE_READKEY_ADDRESS) + 1U
  );

  // SE_KEY_REGION_ROM_START 指向受 MPU 保护的安全内核区域
  SE_ReadKey(&(key[0U]));

  // 如果安全保护生效 → 应该在执行到 SE Core 代码时触发 MPU 异常
  // 如果保护未生效 → 读出了密钥,说明安全配置有漏洞
  if (memcmp(key, pattern, 16) != 0U)
  {
    printf("  -- Key: %s \r\n\n", key);
    printf("  -- !! Secure User Memory protection is NOT ENABLED !!\r\n\n");
  }
}

8.3 Test 3:IWDG 测试

c 复制代码
static void TEST_PROTECTIONS_RunIWDG(void)
{
  printf("  -- Waiting %d (ms). Should reset if IWDG is enabled. \r\n\n", 16000);
  HAL_Delay(16000);  // 停止喂狗 16 秒

  // 如果到达这里 → IWDG 未使能
  printf("  -- !! IWDG protection is NOT ENABLED !!\r\n\n");
}

SBSFU 配置的 IWDG 超时通常为几秒到十几秒。这个测试中 UserApp 故意停止喂狗,如果 IWDG 正常工作,16 秒内 MCU 将被强制复位。

8.4 Test 4:TAMPER 测试

c 复制代码
static void TEST_PROTECTIONS_RunTAMPER(void)
{
  printf("  -- Pull PA0 (CN7.28) to GND \r\n\n");
  printf("  -- Waiting for 10 seconds...\r\n\n");

  // 等待 10 秒,监听 TAMPER 事件
  while ((i < 10) && (m_uTamperEvent == 0U))
  {
    WRITE_REG(IWDG->KR, IWDG_KEY_RELOAD);
    HAL_Delay(1000U);
    i++;
  }

  if (m_uTamperEvent != 0U)
  {
    printf("  -- TAMPER Event detected!!\r\n\n");
    NVIC_SystemReset();
  }
}

// TAMPER 中断回调
void CALLBACK_Antitamper(void)
{
  m_uTamperEvent = 1U;
}

TAMPER 引脚(PA0,CN7 连接器的 28 脚)内部有上拉电阻。用户将 PA0 接地时,触发 TAMPER 事件,SBSFU 会检测到入侵并删除安全数据后复位。


9. SE 用户服务调用(se_user_code.c)

这个模块演示了 UserApp 如何通过 SE 接口安全地查询固件信息:

c 复制代码
static void SE_USER_CODE_GetFwInfo(uint32_t SlotNumber)
{
  SE_ErrorStatus se_retCode = SE_ERROR;
  SE_StatusTypeDef se_Status = SE_KO;
  SE_APP_ActiveFwInfo_t sl_FwInfo;

  memset(&sl_FwInfo, 0xFF, sizeof(SE_APP_ActiveFwInfo_t));

  printf("If the Secure User Memory is enabled you should not be able to call "
         "a SE service and get stuck.\r\n\n");

  // 调用 SE 接口函数(通过 CallGate 机制)
  se_retCode = SE_APP_GetActiveFwInfo(&se_Status, SlotNumber, &sl_FwInfo);

  if ((SE_SUCCESS == se_retCode) && (SE_OK == se_Status))
  {
    printf("Firmware Info:\r\n");
    printf("\tActiveFwVersion: %d\r\n", sl_FwInfo.ActiveFwVersion);
    printf("\tActiveFwSize: %d bytes\r\n", sl_FwInfo.ActiveFwSize);
  }

  // 如果安全保护生效,程序永远不会到达这里
  printf("  -- !! Secure User Memory protection is NOT ENABLED !!\r\n\n");
}

关键知识点SE_APP_GetActiveFwInfo() 不是普通的函数调用。这个函数位于 SE 接口层(0x08006000-0x080065FF),它的内部通过 CallGate 机制调用 SE Core 中的受保护代码。CallGate 是一组严格的入口点,确保用户代码只能以预定义的方式访问安全服务,不能随意跳转到 SE Core 的任意地址。

当安全用户内存保护使能时,SE Core 代码区受到 MPU 的"不可执行"保护,任何对此区域代码的执行尝试都会导致内存管理故障(MemManage Fault),程序进入 HardFault 处理程序中的死循环。


10. 差分固件更新(Partial Update)

10.1 什么是差分更新

常规的固件更新需要传输整个固件文件(可能几百 KB)。差分更新(Partial Update)只传输新旧固件之间的差异部分,大大减少传输数据量。

10.2 工作原理

复制代码
原有固件:        [A][B][C][D][E][F][G][H]    (RefUserApp.bin)
修改后固件:      [A][B][C'][D'][E][F][G'][H]  (UserApp.bin, 修改了 C, D, G)
                                              ↓ prepareimage.py diff
差异固件:        [C'][D'][G']                  (PartialUserApp.bin, 只有变更部分)
                                              ↓ encrypt + pack
差异固件包:      PartialUserApp.sfb

10.3 生成差分固件的步骤

  1. 编译原始固件,生成 UserApp.bin
  2. UserApp.bin 复制为 RefUserApp.bin(放到 2_Images_UserApp\MDK-ARM\ 下)
  3. 修改源代码(例如把 UserAppId'A' 改为 'B'
  4. 重新编译 -- 后编译脚本自动检测到 RefUserApp.bin 存在,生成 PartialUserApp.sfb

对应的批处理命令(来自 SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384.bat):

batch 复制代码
:: 第1步:计算差异
prepareimage.py diff -1 RefUserApp.bin -2 UserApp.bin PartialUserApp.bin -a 16 --poffset PartialUserApp.offset

:: 第2步:加密差异固件
prepareimage.py enc -k OEM_KEY_COMPANY1_key_AES256_CBC.bin -i iv.bin PartialUserApp.bin PartialUserApp.sfu

:: 第3步:计算差异固件哈希
prepareimage.py sha384 PartialUserApp.bin PartialUserApp.sign

:: 第4步:打包差分固件(含完整固件的 sfh 作为参考)
prepareimage.py pack -m SFU1 -k ECCKEY1-384.txt -r 4 -v 2 -i iv.bin ^
  -f UserApp.sfu -t UserApp.sign ^
  --pfw PartialUserApp.sfu --ptag PartialUserApp.sign --poffset PartialUserApp.offset ^
  -o 4096 PartialUserApp.sfb

注意 pack 命令同时包含了完整固件(-f-t)和差分固件(--pfw--ptag--poffset)。差分固件包的头部包含了 PartialFwOffset 字段,告诉 SBSFU 差异数据应该写入 Active Slot 中的哪个偏移位置。

10.4 差分更新的优势

对比项 完整更新(UserApp.sfb) 差分更新(PartialUserApp.sfb)
固件大小 完整固件(可达 216KB) 仅差异部分(通常几 KB)
传输时间 较长 非常短
YMODEM 数据包数 数百个包 几个到几十个包
前提条件 设备中必须有参考固件已安装
应用场景 初次量产烧录 现场固件升级

总结

UserApp 工程是 SBSFU 体系中与用户交互最紧密的一层。它既是一个正常的应用程序,也是一个固件更新客户端。通过 YMODEM 协议和 SE 接口,它能够在安全框架的保护下,安全地接收、存储和触发新固件的安装。理解 UserApp 的每个模块,是掌握整个 SBSFU 安全启动链的关键一步。

下一篇将深入剖析双镜像机制的核心 -- 活动槽与下载槽的协同工作。


本文基于 X-CUBE-SBSFU v2.6.2 实际源代码撰写,所有代码片段均来自项目文件。

相关推荐
AC赳赳老秦1 小时前
数据安全合规:OpenClaw 敏感信息脱敏、操作日志审计、权限精细化管控方案,符合等保要求
网络·数据库·python·安全·web安全·oracle·openclaw
Paranoid-up2 小时前
安全启动和安全固件更新(SBSFU)10:双镜像机制 – 活动槽与下载槽的协同工作
安全·iap·安全启动·安全升级·sbsfu
XD7429716362 小时前
科技早报晚报|2026年5月10日:Agent 安全沙箱、可审计编程代理与持久化产品上下文,今晚更值得做的 3 个开源机会
科技·安全·开源·开源项目·ai agent·开发者工具
@insist1232 小时前
信息安全工程师-入侵阻断与网络流量清洗技术详解
网络·安全·软考·信息安全工程师·软件水平考试
小小测试开发2 小时前
LLM 文档处理安全指南:如何避免 AI 静默篡改你的重要数据
人工智能·安全
vortex52 小时前
无人机系统安全攻防技术深度解析
安全·系统安全·无人机
openKylin2 小时前
紧急安全通告|Linux内核Dirty Frag漏洞(CVE-2026-43284、CVE-2026-43500)
linux·安全·web安全
柠檬威士忌9852 小时前
2026-05-10 AI前沿日报:算力、模型与安全评测同时加速
人工智能·安全
鹿角片ljp2 小时前
全局哈希去重原理与数据集实践
算法·安全·哈希算法