安全启动和安全固件更新(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_START 和 SLOT_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 实际源代码撰写,所有代码片段均来自项目文件。

相关推荐
北京盛世宏博34 分钟前
智慧档案馆一体化建设:多维度库房环境感知与安全预警系统方案
大数据·安全
小蒋观天下36 分钟前
两轮车检测AI摄像头:场景分化下的行业竞争与卡位机遇
人工智能·安全·计算机视觉·语音识别·ai大模型
zhangduang_KHKW1 小时前
功能安全标准体系与工程边界
安全·汽车
xixiaoyunya2 小时前
数据备份的方法有哪些?六种主流方案的原理、边界与组合策略
安全·备份
千舟软件5 小时前
【宿舍管理·产品功能】宿舍水电账单自动算
大数据·运维·科技·安全·业界资讯
honsor6 小时前
PoE温湿度传感器:一根网线供电+通信,即插即用,机房/配电室/仓库温湿度监测首选
运维·服务器·网络·数据库·人工智能·安全
林伽一6 小时前
智能体安全下沉芯片层,推理效率与资本重估同场角力|2026年09月30日
人工智能·科技·安全·ai
小蒋观天下6 小时前
两轮车检测AI摄像头行业竞争格局与企业突围路径解析
大数据·人工智能·安全·计算机视觉·ai大模型
数字供应链安全产品选型7 小时前
智能体安全选型建议:抛开功能清单,用运行时硬指标甄别真实防护能力
网络·安全
大棉花哥哥7 小时前
太空安全,正式上线 —— 博客改版与双专栏发布记
安全·网络安全·卫星安全