第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()。这个函数做了以下事情:
- 打印 "New Fw Download" 横幅
- 调用
SFU_APP_GetDownloadAreaInfo(SLOT_DWL_1, &fw_image_dwl_area)获取下载槽信息(起始地址、最大容量) - 调用
FW_UPDATE_DownloadNewFirmware()启动 YMODEM 接收 - 下载成功后读取固件头,调用
SFU_APP_InstallAtNextReset()标记安装请求 - 调用
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 生成差分固件的步骤
- 编译原始固件,生成
UserApp.bin - 将
UserApp.bin复制为RefUserApp.bin(放到2_Images_UserApp\MDK-ARM\下) - 修改源代码(例如把
UserAppId从'A'改为'B') - 重新编译 -- 后编译脚本自动检测到
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 实际源代码撰写,所有代码片段均来自项目文件。