利用STM32F103RC的USB将W25Q64模拟成U盘
在工业控制、智能设备和自动化测试场景中,我们常常遇到一个看似简单却颇具挑战的问题:如何让一台没有传统存储接口的嵌入式设备,像U盘一样被PC直接识别并读写?更进一步,如果这个“U盘”还能预装固件、记录日志、甚至支持批量配置分发,那它的价值就远不止一个移动硬盘那么简单。
这正是虚拟U盘技术的魅力所在。通过微控制器模拟标准USB大容量存储设备(Mass Storage Class, MSC),我们可以把外挂Flash芯片伪装成一个即插即用的可移动磁盘。而今天要讲的方案—— 用STM32F103RC驱动W25Q64实现虚拟U盘 ——不仅成本极低,而且兼容性极强,是很多量产项目中的“隐形功臣”。
为什么选STM32F103RC?
你可能会问:现在有那么多带USB的MCU,为什么要用这款“老将”?毕竟它属于早已发布的STM32F1系列,主频72MHz,128KB Flash、20KB RAM,在当下动辄几百MHz主频、内置高速SRAM的新平台面前显得有些寒酸。
但正是这种“够用就好”的定位,让它在性价比敏感的应用中依然坚挺。更重要的是, 它原生支持USB 2.0全速设备模式(12Mbps) ,配合成熟的HAL库或LL库,可以快速搭建MSC应用。不需要额外PHY芯片,也不需要复杂的PCB布局——PA11/PA12引脚直连D+/D-即可。
不过这里有个关键细节容易被忽略: STM32F103RC内部没有集成D+上拉电阻 。这意味着你必须通过一个GPIO控制外部1.5kΩ电阻连接到3.3V电源,才能向主机宣告“我来了”。代码里通常是这样操作:
// 模拟上拉,触发枚举
HAL_GPIO_WritePin(USB_PULLUP_PORT, USB_PULLUP_PIN, GPIO_PIN_SET);
一旦这一步没做,PC根本不会发现设备的存在,调试时只能看到“未知USB设备”或者干脆无反应。
W25Q64:小身材,大用途
作为存储介质,W25Q64是一款典型的SPI Flash芯片,容量8MB(64Mbit),采用SOP8封装,仅需CS、SCK、MOSI、MISO四根信号线即可通信。它的优势在于价格低廉(几元人民币)、非易失性、擦写寿命高达10万次,非常适合用于存放固件镜像或运行日志。
但要注意,Flash不是RAM,不能随意写入。每次写之前必须先擦除,且最小擦除单位是4KB扇区,而写入单位是256字节页。如果你试图在一个未擦除的区域写数据,结果只会是“掩码叠加”,旧数据并不会消失。
因此,在实现文件系统时必须做好抽象层管理。比如当主机下发一个512字节的写请求时,我们的处理流程应该是:
- 锁定对应的LBA地址;
- 读取整个4KB扇区到缓冲区;
- 在缓冲区中修改目标512字节;
- 擦除原扇区;
- 将更新后的4KB数据重新写回。
虽然听起来繁琐,但在实际应用中可以通过合理设计簇大小来减少这类操作频率。
协议栈是怎么跑起来的?
当你把板子插入PC,背后其实是一场精密的“对话”。整个过程从USB枚举开始,然后进入SCSI命令交互阶段。PC并不关心你是真U盘还是假U盘,它只认标准协议。
首先,设备发送自己的描述符,声明自己是一个
大容量存储类设备(bInterfaceClass = 0x08)
。接着,主机发起一系列SCSI Inquiry命令获取设备信息,例如厂商名、产品型号、是否可移除等。这些都可以在
USBD_Storage_fopsTypeDef
结构体中自定义:
static int8_t USER_GetInquiryData(uint8_t lun, uint8_t *data) {
const char info[] = "STMicroelectronics";
memcpy(data, info, strlen(info));
return USBD_OK;
}
确认身份后,主机会查询容量:
int8_t USER_GetCapacity(uint8_t lun, uint32_t *block_num, uint16_t *block_size) {
*block_num = 16384; // 8MB / 512B per sector
*block_size = 512;
return USBD_OK;
}
这里的512字节扇区是硬性要求——无论底层Flash物理页多大,对外都必须模拟成标准块设备。这也是FatFs能正常工作的前提。
接下来就是真正的读写操作了。每当用户拖拽文件到设备,Windows就会发出
WRITE(10)
命令,携带LBA地址和数据长度。STM32接收到后,调用注册的
USER_Write
函数,最终落地到SPI Flash。
FatFs:嵌入式文件系统的“瑞士军刀”
说到文件系统,大多数人第一反应是FAT。没错,尽管它古老,但它几乎被所有操作系统原生支持,无需安装驱动。对于8MB的小容量设备,使用FAT12或FAT16最为合适。
我们采用ChaN开发的 FatFs R0.14 ,这是一个轻量级、模块化、高度可移植的开源库,专为资源受限系统设计。它不依赖操作系统,只需实现几个底层接口就能跑起来。
核心文件是
diskio.c
,其中最关键的是五个函数:
DSTATUS disk_initialize(BYTE pdrv);
DSTATUS disk_status(BYTE pdrv);
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count);
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);
它们构成了FatFs与硬件之间的桥梁。比如
disk_ioctl(GET_SECTOR_COUNT)
返回总扇区数,
CTRL_SYNC
用于确保写操作完成。
特别提醒一点: 不要在中断上下文或USB回调中直接调用SPI读写 !否则可能导致时序冲突或死锁。建议将数据拷贝到缓冲区后再异步处理,必要时加入互斥锁保护共享资源。
实际开发中的坑与对策
1. PC不识别设备?
最常见的原因是
USB描述符配置错误
。尤其是
bDeviceClass
和
bInterfaceClass
字段:
-
bDeviceClass = 0x00(指定由接口决定) -
bInterfaceClass = 0x08(MSC类) -
bInterfaceSubClass = 0x06(SCSI透明命令集) -
bInterfaceProtocol = 0x50(Bulk-Only Transport)
任何一个出错,主机都会拒绝加载MSC驱动。
2. 写入失败或文件损坏?
检查是否遗漏了
扇区擦除步骤
。Flash特性决定了“先擦后写”是铁律。另外,编译器优化等级过高可能打乱SPI时序,建议对关键函数加
__attribute__((optimize("O1")))
降级优化。
3. 插拔几次后变砖?
多半是电源问题。USB总线供电能力有限,而W25Q64在编程时瞬态电流可达8mA以上。若系统电源滤波不足,会造成MCU复位或Flash写入异常。推荐在VCC处并联10μF电解电容 + 0.1μF陶瓷电容。
4. 文件系统频繁崩溃?
可以考虑预先烧录一个干净的FAT12镜像进Flash起始位置。使用工具如
fatgen103.pdf
规范生成MBR、DBR、FAT表和根目录,确保首次插入即能被正确识别。也可以启用FatFs的
_USE_MKFS
宏,在初始化时自动格式化。
如何提升稳定性与用户体验?
- 加入DMA传输 :SPI使用DMA可大幅降低CPU占用率,尤其在连续读写大文件时效果明显。
- 设置写保护开关 :通过GPIO检测物理跳线或软件标志位,防止误删关键数据。
- 支持热插拔检测 :监听USB断开事件,及时释放资源,避免下次连接失败。
- 统一VID/PID :使用ST官方默认值(0x0483/0x5740)可提高兼容性,避免杀毒软件误报。
- 日志分区隔离 :将用户数据区与系统日志区分开,便于维护和恢复。
它能在哪些地方发光?
这个方案已经在多个实际场景中证明了自己的价值:
- 产线烧录工具 :工人只需将模块插入PC,自动弹出U盘,里面放着对应机型的固件包,双击即可安装。
- 仪器数据导出 :医疗设备运行过程中持续记录日志到Flash,关机后拔下核心板,就像取出一张U盘。
- 教学实验平台 :学生不仅能学会SPI、USB协议,还能深入理解MBR、BPB、FAT表等底层结构,比单纯调API有意义得多。
甚至有人把它做成“伪装U盘”,插入电脑后自动执行特定任务(当然要合法合规使用)。
还能怎么升级?
未来优化方向还有很多:
- 引入简单的 磨损均衡算法 ,延长Flash寿命;
- 使用 TinyUSB 替代HAL库的USB堆栈,代码更精简,资源占用更低;
- 增加RTC模块,为每个文件打上时间戳,打造微型数据记录仪;
- 改造成复合设备,同时具备U盘 + 虚拟串口功能,方便调试;
- 支持双分区:一个只读区放固件,一个可读写区供用户存配置。
这种将普通MCU变身标准外设的设计思路,充分体现了嵌入式开发中“以软补硬”的智慧。用不到十元的成本,换来跨平台即插即用的能力,不仅是技术上的胜利,更是工程性价比的典范。当你亲手做出第一个能被Windows自动识别的“自制U盘”时,那种成就感,或许才是驱动无数工程师深夜敲代码的最大动力。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
180

被折叠的 条评论
为什么被折叠?



