手把手教你用STM32和CH376芯片读写U盘(附完整SPI驱动代码)
2026/4/19 19:37:12 网站建设 项目流程

STM32与CH376芯片U盘读写实战:从SPI驱动到文件系统全解析

在嵌入式系统中扩展USB主机功能一直是开发者面临的挑战。想象一下,你的STM32项目需要记录传感器数据到U盘,或者从U盘中读取配置文件——这正是CH376这类USB主机控制芯片大显身手的场景。不同于直接使用MCU内置USB外设的复杂协议栈,CH376通过简单的SPI接口为嵌入式系统提供了即插即用的U盘读写能力。本文将带你从零构建一个完整的解决方案,涵盖硬件连接、SPI驱动调试、文件系统操作等关键环节,并提供可直接复用的代码模块。

1. 硬件设计与SPI接口配置

1.1 CH376硬件连接要点

CH376支持三种通信接口,其中SPI模式因其硬件资源占用少、速率适中最受青睐。典型连接方案需要关注以下关键点:

  • 电源设计:CH376T(3.3V版本)需稳定供电,建议在VCC引脚附近放置0.1μF去耦电容
  • SPI信号线
    • SCK:时钟线,建议使用GPIO推挽输出
    • SDO(MISO):主设备输入,需配置为上拉输入
    • SDI(MOSI):主设备输出,推挽输出模式
    • SCS:片选信号,低电平有效
  • 中断信号:INT引脚需配置为下拉输入,用于事件通知
  • U盘接口:VBUS需提供5V电源(可通过MOSFET控制)

提示:若使用软件模拟SPI,所有GPIO速度建议设置为2MHz以避免信号振铃

1.2 STM32端GPIO初始化代码

void CH376_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 启用GPIO时钟(根据实际连接修改) __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置SCK, SDI, SCS为推挽输出 GPIO_InitStruct.Pin = CH376_SCK_PIN | CH376_SDI_PIN | CH376_SCS_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(CH376_PORT, &GPIO_InitStruct); // 配置SDO为上拉输入 GPIO_InitStruct.Pin = CH376_SDO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(CH376_PORT, &GPIO_InitStruct); // 配置INT为下拉输入 GPIO_InitStruct.Pin = CH376_INT_PIN; GPIO_InitStruct.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(CH376_INT_PORT, &GPIO_InitStruct); // 初始状态:片选置高 HAL_GPIO_WritePin(CH376_PORT, CH376_SCS_PIN, GPIO_PIN_SET); }

2. SPI通信层实现与调试

2.1 软件SPI时序精准控制

硬件SPI虽然方便,但在某些引脚受限的场景下,软件模拟SPI反而更具灵活性。关键是要确保时序符合CH376的规格要求:

  • 时钟空闲状态为高电平
  • 数据在时钟上升沿采样
  • 最小半周期延迟为0.5μs(对应最大1MHz时钟)
// 精确的微秒级延迟函数(需根据CPU频率校准) void CH376_DelayUS(uint32_t us) { uint32_t ticks = SystemCoreClock / 1000000 * us / 5; while(ticks--); } // 软件SPI写一个字节 void CH376_SPI_WriteByte(uint8_t data) { HAL_GPIO_WritePin(CH376_PORT, CH376_SCS_PIN, GPIO_PIN_RESET); for(uint8_t i=0; i<8; i++) { HAL_GPIO_WritePin(CH376_PORT, CH376_SCK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(CH376_PORT, CH376_SDI_PIN, (data & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET); CH376_DelayUS(1); HAL_GPIO_WritePin(CH376_PORT, CH376_SCK_PIN, GPIO_PIN_SET); CH376_DelayUS(1); data <<= 1; } HAL_GPIO_WritePin(CH376_PORT, CH376_SCS_PIN, GPIO_PIN_SET); } // 软件SPI读一个字节 uint8_t CH376_SPI_ReadByte(void) { uint8_t data = 0; HAL_GPIO_WritePin(CH376_PORT, CH376_SCS_PIN, GPIO_PIN_RESET); for(uint8_t i=0; i<8; i++) { HAL_GPIO_WritePin(CH376_PORT, CH376_SCK_PIN, GPIO_PIN_RESET); CH376_DelayUS(1); data <<= 1; if(HAL_GPIO_ReadPin(CH376_PORT, CH376_SDO_PIN)) data |= 0x01; HAL_GPIO_WritePin(CH376_PORT, CH376_SCK_PIN, GPIO_PIN_SET); CH376_DelayUS(1); } HAL_GPIO_WritePin(CH376_PORT, CH376_SCS_PIN, GPIO_PIN_SET); return data; }

2.2 通信链路验证技巧

在进入文件操作前,必须确保SPI通信可靠。CH376提供了专门的测试命令:

