ARTICLE DETAIL

资讯详情

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

STM32+CH376S U盘读写实战:从SPI驱动到文件系统完整方案

STM32+CH376S U盘读写实战:从SPI驱动到文件系统完整方案 做嵌入式久了总会碰到“数据怎么拿出来”的问题。设备跑了好几天攒了一堆日志、采集数据总不能每次都用调试器连电脑去读Flash吧。以前我第一反应是插SD卡挂FatFs但SD卡的座子、热插拔、供电问题也很烦。后来在项目里换成了CH376S这颗USB接口芯片往U盘里写文件这件事一下就轻了——STM32这边走SPI发几条命令芯片自己就把USB设备的枚举、SCSI/UFI命令、FAT表更新全处理掉通电验证十分钟不到就跑通了一次完整读写。这篇文章就把我这次STM32F103C8T6 CH376S的实战过程完整记录下来硬件接线、底层SPI驱动、创建文件、写入、读取的代码都会贴出来。想给设备加数据记录、离线导出配置或者纯粹想在嵌入式项目里打通U盘文件读写这篇可以直接照抄。1. 为什么选CH376S而不是硬刚USB Host协议栈1.1 让STM32老老实实当“主机”工程量比想象中大很多人第一反应是现在的MCU不是都有USB外设吗比如STM32F407、H7都带USB OTG直接把U盘当大容量存储设备来枚举不就行了理论上可以实际操作会撞上一堆协议栈的墙。要把U盘识别成一个块设备MCU要经历USB总线复位、设备枚举、读设备描述符、配置接口、BOT传输协议Bulk-Only Transport、SCSI命令集INQUIRY、READ CAPACITY、READ10/WRITE10这一长串流程。这还没完拿到扇区数据之后你还得在MCU上移植FatFs并自己解析MBR、DBR、FAT表、目录项。ST官方的HAL库确实提供了USB Host库但它对HAL版本、堆栈大小、U盘主控兼容性都挺敏感稍微换个U盘就可能枚举失败。而且F103这种经典芯片根本没有USB Host功能只有设备模式你想做Host还得换芯片。所以问题不是“能不能”而是“划不划算”。如果你的需求只是“把一小包数据存成文件或者把配置文件读出来”为一个USB Host协议栈投入一周甚至几周的时间太浪费了。1.2 CH376S相当于“USB Host 文件系统”二合一CH376S是沁恒出品的一颗USB接口控制芯片它最大的特点是把USB Host控制器和FAT文件系统固件都集成在了芯片里。MCU只需要用SPI、UART或者8位并口给它发命令它会自己完成U盘的枚举、SCSI指令、FAT表的读写和文件目录管理。对单片机来说操作U盘文件跟操作一块“智能外设”差不多你发个“创建文件”命令它就把文件建好了发个“写入数据”命令它就把数据写进当前文件。芯片内部固件分两种模式。内置固件模式是默认状态芯片自带的固件帮你处理好了USB和FAT实战中绝大多数场景都用这个模式。另一种是外部固件模式把CH376S当纯USB Host控制器用文件系统完全由MCU自己实现这个基本没人用除非你要做特殊协议。需要说明的是内置固件对文件系统的支持是FAT12、FAT16和FAT32文件名字符串只支持8.3格式的大写英文名和当年DOS时代的命名规则一样。长文件名、中文名、NTFS、exFAT这些不要指望它这是这套方案最大的边界后面内容都是在这个边界里玩。1.3 SPI、UART、并口三种接口怎么挑CH376S对外提供了三种通信接口。8位并口速度最快适合吞吐量大的场景但占用IO太多小单片机一般不推荐。UART接口最简单全双工异步串口几乎不挑MCU但波特率一般建议设置在9600到115200之间传文件速度偏慢。SPI接口是各方面都比较折中的选择速度足够用占IO少常见工程里也最多人用。这次实战我用SPI模式。STM32F103C8T6的硬件SPI1直接接CH376S时钟速率我放在1MHz左右总线约2.25Mbps视分频而定对文件读写来说完全够。第一次调建议先在CubeMX里把SPI速率降下来等通信稳定了再提速度排查问题会轻松很多。2. 硬件接线SPI模式下的最小系统2.1 引脚对照与接线表CH376S模块的核心引脚不多接到STM32上就是6根线片选、时钟、SPI收发、中断、复位再加电源和地。我用的是STM32F103C8T6最小系统板加一个成品CH376S模块接线如下CH376S 引脚功能STM32F103C8T6说明SCSSPI片选低有效PA4GPIO推挽输出SCKSPI时钟PA5SPI1_SCKSDISPI数据输入MCU发出的MOSIPA7SPI1_MOSISDOSPI数据输出发给MCU的MISOPA6SPI1_MISOINT中断输出低有效PA3GPIO输入上拉RST复位低有效PA2GPIO推挽输出VCC模块供电3.3V或5V见说明以模块要求为准GND地GND共地如果你自己画的板子上CH376S不是模块形式注意确认芯片的V3引脚、晶振电路、去耦电容这些外围器件别漏了。模块上一般已经把这些都做好了省心很多。2.2 供电、复位和中断引脚的注意事项供电是这颗芯片最容易翻车的地方。CH376S作为USB Host时需要给U盘提供5V电源这个5V不是从芯片里变出来的需要外部供电。很多模块会带一个5V输入引脚板上再用LDO转出3.3V给芯片逻辑。如果你的板子只给CH376S的VCC供了3.3VU盘可能偶尔能识别、读写一两次就掉线甚至完全没反应。U盘工作电流一般在100mA到300mA峰值可能更高最好用能提供500mA以上的5V电源给U盘供电。复位引脚RST是低有效上电后需要拉低至少几十毫秒再拉高让芯片完成内部初始化。我实测下来上电后给个200毫秒左右的延时最稳太着急发命令容易吃闭门羹。INT引脚是开漏输出低电平表示有事件需要MCU处理。开漏输出不能输出高电平所以必须在外部接上拉电阻或者用MCU内部上拉。我用的STM32F103GPIO配置成输入上拉不需要额外加电阻实测没问题。提示如果在系统里看到INT引脚一直是低电平那是因为它没被拉高可能是GPIO没配置上拉或者模块上INT线虚焊。INT被卡死会导致后续所有命令都以为有事件整个流程就会乱掉。2.3 买模块还是自己画板第一次接触CH376S我的建议是直接买成品模块。好处是晶振、LDO、USB母座、通信接口电平转换都给你画好了你只需要接几根杜邦线到STM32最多十分钟就能把硬件搭起来。模块上一般会标注SCS、SCK、SDI、SDO这些引脚照着接线表对号入座即可。等验证完功能再考虑把CH376S画进自己的板子里。自己画的时候需要注意CH376S需要12MHz晶振V3引脚需要接一个0.1uF电容到地USB Host口的D和D-走线尽量短、等长U盘电源的5V输入处加一个100uF左右的电解电容和0.1uF小电容避免U盘主控瞬间拉低电压把整个系统搞复位。3. 代码实现从底层驱动到U盘文件读写这一节我直接把核心代码贴出来逻辑是自底向上的先做SPI底层通信再做CH376S命令交互最后封装文件读写接口。CubeMX配置那里我用的是HAL库SPI1配置成全双工主机、8位数据、MSB先行、Mode 0速率分频到约1MHz别的默认就行。3.1 通信测试先确认SPI链路通不通任何通信芯片第一步都是确认链路通CH376S有一个专门的命令叫 CHECK_EXIST0x06。发送任意一个字节芯片会把你发的字节取反后回传。比如你发0x55正常会读回0xAA。用这个命令可以快速排查接线、SPI配置问题。/* ch376s.h 关键定义命令码以CH376S数据手册为准 */ #define CMD_CHECK_EXIST 0x06 #define CMD_SET_USB_MODE 0x15 #define CMD_GET_STATUS 0x22 #define CMD_RD_USB_DATA0 0x27 #define CMD_DISK_MOUNT 0x31 #define CMD_FILE_OPEN 0x32 #define CMD_FILE_CREATE 0x34 #define CMD_FILE_ERASE 0x35 #define CMD_FILE_WRITE 0x36 #define CMD_FILE_READ 0x37 #define CMD_FILE_CLOSE 0x3C #define USB_INT_SUCCESS 0x14 #define USB_INT_DISK_READ 0x20 #define USB_INT_DISK_ERR 0x1D #define USB_INT_FILE_ERR 0x1B #define ERR_MISS_FILE 0x42 #define ERR_EXIST_NAME 0x47static uint8_t SPI_Xfer(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, 10); return rx; } static void WriteCmd(uint8_t cmd) { CS_LOW(); SPI_Xfer(cmd); CS_HIGH(); } static void WriteData(uint8_t data) { CS_LOW(); SPI_Xfer(data); CS_HIGH(); } static uint8_t ReadData(void) { uint8_t data; CS_LOW(); data SPI_Xfer(0xFF); CS_HIGH(); return data; }通信测试代码放到初始化函数里void CH376_Init(void) { RST_LOW(); HAL_Delay(20); RST_HIGH(); HAL_Delay(200); WriteCmd(CMD_CHECK_EXIST); WriteData(0x55); uint8_t r ReadData(); if (r ! 0xAA) { /* 到这里说明SPI没通先查接线、SPI模式、片选 */ Error_Handler(); } }如果程序死在Error_Handler不要急着往下走先用逻辑分析仪或者示波器看SCK和MOSI上的波形确认命令字节真的发出去了再查MISO上的回传。3.2 切换USB主机模式并挂载U盘SPI通了之后要让CH376S进入USB Host模式发送命令 SET_USB_MODE0x15参数是0x06内置固件主机模式。这个命令不需要等中断直接读回一个状态字节正常返回0x51。如果返回值不对基本可以判定是芯片没进入预期状态。uint8_t CH376_SetUSBMode(uint8_t mode) { WriteCmd(CMD_SET_USB_MODE); WriteData(mode); return ReadData(); /* 0x51 成功 */ }切成主机模式后插入U盘用 DISK_MOUNT0x31命令挂载磁盘。挂载内部会完成U盘枚举、读文件系统引导区的动作返回状态一般就是USB_INT_SUCCESS0x14。挂载失败时常见返回是USB_INT_DISK_ERR0x1D或者USB_INT_FILE_ERR0x1B。等待中断的函数我直接轮询INT引脚static uint8_t WaitINT(void) { uint32_t timeout 0xFFFFFF; while (INT_IS_LOW() 0) { if (--timeout 0) return 0xFF; } WriteCmd(CMD_GET_STATUS); return ReadData(); }就是等INT变低然后用GET_STATUS命令读取状态码。状态码会同时清除CH376S的中断标志所以每次事件处理完INT会重新变高等着下一个事件来。3.3 写文件创建、写入、关闭的三板斧写文件看起来是三个动作实际上是四步删除旧文件、创建文件、按块写入数据、关闭文件并更新长度。CH376S的CMD_FILE_CREATE在文件已存在时会返回ERR_EXIST_NAME0x47所以我在创建前先执行一次删除操作忽略“文件不存在”的错误。uint8_t CH376_DeleteFile(char *name) { WriteCmd(CMD_FILE_ERASE); WriteString(name); return WaitINT(); } uint8_t CH376_WriteFile(char *name, uint8_t *buf, uint16_t len) { uint8_t s; s CH376_DeleteFile(name); if (s ! USB_INT_SUCCESS s ! ERR_MISS_FILE) return s; WriteCmd(CMD_FILE_CREATE); WriteString(name); s WaitINT(); if (s ! USB_INT_SUCCESS) return s; uint16_t offset 0; while (offset len) { uint8_t block (len - offset) 64 ? 64 : (len - offset); WriteCmd(CMD_FILE_WRITE); WriteData(block); for (uint8_t i 0; i block; i) WriteData(buf[offset i]); s WaitINT(); if (s ! USB_INT_SUCCESS) return s; offset block; } WriteCmd(CMD_FILE_CLOSE); WriteData(0x01); /* 参数1表示关闭并更新文件长度 */ s WaitINT(); return s; }这里有几个关键点。第一文件名必须以0结尾的ASCII字符串格式必须是8.3大写比如“TEST.TXT”、“LOG001.CSV”不要写“test.txt”或者“我的文件.txt”。第二每次写入的数据块不要超过64字节这是CH376S内部缓冲区的基本尺寸超出后芯片处理不过来老老实实分批写。第三写完文件一定要调用CMD_FILE_CLOSE并且参数传1这个参数会告诉芯片把文件的真实长度更新进FAT目录项。如果漏了这步U盘上确实会出现这个文件但大小是0字节内容全丢了。3.4 读文件打开、循环读取、结束判断读取流程相对于写入更简单一点核心是打开文件后反复调用CMD_FILE_READ每次有数据时状态码是USB_INT_DISK_READ0x20全部读完时返回USB_INT_SUCCESS。读取到的数据要先发CMD_RD_USB_DATA0命令从芯片缓冲区里取出本次的长度和数据。uint8_t CH376_ReadFile(char *name, uint8_t *buf, uint16_t maxLen, uint16_t *readLen) { uint8_t s; *readLen 0; WriteCmd(CMD_FILE_OPEN); WriteString(name); s WaitINT(); if (s ! USB_INT_SUCCESS) return s; while (1) { WriteCmd(CMD_FILE_READ); s WaitINT(); if (s USB_INT_SUCCESS) break; /* 文件结束 */ if (s ! USB_INT_DISK_READ) return s; WriteCmd(CMD_RD_USB_DATA0); uint8_t cnt ReadData(); for (uint8_t i 0; i cnt; i) { uint8_t d ReadData(); if (*readLen maxLen) buf[(*readLen)] d; } } return USB_INT_SUCCESS; }第一次写这个循环的时候我犯过一个低级错误每次读完数据没有继续发CMD_FILE_READ结果只读到一块数据就结束了。一定要记住CMD_FILE_READ是“继续读取下一块”的意思要读到USB_INT_SUCCESS才算完。3.5 完整代码工程与CubeMX配置要点把上面这些函数组合起来配合串口打印主程序可以这样跑uint8_t wbuf[] Hello CH376S!\r\nSTM32 - CH376S - U盘 读写测试\r\n; uint8_t rbuf[256]; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); CH376_Init(); uint8_t mode CH376_SetUSBMode(0x06); printf(SetUSBMode: 0x%02X\r\n, mode); uint8_t st; do { HAL_Delay(200); st CH376_DiskMount(); } while (st ! USB_INT_SUCCESS); printf(Disk Mount OK\r\n); st CH376_WriteFile(TEST.TXT, wbuf, sizeof(wbuf) - 1); printf(WriteFile: 0x%02X\r\n, st); uint16_t rlen 0; st CH376_ReadFile(TEST.TXT, rbuf, sizeof(rbuf), rlen); printf(ReadFile: 0x%02X, len%d\r\n, st, rlen); if (st USB_INT_SUCCESS) printf(Content: %.*s\r\n, rlen, rbuf); while (1) { HAL_Delay(1000); } }CubeMX里SPI1的配置我建议这样填全双工主机模式8位数据MSB先行时钟极性CPOL0、时钟相位CPHA0预分频先给64总线时钟72MHz下就是约1.125MHz的SPI时钟。所有GPIO里PA4、PA2配成推挽输出PA3配成上拉输入PA5、PA6、PA7由SPI外设接管不用手动配置模式。提示如果读完一遍还不理解命令交互建议先把CH376S的数据手册里“命令详解”那章翻一遍。芯片文档写得很工程化每个命令都有时序图比在网上找零碎博客高效得多。4. 常见问题与排障实录这部分是我在实际调试中遇到最多、也是最值得写下来的问题。如果你按上面的代码跑不通大概率是下面这几类情况之一。4.1 U盘插上没反应/挂载失败八成是电源和格式“插上U盘但程序一直挂载不成功”是最常见的问题。我排查这个问题的顺序一般是先看供电再看通信最后看U盘格式。供电问题前面提过Host模式下U盘必须要有5V电源。用万用表量一下USB母座VBUS和GND之间的电压如果低于4.7V就要怀疑电源能力。很多开发板是从USB口取电的再带一个U盘电流就可能不够了。我踩过的具体坑是用了带灯的U盘扩展坞还插了一根一米多长的USB延长线结果电压掉到4.2VU盘怎么都枚举不上换短线直插就好了。格式问题也很好排除。把U盘用Windows格式化工具重新格式化成FAT32分配单元大小选默认快速格式化即可。NTFS和exFAT在这颗芯片上基本没戏别浪费时间。如果你是老手可能会想用DiskGenius把U盘分区并做成FAT32那是另一种玩法但普通调试不需要。如果这些都没问题再回头查SPI链路。用CHECK_EXIST命令确认通信正常用SET_USB_MODE看返回是否0x51这两步过了硬件基本没问题。4.2 文件写不进/写完是0字节最典型的原因就是我前面反复强调的写完数据没有调用CMD_FILE_CLOSE或者调用了但参数传成了0。CH376S只有在关闭文件并传参1的时候才会把内容长度更新到FAT目录项里。如果你中途掉电、复位没走到这句话那U盘上就留下一个0字节的文件。第二个原因是文件已经存在。直接执行CMD_FILE_CREATE成功不了会返回0x47。所以我写文件前先删除同名文件把“文件已存在”这个坑提前填掉。如果你想让文件内容覆盖而不是追加这个流程就是标准姿势。第三个原因是文件名不符合8.3格式。比如“DATA123456.TXT”主文件名已经9个字符了超了再比如“传感器.CSV”中文和编码都不行。文件名就按“8个字符以内的大写ASCII 点 3个字符以内的扩展名”来定简单。4.3 读出来的数据乱码或者不全读出来的数据乱码多数不是CH376S的问题而是数据本身的编码问题。比如你在电脑上用记事本保存了一个UTF-8的文本在单片机上按字节读出来再通过串口按ASCII打印中文部分肯定乱因为UTF-8一个汉字占3字节你按单字节处理并一个个打印自然对不上。解决办法是按原始字节流处理不要对内容做任何编码假设如果要在Excel里看中文就按后面5.2节说的写入UTF-8 BOM。读不全的情况要先检查读取循环是否在收到USB_INT_SUCCESS之前就break了。另外如果文件数据量比你的buf大我的代码是“读到maxLen就停止存但还会继续把剩余数据读完”这样不会破坏读取循环状态。4.4 SPI通信不稳定的几个隐藏雷点第一是片选CS的控制。如果CubeMX里SPI的NSS配置成了硬件自动模式芯片可能会自己拉低NSS和软件控制的CS打架。建议把NSS配置成Disable硬件管理CS引脚完全用GPIO手动控制。第二是SPI模式配置错误。CH376S手册支持模式0和模式3我用的是模式0。如果你在别的工程里看到模式3也有道理但一定要和发送接收端匹配。前几年我做过一次LPC1768的移植就是SPI模式没对齐收发数据全是0xFF查了一下午。第三是等待中断的延时问题。CH376S操作U盘有时需要几十毫秒甚至上百毫秒比如格式化、大量写数据。我的WaitINT里有一个很大的超时计数但如果你的系统有其它任务需要及时响应建议把超时逻辑改成基于HAL的毫秒计数避免死等。5. 扩展把U盘读写做成数据记录器跑通读写只是第一步。工程上真正有用的往往是“反复写日志、不断追加记录数据”。这一节我给出几个可以直接落地的扩展思路。5.1 追加日志不覆盖老数据最稳的追加方式不是去定位文件末尾而是用带编号的文件名。比如每次开机创建一个新文件文件名是“LOG001.TXT”、“LOG002.TXT”每次写入前用CMD_FILE_OPEN去探测下一个编号是否存在从1开始往后找直到找到一个不存在编号再创建。这样既避开了CH376S的定位命令也天然形成了日志归档。缺点是文件多但你可以做成固定数量比如只保留最近10个文件超过就删掉最旧的编号。如果设备有RTC更实用的是直接用日期时间当文件后缀比如“26012513.TXT”表示2026年1月25日13时但注意8.3格式里主文件名字符最多8个这种“YMMDDHH”类的编码需要自己算好长度比如“262513.TXT”就只用了6个字符。5.2 直接生成Excel能打开的UTF-8 CSV记录传感器数据时CSV格式最省事。要让Excel直接识别UTF-8编码的中文写入CSV内容之前先向文件写入3个字节的BOM0xEF、0xBB、0xBF。之后每行数据用逗号分隔最后以“\r\n”结束。比如写入“时间,温度,湿度\r\n”这种表头再逐行追加数据保存成“LOG001.CSV”拿到电脑上直接用Excel打开就不会乱码。注意CSV文件本身的文件名依然得符合8.3格式内容编码和文件命名规则是两回事。5.3 写入频率、掉电保护与U盘寿命U盘的Flash是有擦写寿命的别把它当成RAM随便写。设计数据记录器时建议攒够一批数据再写入一次比如每512字节写一次而不是每收到一个传感器点就写一次。每次写完都关闭文件既能更新长度也能降低FAT表损坏的概率。掉电保护是个绕不开的话题。如果设备写文件写到一半断电文件长度更新不了这次的数据大概率丢甚至可能导致FAT表异常。工程上的简单做法是双文件轮流写比如A文件满了写B文件B文件写完校验又回到A保证总有一份数据是完整的。更稳妥的方案是加一个超级电容或小容量电池检测到掉电后延迟给系统断电几百毫秒利用这点时间把当前文件正常关闭。后者虽然多了一点成本但对数据可靠性要求高的场景非常值得。我在实际调试中体会最深的一件事是这类带固件的接口芯片只要通信链路通了后续的命令交互基本都是套路最难查的反而是供电、U盘格式、文件关闭这些“看起来不起眼”的环节。正式做产品之前我强烈建议你把同一套代码多换几个U盘、几根USB线测一测因为U盘主控的兼容性差异是真实存在的。CH376S这套方案肯定不完美8.3文件名、无长目录这些限制摆在那但如果你只要“把数据稳定存进U盘、能方便导出来”它已经是性价比很高的选择了。后面我还会把手上的代码封装成支持多文件管理、循环覆盖的通用组件到时候再整理成文章跟各位交流。
返回列表