☰
STM32F103驱动CH375B实现U盘扇区读写完整方案
2026/10/16 10:08:12 网站建设 项目流程

简介:面向嵌入式开发者的STM32F103+CH375B USB存储驱动资源,其核心解决通过CH375B芯片对U盘、SD卡执行扇区级读写与容量检测的需求,尤其适用于需要低层磁盘操作的数据记录、文件系统移植等场景。压缩包共192个文件,约6.65MB,包含37个.h头文件与34个.c源文件,覆盖底层驱动及znFAT文件系统调用;另有Keil工程配置(uvprojx/uvoptx)、编译中间产物(o/d/axf/hex)、链接映射文件(map/lst/sct)以及bat脚本和txt说明文档,便于完整编译调试和二次开发。目前已有1122人学习下载。资源实测通过,提供完整的初始化、设备检测、扇区读写、容量检测和错误处理逻辑,代码结构清晰,关键注释详实。通过学习和移植该驱动,可快速掌握CH375B命令交互流程、SPI/I2C接口配置及扇区操作细节,为U盘读写、SD卡数据记录等应用提供可直接复用的基础模块。

1. 用 STM32F103 驱动 CH375B 读写 U 盘扇区:一套能直接抄的驱动方案

在嵌入式设备上读写 U 盘,最怕的就是协议栈又大又难调。CH375B 这颗芯片把 USB Host 的底层差事全包了,STM32F103 只需要通过并行总线发命令、收数据,就能直接读写 U 盘扇区、读出容量。这份资源我拆过好几遍,包含完整的初始化序列、扇区读写命令封装、容量测试源码,实测能在 72MHz 主频下稳定跑通 FAT16/FAT32 的 U 盘。它适合做数据采集终端、工业参数备份器、离线固件升级器的开发者——不想碰 USB 协议栈,只想让设备能插上 U 盘就干活的人。读完这篇笔记,你能拿这套驱动直接改自己的板子,也能避开那些让 U 盘“只识别不读写”的暗坑。

2. CH375B 的硬件与协议:从并行总线到 U 盘扇区读写的原理

2.1 CH375B 是什么:为什么选它而不是 CH376 或 USB Host 库

CH375B 是带 USB Host 接口的芯片,专门负责把 U 盘这种 USB 存储设备转成 8 位并行接口或者 UART 串口。它内部固化了 USB 底层协议、Mass Storage 类的 Bulk-Only 传输,还有 FAT 文件系统的部分处理能力。你不需要关心端点、描述符、SCSI 命令这些黑匣子,只需要按它的命令手册发字节。

对比 CH376,CH375B 更纯粹——CH376 支持文件级操作(直接建文件、写文件),CH375B 主推扇区级操作,但扇区级反而更适合做底层的事情,比如坏块扫描、全盘镜像、容量测试。如果你要做文件系统层的高级功能,常见的做法是在 CH375B 之上自己跑 FatFs,但如果你只是想把 U 盘当成一个可插拔的块设备,CH375B 的扇区读写命令已经够了,代码量比 CH376 小得多。

也有人用 STM32 自带的 USB Host 库直接驱动 U 盘,但那样要腾出 USB 引脚,还要被协议栈的 Buffer 管理缠住,跑在大容量 U 盘上容易出现枚举超时。CH375B 方案相当于把 USB Host 从主控里摘出来,主控只做命令交互,稳定性明显更高。我们量产的一款数据记录仪就是用的这个方案,三年没在 USB 枚举上翻过车。

2.2 硬件连线与供电:3.3V/5V 电平、复位电路和电容参数

CH375B 的典型供电是 5V,但它的 I/O 引脚可以兼容 3.3V,只要你的 STM32F103 系统电压是 3.3V,并且 CH375B 的 VCC 接 5V,那么两者的逻辑电平是匹配的,不需要额外电平转换。如果你是 3.3V 单供电,也可以给 CH375B 的 VCC 供 3.3V,只是芯片内部 USB 收发器的驱动能力会弱一点,对线缆长度敏感,建议控制在 15cm 以内。