uint8_t CH376_TestConnection(void) { CH376_SPI_WriteByte(CMD11_CHECK_EXIST); CH376_SPI_WriteByte(0x55); // 测试模式 uint8_t response = CH376_SPI_ReadByte(); if(response != 0xAA) { printf("SPI通信测试失败,收到:0x%02X\r\n", response); return 0; } printf("SPI通信测试成功\r\n"); return 1; }

常见通信问题排查表:

现象可能原因解决方案
无响应片选信号异常检查SCS引脚连接和电平
返回0xFF时钟相位错误调整SCK边沿采样点
随机错误电源不稳定增加电源去耦电容
间歇性失败中断冲突检查INT引脚配置

3. U盘文件系统操作实战

3.1 文件操作基本流程

CH376内置了FAT文件系统引擎,简化了文件操作。典型工作流程如下:

  1. 初始化芯片:设置USB主机模式
  2. 检测设备连接:轮询或中断方式
  3. 打开文件:支持创建新文件或打开现有文件
  4. 读写操作:支持顺序和随机存取
  5. 关闭文件:确保数据写入物理设备
// 初始化USB主机模式 uint8_t CH376_InitUSBHost(void) { CH376_SPI_WriteByte(CMD11_SET_USB_MODE); CH376_SPI_WriteByte(0x06); // 模式6:USB主机模式 uint8_t status = CH376_SPI_ReadByte(); if(status != CMD_RET_SUCCESS) { printf("USB主机模式设置失败:0x%02X\r\n", status); return 0; } return 1; } // 等待U盘连接(超时单位:毫秒) uint8_t CH376_WaitDiskReady(uint32_t timeout) { uint32_t start = HAL_GetTick(); while(HAL_GetTick() - start < timeout) { CH376_SPI_WriteByte(CMD01_DISK_CONNECT); uint8_t status = CH376_SPI_ReadByte(); if(status == USB_INT_SUCCESS) return 1; HAL_Delay(100); } return 0; }

3.2 文件创建与写入示例

// 创建并写入文本文件 uint8_t CH376_WriteTestFile(void) { // 打开/创建文件 CH376_SPI_WriteByte(CMD0H_FILE_OPEN); uint8_t status = CH376_SPI_ReadByte(); if(status != USB_INT_SUCCESS) { printf("文件打开失败:0x%02X\r\n", status); return 0; } // 设置文件指针到开始 CH376_SPI_WriteByte(CMD4H_BYTE_LOCATE); CH376_SPI_WriteByte(0x00); CH376_SPI_WriteByte(0x00); CH376_SPI_WriteByte(0x00); CH376_SPI_WriteByte(0x00); status = CH376_SPI_ReadByte(); // 准备写入数据 uint8_t data[] = "STM32 CH376测试数据\r\n"; uint16_t length = sizeof(data) - 1; // 开始写入 CH376_SPI_WriteByte(CMD2H_BYTE_WRITE); CH376_SPI_WriteByte((uint8_t)length); CH376_SPI_WriteByte((uint8_t)(length >> 8)); for(uint16_t i=0; i<length; i++) { CH376_SPI_WriteByte(data[i]); } status = CH376_SPI_ReadByte(); // 关闭文件 CH376_SPI_WriteByte(CMD0H_FILE_CLOSE); CH376_SPI_WriteByte(0x01); // 更新文件长度 status = CH376_SPI_ReadByte(); return 1; }

注意:文件名必须使用大写字母且包含完整路径,如"/DATA.TXT"

4. 高级应用与性能优化

4.1 中断驱动设计

轮询方式效率低下,利用INT引脚中断可大幅提升系统响应速度:

// 中断服务例程 void CH376_INT_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(CH376_INT_PIN) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(CH376_INT_PIN); CH376_SPI_WriteByte(CMD01_GET_STATUS); uint8_t status = CH376_SPI_ReadByte(); switch(status) { case USB_INT_DISK_READ: // 处理磁盘读取完成 break; case USB_INT_DISK_WRITE: // 处理磁盘写入完成 break; case USB_INT_DISK_CONNECT: // 处理U盘插入 break; case USB_INT_DISK_DISCONNECT: // 处理U盘拔出 break; } } }

4.2 文件插入操作技巧

在现有文件中插入数据是个常见需求,但直接插入会覆盖后续内容。正确做法是:

  1. 定位到插入位置
  2. 读取后续内容到缓冲区
  3. 写入新数据
  4. 追加原缓冲区内容
uint8_t CH376_FileInsert(const char* filename, uint32_t offset, uint8_t* data, uint16_t length) { // 1. 打开文件 // 2. 定位到offset+length位置 // 3. 读取后续数据到缓冲区 // 4. 定位回offset位置 // 5. 写入新数据 // 6. 追加缓冲区内容 // 7. 关闭文件 // 错误处理省略... }

4.3 性能优化对比表

优化措施执行时间(ms)内存占用实现复杂度
轮询模式1200简单
中断驱动300中等
DMA传输150复杂
双缓冲100较高复杂

在实际项目中,使用中断驱动配合合理的缓冲区大小(通常4KB)能达到最佳性价比。

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

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

立即咨询