手把手教你用STM32和CH376芯片读写U盘(附完整工程代码)
STM32与CH376芯片实现U盘数据存储的实战指南
在嵌入式开发中,数据存储是一个常见需求。无论是工业设备运行日志、环境监测数据,还是固件升级包,都需要一种可靠、便捷的存储方案。而U盘作为一种通用存储介质,具有容量大、便携性强、成本低的优势。本文将带你从零开始,使用STM32微控制器和CH376芯片构建一个完整的U盘数据存储系统。
1. 项目准备与硬件连接
1.1 硬件选型与原理
CH376是南京沁恒推出的一款USB主机控制器芯片,它最大的特点是内置了USB协议栈和文件系统,开发者无需深入理解复杂的USB协议和FAT文件系统细节。我们选择STM32F103C8T6作为主控,这款Cortex-M3内核的MCU性价比高,资源丰富,非常适合嵌入式学习。
硬件连接采用SPI接口,这是最常用的通信方式。CH376支持硬件SPI和软件模拟SPI,考虑到不同STM32型号的SPI外设差异,本教程使用软件模拟SPI,确保代码可移植性。所需引脚连接如下:
| STM32引脚 | CH376引脚 | 功能说明 |
|---|---|---|
| PB12 | SCS | 片选信号 |
| PB13 | SCK | 时钟信号 |
| PB14 | SDO | 数据输出 |
| PB15 | SDI | 数据输入 |
| PA8 | INT | 中断信号 |
提示:INT引脚建议配置为外部中断输入模式,以便及时响应CH376的中断请求。
1.2 开发环境搭建
- 安装Keil MDK或STM32CubeIDE开发环境
- 准备一个FAT32格式的U盘(容量建议不超过32GB)
- 下载CH376官方库文件(包含SPI驱动和文件系统API)
- 创建STM32工程,配置系统时钟和基本外设
// 示例:系统时钟配置(STM32F103 @72MHz) void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; HAL_RCC_OscConfig(&RCC_OscInitStruct); RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); }2. CH376驱动开发
2.1 SPI接口初始化
软件模拟SPI的核心是精确控制GPIO的电平变化。以下是关键初始化代码:
void CH376_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能GPIO时钟 __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // 配置SCS、SCK、SDI为推挽输出 GPIO_InitStruct.Pin = GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 配置SDO为上拉输入 GPIO_InitStruct.Pin = GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 配置INT为上拉输入,并启用外部中断 GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 设置NVIC优先级并启用中断 HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); }2.2 基本通信测试
在正式使用前,必须验证MCU与CH376的通信是否正常:
uint8_t CH376_TestConnection(void) { uint8_t test_data = 0x57; // 任意测试数据 uint8_t response; CH376_SPI_Select(); // 拉低片选 CH376_WriteCmd(CMD_CHECK_EXIST); CH376_WriteData(test_data); response = CH376_ReadData(); CH376_SPI_Release(); // 释放片选 if(response != (uint8_t)(~test_data)) { return 0; // 通信失败 } return 1; // 通信成功 }常见通信问题排查:
- 检查所有连接线是否接触良好
- 确认电源电压稳定(CH376需要3.3V供电)
- 测量SCK信号是否正常(可用逻辑分析仪观察)
- 检查片选信号是否正确控制
3. U盘文件操作实战
3.1 初始化与U盘检测
成功建立通信后,需要设置CH376的工作模式并检测U盘插入:
uint8_t CH376_DiskInit(void) { uint8_t status; // 设置USB主机模式 CH376_WriteCmd(CMD_SET_USB_MODE); CH376_WriteData(0x06); // 模式6:自动检测U盘 HAL_Delay(20); status = CH376_ReadData(); if(status != CMD_RET_SUCCESS) { return status; // 返回错误代码 } // 检测U盘连接 CH376_WriteCmd(CMD_DISK_CONNECT); HAL_Delay(100); status = CH376_ReadData(); if(status == USB_INT_SUCCESS) { return 0; // U盘已连接 } else { return status; // 返回错误代码 } }3.2 文件创建与写入
创建一个CSV格式的数据文件并写入传感器数据:
uint8_t CreateAndWriteFile(const char* filename, const char* data) { uint8_t status; uint16_t len = strlen(data); // 创建文件 CH376_WriteCmd(CMD_FILE_CREATE); CH376_WriteData((uint8_t*)filename, strlen(filename)); status = CH376_ReadData(); if(status != USB_INT_SUCCESS) { return status; } // 打开文件 CH376_WriteCmd(CMD_FILE_OPEN); CH376_WriteData((uint8_t*)filename, strlen(filename)); status = CH376_ReadData(); if(status != USB_INT_SUCCESS) { return status; } // 设置文件指针到末尾 CH376_WriteCmd(CMD_FILE_WRITE); CH376_WriteData(0xFF); CH376_WriteData(0xFF); CH376_WriteData(0xFF); CH376_WriteData(0xFF); status = CH376_ReadData(); if(status != USB_INT_SUCCESS) { return status; } // 写入数据 CH376_WriteCmd(CMD_BYTE_WRITE); CH376_WriteData(len & 0xFF); CH376_WriteData((len >> 8) & 0xFF); CH376_WriteData((uint8_t*)data, len); status = CH376_ReadData(); // 关闭文件 CH376_WriteCmd(CMD_FILE_CLOSE); CH376_WriteData(0x01); // 更新文件长度 return status; }注意:文件名必须使用大写字母,且建议包含完整路径(如"/DATA/TEMP.CSV")
4. 高级功能与性能优化
4.1 数据缓存与批量写入
频繁的小文件写入会显著降低性能并缩短U盘寿命。解决方案是采用缓存机制:
#define BUFFER_SIZE 512 typedef struct { char data[BUFFER_SIZE]; uint16_t index; } FileBuffer; void Buffer_Init(FileBuffer* buf) { buf->index = 0; memset(buf->data, 0, BUFFER_SIZE); } uint8_t Buffer_Write(FileBuffer* buf, const char* filename, const char* str) { uint16_t str_len = strlen(str); if((buf->index + str_len) >= BUFFER_SIZE) { // 缓冲区将满,先写入文件 uint8_t status = CreateAndWriteFile(filename, buf->data); if(status != USB_INT_SUCCESS) return status; Buffer_Init(buf); } memcpy(&buf->data[buf->index], str, str_len); buf->index += str_len; return USB_INT_SUCCESS; }4.2 错误处理与恢复
稳定的文件系统操作需要完善的错误处理机制。常见错误及解决方案:
| 错误代码 | 含义 | 解决方案 |
|---|---|---|
| 0x15 | 磁盘未连接 | 检查U盘是否插好,重新初始化 |
| 0x22 | 中断请求失败 | 调整SPI时序,增加适当延时 |
| 0x41 | 文件未找到 | 确认文件名和路径正确 |
| 0x42 | 目录未找到 | 先创建目录再操作文件 |
| 0x43 | 文件已存在 | 删除旧文件或使用不同名称 |
void Handle_CH376_Error(uint8_t error_code) { switch(error_code) { case 0x15: printf("磁盘未连接,请检查U盘\r\n"); CH376_DiskInit(); // 尝试重新初始化 break; case 0x22: printf("通信时序错误,调整延时\r\n"); SPI_Delay += 1; // 增加延时 break; // 其他错误处理... default: printf("未知错误: 0x%02X\r\n", error_code); } }4.3 文件读取与数据解析
从U盘读取数据同样重要,特别是固件升级场景:
uint8_t ReadFileContents(const char* filename, char* buffer, uint16_t max_len) { uint8_t status; uint16_t bytes_read = 0; // 打开文件 CH376_WriteCmd(CMD_FILE_OPEN); CH376_WriteData((uint8_t*)filename, strlen(filename)); status = CH376_ReadData(); if(status != USB_INT_SUCCESS) return status; // 设置读取位置(文件开头) CH376_WriteCmd(CMD_FILE_READ); CH376_WriteData(0x00); CH376_WriteData(0x00); CH376_WriteData(0x00); CH376_WriteData(0x00); status = CH376_ReadData(); if(status != USB_INT_SUCCESS) return status; // 读取数据 CH376_WriteCmd(CMD_BYTE_READ); CH376_WriteData(max_len & 0xFF); CH376_WriteData((max_len >> 8) & 0xFF); bytes_read = CH376_ReadData16(); CH376_ReadData((uint8_t*)buffer, bytes_read); // 关闭文件 CH376_WriteCmd(CMD_FILE_CLOSE); CH376_WriteData(0x00); return bytes_read; }5. 实际项目集成建议
5.1 传感器数据记录系统
将上述功能集成到实际项目中,创建一个完整的数据记录系统:
硬件组成:
- STM32F103C8T6最小系统板
- CH376S模块(SPI接口)
- 温度传感器(如DS18B20)
- 实时时钟模块(如DS1302)
软件流程:
graph TD A[系统初始化] --> B[传感器初始化] B --> C[CH376初始化] C --> D[创建数据文件] D --> E[定时读取传感器] E --> F[数据格式化存储] F --> G[缓冲区满?] G -- 是 --> H[写入U盘] G -- 否 --> E数据格式示例:
时间戳,温度(℃),湿度(%) 2023-07-20 14:30:00,25.6,45.2 2023-07-20 14:31:00,25.7,45.1
5.2 固件升级方案
利用U盘实现设备固件升级(DFU):
- 设计一个特殊的升级文件(如FIRMWARE.BIN)
- 设备启动时检查U盘中的升级文件
- 验证文件完整性后,跳转到Bootloader程序
- 擦除旧固件,写入新固件
- 重启设备,完成升级
关键代码片段:
void Check_Firmware_Update(void) { if(CH376_DiskInit() == USB_INT_SUCCESS) { if(FileExists("/UPDATE/FIRMWARE.BIN")) { uint32_t file_size = GetFileSize("/UPDATE/FIRMWARE.BIN"); if(file_size > 0 && file_size < FLASH_SIZE) { Start_DFU_Process(); } } } }5.3 性能优化技巧
SPI时钟优化:
- 软件SPI:调整延时函数,找到最快稳定频率
- 硬件SPI:配置为模式0,时钟分频适当降低
文件系统优化:
- 避免频繁打开/关闭文件
- 使用适当大小的缓冲区(通常512字节或1KB)
- 定期调用
CH376_WriteVar32(CMD_DISK_UPDATE)更新磁盘信息
电源管理:
- 不操作U盘时,调用
CH376_WriteCmd(CMD_ENTER_SLEEP)进入低功耗模式 - 检测到U盘拔出时,及时释放相关资源
- 不操作U盘时,调用
void CH376_PowerSave(void) { CH376_WriteCmd(CMD_ENTER_SLEEP); // 配置INT引脚唤醒 EXTI->IMR |= CH376_INT_EXTI_LINE; EXTI->RTSR |= CH376_INT_EXTI_LINE; // 上升沿触发 }