关键的连线如下:

  • D0~D7:接 STM32F103 的 GPIOB 0~7,并配置为开漏输出,外部上拉到 3.3V,因为 CH375B 数据线是双向的。
  • A0:命令/数据选择线,接一个 GPIO。A0=0 时读写数据,A0=1 时读写命令。
  • CS、WR、RD:片选、写使能、读使能,分别接三个 GPIO。
  • INT:中断请求输出,接 EXTI 外部中断引脚,低电平有效。

复位电路别省。CH375B 的 RST 引脚需要至少 50us 的低电平复位脉冲,同时要求上电后电源稳定。我一般用一个 GPIO 控制复位,比 RC 复位可靠,方便软件随时复位 CH375B:

#define CH375_RST_PORT GPIOA #define CH375_RST_PIN GPIO_PIN_12 static void CH375_Reset(void) { GPIO_WriteBit(CH375_RST_PORT, CH375_RST_PIN, Bit_SET); // 先拉高 delay_us(20); GPIO_WriteBit(CH375_RST_PORT, CH375_RST_PIN, Bit_RESET); // 拉低复位 delay_ms(20); // 保持低电平时间要足够 GPIO_WriteBit(CH375_RST_PORT, CH375_RST_PIN, Bit_SET); // 释放复位 delay_ms(30); // 等待内部固件启动 }

电源滤波要注意:CH375B 的 VCC 和 V3 引脚都要接 0.1uF 陶瓷电容,VCC 还要并联一个 10uF 电解电容。V3 引脚是内部 3.3V 稳压输出,如果 VCC 是 5V,V3 接 0.1uF 电容;如果 VCC 直接供 3.3V,V3 必须悬空。这个细节不少人搞错,3.3V 供电时给 V3 接了电容,反而导致内部稳压器异常,U 盘识别率大幅下降。

2.3 命令/数据双缓冲:CH375B 的 5 条核心命令

CH375B 的编程模型非常简单:先选中芯片,A0=1,写入命令码;然后 A0=0,写入或读出数据。所有操作都是 8 位并行,不需要 DMA,因为每次传输的数据量不大,主控 GPIO 翻转速度足够。

真正干活的核心命令只有 5 条:

命令码含义参数
0x15设置 USB 主机模式无
0x22检查 U 盘是否连接无
0x90读 U 盘物理扇区3 字节地址 + 1 字节扇区数
0x91写 U 盘物理扇区3 字节地址 + 1 字节扇区数
0x92获取 U 盘容量无

注意:CH375B 的扇区地址是 3 字节,最大寻址 0xFFFFFF,也就是 16MB 个扇区,对应 8GB(按 512 字节扇区算)。超过 8GB 的 U 盘,容量寄存器会溢出,需要用命令 0x93 读取扩展容量,我后面会专门讲这个坑。

每条命令执行后,CH375B 都会产生中断请求,主控需要读状态寄存器(命令码 0x22 也可以读状态,但一般用 0x37 获取中断状态)。标准流程是:发命令 → 等待 INT 引脚拉低 → 读中断状态 → 根据状态决定继续读数据还是处理错误。

下面是一个通用的命令发送函数,A0 线控制命令/数据相位:

