简介:本资源是一套基于STM32F103单片机实现U盘文件读写功能的完整嵌入式开发DEMO例程,面向嵌入式初学者、课程设计学生及硬件工程师,解决USB主机模式下外接U盘进行FAT文件系统操作的核心技术难点。压缩包共286个文件,含56个头文件(.h)定义硬件接口与协议结构、52个C源文件(.c)实现CH376 USB主控芯片驱动、SPI通信、OLED显示、I2C外设控制及主循环逻辑,另有.o、.d、.crf等编译中间文件及Keil工程(.uvproj)、可执行镜像(.hex/.axf)等,总大小8.01MB,开箱即用。已有893人学习下载,配套代码结构清晰,包含CH376初始化、U盘识别、文件创建/读取/写入全流程,并集成OLED状态显示、触摸按键交互与继电器控制等扩展模块,便于理解USB Host协议栈与嵌入式文件系统移植的关键环节。 做嵌入式数据记录的项目,最常被甲方追问的一句话就是:数据怎么拿出来?SD卡方案做了好几个,但现场的人手里最不缺的就是U盘。我最近正好把这套需求整理成了一个能直接跑起来参考的DEMO:单片机上通过U盘读写文件,核心是STM32F103外接USB Host芯片,让单片机自己去枚举U盘、在FAT32文件系统里建文件、写数据、再读出来。这篇文章把代码架构、硬件连接、调试验证过程都摊开讲,从原理到底层代码再到踩坑,给想在项目里加U盘读写功能的人一个可以直接抄作业的模板。
1. 先把USB Host这个前提掰扯清楚:F103自身干不了这件事
1.1 USB Device和USB Host是两个相反的方向
你打开STM32F103的数据手册,里面确实有一个USB外设,但它叫USB Device控制器。它的工作是让单片机扮演一个“外设”:模拟成串口、模拟成键盘、模拟成U盘让电脑读写,这个方向上单片机是被主机枚举的对象。而“单片机去读写U盘”完全是另一回事,单片机要主动发起枚举、发送IN/OUT令牌、维护传输调度,简单说就是充当USB主机,也就是Host角色。F103内部的USB外设根本没有Host控制器逻辑,也没有端点和事务调度能力,所以光靠F103是无法直接插U盘的。
很多人第一次看到“STM32F103 U盘读写”这类标题时,会以为把U盘往板子上一插就能用,这是最大的认知误区。如果非要在纯F103上实现主机功能,只有一条路:外扩一颗USB Host控制芯片,让这颗芯片干USB协议层的活,F103通过SPI或并行总线去命令它。所以这套DEMO严格来说,是F103加CH376的联合方案,F103负责业务逻辑和文件系统,CH376负责USB传输和U盘通信。
1.2 三种常见实现方案怎么选
做U盘读写,嵌入式圈子里比较常见的路线大致有三种,我做了个对比表:
| 方案 | 硬件成本 | 代码量 | 写文件速度 | 适合场景 |
|---|---|---|---|---|
| STM32F103 + CH376(SPI从模式) | 低,模块十几元 | 低,官方库可参考 | 几十KB/s级别 | 数据导出、日志记录、配置导入 |
| STM32F103 + CH374(并口) | 中,占用引脚多 | 中,操作繁琐 | USB 1.1全速 | 老产品维护、对引脚不敏感的项目 |
| 换STM32F105/F107或F4系列 | 芯片成本略高,开发量大 | 高,需自己跑USB Host协议栈 | USB FS/HS,速度高 | 大批量传输、高速存储、复杂应用 |
我最终选CH376,理由很实在:F103已经很熟了,CH376把USB Host协议、Mass Storage类、甚至一部分FAT文件操作都固化了,单片机这边的代码量能压到很低。对于大多数数据采集设备,几十KB/s的写入速度完全够用,没必要为速度去折腾USB OTG协议栈。
1.3 这个DEMO适合放在什么场景里用
这版DEMO适合的场景很明确:设备离线导出数据、仪器仪表配置导入、固件升级时把bin文件放在U盘里让单片机自己刷。这类产品有几个共同点:数据量不大、写入不频繁、现场操作人员不太懂技术。用U盘导出比串口线省事太多,也比SD卡更通用。
不适合的场景也要说清楚:如果设备要连续录制音视频、实时写大量数据,或者要求每秒写入几MB,那F103加CH376这套组合就别考虑了,老老实实换带USB HS的高端平台。
2. DEMO的整体结构:文件系统放在哪一层,决定你能走多远
2.1 让CH376管文件,还是让F103管文件
CH376这颗芯片有点特殊,它固件里自带FAT16/FAT32的文件管理,所以有两条路可以走。
第一条路:直接用CH376的文件级API。CH376FileOpen、CH376FileWrite、CH376FileClose,这些函数直接对文件名操作,单片机不用管FAT表、目录项,代码特别简单。缺点是文件逻辑锁死在CH376的库和固件里,想加日志轮转、自定义目录结构、处理异常边界条件,都得看库的脸色。
第二条路:把CH376当裸扇区设备,在STM32F103上跑FatFS。diskio底层通过CH376的扇区读写命令和U盘打交道,上层用标准f_open、f_write、f_read,文件系统控制权完全在自己手里。这比我一开始用CH376文件API多了几百行代码,但后面扩展的空间大得多,排查问题时每一层都能单独打日志验证。
我这个DEMO选的就是第二条路。原因很简单:一旦文件系统跑在自己这边,以后要改文件名规则、要按日期建目录、要做掉电保护,都是FatFS的配置问题,不用再依赖芯片库。而且FatFS的中文资料和案例很多,团队接手成本低。
2.2 工程目录的组织方式
工程结构是这样的:
UdiskDemo/ ├─ App/ │ ├─ main.c // 系统初始化、主循环 │ ├─ udisk_demo.c // U盘读写业务逻辑 │ └─ debug_uart.c // 串口调试打印 ├─ Hardware/ │ ├─ ch376/ │ │ ├─ ch376_hal.c // SPI读写、GPIO控制、中断读取 │ │ └─ ch376_msc.c // CH376扇区读写封装 │ └─ spi/ │ └─ spi1.c ├─ Middlewares/ │ └─ FatFs/ // FatFS源码和diskio接口 └─ Project/ └─ udisk_demo.uvprojxCH376底层我单独隔离成一层,这个决定在后来的调试中帮了大忙。出了问题,先看SPI通信是否正常,再看CH376命令是否应答,最后才怀疑FatFS,每一层都有清晰的边界。
2.3 上电到文件落盘的主流程
主循环的节奏是这样的:
- 系统时钟、GPIO、串口、SPI初始化,读取CH376版本号确认SPI通信正常。
- 等待U盘插入,CH376收到设备连接事件后进入下一步。
- 对U盘执行挂载操作,获取扇区大小和总扇区数。
- 调用f_mount挂载FatFS文件系统。
- 创建或打开目标文件。
- 写入数据,每写一块调用f_sync。
- 关闭文件,等待拔盘或进入下一次写入。
这个流程我建议不要乱改顺序。先确认基础通信,再接触USB协议,最后才碰文件系统,每一步都有前置验证,出了问题定位会非常快。
3. 硬件连接:CH376不是插上就能用的,这三个细节必须做对
3.1 接线表与电平匹配
CH376 SPI从模式的标准接线如下:
| STM32F103引脚 | CH376引脚 | 说明 |
|---|---|---|
| PA4 | CS# | 片选,低有效,软件控制 |
| PA5 | SCK | SPI时钟 |
| PA6 | MISO | CH376输出给STM32 |
| PA7 | MOSI | STM32输出给CH376 |
| 任意GPIO | INT# | 命令完成中断,低电平有效 |
| 3.3V | VCC | 给CH376供电 |
| GND | GND | 共地 |
注意,CH376V是3.3V版本,可以直接和F103对接。如果拿到的是CH375,那是5V版本,单片机这边要考虑电平转换,别直接往上插。我一开始用的是CH376模块,板上已经带好晶振和USB座,省了不少事。
3.2 USB口供电和D+/D-走线
这个细节坑了很多人。U盘在插入瞬间电流很大,VBUS如果由LDO直接供电,LDO输出能力不够的话,电压会被瞬间拉低,CH376枚举U盘直接失败,表现就是插上U盘没反应,代码反复超时。
正确做法是在USB座的VBUS引脚附近放一颗100uF电解电容或钽电容,再加一颗0.1uF陶瓷电容去耦。如果用的是现成CH376模块,先确认模块上的电容是否齐全。D+/D-是差分信号线,用杜邦线连接时尽量短,别超过20cm,飞线太长容易在高速信号下丢包。
3.3 SPI速率和上电时序的实测经验
F103的SPI1在72MHz主频下最高能到18MHz,但CH376 SPI从模式实际测试下来,稳定工作最好控制在2MHz以内,尤其是用杜邦线连接的时候。我第一次做的时候按习惯分了频就跑18MHz,版本号读出来很正常,但一写大文件就随机出错,折腾了整整两天才发现是SPI速率问题。后来把主频72MHz下的预分频改成64,实际SPI时钟约1.125MHz,问题彻底消失。
另外上电后不要立刻发命令,要给CH376外接晶振一点起振时间。我代码里在读取版本号之前加了约100ms的延时,实测稳定。如果你用的是其他Host芯片,这个时序要重新验证。
4. 核心代码链路:从SPI发一个字节到U盘里出现一个文件
4.1 第一步永远是读取CH376版本号
SPI初始化的代码是标准库风格的,主机模式、8位数据、软件NSS。时钟极性和相位我配置的是Mode0,也就是CPOL为低、CPHA为第一个边沿采样,不过CH376到底工作在Mode0还是Mode3,不同模块可能不一样,如果通信不对就四种模式都试一遍,看读回的版本号即可判断。
void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }初始化之后,第一件事就是读CH376的版本号,用它判断SPI通信链路是否正常。如果读回来的全是0x00或者0xFF,别急着查U盘,先查SPI接线、电平匹配、供电,这些基础问题不解决,后面全白搭。
uint8_t CH376_GetICVersion(void) { uint8_t version; CH376_CS_LOW(); CH376_WriteByte(CMD_GET_IC_VER); version = CH376_ReadByte(); CH376_CS_HIGH(); return version; }如果版本号读出来是有效值,说明SPI物理链路通了,可以往下走。
4.2 FatFS的diskio层怎么对接CH376
FatFS要跑起来,需要实现disk_status、disk_initialize、disk_read、disk_write、disk_ioctl这几个函数。其中disk_read和disk_write是对接CH376的核心。
DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; if (count == 0) return RES_PARERR; if (CH376_DiskRead(sector, count, buff) == CH376_ERR_SUCCESS) return RES_OK; return RES_ERROR; }CH376的底层扇区读写一次可以连续操作多个扇区,但实际使用时要注意单片机的RAM容量,一次读太多扇区,缓冲数组可能放不下。而且底层等待中断不能无限死等,必须加超时判断。我用的超时是100ms,超过就返回错误并打印日志。
disk_ioctl里要返回扇区大小和总扇区数,这里我直接用CH376挂载后获取到的数据。FAT32格式化后的U盘扇区大小通常是512字节,CH376可以正常返回。
4.3 应用层读文件和写文件的调用方式
文件层就比较清爽了,和普通FatFS用法完全一样。写文件的示例:
FIL file; FRESULT res; UINT bw; res = f_open(&file, "0:/LOG001.TXT", FA_CREATE_ALWAYS | FA_WRITE); if (res == FR_OK) { f_write(&file, data_buf, data_len, &bw); f_sync(&file); f_close(&file); }注意这里的“0:/”盘符,对应FatFS的第0卷,也就是挂在CH376上的U盘。如果后面还挂了SD卡,SD卡就是第1卷。
追加写入的写法也很常用,设备记录日志时每次开机都要往同一个文件末尾追加,而不是覆盖。这时候要用FA_OPEN_ALWAYS,然后再用f_lseek把读写指针移到文件末尾:
res = f_open(&file, "0:/LOG001.TXT", FA_OPEN_ALWAYS | FA_WRITE); if (res == FR_OK) { f_lseek(&file, f_size(&file)); f_write(&file, data_buf, data_len, &bw); f_sync(&file); f_close(&file); }读文件就更直接了,f_open用FA_READ,然后f_read按块读出来处理。要注意f_read的返回值,如果文件比预期短,读到的字节数会小于请求大小,这时要判断是不是读到文件结尾了。
5. 我的调试记录:四个翻车现场与排查思路
5.1 f_mount返回FR_NOT_READY,U盘插了等于没插
这个现象我在论坛上见过无数次,自己也遇到过。f_mount返回值是FR_NOT_READY,说明底层报告设备没有准备好,但U盘明明是插着的。
排查链路我是这样走的:
- 先确认SPI层没问题:读CH376版本号,如果乱码,回到硬件查SPI。
- 再看CH376有没有收到U盘插入事件:检查INT引脚是否触发,读取状态寄存器。
- 测VBUS电压:接上U盘后电压有没有被拉低,如果LDO发烫或者电压掉到4V以下,就是供电问题。
- 换一个旧一点的正品U盘测试。
最后发现大多数情况是供电问题或者U盘兼容性问题。CH376这类芯片对U盘还是有点挑的,一些新出的U盘、国产品牌的小容量盘反而容易卡在枚举阶段。代码层面能做的就是做好日志,把错误码记录下来,至少让现场知道问题出在哪一步。
5.2 SPI偶发乱码,写出来的文件内容错位
这个坑最隐蔽。文件能创建、能写,但读回来发现个别字节不对,或者文件内容错位。看起来像是FatFS写错了,其实问题出在SPI传输上。
排查思路:先把SPI时钟降到最低,比如预分频256,如果问题消失,说明是速率问题。再把接线改短,特别是GND回流线,杜邦线太长会导致信号完整性变差。我的案例里,降低SPI速率后问题就没了,后来重新画板时把线长控制在5cm以内,速率提到2MHz也没再出问题。
经验是:怀疑SPI丢字节时,读版本号没用,因为版本号读取是固定短指令,不一定触发问题。真正可靠的做法是连续读写一块随机数据,比如向CH376的RAM缓冲区写入256字节再读回比对,循环1000次,能暴露问题。
5.3 文件写一半卡死,复位后文件还在
写到一半程序卡死,这个现象最让人崩溃。复位后文件还在,说明数据已经写入了一部分,问题出在写入过程中。
我定位到的根因是等待中断的死循环。CH376执行完一条命令后通过INT引脚通知单片机,如果上一条命令没有正常完成,INT可能不触发,程序就在等中断的死循环里出不来了。或者上一条命令没完成就灌了下一条命令,CH376命令队列就乱套了。
解决方案是给所有等中断的地方加超时:
uint8_t CH376_WaitINT(uint16_t timeout_ms) { uint16_t cnt = 0; while (CH376_INT_READ() != 0) { delay_ms(1); cnt++; if (cnt > timeout_ms) { return CH376_ERR_TIMEOUT; } } return CH376_ERR_SUCCESS; }另外命令之间加一点小延时,比如1ms,给CH376一点处理时间,不要连续高速往里面灌命令。
5.4 直接拔盘之后,文件丢失或大小是0
这个现象在功能测试里很常见。程序写文件没报错,但直接拔U盘插到电脑上看,文件没了或者大小是0字节。
原因很简单:FatFS的数据还在文件系统缓冲区里,没有落盘。FAT表项、目录项、数据块都还没写入U盘,U盘就被拔掉了。解决思路有两个层面:
代码层面,每写一个数据块就调一次f_sync,把FatFS缓冲区的内容刷到U盘。虽然会牺牲一点速度,但对数据安全来说值得。如果写的是连续日志,可以在写完一个完整记录之后f_sync,不用每字节都刷。
设计层面,最好做一个“安全退出”机制。设备上放一个按键或者串口命令,拔出U盘前先按一下,程序把文件关闭、文件系统卸载,然后提示“可以拔出”。这个处理对工业现场尤其重要,操作人员不会管你底层有什么缓存。
6. 这个DEMO的边界在哪里,后续可以怎么扩展
6.1 它能做什么,做不到什么
这版DEMO能做到的事情:自动枚举U盘、挂载FAT32文件系统、创建文件、追加写、读取文件、写后落盘保障。
做不到的事情也要认清。首先是速度,USB 1.1全速加SPI通信,实际写文件速度在几十KB/s级别,拷一个几十MB的文件会非常煎熬。其次是文件系统格式,FatFS本身支持exFAT,但CH376的老固件对exFAT兼容性存疑,我实际测试还是FAT32最稳,建议出厂默认格式化FAT32,簇大小用默认值。最后是热插拔安全,虽然CH376支持插拔检测,但写文件过程中突然拔盘依然有损坏数据的风险,必须在设计上规避。
6.2 值得扩展的几个方向
第一个方向是日志轮转。文件写到一定大小自动新建一个文件,按日期或序号命名,避免单个文件无限增长。这个用FatFS的f_size配合f_rename就能实现,代码量不大但实用性很强。
第二个方向是配置导入。用U盘放一个config.ini或者配置文件,设备启动时自动读取并应用配置,对批量部署设备特别方便,不用挨个连串口下发。
第三个方向是固件升级。把bin文件放U盘,单片机读取后写入内部Flash,再跳转到BootLoader执行。这个需要和BootLoader配合,处理起来要仔细,尤其是Flash写入不够的机型要先考虑双Bank机制。
第四个方向是SD卡和U盘共存。我现在的diskio层已经有pdrv参数了,
本文还有配套的精品资源,点击获取