ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103外挂CH375B实现U盘扇区读写与调试实战

STM32F103外挂CH375B实现U盘扇区读写与调试实战 简介STM32F103驱动CH375B的优盘扇区读写工程附带容量检测功能面向需要操作U盘、SD卡等USB存储设备的嵌入式开发者与单片机学习者。这套驱动已在STM32F103RC上测试通过可实现单个及连续扇区读取/写入并能获取存储设备容量为数据记录、文件系统扩展等应用提供底层基础。压缩包共192个文件约6.65MB包含大量C源文件h、c、编译中间文件o、crf、d以及可烧录的hex/axf固件另有Keil工程配置uvprojx、uvoptx和znFAT文件系统相关代码便于直接打开工程理解调用关系或快速移植到自己的项目中。内容还覆盖CH375B芯片初始化流程、设备检测机制及常见错误处理思路适合正在调试USB存储设备、希望绕过复杂USB协议栈的开发者参考。已有1115人学习/下载是一份兼顾完整性与可读性的实用参考工程。 在嵌入式项目里遇到“要读写U盘”这个需求多数人第一反应是换一颗带USB OTG的单片机比如F407、H750。但如果你手里已经有一块STM32F103或者产品主控定死了不能换那外围挂一颗CH375B就是很务实的方案。我最近刚把“STM32F103驱动CH375B读写U盘扇区”这套代码调通读扇区、写扇区、测U盘容量都能跑而且写进去的数据读回来逐字节比对完全一致。这篇把硬件连接、命令流程、代码实现和调试时踩过的坑一起整理出来给同样被USB Host折腾过的人一个参考。这套东西适合谁一个是想在不换主控的前提下给产品加U盘交互功能的工程师另一个是刚接触CH375B、想跳过底层协议直接操作扇区的学习者。CH375B本质上是把USB Mass Storage的枚举、Bulk-Only传输、SCSI命令全部封装好了你只需要像操作普通外设一样给它发命令、收数据所以哪怕你对USB协议只有模糊概念也能在半天内把扇区读写跑起来。1. 为什么绕开STM32F103自带的USB外挂CH375B的思路与适用场景STM32F103全系列都带USB接口但这里有个硬限制F103的USB控制器只能做Device不能做Host。也就是说它可以扮演鼠标、键盘、虚拟串口这类从设备但没法主动去枚举U盘、发SCSI命令。想让它变成主机去读U盘通常有三条路换带OTG的型号、用软件模拟USB Host、外挂USB Host芯片。第一种要动硬件平台第二种对单片机主频和实时性要求太高基本不现实第三种就是本文的CH375B路线。CH375B这颗芯片的方案价值在于“把协议栈装进一颗小芯片”。它内部已经跑完了USB的物理层和协议层你感知不到U盘内部有多少端点、控制传输怎么发、CBW/CSW怎么打包。你需要做的只是通过并口或串口给它写命令字比如“初始化U盘”“读扇区”“写扇区”然后等它的中断引脚拉低再从数据口把结果搬回来。在决定用CH375B之前我也对比过另一颗常见芯片CH376二者核心差别是CH376额外内置了FAT文件系统可以直接按文件名读写文件而CH375B不带文件系统只能按扇区操作。对于只需要“读扇区、写扇区、测容量”这个需求CH375B更便宜、更纯粹而且从扇区往上构建文件系统逻辑的灵活性反而更大。比如你想做个坏块检测工具、U盘全盘清空工具或者自定义存储格式CH375B这种扇区级访问方式更合适。还有一个很现实的考虑CH375B的驱动代码在网上有大量参考但我强烈建议不要直接抄老版本代码就上板子尤其是命令时序部分不同版本固件的细节有差异。我后面会贴出我调试通过的思路和关键函数你可以对照数据手册核实而不是盲信网上任何一份代码。2. 硬件连接与接口模式选择并口比串口好在哪CH375B支持两种与MCU通信的接口8位并口和UART串口。两种模式通过TEN引脚的电平决定TEN拉低走并口TEN悬空或拉高走串口。我实际用的是并口模式原因很简单STM32F103的IO资源够用并口一字节一字节地搬数据速度明显比串口快而且并口时序用GPIO模拟起来并不复杂。2.1 最小连接方式我用的连接方式如下供参考数据线D0~D7接STM32F103同一组GPIO的低8位我用的是PB0~PB7A0命令/数据选择线接PB8WR写选通接PB9RD读选通接PB10CS片选接PB11INT中断输出接PB12并配置为下拉输入TEN接口模式选择直接接地锁定并口模式这套连接没有用到CH375B的DMA或硬件中断映射完全靠普通GPIO模拟。INT引脚是关键CH375B完成一条命令后会把INT拉低MCU在等待命令完成时只需要轮询这个引脚或者在外部中断里置标志位。我的演示代码用的是轮询加超时这样逻辑最简单也不容易漏事件。2.2 电平匹配与电源处理这里特别说下电平问题。CH375B是5V供电芯片但它的输入引脚兼容TTL电平STM32F103的3.3V高电平对CH375B来说是能被识别的高电平所以从F103到CH375B的写数据、写命令信号可以直接连不需要电平转换。反过来CH375B输出的高电平是5V直接进STM32的引脚会有风险。实际怎么处理数据手册写得很清楚CH375B的IO在输出时会按VCC电平输出如果VCC是5V那D0~D7在输出状态下也是5V高电平。所以要么在数据线上加电平转换芯片要么给CH375B的VCC单独供3.3V要么在CH375B输出引脚和F103输入引脚之间串限流电阻配合STM32引脚的钳位二极管来保护。我的板子上是给IO口串了330欧姆电阻实测稳定。如果是做产品建议用正规电平转换方案比如TXS0108E这类双向电平转换芯片别学我这么省。电源方面U盘插入瞬间冲击电流能到几百毫安CH375B的5V供电端最好就近放100uF以上电解电容再并一个0.1uF陶瓷电容去高频噪声。我一开始用面包板飞线U盘插上去经常初始化失败后来查了示波器才发现是插入瞬间5V被拉掉将近1V加了大电容之后问题消失。2.3 为什么最终选了并口串口模式只占用TX、RX两根线布线省很多但代价是CH375B把并口的8位数据拆成字节流从串口发底层传输效率天然低一截。对于扇区级的批量读写并口一个512字节扇区只需要拉512次数据总线串口要按波特率一位一位传差距非常明显。如果你只是偶尔读几KB数据串口也凑合但考虑到项目里还要“测容量”可能要连续读多个扇区来验证并口的优势就体现出来了。另外并口模式的数据手册时序图更明确软件操作步骤也更直观对新手来说反而更容易从逻辑上理清。3. CH375B的扇区命令集看懂这5条命令就够了CH375B内部固件分两层USB底层传输和文件系统层。文件系统层有按文件名打开、读写、关闭这类命令但既然标题是“可读写优盘扇区”我们用的是更底层的磁盘操作命令。这些命令直接调用CH375B固件封装好的SCSI命令本质上每次读写就是一次USB Bulk-Only传输。我调试中真正用到的命令其实就5条下面用表格列出来对应中断状态码一并标注方便排查问题命令字宏名称功能完成后中断状态码0x06CMD_CHECK_EXIST检测芯片是否存在无中断直接回读数据0x51CMD_DISK_INIT初始化U盘0x14成功/ 0x15失败0x53CMD_DISK_SIZE获取U盘总扇区数0x1C0x54CMD_DISK_READ读扇区0x1D0x56CMD_DISK_WRITE写扇区准备阶段0x1E0x57CMD_DISK_WRITE_GO写扇区执行阶段0x1D有一点要特别注意CH375B上电后必须先执行CMD_CHECK_EXIST确认芯片在线再执行CMD_DISK_INIT完成U盘枚举和容量读取然后才能做扇区操作。顺序乱了芯片会一直返回异常状态这不是代码逻辑问题是CH375B自身状态机的约束。3.1 命令分层的概念如果你看过CH375B的数据手册会发现它把命令分成“USB命令”和“文件级命令”两层。扇区命令属于前者它的特点是你不关心U盘里是FAT16、FAT32还是exFAT只要U盘能完成USB Mass Storage枚举命令就能执行。这跟那些要求U盘必须是FAT格式的“文件级命令”有本质区别。说白了扇区命令是纯硬件层面的操作U盘格式只是数据内容不影响读写流程。这个特性很有用。比如你想做“U盘全盘覆盖测试”无论是往里面写0x55、0xAA还是随机数据都不受文件系统限制。我在工程里就是先读0号扇区验证链路然后跳到最后几个扇区做一次写读比对用来确认容量计算是否正确。3.2 命令时序的通用套路所有命令的时序套路都差不多顺着这个思路看代码不会乱把CS拉低选中CH375B设置A0电平命令阶段A01数据阶段A00在WR上升沿写入命令字节或数据字节命令执行完成后INT引脚拉低MCU读取中断状态寄存器判断是成功还是失败准备好数据缓冲区按命令要求搬运数据把CS拉高结束本次操作前面提到等待INT有两种方式轮询和外部中断。我的演示工程里用的是“轮询加超时”原因是不想在中断里处理复杂的命令状态机而且U盘读写本身不算高速外设轮询完全来得及。实际测试中读一个扇区从发命令到数据全部取走大约在几毫秒级别这个速度对绝大多数应用完全够用。4. 核心函数拆解读扇区、写扇区、测容量怎么实现代码结构上我分了三层底层是GPIO模拟总线中间是CH375B命令封装顶层是演示测试逻辑。下面把关键函数贴出来讲注意这是STM32标准外设库写法用HAL库的读者对照改一下GPIO操作即可。4.1 底层总线函数先定义几个端口操作宏把数据线和控制线的操作收敛起来// 假设数据线接在GPIOB的低8位 #define CH375_DATA_OUT() { GPIOB-CRL ~0x000000FF; GPIOB-CRL | 0x00000033; } #define CH375_DATA_IN() { GPIOB-CRL ~0x000000FF; GPIOB-CRL | 0x00000044; } #define CH375_DATA_WR(d) { GPIOB-ODR (GPIOB-ODR 0xFF00) | (d 0x00FF); } #define CH375_DATA_RD() (uint8_t)(GPIOB-IDR 0x00FF) #define CH375_CS_L() GPIOB-BRR GPIO_Pin_11 #define CH375_CS_H() GPIOB-BSRR GPIO_Pin_11 #define CH375_WR_L() GPIOB-BRR GPIO_Pin_9 #define CH375_WR_H() GPIOB-BSRR GPIO_Pin_9 #define CH375_RD_L() GPIOB-BRR GPIO_Pin_10 #define CH375_RD_H() GPIOB-BSRR GPIO_Pin_10 #define CH375_A0_CMD() GPIOB-BSRR GPIO_Pin_8 #define CH375_A0_DAT() GPIOB-BRR GPIO_Pin_8 #define CH375_INT_GET() (uint8_t)(GPIOB-IDR GPIO_Pin_12)底层读写函数很简单// 写命令字节A01 void CH375_WR_CMD(uint8_t cmd) { CH375_DATA_OUT(); CH375_CS_L(); CH375_A0_CMD(); CH375_DATA_WR(cmd); CH375_WR_L(); CH375_WR_H(); CH375_CS_H(); } // 写数据字节A00 void CH375_WR_DAT(uint8_t dat) { CH375_DATA_OUT(); CH375_CS_L(); CH375_A0_DAT(); CH375_DATA_WR(dat); CH375_WR_L(); CH375_WR_H(); CH375_CS_H(); } // 读数据字节A00 uint8_t CH375_RD_DAT(void) { uint8_t dat; CH375_DATA_IN(); CH375_CS_L(); CH375_A0_DAT(); CH375_RD_L(); dat CH375_DATA_RD(); CH375_RD_H(); CH375_CS_H(); return dat; }这段代码里有个容易被忽略的点读数据前必须先把数据线切成输入模式否则STM32和CH375B两侧都在推电平轻则读回来全是0xFF重则长期挂载会损伤IO。CH375_DATA_IN和CH375_DATA_OUT这两个宏就是干这件事的初学者很容易漏掉。4.2 读扇区函数读扇区命令的输入参数是起始扇区的LBA地址4字节小端在前和扇区数量1字节发完命令后需要等中断然后进入数据搬运阶段uint8_t CH375_DiskRead(uint32_t start_sector, uint8_t sectors, uint8_t *buf) { uint8_t status, s; uint16_t len, i; CH375_WR_CMD(CMD_DISK_READ); CH375_WR_DAT((uint8_t)(start_sector 0xFF)); CH375_WR_DAT((uint8_t)((start_sector 8) 0xFF)); CH375_WR_DAT((uint8_t)((start_sector 16) 0xFF)); CH375_WR_DAT((uint8_t)((start_sector 24) 0xFF)); CH375_WR_DAT(sectors); if (CH375_Wait_Interrupt(200) ! 0) return 0x11; // 超时 status CH375_RD_DAT(); if (status ! USB_INT_DISK_READ) return status; // 读取数据包长度再取数据 CH375_WR_CMD(CMD_RD_USB_DATA); len CH375_RD_DAT(); len | (uint16_t)CH375_RD_DAT() 8; for (i 0; i len; i) { buf[i] CH375_RD_DAT(); } return 0; }注意两个细节一是CMD_RD_USB_DATA0x28其实是“从内部缓冲区读数据”的命令每次读数据前要先取长度这个长度的低字节在前二是对于连续多个扇区的读取CH375B一次中断可能只返回当前缓冲区长度所以如果要读多个扇区建议在应用层循环调用这个函数一次读一个扇区逻辑更稳。我正在读的时候常犯一个错一次读512字节直接认为缓冲长度一定是512。实际上CH375B的缓冲区机制允许USB包大小不同虽然标准U盘单个扇区读出来就是512字节但代码里做成“按返回长度搬数据”更健壮遇到特殊U盘也不会出问题。4.3 写扇区函数写扇区比读扇区多一步整个过程是“准备数据-写入U盘-确认结果”uint8_t CH375_DiskWrite(uint32_t start_sector, uint8_t sectors, uint8_t *buf) { uint8_t status; uint16_t i; CH375_WR_CMD(CMD_DISK_WRITE); CH375_WR_DAT((uint8_t)(start_sector 0xFF)); CH375_WR_DAT((uint8_t)((start_sector 8) 0xFF)); CH375_WR_DAT((uint8_t)((start_sector 16) 0xFF)); CH375_WR_DAT((uint8_t)((start_sector 24) 0xFF)); CH375_WR_DAT(sectors); if (CH375_Wait_Interrupt(200) ! 0) return 0x11; status CH375_RD_DAT(); if (status ! USB_INT_DISK_WRITE) return status; // 把扇区数据写入CH375B的发送缓冲区 CH375_WR_CMD(CMD_WR_USB_DATA); CH375_WR_DAT((uint8_t)(512 0xFF)); CH375_WR_DAT((uint8_t)((512 8) 0xFF)); for (i 0; i 512; i) { CH375_WR_DAT(buf[i]); } // 真正触发U盘写入 CH375_WR_CMD(CMD_DISK_WRITE_GO); if (CH375_Wait_Interrupt(500) ! 0) return 0x12; status CH375_RD_DAT(); if (status ! USB_INT_DISK_READ) return status; // 最终完成状态复用0x1D return 0; }写扇区容易翻车的点是CMD_WR_USB_DATA0x2D前的长度字节先写低字节再写高字节而且长度值要和实际扇区大小严格一致。如果你写了512却只搬了256字节数据后续的CMD_DISK_WRITE_GO会因为缓冲区数据不完整而卡死。我在调试时用逻辑分析仪抓过波形卡死现象就是INT引脚一直不拉低最后只能靠超时退出。4.4 容量探测函数CH375B获取U盘容量是通过CMD_DISK_SIZE命令成功后读4个字节得到的就是总扇区数容量就是扇区数乘以512uint8_t CH375_DiskGetSize(uint32_t *total_sectors) { uint8_t status; uint32_t sectors 0; CH375_WR_CMD(CMD_DISK_SIZE); if (CH375_Wait_Interrupt(200) ! 0) return 0x11; status CH375_RD_DAT(); if (status ! USB_INT_DISK_SIZE) return status; sectors (uint32_t)CH375_RD_DAT(); sectors | (uint32_t)CH375_RD_DAT() 8; sectors | (uint32_t)CH375_RD_DAT() 16; sectors | (uint32_t)CH375_RD_DAT() 24; *total_sectors sectors; return 0; }测容量时需要注意CH375B返回的扇区数是“U盘报告给主机的可用扇区数”通常和Windows里格式化后看到的“容量”一致。如果你想进一步验证容量读取是否可靠可以在拿到扇区总数后尝试读取最后一个扇区、写读最后一个扇区能通过基本说明容量计算和扇区寻址是对得上的。4.5 main里的测试流程测试流程我把步骤串成一个循环方便在调试串口里打印阶段性结果int main(void) { uint8_t buf[512]; uint32_t sectors, i; Delay_Init(); UART1_Init(115200); CH375_GPIO_Init(); printf(CH375B test start\n); // 第一步检测芯片 CH375_WR_CMD(CMD_CHECK_EXIST); CH375_WR_DAT(0xA5); if (CH375_RD_DAT() ! 0x5A) { printf(chip not found\n); while(1); } // 第二步初始化U盘 if (CH375_DiskInit() ! 0x14) { printf(disk init failed, check USB cable\n); while(1); } // 第三步读取容量 if (CH375_DiskGetSize(sectors) ! 0) { printf(get size failed\n); while(1); } printf(total sectors: %lu, size: %lu MB\n, sectors, sectors / 2048); // 第四步读0号扇区 if (CH375_DiskRead(0, 1, buf) ! 0) { printf(read sector 0 failed\n); while(1); } printf(sector 0 first 16 bytes:\n); for (i 0; i 16; i) printf(%02X , buf[i]); printf(\n); // 第五步写最后一个扇区再读回校验 for (i 0; i 512; i) buf[i] 0x5A; if (CH375_DiskWrite(sectors - 1, 1, buf) ! 0) { printf(write last sector failed\n); while(1); } memset(buf, 0, 512); if (CH375_DiskRead(sectors - 1, 1, buf) ! 0) { printf(re-read last sector failed\n); while(1); } for (i 0; i 512; i) { if (buf[i] ! 0x5A) { printf(data compare fail at %d\n, i); while(1); } } printf(ALL TESTS PASSED\n); while(1); }这个流程覆盖了“芯片通路-磁盘初始化-容量获取-扇区读写-数据完整性”五层验证任何一环出问题都能及时在串口输出定位。实际操作时建议把U盘先备份数据尤其是测试写最后一个扇区的步骤对有数据的U盘属于危险操作我这里用的是空闲盘。5. 实测结果与参数测试通过的标准是什么测试通过不是“屏幕上没报错”就算数我在实际验证时划了几道硬性标准任何一条不满足都算失败。5.1 测试环境硬件部分STM32F103C8T6最小系统板8MHz外部晶振CH375B模块USB转TTL调试串口U盘一个容量8GB文件系统是FAT32。芯片工作电压5VSTM32侧的IO口串了330欧姆电阻做保护。软件部分标准外设库工程编译采用Keil MDK5优化等级-O0方便在线调试时观察中间变量。5.2 测试结果实测数据如下芯片检测上电后发0xA5回读0x5A一次通过磁盘初始化U盘插入后执行CMD_DISK_INIT返回0x14耗时约300~500ms容量读取总扇区数返回15138784换算后容量约7.22GB与Windows下看到的FAT32可用空间吻合读扇区读取0号扇区前16字节打印出EB 58 90这是FAT32卷引导记录的标志说明数据链路正确写扇区往最后一个扇区写入512字节0x5A再读回比对逐字节一致无一位错误稳定性连续执行了100次“写-读-校验”循环全部通过没有出现超时或数据错乱测试时还有个意外发现某些U盘在命令间隔太短时容易出错比如连续紧挨着发CMD_DISK_INIT和CMD_DISK_SIZE偶发返回0x15。后来在两条命令之间加了10ms延时问题消失。这应该是U盘内部主控的响应时间问题跟CH375B关系不大但在应用层用循环读写扇区时建议每次命令之间留一点间隔别压得太死。6. 踩过的坑排查U盘识别失败和读写不稳定的通路如果这部分早点写出来我至少能省两天的调试时间。下面按我实际踩坑的顺序来记录排查思路比最终答案更重要。6.1 上电时序与复位信号第一次上板调试最经典的现象是“芯片检测通过但CMD_DISK_INIT永远返回失败”。我一开始怀疑U盘坏了换了好几个都是同样问题后来查了CH375B数据手册才发现问题不在U盘而在复位时序。CH375B的复位引脚需要拉低至少30ms再释放释放之后还要等固件初始化完成这个时间大概在几十毫秒量级。我之前的代码里复位给得太短芯片还没准备好就发了CMD_DISK_INIT自然失败。修复办法是上电后先拉低RST引脚100ms再拉高再延时50ms之后才发第一条命令。这个时间参数可以在工程里做成宏方便后续调整。6.2 电源稳定性U盘插入瞬间的电流冲击是最大隐患。面包板飞线阶段我遇到过“插入U盘后芯片直接复位”的现象用示波器看是5V电源被拉低到3.2V左右。解决办法前面提过在CH375B模块的电源输入端加100uF电解电容加0.1uF陶瓷电容。但还有一个隐蔽问题如果CH375B模块通过杜邦线从STM32板子上取5V线太长太细线阻会把电压跌落问题放大。后来我改成从USB转TTL模块的5V输出单独给CH375B供电地线共地U盘初始化成功率明显提升。做产品的话建议CH375B和U盘座共用一路5V电源用粗线或铺铜连接。6.3 命令超时处理CH375B在U盘未插入、U盘故障、USB线松脱等情况下命令会一直处于等待状态。如果代码里只做“等待INT拉低”的死等整个系统就会卡死在CH375B相关函数里。调试串口没有任何输出连锤子都懒得敲。我在CH375_Wait_Interrupt函数里加了超时参数单位为毫秒超时直接返回错误码。这样U盘异常时程序能走完整个错误处理流程把状态打印出来。这个方法强烈建议保留后续接RTOS或者状态机时也会用得上。uint8_t CH375_Wait_Interrupt(uint16_t timeout_ms) { while (timeout_ms--) { if (CH375_INT_GET() 0) { return 0; } Delay_Ms(1); } return 0xFF; }6.4 兼容性不是所有U盘都能一把过CH375B毕竟是把U盘当成标准Mass Storage设备来枚举市面上主控芯片五花八门偶尔会遇到兼容性比较差的U盘。我测了四个U盘三个顺利通过一个老款2GB的金士顿在CMD_DISK_INIT阶段偶发失败重试几次又好了。对这个问题最实用的处理方式是在应用层加“初始化失败重试”机制。CH375B的数据手册也建议对磁盘操作命令做重试通常重试2到3次就能把偶发错误消化掉。我现在的工程里CMD_DISK_INIT失败后自动重试最多重试3次每次间隔50ms。实测四个U盘全部能在重试机制下正常初始化。另外要提醒的是如果U盘是exFAT或者NTFS格式扇区命令依然可以读写因为扇区命令不涉及文件系统。但如果你只是想读写文件那就必须用FAT16/FAT32格式。这个区分在需求评审阶段就要想清楚不然交付的时候容易被问住。7. 一点经验补充扇区操作只是起点往上是文件系统我最初做这个项目时以为“能读扇区”就完事了后来发现在工业现场客户往往更关心“能不能把数据按文件名存到U盘里”。扇区读写是地基文件系统是房子。CH375B这颗芯片在文件级操作上有现成的命令比如CMD_FILE_OPEN、CMD_FILE_WRITE等但那是另一套API依赖U盘是FAT分区。如果后续有文件级需求建议先在扇区层把“读0号扇区、解析BPB、找到FAT表位置”这套逻辑跑通这样你对U盘的存储结构会有一个很直观的认识再上手CH375B的文件级命令时会轻松很多。我踩过的一个坑是直接把网上其他人写的“文件读写库”拉进工程结果FAT表解析和CH375B固件版本对不上文件创建出来是坏的。最后是老老实实读数据手册把扇区层验证扎实了才逐步把文件功能加上去。关于测容量还有一个实用心得拿到扇区总数后别急着信用“读最后一个扇区”来验证寻址范围是低成本且有效的手段。如果能读通最后一个扇区说明整个寻址范围内基本是健康的如果读不通多半是容量计算有问题或者U盘有坏道。这个检查手段对“测U盘容量”这个需求来说比单纯拿命令返回值更有说服力。本文还有配套的精品资源点击获取
返回列表