STM32F103 SPI驱动CH376实现TF卡文件读写与工程实践

STM32F103 SPI驱动CH376实现TF卡文件读写与工程实践 简介面向STM32F103嵌入式开发者该资源提供了一套基于CH376模块通过SPI接口读写TF卡的完整工程样例可用于快速搭建文件系统存储、日志记录或数据采集等应用场景。压缩包共163个文件约4.41MB主要包含STM32标准外设库的c/h源码、CH376驱动实现、Keil MDK工程文件uvprojx、uvoptx以及编译生成的hex、axf、map、lst等输出文件便于直接打开工程查看配置或烧录验证。工程内还附带keilkilll.bat等辅助脚本目录结构清晰适合初学者参照学习SPI通信时序与CH376命令交互流程。目前已有993人学习下载对于正在调试TF卡读写或希望移植CH376驱动的开发者而言这份资料可提供直接的代码参考和排错思路。1. CH376模块与STM32F103 SPI通信的选型逻辑CH376模块把SD卡协议和FAT文件系统封装在内部外部MCU只通过SPI/UART发命令和数据块自己完成目录、FAT表更新和ECC校验。对于stm32f103最小系统上的数据记录、字库升级、配置参数掉电保存这类应用CH376能省掉FatFs移植和TF卡底层调试的脏活。这篇从SPI引脚分配讲起围绕命令交互、TF卡初始化、文件读写三个层面展开最后给SPI速率和逻辑分析仪排查的几条实测经验。适合手里有现成CH376模块、打算用STM32F103跑文件操作的人也适合想搞清CH376 SPI时序细节的工程师。2. STM32F103 SPI初始化从引脚规划到时序验证2.1 引脚分配与SPI模式选择CH376模块的SPI接口通常有SCS、SCK、MISO、MOSI、INT、RST六个信号电源用3.3V。我习惯把片选和中断挂到普通GPIO不使用SPI硬件NSS因为CH376的命令传输需要多次拉低/拉高CS来区分命令阶段和数据阶段硬件NSS在接收模式切换时会自动翻转反而不可控。下面是一组实际验证过的连接方案模块引脚STM32F103引脚GPIO模式说明SCSPA4推挽输出片选低有效手动控制SCKPA5复用推挽SPI1_SCKMISOPA6上拉输入SPI1_MISOMOSIPA7复用推挽SPI1_MOSIINTPA3上拉输入CH376中断请求低有效RSTPA2推挽输出复位低有效复位注意如果板子把PA2/PA3占用了可以把INT和RST换到PB0/PB1不影响SPI逻辑。CH376手册标注SPI模式0和模式3都支持实际测试模式0在杜邦线较长时更稳所以下面代码按模式0配置也就是CPOLLow、CPHA1Edge数据在SCK第一个边沿被采样。2.2 标准外设库下的SPI1初始化采用标准外设库V3.5版本配置SPI1完整初始化代码如下/* 标准外设库SPI1 初始化模式08bit数据 */ void CH376_SPI_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* PA5 SCK PA7 MOSI 复用推挽 */ gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); /* PA6 MISO 上拉输入 */ gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, gpio); /* PA4 CS 推挽输出默认高 */ gpio.GPIO_Pin GPIO_Pin_4; gpio.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, gpio); GPIO_SetBits(GPIOA, GPIO_Pin_4); /* PA2 RST 推挽输出默认高PA3 INT 上拉输入 */ gpio.GPIO_Pin GPIO_Pin_2; gpio.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, gpio); GPIO_SetBits(GPIOA, GPIO_Pin_2); gpio.GPIO_Pin GPIO_Pin_3; gpio.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, gpio); /* 低速模式16MHz/64 250kHz */ spi.SPI_Direction SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_Low; spi.SPI_CPHA SPI_CPHA_1Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_64; spi.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }代码逻辑不复杂先打开GPIOA和SPI1时钟SCK和MOSI必须配置成复用推挽MISO配置成上拉输入避免读不到CH376返回的电平。SPI时钟先压到250kHz是有意的CH376刚上电时如果SCK跑太快容易收到错位数据等初始化完成后再提频。SPI_FirstBit_MSB对应最高位先出CH376的命令字节从bit7开始这个参数不要改成LSB。接着封装一个SPI读写字节函数。CH376在物理上是标准四线SPI所以一个函数同时完成发送和接收/* 写一个字节同时读回一个字节 */ uint8_t CH376_SPI_RW(uint8_t data) { SPI_I2S_SendData(SPI1, data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }发送后先等TXE再等RXNE这个顺序不能颠倒。如果只等发送完成就立刻读寄存器读到的还是上一次的残留数据。很多CH376调不通的情况就是在这里丢掉了第一个字节。2.3 软件SPI兜底方案如果SPI1引脚被其他外设占用或者板子上的硬件SPI时序不正常软件SPI也能把CH376跑起来。下面这段代码用在调试阶段很有用它能精确控制每个边沿缺点是速度慢大约20kHz左右读目录时等待明显#define CH_SCK_L() GPIO_ResetBits(GPIOA, GPIO_Pin_5) #define CH_SCK_H() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define CH_MOSI_H() GPIO_SetBits(GPIOA, GPIO_Pin_7) #define CH_MOSI_L() GPIO_ResetBits(GPIOA, GPIO_Pin_7) #define CH_MISO() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) uint8_t CH376_SW_SPI_RW(uint8_t data) { uint8_t i, ret 0; for (i 0; i 8; i) { if (data 0x80) CH_MOSI_H(); else CH_MOSI_L(); data 1; CH_SCK_H(); ret 1; if (CH_MISO()) ret | 0x01; CH_SCK_L(); delay_us(2); /* 按CH376标称SPI速率留足余量 */ } return ret; }软件SPI的时序要点是CH376在SCK上升沿采样输入数据所以先准备好MOSI电平再拉高SCK然后读MISO最后拉低SCK。延时函数建议用SysTick做微秒级延时不要用空循环否则开优化后代码会被编译器优化掉。3. CH376命令交互与TF卡识别流程3.1 命令、数据、状态三端口模型CH376在SPI模式下的操作模型是所有读写都围绕命令端口展开。主机拉低CS发送一个单字节命令码再根据命令定义继续发送参数或读取数据拉高CS表示当前操作结束。这与SD卡直接读写不同SD卡需要发送CRC、arg、cmd等复杂帧CH376把协议细节屏蔽了。常用命令码如下表具体数值以模块配套头文件为准不同批次固件对保留命令可能有差异功能命令码命令名测试芯片连接0x06CMD_CHECK_EXIST彻底复位0x05CMD_RESET_ALL查询芯片版本0x01CMD_GET_IC_VER初始化磁盘0x51CMD_DISK_INIT查询磁盘容量0x50CMD_DISK_SIZE读扇区0x52CMD_DISK_READ读扇区数据0x54CMD_DISK_READ_GO写扇区0x53CMD_DISK_WRITE与命令端口配套的是中断引脚。每次CH376执行完一个耗时命令都会拉低INT通知主机。主机的处理顺序是等待INT变低拉低CS发送CMD_GET_STATUS0x22读取中断状态字节拉高CS。CH376会在读状态后自动清除中断这个状态必须及时读否则下一次命令会被旧的INT状态干扰。3.2 连接检测与复位CHECK_EXIST实战上电后第一件事不是初始化TF卡而是确认CH376在SPI总线上是活的。CHECK_EXIST的协议很简单发送命令码0x06再发送任意字节芯片会把该字节取反后从MISO返回。所以发送0x55应该收到0xAAuint8_t CH376_CheckExist(void) { uint8_t back; CH376_CS_L(); CH376_SPI_RW(CMD_CHECK_EXIST); /* 0x06 */ back CH376_SPI_RW(0x55); /* 发0x55并读回取反 */ CH376_CS_H(); return (back 0xAA); /* 0x55取反0xAA */ }失败时先不要怀疑代码用万用表量CS是否被正确拉低。CH376的CS低有效而且命令过程中不能出现上升沿否则后面的字节会被芯片当成新命令。也可以加一条CMD_GET_IC_VER命令读固件版本常见返回值是0x11或0x13和CHECK_EXIST一起确认通信链路。3.3 磁盘初始化与中断状态读取TF卡初始化比普通SPI设备慢CH376内部要等卡上电稳定、读取CID/CSD寄存器。所以发送CMD_DISK_INIT之后不要立刻读INT先延时100ms再轮询。下面这段代码带超时保护/* 读取并清除CH376当前中断状态 */ uint8_t CH376_GetIntStatus(void) { uint8_t st; CH376_CS_L(); CH376_SPI_RW(0x22); /* CMD_GET_STATUS */ st CH376_SPI_RW(0x00); CH376_CS_H(); return st; } uint8_t CH376_DiskInit(void) { uint8_t i, status; CH376_CS_L(); CH376_SPI_RW(CMD_DISK_INIT); /* 0x51 */ CH376_CS_H(); for (i 0; i 200; i) { /* 最长等2秒 */ if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) RESET) { status CH376_GetIntStatus(); if (status 0x14) return 1; /* USB_INT_DISK_READY */ break; } delay_ms(10); } return 0; }CH376_GetIntStatus必须在INT拉低后调用读一次状态会自动清除中断。如果INT脚没接也可以固定延时后直接读状态但可能读到上一次未清除的旧值所以中断引脚尽量别省。磁盘初始化成功后TF卡已经被CH376锁定MCU不需要也不能再去直接操作TF卡的CS。4. 基于CH376的TF卡读写实现从扇区到文件4.1 文件创建与写入命令序列底层扇区验证通过后就可以用CH376内置FAT系统操作文件。文件创建流程分四步发送CMD_FILE_CREATE并加载文件名等待中断检查状态然后进入写数据流程。文件名必须使用8.3短文件名LOG2024.TXT没问题LOG_2024_0701.TXT会被拒绝。下面是可编译的创建函数#define CMD_FILE_CREATE_CMD 0x67 #define CMD_FILE_OPEN_CMD 0x64 #define CMD_FILE_WRITE_CMD 0x6B #define CMD_FILE_WRITE_GO 0x55 #define CMD_FILE_CLOSE_CMD 0x71 /* 新建或覆盖文件name为8.3格式如DATA.BIN */ uint8_t CH376_FileCreate(uint8_t *name) { uint8_t status; CH376_CS_L(); CH376_SPI_RW(CMD_FILE_CREATE_CMD); while (*name) CH376_SPI_RW(*name); CH376_SPI_RW(0x00); /* 文件名结束符 */ CH376_CS_H(); if (CH376_WaitInt(200) 0) return 0; status CH376_GetIntStatus(); return (status 0x14); }CH376_WaitInt和前面磁盘初始化里的轮询逻辑一样返回0表示超时返回1表示INT已触发。创建成功后目录项已建立但文件长度为0。接下来写入内容uint8_t CH376_FileWriteData(uint8_t *buf, uint16_t len) { uint16_t i; CH376_CS_L(); CH376_SPI_RW(CMD_FILE_WRITE_CMD); /* 0x6B */ CH376_SPI_RW(len 8); CH376_SPI_RW(len 0xFF); for (i 0; i len; i) { CH376_SPI_RW(buf[i]); /* 数据阶段 */ } CH376_SPI_RW(CMD_FILE_WRITE_GO); /* 0x55触发实际写入 */ CH376_CS_H(); if (CH376_WaitInt(500) 0) return 0; return (CH376_GetIntStatus() 0x14); }len是本次写入的字节数CH376单次支持较大长度但我建议每片不超过512字节和扇区对齐。写入完成后必须调用CH376_FileClose否则FAT表项没有更新掉电后文件大小会变成0。关闭命令如下void CH376_FileClose(void) { CH376_CS_L(); CH376_SPI_RW(CMD_FILE_CLOSE_CMD); /* 0x71 */ CH376_CS_H(); delay_ms(50); /* 等FAT缓存落盘 */ }注意这里的CMD_FILE_WRITE_GO是在数据发送完之后、CS拉高之前发送的它不是一个普通数据而是通知CH376开始执行写入的控制命令。不同固件版本对这个命令的响应略有差异如果你拿到的库源码里没有这一步以源码为准。4.2 读取已有文件打开、定位、循环读读文件的命令序列与写类似先发送CMD_FILE_OPEN等待中断再读取文件长度。打开函数uint8_t CH376_FileOpen(uint8_t *name) { uint8_t status; CH376_CS_L(); CH376_SPI_RW(CMD_FILE_OPEN_CMD); /* 0x64 */ while (*name) CH376_SPI_RW(*name); CH376_SPI_RW(0x00); CH376_CS_H(); if (CH376_WaitInt(200) 0) return 0; status CH376_GetIntStatus(); return (status 0x14); }文件打开后CH376支持通过CMD_GET_FILE_SIZE0x0C读文件长度。常见实现是连续读三个字节因为CH376内置文件长度的寄存器是24位最大支持16MB文件。如果你要读超过16MB的文件需要自己分段定位。读取函数uint16_t CH376_FileRead(uint8_t *buf, uint16_t len) { uint16_t i; CH376_CS_L(); CH376_SPI_RW(0x6A); /* CMD_FILE_READ */ CH376_SPI_RW(len 8); CH376_SPI_RW(len 0xFF); for (i 0; i len; i) { buf[i] CH376_SPI_RW(0xFF); /* 发送任意字节读回数据 */ } CH376_CS_H(); if (CH376_WaitInt(500) 0) return 0; return (CH376_GetIntStatus() 0x14) ? len : 0; }读数据阶段和写一样CS必须保持低电平直到读完整段不能在中间拉高再拉低。我见过有人把读字节封装成“每次发命令读一字节”结果CH376每次都重新解析命令读到文件后半部分全是0。实际项目中可以用这个函数配合文件长度循环读取每次512字节。4.3 日志型应用中的写入节奏控制文件系统最怕频繁创建和删除。如果应用每秒写一次日志每次重新创建文件TF卡的根目录区和FAT表会被反复擦写速度越来越慢。我在类似项目中把日志积攒到1KB再一次性写入固定使用DATA.BIN写入完成后调用CH376_FileClose这样CH376内部缓存能一次性更新FAT效果比频繁小包写入好很多。另一个细节是追加写CH376没有直接“打开追加”命令需要打开文件后用文件长度做偏移定位到末尾再写。定位操作在不同厂商库中封装不同常见是发送CMD_FILE_LOCATE并指定偏移如果你只做覆盖写就每次都用FileCreate。5. 进阶SPI时钟上限、FAT边界与逻辑分析仪验证5.1 找到你的SPI速率边界CH376模块标称SPI速率很高但实际受杜邦线、模块PCB、TF卡槽接触影响跑满速很容易出错。我实测下来2MHz读写稳定8MHz文件打开偶尔失败16MHz时连CHECK_EXIST都不动。建议初始化用250kHz磁盘初始化成功后切换2MHz写入100次无错再往上提。切速率的代码void CH376_SetSPIFast(void) { SPI_Cmd(SPI1, DISABLE); SPI_InitTypeDef spi; /* 复用之前的配置 */ spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; /* 2MHz */ SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }如果你用stm32f103库v3.50SPI_Init内部不会自动关闭SPE所以要先SPI_Cmd(DISABLE)再重新Init。HAL库的行为也不完全一样用CubeMX配置时要注意HAL_SPI_Init会重置CR1但分频参数同样以最终配置为准。5.2 文件碎片与FAT表异常长时间写写删删后TF卡会出现“假死”表现是创建文件越来越慢、写入偶尔卡住。这不是MCU代码问题而是文件系统在找空闲簇时跨越多级FAT表。简单验证格式化TF卡后重跑同样流程如果速度恢复就是碎片问题。CH376没有碎片整理命令应用层最好固定文件个数比如LOG1.BIN到LOG3.BIN循环覆盖避免单文件无限增长。还可以用CMD_DISK_SIZE读取剩余容量低于阈值时主动停止写入。5.3 用逻辑分析仪抓完整交易当软件SPI和硬件SPI结果不一致或者文件读写偶发失败时用逻辑分析仪抓SCK、CS、MISO、MOSI四根线。重点看三个点CS在命令阶段必须持续拉低SCK空闲电平必须为低0x55后的回读必须是0xAA。下面是我常用的排查表现象优先检查项常见原因发送0x55回读不是0xAACS时序、SCK极性CPOL/CPHA配置错回读正确但磁盘初始化失败供电、TF卡接触CH376和卡套电源纹波大文件打开成功但读回0文件名格式、偏移定位长文件名未转8.3写入后文件大小不变关闭流程漏发CMD_FILE_CLOSE提示CH376 的 SPI 命令阶段和数据阶段都要求 CS 持续拉低只有一帧完整操作结束后才能拉高。排查时先看逻辑分析仪的 CS 波形。最后提醒一点stm32f103最小系统板上PA4不是唯一能用的CS但一定要避开被调试器占用的PB3/PB4引脚。DAP下载失败时先查BOOT0跳线再看SWD引脚是否被复用别错怪CH376模块。本文还有配套的精品资源点击获取