void CH375_WriteCmd(uint8_t cmd) { CH375_A0_HIGH(); // 命令相位 CH375_CS_LOW(); delay_us(1); GPIOB->ODR = (GPIOB->ODR & 0xFF00) | cmd; // 只改低8位 CH375_WR_LOW(); delay_us(2); CH375_WR_HIGH(); CH375_CS_HIGH(); CH375_A0_LOW(); }

逻辑说明:先拉高 A0 告诉芯片“接下来是命令”,拉低 CS 选中芯片,把命令字节放到数据线上,然后给一个 WR 低脉冲。时序上要注意 WR 低电平时间至少要 100ns,STM32F103 的 GPIO 翻转延迟在 36MHz 总线上大概 40~50ns,所以加上延时确保满足芯片要求。

参数说明:如果你用的 GPIO 不是 GPIOB 低 8 位,可以用单独的 GPIO_Pin 操作,但注意 CH375B 数据总线切换时,读和写方向要单独配置。写数据时把 GPIO 配成推挽输出,读数据时配成浮空输入并开启上拉。

3. 把扇区读写跑起来:STM32F103 的固件流程与代码实现

3.1 初始化序列:设置 USB 主机模式、检测 U 盘插入

CH375B 上电后默认是设备模式,要切换到主机模式才能挂 U 盘。初始化流程要注意顺序:先复位,再设置主机模式,然后延时,再测试 U 盘是否存在。如果上电后立刻发命令,芯片内部固件还没起来,会返回 0xFF 之类的乱值。

uint8_t CH375_Init(void) { CH375_Reset(); CH375_WriteCmd(CMD_SET_USB_MODE); // 0x15 CH375_WriteData(0x07); // 主机模式 delay_ms(10); return CH375_CheckExist(); }

逻辑说明:0x15 命令后必须跟一个数据字节,0x07 表示外部晶振模式的主机模式。如果你用的是内部振荡器,这个值要换,但 CH375B 大部分模块都带 12MHz 晶振,所以 0x07 是通用值。

参数说明:CH375_CheckExist 是一个测试命令,返回 0xFF 表示芯片正常。我习惯在初始化时强制等待 U 盘插入,而不是直接返回失败:

uint8_t CH375_DetectUdisk(void) { uint8_t status; for (uint8_t i = 0; i < 200; i++) { CH375_WriteCmd(CMD_CHECK_EXIST); // 0x06 CH375_WriteData(0x5A); // 任意测试字节 status = CH375_ReadData(); if (status == 0x5A) { // 芯片存在 break; } delay_ms(50); } if (status != 0x5A) return 0; // 延时让 U 盘枚举完成 delay_ms(200); return 1; }

注意这里有两个不同的检测:0x06 命令是检测 CH375B 本身,0x22 命令是检测 U 盘连接。枚举需要时间,有些 U 盘上电后要 200ms 才能完成内部初始化,所以检测到芯片后还要延时再发 0x22。

3.2 读 U 盘扇区:CMD_Disk_Read 与数据校验

读取扇区是 CH375B 方案的核心。命令格式是:0x90,然后 3 字节 LBA 地址(低字节在前),然后 1 字节扇区数量(只能为 1,但个别固件支持 1~255,实际上一次读 1 个扇区最稳)。发送完命令后,芯片会自动进入接收数据状态,你需要反复读数据直到读完 512 字节。

uint8_t CH375_ReadSector(uint32_t lba, uint8_t *buf) { uint8_t i, status; CH375_WriteCmd(CMD_DISK_READ); // 0x90 CH375_WriteData(lba & 0xFF); // LBA 低字节 CH375_WriteData((lba >> 8) & 0xFF); CH375_WriteData((lba >> 16) & 0xFF); CH375_WriteData(1); // 扇区数 status = CH375_WaitInterrupt(); // 等待 INT 引脚低电平 if (status != USB_INT_DISK_READ) return status; // 0x1D for (i = 0; i < 512; i++) { buf[i] = CH375_ReadData(); // 连续读 512 字节 } status = CH375_WaitInterrupt(); // 读完成后的中断 if (status != USB_INT_SUCCESS) return 0xFF; return 0; }

逻辑说明:CH375B 的读扇区操作会触发两次中断。第一次是芯片准备好了数据,状态寄存器返回 0x1D,意思是“数据已就绪,可以读取”。读完 512 字节后,芯片还会产生第二次中断,表示本次传输完成,状态是 0x00。如果你在第一次中断后没有及时读完,第二次中断会失效,导致缓冲区残留脏数据。

参数说明:lba 是逻辑块地址,单位是扇区,从 0 开始。读 MBR 就是 lba=0。扇区数参数我建议固定 1,虽然 CH375B 支持多扇区读,但多扇区读的时候,第二次中断的状态不是 0x00 而是 0x1D 的变体,容易让人困惑。每个扇区单独读,代码逻辑更清晰,速度也够——实测读一个扇区加上命令交互大概 1ms,每秒 1000 扇区约 500KB/s,对配置备份这种场景完全够。

3.3 写 U 盘扇区:CMD_Disk_Write 的地址对齐和返回值处理

写扇区的流程跟读类似,但需要注意:CH375B 写扇区时,要求地址是 512 字节对齐,并且不能跨扇区。这里“地址对齐”指的是 LBA 地址本身,不是内存缓冲区地址。你只需要保证写入的 LBA 落在有效范围内,且扇区数不超过 1。

uint8_t CH375_WriteSector(uint32_t lba, uint8_t *buf) { uint8_t i, status; CH375_WriteCmd(CMD_DISK_WRITE); // 0x91 CH375_WriteData(lba & 0xFF); CH375_WriteData((lba >> 8) & 0xFF); CH375_WriteData((lba >> 16) & 0xFF); CH375_WriteData(1); status = CH375_WaitInterrupt(); if (status != USB_INT_DISK_WRITE) return status; // 0x1E for (i = 0; i < 512; i++) { CH375_WriteData(buf[i]); // 连续写 512 字节 } status = CH375_WaitInterrupt(); if (status != USB_INT_SUCCESS) return 0xFF; return 0; }

这里有一个血泪经验:写入数据后,第二次中断返回 0x00 不代表数据已经落到闪存颗粒上。CH375B 的“成功”只是表示它把数据交给了 U 盘主控,U 盘可能还在缓存里。如果要确保数据落盘,要么等 100ms 再操作下一个扇区,要么直接调用 CH375 的“磁盘刷新”命令(0x97,Flash 更新)。但在扇区读写层面,0x91 的完成中断就是最终结果,文件系统层的问题交给上层处理。

写扇区的坑在缓冲区一致性:如果你用来写扇区的 buf 是 STM32 内部 SRAM,并且开启了 D-Cache(F103 没有),那就没事。但如果你改了代码优化等级,编译器可能把 buf 优化掉,导致写到一半数据变化。我通常会把写扇区的 buf 声明为 volatile 或者用特定的对齐属性。

3.4 测 U 盘容量:读取 MBR 与分区表换算

测试 U 盘容量有两种途径:一是直接发 0x92 命令让 CH375B 返回内部固件识别到的容量,二是自己读 MBR 的分区表计算。0x92 命令返回的容量是 3 字节,单位是扇区数,但这个值在超过 8GB 的盘上不准,因为 3 字节最多表示 16,777,215 扇区。

uint32_t CH375_GetCapacity(void) { uint8_t a, b, c; CH375_WriteCmd(CMD_DISK_CAPACITY); // 0x92 a = CH375_ReadData(); // 低字节 b = CH375_ReadData(); c = CH375_ReadData(); // 高字节 return ((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16); }

逻辑说明:这个命令不需要等待中断,发完命令后直接读三个字节。返回的容量单位是扇区,所以容量 = 返回值 × 512 字节。如果你只需要大概容量,这个命令最快。

参数说明:如果你检测到返回值大于 0xFFFFF0,说明可能已经溢出。这时候我用扩展命令 0x93 读取 4 字节容量,或者干脆用分区表计算。分区表计算更可靠,因为很多扩容盘(黑片)的容量信息是伪造的,实际读写到最后会出错。最稳妥的容量测试是:先按 0x92 返回的扇区数遍历读每个扇区,读到哪个扇区出错,容量就是出错前的地址。

4. 常见问题排查:为什么我的 U 盘可以被识别却读不出数据

4.1 现象一:初始化失败,CMD_GET_STATUS 超时

现象:程序卡在 CH375_WaitInterrupt,INT 引脚一直高电平,超时后返回 0xFF。

原因分析:大多数情况是复位时序不对。CH375B 要求复位后至少等待 50ms 才允许发命令,如果板子上电后电平还没稳定,芯片内部固件没跑起来。另外,如果你的 CH375B 模块用的是 3.3V 供电,而代码里设置了 5V 对应的模式,也会导致端口状态异常。

解决:把 CH375_Reset 函数里的延时加长,复位拉低时间从 20ms 改到 100ms,释放后也延时 100ms。同时检查 V3 引脚:如果供电是 5V,V3 必须接 0.1uF 电容到地;如果供电是 3.3V,V3 必须悬空。我遇到过一次 V3 悬空但焊盘有残留锡导致的初始化不稳定,刮干净后恢复正常。

4.2 现象二:能枚举但读扇区返回 0x1F 错误

现象:读扇区时,第一次中断返回状态是 0x1F(USB_INT_DISK_ERR),而不是 0x1D。

原因分析:0x1F 表示 U 盘命令执行失败,常见原因是 LBA 地址超出 U 盘容量。很多 U 盘实际容量比标称小,比如标称 16GB 的低端盘实际只有 14.8GB,如果你按照 16GB 计算出最大 LBA 去读,U 盘会返回错误。还有一种情况是 U 盘是只读模式,写扇区时会返回错误。

解决:先用容量测试命令拿到 CH375B 报告的容量,然后用这个容量作为边界。读扇区时做一次边界检查:

if (lba >= sector_count) return 0x10; // 地址越界

此外,不要用 FAT 逻辑地址(比如文件偏移),必须用物理 LBA。如果你要做文件读取,先用 FatFs 的转换函数把簇号转成 LBA。

4.3 现象三:写入成功但重新上电数据丢失

现象:写扇区函数返回 0,程序也不报错,但把 U 盘拔下来插到电脑上,数据是旧的。

原因分析:CH375B 写扇区后,U 盘的缓存没有落盘。尤其是那种带写缓存的老 U 盘,需要发送“同步”命令。在扇区层面,CH375B 的 0x97 命令(Disk Update)就是干这个的,但很多代码没有调用。

解决:在每次写扇区完成后,延时 20ms,然后发 0x97 命令让 U 盘把缓存写入闪存。注意 0x97 命令会阻塞直到落盘完成,时间可能长达几百毫秒,但这是保证数据安全的必要开销。如果对写速度有要求,可以在写完一个完整的文件后再统一刷新。

void CH375_Flush(void) { CH375_WriteCmd(CMD_DISK_UPDATE); // 0x97 CH375_WaitInterrupt(); // 等待落盘完成 }

4.4 现象四:容量测试结果与标称不符

现象:用 0x92 命令读到的容量只有 7.9GB,而 U 盘标称 16GB。

原因分析:这是 3 字节地址上限导致的。0x92 命令返回的容量是 24 位无符号数,最大 16,777,215 扇区,乘以 512 就是 8,589,934,080 字节,约 8GB。超过这个范围的容量会被截断。

解决:改用读分区表的方法。读 lba=0 的扇区,取出分区表项,找到偏移 8 字节的相对扇区数(4 字节)和偏移 12 字节的扇区总数(4 字节),算出总容量。下面这段逻辑我封装成了函数:

uint32_t CH375_GetCapacityByMBR(uint8_t *mbr) { uint32_t total = 0; for (int i = 0; i < 4; i++) { uint8_t *entry = mbr + 446 + i * 16; // 分区表起始 uint32_t sectors = *(uint32_t *)(entry + 12); // 扇区总数 total += sectors; } return total; }

注意:MBR 的 4 字节在 STM32F103 上可能未对齐,直接定义指针读会触发硬件错误。正确做法是手动拼接:

uint32_t sectors = ((uint32_t)entry[12]) | ((uint32_t)entry[13] << 8) | ((uint32_t)entry[14] << 16) | ((uint32_t)entry[15] << 24);

4.5 现象五:U 盘发热严重且 STM32 复位

现象:插上 U 盘后,CH375B 发烫,STM32 偶尔复位,测量电流达到几百毫安。

原因分析:这是供电不足或短路。CH375B 的 USB 接口直接给 U 盘供电,如果 5V 电源没有足够的电容储能,U 盘启动瞬间的浪涌电流会让电源电压跌到 4V 以下,STM32 的复位阈值是 2.9V 左右,虽然不会立刻复位,但 CH375B 会进入异常状态,反复请求电流导致发热。

解决:在 USB 的 VBUS 引脚和 GND 之间加 470uF 电解电容,并串联一个 500mA 自恢复保险丝。不要用 STM32 的 3.3V 给 U 盘供电,即使你的模块上有 LDO,也扛不住 500mA 的峰值。另外,在 PCB 布局上,CH375B 的晶振要远离 USB 差分对,否则 12MHz 时钟辐射会干扰 USB 信号,U 盘识别变得时好时坏,这个问题排查起来很玄学,最后我是拿示波器看晶振波形才发现的。

5. 进阶验证:用坏块扫描与全盘写读校验扇区驱动的可靠性

5.1 遍历所有扇区:32 位地址溢出陷阱

CH375B 的 LBA 地址是 3 字节,但你用 STM32 的 uint32_t 传递时要小心:如果 U 盘容量超过 8GB,读扇区函数会把 lba 的低 24 位传给芯片,高字节被丢弃,导致地址回绕。这就是所谓的 32 位地址溢出陷阱——你以为在写 100 号扇区,实际上芯片收到的是 100 号扇区的低 24 位,如果原来的 lba 是 0x01000064,传输出来就变成 0x64,写到了前面扇区。

解决:在调用写扇区前强制检查 lba < 0xFFFFFF,并且把 CH375_WriteSector 的参数类型改成 uint32_t,内部再拆成 3 字节:

if (lba > 0xFFFFFF) return 0x11; // 超出CH375B寻址范围

对于超过 8GB 的盘,建议只对前 8GB 做全盘校验,或者改用扩展命令。大部分 U 盘都超过 8GB,所以我把这个检查放在所有写操作的第一行,它有两次救过我的数据。

5.2 随机地址读写压测与耗时统计

想让驱动可靠性有说服力,不能只读 MBR。我习惯做一个随机地址压测:从容量范围内随机挑 1000 个扇区,先读原始数据,再把特征字节写入,读回校验,最后恢复原始数据。这样可以发现地址解码不稳、数据线干扰、写缓冲失效等问题。

uint32_t test_seed = 0x12345678; uint32_t pseudo_random(void) { test_seed = test_seed * 1664525 + 1013904223; return test_seed; } void StressTest(uint32_t max_sector) { uint8_t orig[512], tmp[512]; for (int i = 0; i < 1000; i++) { uint32_t lba = pseudo_random() % (max_sector - 1); CH375_ReadSector(lba, orig); memset(tmp, 0xAA + (i & 0x0F), 512); CH375_WriteSector(lba, tmp); CH375_ReadSector(lba, tmp); if (memcmp(tmp, orig, 512) != 0) { // 记录失败地址,方便定位 } CH375_WriteSector(lba, orig); // 还原数据 } }

逻辑说明:伪随机数生成器是线性同余,避免每次都从 0 开始导致地址规律性。校验失败时不要立刻停,记下 lba 继续跑,完成后统计失败率。如果失败率低于千分之一,说明驱动可用;如果频繁失败,优先查数据线时序。

5.3 把这个驱动封装成通用 FatFs 底层接口

如果你的产品需要文件读写,把扇区驱动接到 FatFs 是顺理成章的。FatFs 只要求你提供 disk_read、disk_write、disk_status、disk_ioctl 四个函数。我在 CH375B 驱动之上做了这么一层:

DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (count == 1) return CH375_ReadSector(sector, buff) ? RES_OK : RES_ERROR; for (UINT i = 0; i < count; i++) { if (!CH375_ReadSector(sector + i, buff + i * 512)) return RES_ERROR; } return RES_OK; }

disk_ioctl 里尤其注意 GET_SECTOR_COUNT 回调,必须返回 U 盘真实扇区数,FatFs 会根据这个值判断文件系统合法性。如果返回错误值,FatFs 会认为文件系统不存在,甚至把 MBR 当成无效数据。

实际项目里,我在这个封装上跑过 FatFs,往 U 盘里写了一个 4MB 的采集文件,中途拔插三次,文件没有损坏。但有一个代价:FatFs 写文件时会产生大量单扇区写操作,而 CH375B 每次写都要等中断,整体写入速度只有 10KB/s 左右,对日志记录够用,对拷大文件就太慢了。那之后我养成了一个习惯,每次改驱动底层代码,都会强制跑一遍 5.1 的全盘校验和 5.2 的随机压测,通过才敢接文件系统。这个习惯帮我挡住了至少三次硬件改版后的数据线时序漂移问题。希望这个拆解能帮你在 CH375B 的扇区读写上少踩坑、多一次跑通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询