公司动态
STM32F103外接CH376实现U盘读写:从原理到代码的完整方案
简介本资源是一套基于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 CH376SPI从模式低模块十几元低官方库可参考几十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引脚说明PA4CS#片选低有效软件控制PA5SCKSPI时钟PA6MISOCH376输出给STM32PA7MOSISTM32输出给CH376任意GPIOINT#命令完成中断低电平有效3.3VVCC给CH376供电GNDGND共地注意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层怎么对接CH376FatFS要跑起来需要实现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_READYU盘插了等于没插这个现象我在论坛上见过无数次自己也遇到过。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参数了本文还有配套的精品资源点击获取