ARTICLE DETAIL

资讯详情

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

CH376与STM32F103的SPI读写:轻松实现TF卡文件系统

CH376与STM32F103的SPI读写:轻松实现TF卡文件系统 简介面向嵌入式开发者的CH376 TF卡读写模块工程包基于STM32F103单片机通过SPI接口与CH376模块通信实现TF卡底层读写与文件操作适用于STM32入门、SPI通信协议学习及小型存储系统开发场景。工程文件共163个包括40个h头文件和39个c源码文件、标准外设库及CH376驱动代码同时包含uvprojx/uvoptx等Keil工程配置、hex/axf烧录文件及map映射文件压缩包约4.41MB。目前已有993人学习下载。资料不仅提供可直接编译的完整工程还包含SPI配置、TF卡初始化、读写扇区及文件管理的关键逻辑读者可在此基础上扩展FATFS文件系统、调试通信时序或移植到其他STM32型号。1. CH376 与 STM32F103 的 SPI 读写到底解决什么问题做数据记录仪、离线升级或简单文件存储时最容易被卡住的不是 SPI 本身而是 TF 卡那块完整协议栈上电初始化序列、CMD0/CMD8/ACMD41、扇区读写、FAT 表维护随便哪一环出错都只能对着逻辑分析仪发呆。CH376 模块的作用就是把这一整块收编进一颗专用芯片主控 STM32F103 只通过 SPI 发命令、收状态文件系统由 CH376 在内部完成。换句话说你写代码时可以把 TF 卡当成一个大号串口设备而不必关心 SD 协议细节。这套方案特别适合 5 年以上嵌入式经验但要快速出样机的工程师也适合刚接触文件系统的新手用来理解「命令-状态-数据」三段式交互模型。它不能给你极致的读写带宽但能给你可控的开发周期和很低的排错成本。2. CH376 的 SPI 从机协议命令、状态与数据缓冲区2.1 为什么主控选 SPI 而不是并口或串口CH376 芯片同时提供 8 位并口、异步串口和 SPI 三种主机接口STM32F103 接哪一种都可以跑通但选型时要看两个约束引脚占用和驱动复杂度。并口需要至少 8 根数据线加上读写控制线在 F103 的 48 脚封装上会挤占大量 GPIO而且时序上要对着 CH376 的片选和读写信号做严格控制代码量并不小。串口只占两根线但 CH376 的 UART 模式速度一般大量连续数据块传输时主控要频繁处理帧头和校验吞吐率上不去。SPI 是这几者里比较均衡的选择3 根信号线加 1 根片选全双工硬件收发HAL 库直接支持配合 F103 的 SPI1 可以在 36 MHz 甚至更高的 APB2 时钟下工作。从模块的实际布局看市售 CH376 模块也普遍引出了 SPI 接口和 STM32F103 最小系统板连线非常顺手。反过来说如果你用的是其他没有硬件 SPI 的小单片机软件模拟 SPI 协议也是可行的因为 CH376 的 SPI 时序就是标准的模式 0时钟空闲为低第一个边沿采样主控只做字节收发不涉及复杂的 DMA 描述符。后面所有示例代码我都按硬件 SPI 写软件模拟时只需把收发函数替换成 GPIO 翻转即可。2.2 CH376 在 SPI 读写下如何拆解 TF 卡协议理解这个模块的关键在于分清哪一层由谁负责。TF 卡本质上是一个 SPI 从设备主机要自己去发初始化命令、读 CSD 寄存器、按扇区读写而 FAT 文件系统又在这一层之上要维护目录项、FAT 表、簇号换算。如果这些全写在 STM32F103 里一个最简单的日志追加功能也要几百行代码而且每换一张卡都可能出现兼容性差异。CH376 把这些封装成了固件内部的「磁盘管理层」。主控通过 SPI 向 CH376 发送命令码比如初始化磁盘、打开文件、写数据CH376 收到后自己操作 TF 卡并维护文件系统的当前状态。它对 STM32F103 暴露的是一个状态寄存器和一个数据缓冲区。主控需要读取文件内容时CH376 先把数据搬进自己的内部缓冲区主控再通过 USB 数据读取命令把缓冲区内容取走。这种设计把协议栈隔离在芯片内部主机侧的代码复杂度可以控制在很少的源文件里。2.2.1 命令-状态-数据三段式帧结构CH376 的 SPI 交互可以抽象成三个动作发命令字节、读状态字节、传数据块。所有操作都从拉低 CS 开始CS 拉高后一次交互结束。命令阶段主控发送一个代表操作码的字节状态阶段主控读取 CH376 返回的操作结果数据阶段只在需要传输文件内容时才出现而且必须先通过状态判断芯片是否准备好避免数据丢失。这里有一个容易踩的认知误区不要把这套协议当成普通的 SPI 外设寄存器访问。CH376 没有像 W25Q64 那样简单的「读 ID/读数据」命令表它更像一个有限状态机每条命令会改变内部工作状态。比如执行打开文件命令后再读数据含义就变成读取目录项信息而不是读取上一次命令的响应。因此驱动代码要按照「命令序列」组织而不是命令散装混用。2.3 命令码取值与缓冲区机制协议里最常用的命令码并不算多核心就十来个。表里列出的是我常用的宏定义具体数值以 CH376 数据手册为准不同批次模块固件不影响这些公开命令号。功能命令码说明测试芯片通信0x06写不同字节读回取反数据用于验证 SPI 链路设置工作模式0x15参数为磁盘模式或 U 盘模式需按手册配置获取状态0x22发送后读回一个字节判断上一条命令结果磁盘初始化和挂载0x51 / 0x52检查 TF 卡是否存在并加载文件系统打开或创建文件0x32 / 0x34按文件名定位或新建文件写文件数据0x35把缓冲区数据按当前偏移写入文件字节读0x3A按当前文件偏移读出指定长度数据写缓冲区0x2E主控把要写入的数据先上传到 CH376 缓冲区缓冲区机制是这个芯片比较特别的地方。CH376 内部 RAM 有限文件数据不直接穿透式写入 TF 卡而是先在主控和芯片缓冲区之间倒腾。写文件时主控用写缓冲区命令把一段最多 255 字节的数据放进 CH376然后发写文件命令CH376 再把缓冲区内容真正落盘。这样做的好处是主控不必考虑 TF 卡扇区边界问题芯片会在内部处理跨扇区写操作但代价是每写一小段就要等待一次 Flash 擦写完成连续大文件写入时会明显看到速度波动。3. STM32F103 硬件 SPI 接线与 CubeMX 初始化配置3.1 引脚分配和模块接线表我推荐用 SPI1 连接 CH376原因是 SPI1 挂在 APB2 总线上时钟比 SPI2 高而且在 F103 上最常见的引脚复用是 PA5 做 SCK、PA6 做 MISO、PA7 做 MOSI。这三个引脚在同一排杜邦线从最小系统板引到 CH376 模块非常短。片选单独用 PA4不首选硬件 NSS因为 CH376 时序要求 CS 的拉低拉高完全由命令序列控制用软件 GPIO 更灵活也方便后面用逻辑分析仪观察。STM32F103 引脚方向CH376 模块引脚说明PA5输出SCKSPI 时钟模式 0PA6输入MISO模块向主控回传数据PA7输出MOSI主控发送命令和数据PA4输出CS软件片选低有效3.3V—VCC给模块供电GND—GND共地必须可靠接线时注意一点CH376 模块上如果同时引出 5V 和 3.3V 引脚要看清楚模块是否自带稳压。多数模块板载 LDO可以从 5V 供电然后输出 3.3V 逻辑但有些精简模块直接把芯片电源引脚引出来只能接 3.3V。F103 最小系统板的 IO 是 3.3V 电平和 3.3V 供电的 CH376 直接对接没有问题如果你用的是 5V 单片机中间最好加电平转换CH376 的输入引脚耐压不一定支持 5V。共地之后用万用表量一下模块 VCC 引脚对地电压确认是 3.3V 左右再插 TF 卡。3.2 用 CubeMX 设置 SPI1 与片选 IO打开 STM32CubeMX 建立 F103C8 工程后把 PA5 设为 SPI1_SCK、PA6 设为 SPI1_MISO、PA7 设为 SPI1_MOSIPA4 设为 GPIO_Output。SPI1 参数按照 CH376 手册要求的模式 0 设置时钟极性选 Low时钟相位选 1 Edge数据大小 8 BitMSB 先行。关键是预分频我一般从 8 分频起步对应 72 MHz 的 SPI1 时钟除以 8 得到 9 MHz这个速度对 CH376 和 TF 卡都算余量充足。如果后续测试稳定再尝试 4 分频提高到 18 MHz但很多廉价 TF 卡在 18 MHz 下时序余量变差读写偶发出错不推荐追求极限。参数项设置值依据ModeFull-Duplex MasterCH376 是 SPI 从机Clock PolarityLowCH376 要求时钟空闲低Clock Phase1 Edge第一个边沿采样即模式 0Prescaler8得到 9 MHz 时钟NSSDisable片选用 GPIO 软件控制配置完生成代码后自己补一段发送和接收的封装。HAL 库的HAL_SPI_TransmitReceive每次调用都会等待完成标志对低速命令交互足够但如果后面要连续搬大量数据可以考虑改用 DMA 传输把 CPU 从字节搬运中解放出来。先看基础版本uint8_t ch376_spi_rw(uint8_t byte) { uint8_t tx byte; uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, tx, rx, 1, 100); return rx; }这段代码做的事情是同时发一个字节和收一个字节。CH376 的 SPI 在发送命令字节时MISO 上同时在返回上一次命令的状态或数据所以收发不能分开做。如果你只调HAL_SPI_TransmitMISO 的数据会被丢掉状态机就会错乱。参数里100是超时毫秒数命令交互阶段给 100 ms 足够到了文件写入阶段如果卡在做擦除状态响应可能会变慢超时值可以加大到 500 ms。3.2.1 SPI 时钟极性、相位和预分频怎么设才不翻车CH376 这类芯片的 SPI 时序严格程度不如普通 Flash但它同样遵循模式 0 定义时钟空闲保持低电平数据在第一个边沿被采样。CubeMX 里如果误选了 Mode 1 或 Mode 2表现很典型——命令发出去没有任何响应CHECK_EXIST读回的全是 0xFF 或随机值。遇到这种情况不要先怀疑芯片坏了先用示波器或者逻辑分析仪抓 SCK 的默认电平再对照 CH376 手册确认时序极性。预分频的选择需要综合考虑 F103 的 APB2 时钟。SPI1 的最大时钟是 36 MHz但这是芯片内部极限不代表 CH376 能承受。CH376 的 SPI 从机接口在数据手册上给出的工作频率并不高保守的工程师会从 1 MHz 级别起步验证链路。我的习惯是先用 32 分频约 2.25 MHz做通CHECK_EXIST和磁盘挂载确认整条链路没有问题后再把分频逐步调小。这样即使之后出现读写异常也能排除时钟太快导致的信号完整性问题。软件模拟 SPI 时同理不要想用翻转 GPIO 模拟出几十 MHz 的时钟CH376 的输入建立时间并不允许。4. CH376 核心操作代码发送命令、读状态与读写数据块4.1 用 HAL 库封装命令发送、状态读取和数据块传输驱动 CH376 的第一步是把基本操作封装成三个函数发命令、读状态、写缓冲区。发命令的动作是拉低 CS发送一个命令字节然后立刻读回一个字节这个读回的字节就是状态最后拉高 CS。这里要特别注意HAL_SPI_TransmitReceive在发命令字节的同一个时钟周期内MISO 上可能还残留上一次的无效数据所以要把读回的字节丢弃或记录下来用于调试不能直接当作状态值。void ch376_write_cmd(uint8_t cmd) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); ch376_spi_rw(cmd); } uint8_t ch376_read_status(void) { uint8_t status ch376_spi_rw(0xFF); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return status; }这段代码展示了最基本的命令-状态交互先拉低片选发送命令字节然后发送一个任意字节0xFF来读取 CH376 返回的状态最后拉高片选结束本次操作。0xFF是 SPI 读操作的惯例主机必须产生时钟才能读到数据而发送什么内容对从机来说无关紧要。之所以把拉高 CS 放在读状态函数里是为了避免每条命令结束后忘记释放总线。片选拉高的时机非常关键CH376 只有在 CS 拉高的上升沿才会真正锁存这次命令的结果如果漏掉这一步后续所有命令都会卡在芯片内部状态机上。4.2 复位与模式设置的最小命令序列芯片上电后不能直接操作 TF 卡要先做一次复位然后设置工作模式。复位命令比较简单发完命令码后延时一段时间让芯片内部固件重新加载。模式设置命令带一个参数字节这个参数决定 CH376 操作的是 TF 卡还是 U 盘具体取值一定要查对接模块所用固件版本的手册不同版本可能预留了不同的模式号。void ch376_reset(void) { ch376_write_cmd(0x05); // 复位命令 HAL_Delay(100); } uint8_t ch376_set_mode(uint8_t mode) { ch376_write_cmd(0x15); // 设置工作模式 ch376_spi_rw(mode); HAL_Delay(20); return ch376_read_status(); }模式设置完成后要等待 CH376 返回USB_INT_SUCCESS。这里不要用一个简单的延时替代状态判断因为 CH376 在初始化 TF 卡物理层时耗时和卡的状态有关新卡和老卡的上电时间差异可以达到几十毫秒。读完状态后如果发现返回值不是成功合理的做法是重试几次仍然失败再报错因为 TF 卡本身在上电瞬间可能出现接触不良或电平未稳定。连接 TF 卡的金手指是镀金层长期裸露容易氧化模块压卡座接触不良时也经常表现为这里初始化失败。4.2.1 常见失败点CS 毛刺和命令间延时接线不规范时最容易出现的一个现象是代码逻辑看起来没问题但每条命令都返回错误。原因往往在 CS 拉低瞬间MOSI 上的数据还没有稳定CH376 的输入锁存到了毛刺电平。排查方法是把 CS 拉低和第一个字节发送之间加一个微秒级延时让信号有时间建立。另一个失败点是复位后立刻发第一条命令CH376 内部固件可能还没完成启动此时芯片对 SPI 数据是忽略的。解决方式是在复位命令后延时至少 50 ms而不是发完就急着初始化。延时参数不是越大越好。CH376 的命令处理速度很快普通状态查询命令在微秒级别就能返回过长的固定延时塞在主循环里会导致整体响应变慢。正确做法是把状态查询做成带超时的循环发送命令后反复读取状态直到返回成功或超时。这样既保证时序严谨又不会拖累读写速度。4.3 状态判断与数据块读写文件级读写命令和底层命令不一样它们不是单次交互而是一个多步骤序列。以写文件为例主控必须先把数据放进 CH376 的缓冲区再发送写文件命令然后循环读取状态直到完成。缓冲区容量有限每次最多写入 255 字节所以大文件要分段写。下面这段代码展示了把一个缓冲区写入当前文件的完整流程uint8_t ch376_file_write(uint8_t *buf, uint16_t len) { uint8_t status; uint8_t i; uint16_t remain len; while (remain 0) { uint8_t chunk (remain 255) ? 255 : remain; ch376_write_cmd(0x2E); // 写 USB 数据到缓冲区 ch376_spi_rw(chunk); // 第一字节是本次长度 for (i 0; i chunk; i) { ch376_spi_rw(buf[len - remain i]); } HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); ch376_write_cmd(0x35); // 请求写入文件 ch376_spi_rw(0xFF); // 产生时钟读状态 status ch376_read_status(); if (status ! 0x14) { // 0x14 表示操作成功 return status; } remain - chunk; } return 0x14; }这段代码里有几个参数值得说明。0x2E是写数据命令它后面跟的第一个字节表示本次数据的长度这个长度不能大于 255因为 CH376 缓冲区就用一个字节来计长。发送完数据后必须拉高 CS表示缓冲区上传阶段结束然后才能发写文件命令。0x14是 CH376 表示操作成功的返回值不同固件版本可能定义为宏建议在头文件里用宏命名而不是直接用魔法数字。这里chunk的计算是核心每轮先算剩余长度和 255 的较小值保证不溢出缓冲区再按偏移从原缓冲区取数据。5. TF 卡文件读写的完整示例与文件系统注意点5.1 写一个 log.bin 的完整调用序列把前面封装好的函数串起来就是一个最小可用的日志写入流程。代码逻辑分三步初始化芯片和磁盘、创建文件、循环写入数据。这和直接用 C 库的fopen/fwrite/fclose很相似只是每个步骤都要显式检查状态不能假设设备一定在线。uint8_t demo_write_log(void) { uint8_t buf[32]; uint16_t i; uint8_t status; ch376_reset(); if (ch376_set_mode(SD_MODE) ! CH376_SUCCESS) { return 0x01; } status ch376_disk_init(); // 等待 TF 卡就绪 if (status ! CH376_SUCCESS) { return 0x02; } ch376_write_cmd(0x34); // 创建文件 ch376_write_str(LOG.BIN); status ch376_read_status(); if (status ! CH376_SUCCESS) { return 0x03; } for (i 0; i 100; i) { snprintf((char *)buf, sizeof(buf), line %d\r\n, i); status ch376_file_write(buf, strlen((char *)buf)); if (status ! CH376_SUCCESS) { return 0x04; } } ch376_write_cmd(0x36); // 关闭文件 ch376_spi_rw(0x02); // 关闭并更新目录项 status ch376_read_status(); return status; }这段代码里有一个不能跳过的细节写文件结束后必须关闭文件而且关闭参数要选择「更新目录项」模式。CH376 在写文件过程中先把数据写到簇里文件的长度和修改时间还留在芯片内部的目录缓存中只有关闭文件时才真正写回 FAT 表。如果写完数据直接断电而不关闭文件TF 卡上可能出现文件长度不对或目录项损坏。很多人在调试时发现文件内容存在但大小为 0十有八九就是这个原因。ch376_write_str是发送文件名字符串的辅助函数文件名必须全大写TF 卡上的 FAT 文件系统对大小写不敏感但目录项存储习惯用大写。5.2 读 TF 卡时 BYTE_READ 的偏移与跨扇区处理读文件和写文件略有不同。写可以分段进行读则要先打开文件然后逐块读取。CH376 的字节读命令会自动维护文件内部偏移不需要主控手动计算扇区号这比直接操作 SD 卡命令要省心得多。每次调用读取命令芯片从当前文件偏移处读出指定长度数据并填入内部缓冲区主控再用读取缓冲区命令把数据搬走。uint8_t ch376_file_read(uint8_t *buf, uint16_t len) { uint8_t status; uint8_t i; uint16_t remain len; while (remain 0) { uint8_t chunk (remain 255) ? 255 : remain; ch376_write_cmd(0x3A); // 请求读取 ch376_spi_rw(chunk); ch376_spi_rw(0xFF); // 读返回状态 status ch376_read_status(); if (status ! CH376_SUCCESS) { return status; } ch376_write_cmd(0x27); // 读取缓冲区数据 ch376_spi_rw(0xFF); // 实际返回长度 uint8_t actual ch376_spi_rw(0xFF); for (i 0; i actual i chunk; i) { buf[len - remain i] ch376_spi_rw(0xFF); } HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); remain - actual; } return CH376_SUCCESS; }这个函数里最关键的是0x27命令后连续读三个字节的含义第一个字节是还没消费的剩余长度第二个字节才是本次实际读取到的主机缓冲区长度后面跟的是数据。很多人在这一步会多读或少读一个字节导致后续数据错位。实际读取长度可能小于请求长度这说明已经读到了文件末端所以要按actual而不是chunk来减少剩余计数否则函数会陷入死循环。5.3 排查 CH376 命令忙与文件系统错误调试过程中最常见的报错是芯片返回CMD_BUSY或文件系统错误码。前者表示 CH376 还在处理上一条命令后者则可能是目录损坏或文件名不符合规范。下表总结了几个典型现象和对应的处理方向。现象可能原因排查方向初始化后状态一直忙复位不彻底或供电不足检查 3.3 V 电压跌落复位后加长延时文件打开失败文件名大小写或扩展名格式不对用短文件名格式确认 8.3 规则写文件后电脑上看到长度为 0没有执行关闭文件命令检查关闭参数是否包含更新目录项读取内容乱码数据块读取时偏移错位核对0x27命令后的长度读取顺序偶尔能读写偶尔失败芯片发热或时钟太快降低 SPI 分频检查排线质量文件系统层面的问题经常被误判为硬件故障。比如 CH376 创建的目录项格式是基于 FAT16/32 的短文件名如果文件名超过 8 个字符的主体或 3 个字符的扩展名芯片会报错。我一般把所有日志文件固定命名为LOG.BIN、DATA001.BIN这样的短名避免节外生枝。如果 TF 卡之前被电脑格式化成了 exFATCH376 可能无法挂载这时要用工具重新格式化为 FAT32簇大小保持默认即可。6. 验证读写和用逻辑分析仪抓取 SPI 时序的三个技巧写完全部驱动后第一件事不是直接插到业务代码里而是做一次独立的回读验证。常见做法是写完一个文件后用0x32命令打开同一个文件然后逐字节读取并和原数据比对。注意比对时不要用memcmp比完就结束要把比对失败的偏移量记下来如果错误位置总是落在 512 字节的整数倍附近基本可以判定为底层扇区跨边界问题这时优先检查 CH376 的缓冲区交互时序而不要怀疑文件系统。逻辑分析仪抓时序时通道分配有讲究。SCK 和 CS 是必须抓的MOSI 抓命令字节MISO 抓状态返回。第一个要看的不是数据内容而是 CS 的拉低时间跨度。一条完整命令的 CS 低电平持续时间应该在几十微秒以内如果发现 CS 低电平长达毫秒级说明主控在执行命令中间被中断或者其他任务插队了这会破坏 CH376 的命令完整性。第二个要看 SCK 的空闲电平必须是稳定的低电平如果看到 SCK 上有毛刺检查 SPI 引脚是否被复用成了其他外设。第三个技巧是抓命令码序列对照手册一条条核对流程比如打开文件前是否真的发了模式设置关闭文件时是否带上了更新参数。关于读写速度这个方案本身不是为高速存储设计的。CH376 的缓冲区大小和 TF 卡擦写延迟决定了单片连续写速度不会快实际测试常见的工业卡在 9 MHz SPI 下写入速度大约在几十 KB/s 量级读取稍快一些。如果你的应用需要高速记录可以考虑把数据先攒在内存里凑够一次较大的连续写入再落盘但单次不能超过 255 字节的限制是芯片硬件决定的。如果确实需要更高吞吐就应该换用 STM32F103 的 SDIO 接口直连 TF 卡或者直接用带文件系统的单片机方案CH376 的价值在于开发速度和协议隔离不在于带宽。本文还有配套的精品资源点击获取
返回列表