在工业数据采集、便携仪表等应用中,经常需要把传感器数据保存到SD卡里,方便后续分析。但直接用单片机读写SD卡,得自己搞定SD卡命令、FAT文件系统,工作量巨大。用CH376 文件管理芯片 ,一切就简单了------它内置了FAT32/FAT16文件系统和SD卡驱动,单片机只需通过串口、SPI或并口发送几条指令,就能像操作电脑文件一样读写SD卡。
今天,就用PIC16F1947通过SPI 接口 控制CH376,实现SD卡的初始化、文件创建、数据写入和读取。
一、CH376芯片简介
CH376是南京沁恒微电子推出的USB/SD卡文件管理控制芯片。它支持三种通信接口:8位并行总线、SPI串行总线、异步串口。对于PIC16F1947这种只有两个串口的单片机,串口需要接触摸屏和一路RS485,所以我使用的是SPI 接口 ,只需4根线(SCS、SCK、SDI、SDO),外加一个中断引脚。
核心功能 :
- 内置FAT12/FAT16/FAT32文件系统固件
- 支持SD卡和U盘
- 提供文件打开、读写、创建、删除等命令
- 以字节或扇区为单位读写
二、PIC16F1947与CH376(SPI模式)
要让CH376工作于SPI从机模式,需要在复位时配置引脚电平。CH376在上电复位时会采样RXD、TXD、WR#、RD#、PCS#、AO引脚,若它们均为高电平或悬空,则选择SPI接口。
- 如果使用5V供电,V3引脚必须接一个0.01μF电容到地;如果使用3.3V供电,则V3直接接VCC,此处PIC16F1947可以3.3V供电,所以CH376也使用3.3V供电。
- 晶振须用12MHz。
SD 卡插座 :CH376可以直接连接SD卡,它的SD_CS、SD_CK、SD_DI、SD_DO引脚直接接SD卡座对应引脚,无需额外电路。注意SD卡电源也要接3.3V。
三、软件设计:PIC16F1947与CH376的SPI通信
CH376的SPI接口支持模式0和模式3(CPOL=0, CPHA=0或CPOL=1, CPHA=1),数据位顺序为MSB first。SPI时钟频率建议不超过2MHz。
3.1 底层SPI读写函数
初始化代码如下:
#include <xc.h>
#include <pic16f1947.h>
// 引脚定义
#define CH376_SCS RC0 // 片选,低有效
#define CH376_SCK RC3
#define CH376_SDI RC4 // MOSI
#define CH376_SDO RC5 // MISO
#define CH376_INT RB0 // 中断输入
// 宏定义操作
#define CS_LOW() RC0 = 0
#define CS_HIGH() RC0 = 1
// SPI初始化(主模式,时钟Fosc/4,空闲低,上升沿采样)
void SPI_Init(void) {
SSP1STAT = 0x40; // CKE=1, 数据在时钟上升沿发送
SSP1CON1 = 0x20; // SSPEN=1, 时钟Fosc/4, CKP=0
TRISC3 = 0; // SCK 输出
TRISC4 = 0; // SDI 输出
TRISC5 = 1; // SDO 输入
}
// SPI交换一个字节
unsigned char SPI_Transfer(unsigned char dat) {
SSP1BUF = dat;
while(!SSP1STATbits.BF);
return SSP1BUF;
}
// 向CH376写命令(一个字节)
void CH376_WriteCmd(unsigned char cmd) {
CS_LOW();
SPI_Transfer(cmd);
CS_HIGH();
}
// 向CH376写数据(一个或多个字节)
void CH376_WriteData(unsigned char *buf, unsigned char len) {
CS_LOW();
while(len--) {
SPI_Transfer(*buf++);
}
CS_HIGH();
}
// 从CH376读数据
void CH376_ReadData(unsigned char *buf, unsigned char len) {
CS_LOW();
while(len--) {
*buf++ = SPI_Transfer(0xFF);
}
CS_HIGH();
}
注意 :CH376的SPI操作中,每发送一个命令后,需要等待芯片完成内部处理(可以查询BZ引脚或延时,或通过中断等待)。一般可以在发送命令后,延时一小段时间再读数据。
3.2 通用命令执行函数
大多数CH376命令需要等待中断(INT变低)。可编写一个函数,发送命令并等待中断,然后获取中断状态。
// 等待CH376中断(超时返回0)
unsigned char CH376_WaitInt(unsigned int timeout_ms) {
while(timeout_ms--) {
if(RB0 == 0) return 1; // 中断发生
__delay_ms(1);
}
return 0;
}
// 执行命令并等待中断,返回中断状态
unsigned char CH376_ExecCmd(unsigned char cmd) {
CH376_WriteCmd(cmd);
if(!CH376_WaitInt(500)) return 0xFF; // 超时
// 获取中断状态
CH376_WriteCmd(0x22); // CMD_GET_STATUS
unsigned char status = SPI_Transfer(0xFF);
CS_HIGH();
return status;
}
四、初始化流程
使用CH376操作SD卡之前,先将其设置为"SD卡主机模式",然后检测并初始化SD卡。
// 初始化CH376并挂载SD卡
unsigned char SD_Init(void) {
// 1. 复位CH376
CH376_WriteCmd(0x05); // CMD_RESET_ALL
__delay_ms(50);
// 2. 设置USB模式为SD卡主机模式(模式3)
CH376_WriteCmd(0x15); // CMD_SET_USB_MODE
CH376_WriteData(&(unsigned char){0x03}, 1); // 模式3:SD卡主机
__delay_us(20);
// 读取操作状态(可选)
CH376_WriteCmd(0x22);
unsigned char status = SPI_Transfer(0xFF);
CS_HIGH();
if(status != 0x51) return 0; // 不是成功
// 3. 检测SD卡是否插入(硬件检测,这里假设已插入)
// 可选:发送CMD_DISK_CONNECT查询
// 4. 初始化磁盘(挂载)
status = CH376_ExecCmd(0x31); // CMD_DISK_MOUNT
if(status != 0x14) return 0; // USB_INT_SUCCESS
return 1; // 成功
}
五、文件写入示例:创建新文件并写入数据
假设我们要创建一个名为"DATA.TXT"的文件,写入"Hello World!"。
// 写入字符串到文件
void Write_File(void) {
unsigned char status;
// 1. 设置文件名
// 命令:CMD_SET_FILE_NAME (0x2F) 后跟字符串(以0结尾)
CH376_WriteCmd(0x2F);
CS_LOW();
SPI_Transfer('/'); // 根目录
SPI_Transfer('D');
SPI_Transfer('A');
SPI_Transfer('T');
SPI_Transfer('A');
SPI_Transfer('.');
SPI_Transfer('T');
SPI_Transfer('X');
SPI_Transfer('T');
SPI_Transfer(0x00); // 字符串结束
CS_HIGH();
// 2. 新建文件(CMD_FILE_CREATE, 0x34)
status = CH376_ExecCmd(0x34);
if(status != 0x14) {
// 错误处理
return;
}
// 3. 写入数据:使用字节写命令
unsigned char data[] = "Hello World!";
unsigned int len = sizeof(data)-1;
// CMD_BYTE_WRITE (0x3C) 需要先写入长度(2字节,低字节在前)
CH376_WriteCmd(0x3C);
CS_LOW();
SPI_Transfer(len & 0xFF);
SPI_Transfer(len >> 8);
CS_HIGH();
// 等待第一次中断(请求数据)
if(!CH376_WaitInt(500)) return;
CH376_WriteCmd(0x22);
unsigned char int_status = SPI_Transfer(0xFF);
CS_HIGH();
if(int_status != 0x1E) return; // USB_INT_DISK_WRITE
// 写入数据块(CMD_WR_REQ_DATA, 0x2D)
CH376_WriteCmd(0x2D);
// 首先读取数据块长度(1字节)
CS_LOW();
unsigned char block_len = SPI_Transfer(0xFF);
if(block_len > 0) {
for(unsigned char i=0; i<block_len; i++) {
if(i < len) SPI_Transfer(data[i]);
else SPI_Transfer(0);
}
}
CS_HIGH();
// 继续写(CMD_BYTE_WR_GO, 0x3D)
status = CH376_ExecCmd(0x3D);
if(status != 0x14) return;
// 最后关闭文件并更新长度
CH376_WriteCmd(0x36); // CMD_FILE_CLOSE
CS_LOW();
SPI_Transfer(1); // 允许更新长度
CS_HIGH();
CH376_WaitInt(500);
}
六、读取文件示例
void Read_File(void) {
unsigned char status;
// 1. 设置文件名(同上)
// 2. 打开文件(CMD_FILE_OPEN, 0x32)
status = CH376_ExecCmd(0x32);
if(status != 0x14) return;
// 3. 读取文件长度(可选)
CH376_WriteCmd(0x0C); // CMD_GET_FILE_SIZE
CS_LOW();
SPI_Transfer(0x68); // 固定输入68H
CS_HIGH();
unsigned long file_len;
CH376_ReadData((unsigned char*)&file_len, 4); // 小端格式
// 4. 读取数据:一次读64字节
unsigned char buf[64];
unsigned short remain = file_len;
while(remain) {
unsigned short req = (remain > 64) ? 64 : remain;
CH376_WriteCmd(0x3A); // CMD_BYTE_READ
CS_LOW();
SPI_Transfer(req & 0xFF);
SPI_Transfer(req >> 8);
CS_HIGH();
// 等待中断
if(!CH376_WaitInt(500)) break;
CH376_WriteCmd(0x22);
unsigned char int_st = SPI_Transfer(0xFF);
CS_HIGH();
if(int_st != 0x1D) break; // USB_INT_DISK_READ
// 读数据块
CH376_WriteCmd(0x2D); // CMD_WR_REQ_DATA? 其实是读数据用CMD_RD_USB_DATA,这里注意
// 正确的是:CMD_RD_USB_DATA (0x27)
CH376_WriteCmd(0x27);
CS_LOW();
unsigned char block_len = SPI_Transfer(0xFF);
for(unsigned char i=0; i<block_len; i++) {
buf[i] = SPI_Transfer(0xFF);
}
CS_HIGH();
remain -= block_len;
// 继续读
CH376_ExecCmd(0x3B); // CMD_BYTE_RD_GO
}
// 关闭文件
CH376_ExecCmd(0x36); // 带参数0
}
七、常见问题与调试经验
- SPI 时序 :确保SCK空闲时为低电平,上升沿采样,下降沿输出。
- 中断处理 :CH376完成命令后INT变低,单片机读取状态后自动清除中断。如果不用中断引脚,可以轮询CMD_GET_STATUS,但效率低。
- 晶振 :必须12MHz,否则SD卡通信会失败。
- 初始化失败 :检查CH376的V3电容、电源纹波。上电后延时100ms再发命令。
- 文件系统 :SD卡必须先格式化为FAT32或FAT16。
- 长文件名 :CH376默认支持短文件名,若需长文件名需额外配置。
八、扩展应用
- 配置文件读取 :从SD卡读取参数,动态修改系统设置。
- 固件升级 :将新固件放在SD卡,单片机读取后写入Flash。
九、总结
通过PIC16F1947的SPI接口控制CH376,可以快速实现SD卡的数据存储功能,省去自己编写FAT文件系统和SD卡驱动的麻烦。CH376的命令集清晰,开发周期短,非常适合嵌入式数据记录场景。
后续干货不断,咱们一起在单片机的世界里,共同进步。