拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F103驱动CH375B实现U盘扇区读写与容量检测

简介针对STM32F103与CH375B的存储设备驱动方案这份压缩包完成优盘扇区级读写与容量检测并已测试通过适合嵌入式开发者、电子竞赛选手及需要底层存储操作的项目参考。包内共192个文件大小6.65MB包含37个h头文件、34个c源文件、34个o目标文件、34个d依赖文件、33个crf编译列表以及清理脚本、MDK工程文件、烧录文件、链接映射与汇编列表等其中头文件负责接口与寄存器声明源文件是驱动主体中间文件用于编译调试工程和固件文件则可直接打开与烧录覆盖从源码编译到下载调试的完整链路。已有1115人学习下载实测结果提升了方案的可信度。资源除了基于znFAT文件系统的读写示例还给出SPI/I2C方式初始化CH375B、设备识别与状态查询、扇区擦写与容量识别等关键代码并附有工程配置与测试结果可帮助读者快速移植到自己的STM32F103板卡省去从芯片手册逐条对照的摸索过程。 前阵子整理硬盘翻出一个压箱底的工程STM32F103驱动CH375B读写U盘扇区还带U盘容量检测当时跑通之后打了个zip包收了起来。这个工程解决的实际痛点很多工程师都遇到过——嵌入式设备想直接读写U盘用来升级固件、导出数据、做离线日志但STM32F103内置USB外设只有Device模式只能连电脑当不了主机接U盘根本不现实。最省事的方案就是外挂一颗CH375B让MCU通过并行接口把读写命令丢给它由它完成USB主机侧的复杂协议和时序。想复现或者参考的朋友这份工程值得过一遍里面包含完整的扇区读写流程、容量检测逻辑和已测试通过的验证代码整体不算复杂但坑不少我把关键内容整理出来。1. 项目背景与方案选型1.1 三个核心能力拆解整个工程围绕三个功能点展开读U盘扇区、写U盘扇区、测U盘容量。这三件事听起来简单对应到底层其实是完全不同的操作路径。读扇区是USB主机向U盘发送CBW指令块请求返回指定LBA地址的数据写扇区则是主机发送写命令然后把512字节数据通过Bulk端点发给U盘。这两个操作在CH375B内部被封装成了命令码程序员只需要按手册顺序发送命令和参数即可。容量测试更直接CH375B提供了一条专门命令返回U盘的总扇区数乘以512字节就是总容量。值得注意的是这套工程工作在“扇区层”不涉及FAT文件系统。也就是说你看到的是裸扇区数据不是一个个文件。这个定位非常重要——扇区读写是一切上层应用的地基FAT文件系统、固件升级协议、自定义存储格式都要在这层之上实现。先玩透扇区后面移植FatFs或者做量产工具都顺理成章。1.2 为什么选CH375B而不是其他方案市面上STM32做USB Host的方案其实有几种我对比了一段时间才定了CH375B。第一个方案是STM32F105/F107等带OTG功能的型号走标准USB Host协议栈这是“名门正派”但F103系列没有OTG外设想用必须换芯片涉及整板改动成本高。第二个方案是纯软件USB Host用GPIO模拟USB时序看起来省钱实际上对定时要求极其苛刻还要处理复杂的USB协议和枚举过程调试周期不可控量产稳定性也存疑。CH375B的方案选择了“外包”USB物理层、协议层、枚举过程全部由CH375B完成MCU只负责并行接口上的命令交互。STM32F103做主机CH375B做协议转换电平匹配也简单代码量被压缩到极小。同类芯片CH376虽然自带文件系统指令可以直接按文件名读写文件但API更复杂扇区级控制能力反而弱一些。对于“先把底层吃透”的开发目标CH375B更合适。2. CH375B硬件连接与时序要点2.1 并行接口信号一览CH375B支持并行和串行两种主机接口工程里用的是并行8位模式。核心信号如下D0到D7是8根双向数据线A0是命令/数据选择线低电平写命令、高电平读写数据WR和RD分别是读写选通CS是片选INT是中断输出芯片有事件时拉低MCU可以查询也可以接外部中断。还有一个关键引脚是RST复位脚。CH375B的复位时序比较讲究上电后需要至少等待芯片内部复位完成最好在软件里再做一次软复位命令。多数CH375B模块板上已经带了RC复位电路但为了保证初始化成功率软件复位还是建议加上。这里面最容易理解错的是A0的作用。很多人把它当成寄存器地址的低位去操作其实它只区分命令口和数据口。MCU对CH375B的操作永远只有两种向命令口写命令字向数据口写参数或读数据。理解了这一点看代码就会非常清晰。2.2 推荐接线方式FSMC还是GPIO模拟STM32F103与CH375B的接线有两条路线。如果用的芯片带FSMC如F103RC/RD/RE可以把CH375B当作外部SRAM设备挂在总线上用地址线的某一位映射A0发送命令就是往某个内存地址写字节写数据就是往另一个地址写字节速度很快代码最简洁。缺点是引脚占用多还需要配置FSMC时序寄存器对于没有FSMC的F103C8T6最小系统板则完全不可用。工程最终选择的是GPIO模拟方式。用一组GPIO接D0到D7再用几个普通GPIO接A0、CS、WR、RD、INT。操作时先配置GPIO输出方向把引脚电平按WR/RD时序拉低拉高完成读写。这种方式的优点是兼容所有F103型号代码直观移植方便缺点是速度比FSMC慢但U盘扇区操作本来就是低频场景几百KB的读写速度完全够用。GPIO模拟的关键是时序顺序。以写命令为例必须先置A0为低、片选拉低、数据线放上命令字再拉低WR保持一段时间后拉高最后片选拉高。这个顺序反了或者中间不加延时芯片可能收到错的命令字。实际代码中用几个__NOP()空指令就能满足CH375B微秒级的建立时间。2.3 电平匹配问题3.3V与5V怎么接CH375B是5V供电芯片而STM32F103是3.3V系统电平匹配是绕不开的问题。从STM32往CH375B方向3.3V的高电平已经超过CH375B输入高电平门槛直接连接通常没问题。但从CH375B往STM32方向5V供电的芯片输出高电平可能接近VCC直接进STM32的GPIO需要确认引脚是否5V tolerant。稳妥的做法是CH375B的数据线和INT线接入STM32前串一个330欧姆电阻限流或者使用电平转换芯片。多数手册和开发板都说明可以直接连接实际测试中很多工程也确实是直连的但从可靠性角度串电阻几乎不增加成本能避免IO口被长期过压损伤。特别是INT脚如果接外部中断建议加个电阻防止上电瞬间的电平抖动干扰MCU。3. 核心代码实现从命令封装到扇区读写3.1 底层命令封装让MCU和CH375B对话要操作CH375B第一步是封装最基本的几个原语写命令、写数据、读数据、等待中断。这四个函数是整个驱动的基石后面的磁盘初始化、扇区读写全部基于它们。// 写命令A00数据线放命令字WR产生低脉冲 void CH375_WrCmd(uint8_t cmd) { CH375_CS_LOW(); CH375_A0_LOW(); // A00选择命令端口 CH375_WR_HIGH(); GPIO_Write(CH375_DATA_PORT, cmd); delay_us(1); CH375_WR_LOW(); delay_us(1); CH375_WR_HIGH(); CH375_CS_HIGH(); } // 写数据A01数据线放数据 void CH375_WrData(uint8_t dat) { CH375_CS_LOW(); CH375_A0_HIGH(); // A01选择数据端口 CH375_WR_HIGH(); GPIO_Write(CH375_DATA_PORT, dat); delay_us(1); CH375_WR_LOW(); delay_us(1); CH375_WR_HIGH(); CH375_CS_HIGH(); } // 读数据A01RD拉低后从数据线读取 uint8_t CH375_RdData(void) { uint8_t dat; CH375_CS_LOW(); CH375_A0_HIGH(); // A01数据端口 CH375_RD_HIGH(); GPIO_Write(CH375_DATA_PORT, 0xFF); // 先把输出切掉数据线释放 delay_us(1); CH375_RD_LOW(); delay_us(1); dat (uint8_t)(GPIO_ReadInputData(CH375_DATA_PORT) 0xFF); CH375_RD_HIGH(); CH375_CS_HIGH(); return dat; }读数据函数里有个细节特别容易踩坑读之前一定要确保数据口是输入模式或者先向GPIO数据寄存器写一个全1再切换输入模式。如果GPIO输出数据还停留在上一次写入的数值数据线上可能被钳位读回来的内容会和实际不符。建议封装一个CH375_DataPort_SetDir(dir)函数在写命令/写数据前切输出在读数据前切输入。3.2 磁盘初始化流程从设备连接到枚举完成CH375B上电后不会自动进入USB主机模式初始化必须按顺序走完这几步先软复位芯片再检查芯片是否在线然后设置USB主机模式之后等待U盘插入、枚举、挂载。uint8_t CH375_DiskInit(void) { uint8_t st; uint16_t retry 1000; // 1. 软复位 CH375_WrCmd(CMD_RESET_ALL); // 0x05 delay_ms(20); // 2. 检查芯片是否存在写0x55读回0xAA CH375_WrCmd(CMD_CHECK_EXIST); // 0x06 CH375_WrData(0x55); if (CH375_RdData() ! 0xAA) { return ERR_CH375_NOT_FOUND; } // 3. 设置USB主机模式 CH375_WrCmd(CMD_SET_USB_MODE); // 0x15 CH375_WrData(0x06); // 0x06 主机模式 if (CH375_RdData() ! CMD_RET_SUCCESS) { return ERR_MODE_SET_FAIL; } // 4. 等待U盘连接自动模式插U盘会产生中断 while (retry--) { st CH375_WaitInterrupt(500); if (st USB_INT_CONNECT) { break; } if (st USB_INT_DISCONNECT) { return ERR_DISK_CONNECT; } } if (retry 0) return ERR_DISK_CONNECT; // 5. 发送磁盘连接命令确认设备就绪 CH375_WrCmd(CMD_DISK_CONNECT); // 0x30 st CH375_WaitInterrupt(2000); if (st ! USB_INT_SUCCESS) return ERR_DISK_CONNECT; // 6. 挂载U盘完成枚举和扇区配置 CH375_WrCmd(CMD_DISK_MOUNT); // 0x31 st CH375_WaitInterrupt(2000); if (st ! USB_INT_SUCCESS) return ERR_DISK_MOUNT; return ERR_SUCCESS; }等待中断函数的实现也要多说一句。CH375B通过INT引脚低电平通知MCU“有事件”MCU读取中断状态后事件清除。等待时超时控制必须有否则U盘没插好或兼容性差时MCU会一直卡死在循环里。我习惯把所有等待中断都加上超时参数超时返回0xFF上层根据上下文判断。3.3 扇区读取流程与代码扇区读取是整个工程最核心的功能。CH375B按LBA地址定位扇区一次可以连续读取多扇区但每一扇区的数据都是通过Bulk端点分次返回MCU需要循环读取。uint8_t CH375_ReadSector(uint32_t lba, uint8_t *buf, uint8_t count) { uint8_t st; uint16_t i; uint32_t timeout; CH375_WrCmd(CMD_DISK_READ); // 0x54读命令 // 写LBA地址低字节在前 CH375_WrData((uint8_t)(lba 0xFF)); CH375_WrData((uint8_t)((lba 8) 0xFF)); CH375_WrData((uint8_t)((lba 16) 0xFF)); CH375_WrData((uint8_t)((lba 24) 0xFF)); // 写扇区数0表示256个扇区 CH375_WrData(count); // 执行读取 CH375_WrCmd(CMD_DISK_RD_GO); // 0x55 // 每个扇区都会产生一次DISK_READ中断 while (count--) { st CH375_WaitInterrupt(1000); if (st ! USB_INT_DISK_READ) { return st; // 读中断不正确直接返回错误码 } // 连续读取512字节 for (i 0; i 512; i) { *buf CH375_RdData(); } } // 查询本次操作最终状态 CH375_WrCmd(CMD_DISK_QUERY); // 0x58 st CH375_WaitInterrupt(1000); if (st ! USB_INT_SUCCESS) { return st; } return ERR_SUCCESS; }读扇区命令有几个细节要注意。LBA地址是四字节小端序和常见的uint32内存布局刚好一致直接移位就能得到。扇区数参数是单字节取值1到255如果想一次读256个扇区就写0。但实际测试不建议一次读太多CH375B内部缓冲区有限大批量读取时建议拆成8个或16个扇区一组循环读稳定性更好。另一个容易忽略的点是读取中途如果等待中断超时返回错误后必须重新发送DISK_QUERY查询状态并清掉残留中断标志否则下一次读操作可能会被上一次的残留状态污染。3.4 扇区写入流程与代码写扇区和读扇区是对称流程区别在于读扇区是“发命令→等数据→收数据”写扇区是“发命令→等请求→传数据→查询状态”。数据方向反过来但代码结构几乎一样。uint8_t CH375_WriteSector(uint32_t lba, uint8_t *buf, uint8_t count) { uint8_t st; uint16_t i; CH375_WrCmd(CMD_DISK_WRITE); // 0x56写命令 // LBA地址低字节在前 CH375_WrData((uint8_t)(lba 0xFF)); CH375_WrData((uint8_t)((lba 8) 0xFF)); CH375_WrData((uint8_t)((lba 16) 0xFF)); CH375_WrData((uint8_t)((lba 24) 0xFF)); CH375_WrData(count); // 每个扇区会收到一次DISK_WRITE中断表示芯片准备好接收数据 while (count--) { st CH375_WaitInterrupt(1000); if (st ! USB_INT_DISK_WRITE) { return st; } // 连续写入512字节 for (i 0; i 512; i) { CH375_WrData(*buf); } } // 查询最终状态 CH375_WrCmd(CMD_DISK_QUERY); st CH375_WaitInterrupt(1000); if (st ! USB_INT_SUCCESS) { return st; } return ERR_SUCCESS; }写扇区要特别注意U盘是只读还是正常状态。如果U盘开了写保护写命令会返回错误码且错误发生在数据传输完成后的查询阶段。另外写操作是有破坏性的——测试写扇区时一定要避免直接覆盖MBR、FAT表等关键区域。我在测试中通常写U盘最后一个扇区因为那部分大概率是空闲区域而且方便验证容量的边界是否正确。还有一种更稳的测试做法先读原扇区数据保存到数组写入测试数据回读校验最后把原数据写回去。流程多几步但安全性提升一个量级。3.5 容量检测一条命令拿到总扇区数容量检测是这些功能里最简单的CH375B专门提供了CMD_DISK_SIZE命令。执行后从数据口连续读取四个字节就是U盘的总扇区数小端序。总容量等于总扇区数乘以512字节。uint32_t CH375_GetDiskSize(void) { uint8_t b[4]; CH375_WrCmd(CMD_DISK_SIZE); // 0x59 b[0] CH375_RdData(); b[1] CH375_RdData(); b[2] CH375_RdData(); b[3] CH375_RdData(); return (uint32_t)b[0] | ((uint32_t)b[1] 8) | ((uint32_t)b[2] 16) | ((uint32_t)b[3] 24); }拿到总扇区数之后换算容量就是纯粹的数学问题。例如返回15158780扇区总字节数就是15158780乘以512等于7760961536字节约7409MB对应标称8GB的U盘。U盘标称容量按1000进制计算实际容量按1024进制计算8GB标称实际7.4GB左右这个差异是正常的看到容量“缩水”不用慌。容量检测还能用来识别扩容盘。如果怀疑U盘是刷了假容量参数的扩容盘可以读最后一个扇区也就是LBA等于总扇区数减一的扇区。能正常读出来且数据地址校验合理基本说明容量真实如果读超时或返回错误很可能是虚标容量。4. 实测记录与排错手册4.1 测试环境和实测结果我在测试时用的是STM32F103C8T6最小系统板加CH375B模块的组合ST-Link下载程序CH340串口模块打印日志U盘是一块普通的8GB闪迪U盘格式化成FAT32。串口波特率115200打印驱动初始化和扇区读写关键节点信息。上电后串口输出如下整个过程从复位到完成容量读取大约2秒左右CH375B init start... CH375B check ok, mode set ok. USB device connected. Disk connect ok. Disk mount ok. Total sectors: 15158780 Disk size: 7409 MB (7.4 GB) Read sector 0: 0x55AA found, MBR OK. Read sector 100, compare OK. Write last sector test, back-read match OK.测试中我做的是扇区数据读写回环验证先记录原始数据写入固定模式数据重新读出来比对确认完全一致后再恢复原始数据。这样既验证了读写正确性又没有破坏U盘原有内容。4.2 常见问题速查表实际调试中遇到的问题五花八门我整理成了一张速查表遇到问题可以直接对照排查。现象可能原因排查方向插U盘无任何中断未进入主机模式 / 供电不足检查CMD_SET_USB_MODE参数是否0x06测5V电流换USB线卡在DISK_MOUNTU盘枚举慢 / 兼容性差增大等待超时换U盘检查DM/DP走线读扇区返回0x1F错误LBA超出容量 / 扇区数参数理解错误确认LBA范围确认count参数0表示256个扇区写扇区失败写保护 / 写入只读区域检查U盘写保护开关避开MBR和FAT区域读到的数据全是0xFF数据口方向未切换读数据前确认GPIO已切输入模式容量数值异常大或异常小字节序解析错误确认四个字节是小端序扇区总数乘以512才是字节数偶发初始化失败上电时序问题软件复位后增加延时多次重试初始化4.3 容易被忽略的几个细节坑第一个坑是GPIO方向切换。STM32的GPIO在读数据前必须从推挽输出切换成浮空输入而且切换后要等几个时钟周期再拉低RD否则读到的第一个字节不稳定。我最初的代码就是漏了这一步导致每次读回的数据第一位总是错。第二个坑是CH375B的复位时序。上电后不要急着发命令建议延时50毫秒以上再软复位复位后同样延时一段时间。如果芯片初始化失败最有效的恢复手段是完整走一遍掉电再上电而不是单纯软件复位。这个在批量测试时遇到过后来在代码里加了初始化重试机制三次失败后才报错问题才彻底解决。第三个坑是U盘供电。CH375B模块上有5V输出脚给U盘供电U盘启动瞬间电流可能达到几百毫安如果USB母座前的电容太小心电压跌落会导致U盘枚举失败。建议在5V电源线上加一个100微法以上电容位置靠近USB座。这个看似和驱动无关的硬件细节实际上决定了U盘识别成功率。第四个坑是等待中断函数的状态机。CH375B中断事件和命令一一对应如果上一次命令的超时中断没有清干净会影响下一次命令。我的做法是在每次命令开始前主动发送一次CMD_DISK_QUERY或者CMD_GET_STATUS把残留状态读走保证状态机干净。代价是每次操作多几十微秒换来的是稳定可靠。最后再分享一个小技巧如果你的项目后续要做完整的U盘文件读写建议在扇区层之上挂一套FAT文件系统FatFs就是很成熟的选择把disk_read和disk_write两个底层函数替换成我们刚才实现的扇区读写函数往上就能按文件名操作了。如果嫌移植麻烦也可以直接换CH376芯片它自带文件系统命令按文件名读写底层不需要自己管代价是控制粒度变粗。我个人更推荐先玩透CH375B这一层扇区操作毕竟USB协议栈和磁盘协议的基本功是在这个层面上真正练出来的。这套代码我后来在好几个量产项目里复用改改引脚定义就能跑收益相当可观。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门