PIC16F1947驱动CH376芯片实现SD卡数据存储

在工业数据采集、便携仪表等应用中,经常需要把传感器数据保存到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
复制代码
}

七、常见问题与调试经验

  1. SPI 时序 :确保SCK空闲时为低电平,上升沿采样,下降沿输出。
  2. 中断处理 :CH376完成命令后INT变低,单片机读取状态后自动清除中断。如果不用中断引脚,可以轮询CMD_GET_STATUS,但效率低。
  3. 晶振 :必须12MHz,否则SD卡通信会失败。
  4. 初始化失败 :检查CH376的V3电容、电源纹波。上电后延时100ms再发命令。
  5. 文件系统 :SD卡必须先格式化为FAT32或FAT16。
  6. 长文件名 :CH376默认支持短文件名,若需长文件名需额外配置。

八、扩展应用

  • 配置文件读取 :从SD卡读取参数,动态修改系统设置。
  • 固件升级 :将新固件放在SD卡,单片机读取后写入Flash。

九、总结

通过PIC16F1947的SPI接口控制CH376,可以快速实现SD卡的数据存储功能,省去自己编写FAT文件系统和SD卡驱动的麻烦。CH376的命令集清晰,开发周期短,非常适合嵌入式数据记录场景。

后续干货不断,咱们一起在单片机的世界里,共同进步。

相关推荐
天青色等烟雨..1 小时前
智慧农林核心遥感技术99个案例实践
运维·人工智能·spring boot·后端·自动化
H__Rick2 小时前
C51学习-DAY8
单片机·嵌入式硬件·学习
youcans_2 小时前
从零搭建 STM32 VSCode 开发环境
vscode·stm32·单片机·嵌入式硬件
KAXA_2 小时前
深度适配仓储物流金属环境:KXA 5100工业无线AP抗干扰部署方案
物联网·仓储物流·无线ap·网络覆盖·抗金属干扰
ye150127774552 小时前
220V降5V0.3A电源芯片WT5104
单片机·嵌入式硬件·其他·硬件工程
第二层皮-合肥2 小时前
【数据采集专栏】输入阻抗
单片机·嵌入式硬件
風清掦2 小时前
【STM32学习笔记-15】FLASH 闪存(Claude)
笔记·stm32·单片机·嵌入式硬件·学习
MXsoft6182 小时前
**智慧校园运维实践:多校区、老旧设备的统一监控方案**
运维·自动化
golfscript3 小时前
Playwright Python:微软出的浏览器自动化库
python·其他·microsoft·自动化