1. 项目概述:STM32F103 U盘升级方案设计
作为一名嵌入式开发者,我经常遇到设备固件更新的需求。传统方式需要拆机连接调试器,既麻烦又容易损坏接口。最近我在一个工业控制器项目中,基于STM32F103C8T6实现了U盘升级功能,让现场维护变得异常简单——只需将固件拷贝到U盘,插入设备就能自动完成升级。这个方案的核心在于利用CH375 USB Host控制器桥接STM32和U盘,通过FATFS文件系统读取升级文件,最终写入内部Flash完成更新。
整个系统的工作流程可以分为五个关键阶段:首先是硬件初始化,包括STM32的SPI接口和CH375控制器;其次是U盘枚举和文件系统挂载;然后是升级文件的读取与校验;接着是Flash的擦除和写入;最后是应用程序的跳转。每个阶段都需要精心设计,特别是要处理好USB协议栈的复杂性和Flash操作的时序要求。
2. 硬件设计详解
2.1 核心组件选型考量
选择STM32F103C8T6作为主控主要基于三点考虑:首先是Cortex-M3内核的72MHz主频足够处理USB协议和文件系统;其次是64KB Flash和20KB RAM的资源在Bootloader场景下够用;最重要的是它的性价比极高,批量采购单价不到10元。对于USB Host功能,STM32F103原生不支持Host模式,所以我们选用CH375作为桥接芯片。相比其他方案,CH375有三大优势:内置USB协议栈减轻MCU负担、SPI接口节省IO资源、成熟的驱动生态。
电源设计需要特别注意电流供给能力。实测发现,某些U盘在初始枚举时峰值电流可能超过500mA。我们在硬件上做了两级处理:5V输入先经过一个1A的自恢复保险丝,再通过AMS1117-3.3转换为3.3V供STM32使用。CH375则直接使用5V供电,确保信号电平匹配。
2.2 硬件连接实战技巧
SPI接口的布线要遵循高频信号原则:SCK、MISO、MOSI三根线尽量等长,走线远离模拟电路。我们在PCB上为这组信号做了阻抗匹配,实测SPI时钟可以稳定工作在18MHz。CH375的中断信号(INT)连接至STM32的PB0,这里有个重要细节——需要在PB0上拉一个4.7K电阻到3.3V,避免浮空状态导致误触发。
实际调试中发现,如果CH375的USB接口没有添加ESD保护器件,热插拔U盘时容易导致芯片复位。建议在USB_DP/DM线上并联TVS二极管,如USBLC6-2SC6。
U盘接口选择上也踩过坑:最初使用直插式USB-A座,但在振动环境中容易接触不良。后来改用带锁紧机构的USB-B型座,配合弯头U盘,机械可靠性大幅提升。下表是我们的最终硬件配置:
| 模块 | 关键参数 | 选型建议 |
|---|---|---|
| 主控MCU | STM32F103C8T6@72MHz | 建议使用TSSOP20封装 |
| USB Host | CH375B SOP28封装 | 注意区分CH375A(并口)版本 |
| 晶振 | 12MHz±50ppm | 必须接22pF负载电容 |
| SPI上拉电阻 | 4.7KΩ 0402封装 | 所有SPI信号线都需要上拉 |
3. 软件架构设计与实现
3.1 存储分区规划策略
Flash分区是Bootloader设计的核心。我们的方案将64KB空间划分为三个区域:前16KB(0x08000000-0x08003FFF)存放Bootloader代码,中间48KB(0x08004000-0x0801FFFF)是应用程序区,最后4字节(0x0801FFFC-0x0801FFFF)作为升级标志位。这种划分基于以下考量:
- Bootloader功能相对固定,16KB空间足够包含USB驱动、FATFS文件系统和Flash操作等所有功能;
- 应用程序区起始地址按16KB对齐,符合STM32向量表对齐要求;
- 标志位放在Flash末尾,避免频繁擦写影响主要代码区。
在实际工程中,需要通过修改链接脚本实现分区。以Keil MDK为例,需要在分散加载文件(.sct)中添加以下定义:
code复制LR_IROM1 0x08000000 0x00004000 { ; Bootloader区域
ER_IROM1 0x08000000 0x00004000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00005000 {
.ANY (+RW +ZI)
}
}
3.2 关键驱动实现
3.2.1 CH375驱动优化
CH375的SPI驱动需要处理几个关键点:首先是片选信号(CS)的时序,必须在SCK空闲电平变化前拉低,在传输完成后拉高。我们通过示波器抓取发现,如果CS切换太快会导致数据丢失,最终在CS变化前后各添加了1us延时:
c复制void CH375_WriteReg(uint8_t reg, uint8_t data) {
uint8_t buf[2] = {reg, data};
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
DWT_Delay_us(1); // 添加延时
HAL_SPI_Transmit(&hspi1, buf, 2, 100);
DWT_Delay_us(1); // 添加延时
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}
其次是中断处理。CH375的中断线在U盘插入、数据收发等事件时都会触发,我们需要在中断服务函数中读取中断状态寄存器来区分事件类型:
c复制void EXTI0_IRQHandler(void) {
if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
uint8_t int_status = CH375_ReadReg(CH375_INT_STATUS);
switch(int_status) {
case USB_INT_CONNECT:
g_usb_status = USB_CONNECTED;
break;
case USB_INT_DISCONNECT:
g_usb_status = USB_DISCONNECTED;
break;
case USB_INT_SUCCESS:
g_usb_status = USB_READY;
break;
}
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}
}
3.2.2 FATFS文件系统适配
FATFS需要实现底层的磁盘IO接口。对于CH375,主要需要完成disk_initialize、disk_read等函数。这里特别要注意的是,CH375的扇区大小固定为512字节,而STM32的Flash编程要求32位对齐,因此需要中间缓冲区做转换:
c复制DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) {
uint8_t status;
for(UINT i=0; i<count; i++) {
CH375_WriteReg(CH375_CMD, CMD_DISK_READ); // 发送读命令
CH375_WriteReg(CH375_DATA, (sector+i) & 0xFF);
CH375_WriteReg(CH375_DATA, ((sector+i)>>8) & 0xFF);
CH375_WriteReg(CH375_DATA, ((sector+i)>>16) & 0xFF);
CH375_WriteReg(CH375_DATA, ((sector+i)>>24) & 0xFF);
status = CH375_WaitInterrupt();
if(status != USB_INT_SUCCESS) return RES_ERROR;
CH375_ReadBlock(buff + i*512, 512); // 读取512字节
}
return RES_OK;
}
实际测试发现,某些U盘的响应时间较长,需要将FATFS的_FS_TIMEOUT设置为5000以上。同时建议在f_mount()后立即调用f_getfree()检查可用空间,这能提前发现文件系统异常。
4. 核心功能实现细节
4.1 固件校验机制设计
安全可靠的升级必须包含完善的校验机制。我们采用三级校验策略:
- 文件头校验:检查固件前8字节是否为合法的ARM向量表(通常第一个字是栈指针,第二个字是复位向量);
- CRC32校验:计算整个文件的CRC值,与存储在文件末尾的预期值比对;
- 写后验证:写入Flash后,逐字节回读比对。
CRC校验使用STM32内置的CRC硬件加速器,相比软件实现速度提升显著:
c复制uint32_t Calculate_CRC32(uint8_t *data, uint32_t len) {
__HAL_RCC_CRC_CLK_ENABLE();
CRC->CR |= CRC_CR_RESET;
for(uint32_t i=0; i<len/4; i++) {
CRC->DR = *((uint32_t*)data + i);
}
// 处理非4字节对齐的剩余数据
if(len%4 != 0) {
uint32_t temp = 0;
memcpy(&temp, data + (len/4)*4, len%4);
CRC->DR = temp;
}
return CRC->DR;
}
4.2 Flash操作优化
STM32F103的Flash编程有几个关键限制:必须先擦除后写入、擦除最小单位为1KB页、写入必须按16位或32位进行。我们的Flash驱动做了以下优化:
- 擦除加速:不是全擦整个应用程序区,而是通过向量表判断已用区域,只擦必要的页;
- 缓冲写入:收集到512字节数据后才执行实际写入,减少Flash操作次数;
- 中断保护:在擦写期间禁用所有中断,避免Flash控制器被抢占。
以下是优化后的Flash写入函数:
c复制void Flash_Write_Optimized(uint32_t addr, uint8_t *data, uint32_t len) {
static uint8_t buffer[512]; // 写入缓冲区
static uint32_t buf_pos = 0;
static uint32_t current_addr = 0x08004000;
if(addr != current_addr + buf_pos) { // 地址不连续,立即写入缓冲数据
if(buf_pos > 0) {
FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, current_addr, *(uint32_t*)buffer);
// ... 写入剩余数据
FLASH_Lock();
}
current_addr = addr;
buf_pos = 0;
}
memcpy(buffer + buf_pos, data, len);
buf_pos += len;
if(buf_pos >= 512) { // 缓冲区满,执行写入
FLASH_Unlock();
for(int i=0; i<128; i++) { // 512/4=128个32位字
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, current_addr + i*4, *(uint32_t*)(buffer + i*4));
}
FLASH_Lock();
current_addr += 512;
buf_pos = 0;
}
}
5. 典型问题排查指南
5.1 U盘兼容性问题
在实测中发现,不同品牌U盘的兼容性差异较大。我们建立了以下排查流程:
- 电源检查:用示波器测量U盘VBUS电压,插入瞬间不应低于4.75V;
- 枚举日志:通过串口输出CH375的枚举过程,观察是否识别到正确的PID/VID;
- 信号质量:用逻辑分析仪抓取USB DP/DM信号,看眼图是否清晰。
兼容性最好的U盘品牌实测结果:
| 品牌 | 型号 | 枚举时间 | 备注 |
|---|---|---|---|
| 闪迪 | Cruzer Blade | 320ms | 兼容性最佳,推荐型号 |
| 金士顿 | DataTraveler | 450ms | 需格式化为FAT32 |
| 东芝 | TransMemory | 380ms | 对ESD敏感 |
5.2 升级失败分析
升级过程中可能遇到的典型故障及解决方法:
-
现象:升级后程序无法运行,但Bootloader正常
- 可能原因:向量表地址未正确配置
- 解决:检查应用程序的SystemInit()函数,确保VTOR寄存器指向0x08004000
-
现象:升级中途断电后设备变砖
- 预防措施:在Flash中保存升级进度,每次重启后继续
- 恢复方法:保留一个备份Bootloader区,通过特定IO组合触发恢复模式
-
现象:文件读取不全
- 诊断:在f_read()后检查f_error(&file)
- 优化:增大FATFS的_MAX_SS设置为512,添加f_sync()确保数据写入
6. 进阶功能扩展
6.1 安全加密升级
为防止固件被篡改,可以增加AES加密功能。升级文件在PC端用预共享密钥加密,Bootloader端解密:
c复制#include "mbedtls/aes.h"
void AES_Decrypt(uint8_t *input, uint8_t *output, uint32_t len) {
mbedtls_aes_context aes;
uint8_t key[16] = {0x01,0x23,...}; // 预共享密钥
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_dec(&aes, key, 128);
for(uint32_t i=0; i<len; i+=16) {
mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_DECRYPT, input+i, output+i);
}
mbedtls_aes_free(&aes);
}
6.2 断点续传功能
对于大容量STM32型号(如F103ZE),可以增加断点续传支持。在Flash中保存以下状态信息:
c复制typedef struct {
uint32_t file_size; // 文件总大小
uint32_t written_size; // 已写入大小
uint32_t crc; // 已写入数据的CRC
uint8_t reserved[16]; // 保留字段
} Update_Status;
每次重启后,先读取状态结构体,然后从断点处继续升级。这种方式特别适合工业现场可能出现的意外断电情况。
7. 生产测试方案
量产时需要验证每个设备的升级功能。我们设计了一个自动化测试流程:
- 测试固件生成:编译一个特殊的测试固件,包含已知的模式数据(如0xAA55AA55);
- 自动化脚本:Python脚本控制USB HUB插拔U盘,通过串口命令触发升级;
- 结果验证:读取Flash内容校验,同时测量升级耗时。
典型测试用例包括:
- 正常升级流程
- 升级中途断电恢复
- 异常文件处理(大小不符、CRC错误等)
- 压力测试(连续升级100次)
通过这个方案,我们实现了STM32F103设备的可靠固件升级,现场维护效率提升了80%以上。整个项目中最关键的经验是:USB Host的稳定性取决于电源质量和信号完整性,而Flash操作的可靠性则依赖于严格的时序控制和错误处理机制。