1. 项目背景与需求解析
最近在做一个嵌入式显示项目,用到了华芯微特的显示屏模组。官方提供的例程虽然实现了将外部Flash模拟成U盘的功能,但只能通过电脑端操作来存放UI图片。而我们的实际需求是要通过U盘直接烧写各种多媒体文件(图片、视频等)到外部Flash中。
这个需求在工业HMI、智能家居控制面板等场景很常见。比如:
- 产线工人需要快速更新设备界面
- 现场维护时需要更换宣传视频
- 系统升级时需要批量更新资源文件
传统做法要么需要拆机用烧录器,要么得接串口慢慢传,效率都很低。而U盘烧写方案就像给设备装了个"USB接口",几分钟就能完成文件更新。
2. 硬件方案设计
2.1 核心硬件选型
项目使用的是华芯微特SWM32系列MCU,具体型号为SWM320RET6。关键外设配置:
- USB 2.0全速接口(12Mbps)
- SPI接口连接的外部Flash(W25Q128JVSIQ,16MB容量)
- 480x272 RGB接口显示屏
提示:选择W25Q128是因为其兼容性好,且16MB容量足够存放大量UI资源。SPI时钟建议设置在30MHz以内以保证稳定性。
2.2 存储架构设计
plaintext复制[U盘]
│
├─ USB Host (SWM320)
│ │
│ ├─ FATFS文件系统
│ │
│ └─ SPI Flash (W25Q128)
│
└─ [显示屏]
这种架构下,MCU需要同时处理:
- USB主机协议栈
- FAT文件系统操作
- Flash存储管理
- 显示刷新控制
3. 软件实现详解
3.1 工程文件结构调整
首先需要整理工程目录,建议采用如下结构:
code复制Project/
├── Drivers/
│ ├── USB/
│ │ ├── usbh_user.c
│ │ └── usbh_user.h
├── Middlewares/
│ └── FATFS/
│ ├── diskio.c
│ └── ffconf.h
└── Src/
└── main.c
3.2 关键文件修改
3.2.1 usbh_user.c 配置
这个文件需要实现USB主机的回调函数。重点修改:
c复制// USB连接状态回调
void USBH_User_Process(USBH_HandleTypeDef *phost, uint8_t id) {
switch(id) {
case HOST_USER_CONNECTION:
printf("USB Device Connected\n");
break;
case HOST_USER_DISCONNECTION:
printf("USB Device Disconnected\n");
break;
}
}
// 添加Mass Storage设备支持
USBH_ClassTypeDef USBH_MSC = {
"MSC",
USBH_MSC_InterfaceInit,
USBH_MSC_InterfaceDeInit,
USBH_MSC_ClassRequest,
USBH_MSC_Process,
USBH_MSC_SOFProcess,
NULL
};
3.2.2 diskio.c 适配
需要实现Flash的底层驱动接口:
c复制DSTATUS disk_initialize(BYTE pdrv) {
if(pdrv == 1) { // 假设1号磁盘是外部Flash
W25Qxx_Init();
return RES_OK;
}
return RES_ERROR;
}
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) {
if(pdrv == 1) {
W25Qxx_Read(buff, sector*512, count*512);
return RES_OK;
}
return RES_ERROR;
}
注意:Flash的擦除块大小通常为4KB,写操作前必须先擦除。建议在disk_write函数中添加擦除判断逻辑。
3.2.3 main.c 主流程
c复制int main(void) {
// 硬件初始化
System_Init();
LCD_Init();
USBH_Init(&hUsbHostFS, USBH_User_Process, 0);
// 挂载文件系统
f_mount(&USBHFatFS, "1:", 1);
while(1) {
USBH_Process(&hUsbHostFS);
// 检测U盘插入
if(USBH_MSC_IsReady(&hUsbHostFS)) {
process_usb_files();
}
}
}
4. 核心功能实现
4.1 U盘文件自动同步
实现当插入U盘时,自动将特定目录下的文件拷贝到Flash:
c复制void process_usb_files(void) {
DIR dir;
FILINFO fno;
// 打开U盘根目录
if(f_opendir(&dir, "1:/") == FR_OK) {
while(f_readdir(&dir, &fno) == FR_OK && fno.fname[0]) {
if(strstr(fno.fname, ".bmp") || strstr(fno.fname, ".jpg")) {
copy_file_to_flash(fno.fname);
}
}
f_closedir(&dir);
}
}
4.2 Flash存储管理
建议采用如下存储结构:
code复制Flash地址布局:
0x000000 - 0x0FFFFF : 系统固件
0x100000 - 0x1FFFFF : 图片资源
0x200000 - 0x3FFFFF : 视频资源
0x400000 - 0xFFFFFF : 预留空间
文件索引表设计:
c复制#pragma pack(1)
typedef struct {
char name[32];
uint32_t addr;
uint32_t size;
uint16_t width;
uint16_t height;
} ResourceEntry;
#pragma pack()
5. 实战经验与优化技巧
5.1 性能优化方案
- 双缓冲机制:
c复制#define BUF_SIZE 4096
uint8_t buf1[BUF_SIZE], buf2[BUF_SIZE];
void copy_file_with_double_buff(FIL* fp) {
uint32_t bytes_read;
uint8_t *active_buf = buf1;
f_read(fp, active_buf, BUF_SIZE, &bytes_read);
while(bytes_read > 0) {
// 启动DMA传输当前缓冲区
start_flash_write(active_buf);
// 立即读取下一块到备用缓冲区
active_buf = (active_buf == buf1) ? buf2 : buf1;
f_read(fp, active_buf, BUF_SIZE, &bytes_read);
// 等待前一次写入完成
wait_flash_write_finish();
}
}
- 文件校验机制:
c复制uint32_t calculate_crc(FIL* fp) {
uint32_t crc = 0xFFFFFFFF;
uint8_t buf[256];
UINT br;
f_lseek(fp, 0);
while(f_read(fp, buf, sizeof(buf), &br) == FR_OK && br > 0) {
for(int i=0; i<br; i++) {
crc ^= buf[i];
for(int j=0; j<8; j++) {
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
}
}
}
return ~crc;
}
5.2 常见问题排查
- U盘识别失败:
- 检查USB数据线质量(建议使用带屏蔽的短线)
- 测量VBUS电压(应在4.75-5.25V之间)
- 确认USB主机模式配置正确
- 文件拷贝中途失败:
- 检查Flash擦除是否充分(建议先全片擦除测试)
- 降低SPI时钟频率测试(特别是长线连接时)
- 增加写入超时判断
- 显示花屏:
- 检查图片格式是否匹配(RGB565/RGB888)
- 确认Flash内容没有位翻转(建议启用ECC)
- 检查DMA传输是否完整
6. 扩展功能实现
6.1 增量更新机制
通过维护版本文件实现增量更新:
c复制// version.txt 格式示例
[version]
main=1.0.2
res=1.0.5
[files]
img/logo.bmp=2023-05-20
video/intro.mp4=2023-05-18
比较算法:
c复制bool need_update(const char* filename, time_t usb_time) {
time_t flash_time = get_file_time_from_flash(filename);
return (usb_time > flash_time);
}
6.2 安全验证方案
- 数字签名验证:
c复制bool verify_signature(FIL* fp, const uint8_t* pub_key) {
uint8_t sig[256];
uint8_t hash[32];
f_lseek(fp, 0);
calculate_sha256(fp, hash);
f_read(fp, sig, sizeof(sig), NULL);
return rsa_verify(pub_key, hash, sig);
}
- 文件加密存储:
c复制void encrypt_file(FIL* src, FIL* dst, const uint8_t* key) {
uint8_t iv[16] = {0};
AES_CTX ctx;
AES_set_key(&ctx, key, 128);
uint8_t buf[16];
UINT br;
while(f_read(src, buf, sizeof(buf), &br) == FR_OK && br > 0) {
AES_cbc_encrypt(&ctx, buf, buf, br, iv);
f_write(dst, buf, br, NULL);
}
}
在实际项目中,我强烈建议添加进度显示功能。可以通过在拷贝每个文件后更新界面进度条,或者在LCD上显示当前操作的文件名。这不仅能提升用户体验,在出现问题时也能快速定位到故障点。
关于Flash寿命问题,W25Q128的典型擦写次数是10万次。如果更新频繁,建议:
- 实现磨损均衡算法
- 对静态资源区设置为只读
- 记录每个块的擦除次数
最后分享一个调试技巧:在USB枚举阶段,可以用逻辑分析仪抓取USB DP/DM信号,配合USB协议分析软件(如WireShark的USB插件)可以直观看到枚举过程,快速定位通信问题。