自学内容网 自学内容网

【STM32】基于SPI协议读写SD,详解!

0 前言

  因为项目需要,使用stm32读写sd卡,这一块网上的资料很多,但是比较杂乱。有些是不能跑,有些是代码可以跑,但是相关的注释或者配置方法、流程不够清晰明确,于是花了几天时间,研究了几个成功案例之后,总结出一个相对明确的流程。【基于STM32F103C8T6

网上有各种流传的例程,经过测试确实可以用,但是魔改得有点多,个人觉得不是很便于理解,所以想着能不能从最开始的FATFS包来自己手动移植一个,最好是这个流程完全可复制,操作也非常简单,就像一个插件一样,基本实现模块化。

1 SD卡的种类和简介

  既然要读写SD卡,那首先要对SD卡的底层有一定的了解,这样才能够真正理解后面的代码。

1.1 SD卡的种类

  首先需要明确的是,SD卡指的是那种大的卡,一般用在相机里面,如下图所示:

在这里插入图片描述在这里插入图片描述

而这种卡:

在这里插入图片描述在这里插入图片描述
一般叫microSD卡TF卡,二者其实相差不大,只是引脚略微不同,其实读写都是一样的,也可以考虑买一个TF卡转SD卡的卡套,来适应两种接口。

  相比于这个SD卡的名字,另一个SD卡的标准显得更加重要。所谓的标准,差别主要体现在容量上面,这个需要在使用前明确。目前仍然有很多老年手机不支持大容量的TF卡,其本质就是因为不支持更高的标准。常见的SD卡标准如下图所示。
在这里插入图片描述
这个标准SD卡和TF卡是一样的,只是名字不同。

参考链接

  另外,根据这个链接, 实际上SD的通信协议也有多个版本,最早支持的版本是1.x,在SDHC之后,基本都是使用2.0版本,来兼容FAT32格式(原来都是FAT和FAT16),这两个协议的区别在驱动方面主要体现在指令上(2.0版本的指令更多,且兼容1.x版本的指令),这个后面有相关介绍,先埋个伏笔。

1.2 SD卡的整体结构

  理解了SD卡的种类,再来看看结构,主要是以下这张图
在这里插入图片描述
简单来说,就是除了存储单元外,还有好几个寄存器用于存放卡相关的信息,这些信息可以通过一些特定的指令读写。

1.3 SD卡运行机制——指令和响应

  SD卡的核心就是存储,那外部的主机如何对这个进行读写呢?就是通过指令。主机发送一条指令,然后SD卡会发送响应,让主机知道指令执行情况。
  每一条指令都是6个字节(48bit),其结构如下所示:
在这里插入图片描述

其中,Command占6位,所以一共有64个指令,从0-63,依次叫CMD0,CMD1,。。。CMD63,但是因为一次性是发送一个字节,也就是8位,所以会加上前面的两位,即0x40+CMDx才是指令。
  紧接着的是32位指令执行的参数,一般是存储地址或者寄存器值等,不是所有指令都有参数,对于没有参数的指令,直接传0即可。
  最后是校验值,这里采用的是循环校验,计算有点复杂,这个其实在后续的代码中,都是把部分常用的指令对应的校验计算出来给他传过去,并没有现场计算。

  指令发出之后,主机要等待SD卡的响应,其响应有很多类型,长度也各自不一样。短的响应只有一个字节,长的响应可以有多个字节。大部分的指令都是R1类型,即只有一个字节,R2表示响应有两个字节,还有一种类型是R1b,即在R1的基础上,后面紧跟着busy信号,可能有多个字节,一般不怎么使用。R1响应的结构各个位都有单独的含义,如下图所示。
在这里插入图片描述
可以看到,第6-1位都是错误,为1表示错误(“有效”),为0表示没有错误;第0位表示卡是否处于空闲状态,一般是发送进入IDLE指令(CMD0)之后会响应,也就是0x01。

  以上就是SD卡使用的基本讨论,即写入一个6字节的指令,然后读取响应的1-2个字节,并判断指令执行状态。时序图如下所示。
在这里插入图片描述
  接下来就是重点:SD卡数据的读写。和上面一样,读写数据之前,需要先发送一个指令,然后再读入或写入数据。对应的指令主要是这几个:
在这里插入图片描述
分别有读单块、读多块、写单块、写多块四个指令。其中,读写多块貌似需要使用到ACMD指令,所以用得比较少,可以通过多次调用读写单块的函数达到读写多块的目的。【一般SD卡一块(block)是512 Byte】
  根据官方的手册,读数据的流程大概是这样:
在这里插入图片描述
即先发送读的指令,然后等待sd卡响应指令后(根据上图,读单块和多块的响应都是R1类型),再读取数据块。
类似地,写指令的操作流程时序如下所示。
在这里插入图片描述
和上面不一样的是,在数据写入完毕后,还会有一个响应(Data Response),表示数据写入的情况,由SD卡传输给主机,是一个字节,其格式如下所示

在这里插入图片描述

  但是,这个时序图中并没有对“Data Block”部分进行展开叙述,但其实其内部结构同样重要,这里根据官方的描述和可行代码自行绘制了这张图:
在这里插入图片描述
其中,First Byte类似于一个启动符号,告知后面有数据来了,然后是一个block的数据,一般是512字节,最后是两位校验码。
  对于读数据,首先要读第一个字节,判断是不是0xFE,如果是,表示后面是数据,要把后面的数据给收了,收完512字节之后,最后的两位校验码可以忽略;对于写数据,是在发完写指令之后,手动写入0xFE,作为写数据的第一个字节,然后再写入512字节数据,最后两位校验码一般直接传0xFF即可。

2 SD卡的通信总线

  上面介绍的是SD卡的运行机制,从上面的结构图可以看出,这个运行机制到MCU控制端还需要一个通信协议,来约定这些数据该如何传输。常见的SD卡通信协议主要有两种:SPI模式SD模式(SDIO),其中两种通信协议下的引脚定义如下图所示。
在这里插入图片描述

在SPI协议中,SD卡扮演的角色是Slave,即从机,故其中MOSI和MISO中“M”指的控制数据读写的芯片,如MCU等;“S”从机是指SD卡。

参考链接

关于引脚的理解:以SPI为例,MCU对SD卡的控制指令都是通过CMD引脚串行传输的,所以CMD引脚是MOSI;而SD卡返回的数据是通过D0传输,所以D0是MISO。而SDIO数据传输可以选定多个引脚,常见的有只使用D0,和使用D0~D3四个引脚,并行传输。

2.1 SDIO

  在STM32F10x系列型号中,只有大容量的芯片才支持这个协议,没有实践过,这里只放一个网上的教程:

值得一提的是,不同协议其实只是传输方式不一样,底层的那些逻辑是差不多的,当然有些指令SPI协议不支持,只支持SDIO协议。

2.2 SPI

  • 概述
      SPI是四线协议:SCK(同步时钟),MOSI(主机到从机的数据),MISO(从机到主机的数据),CS(片选)。和IIC类似,也是一个串行协议,因为有时钟信号,所以是一个同步传输的协议(UART是异步协议)。但是,值得一提的是,因为收发数据是两根线,所以SPI是全双工协议,而IIC因为只有SCK和SDA,所以是半双工协议。

  • 运行模式
      SPI比较特殊的地方在于,它的电平和采样边沿可以额外设置,也就是设置不同的传输模式,这个设置由两个变量来确定:CPOL(Clock Polarity)、CPHA(Clock Phase),这两个变量分别可以设置0或1,因此组合起来有四种模式:

    • 0 0 CLK空闲时为低电平,CLK上升沿(第一个边沿)采样数据。
    • 0 1 CLK空闲时为低电平,CLK下降沿(第二个边沿)采样数据。
    • 1 0 CLK空闲时为高电平,CLK下降沿(第一个边沿)采样数据。
    • 1 1 CLK空闲时为高电平,CLK上升沿(第二个边沿)采样数据。
  • 数据同步
      由于SPI是全双工协议,且时钟只能是主设备发出,所以在主设备看来,不管是发送还是接收数据,都必须提供时钟,加上数据发送和接收是分开的两根线,所以数据在发送时也需要接收,或者说,接收时因为需要时钟,所以其实接收缓冲区也会新增数据,只是用不用的问题。
      那问题来了,如果我要收一个数据,必须发一个数据,那对方因为该数据误操作了怎么办?所以在接收数据时,要发送一个对从机设备来说无效的数据,也就是所谓的dummy data,这样就不会误响应了。

  • 代码配置
      网上有很多流传的软件SPI,即在理解SPI协议的基础上,使用IO口实现这个时序,但是这样一方面是代码比较麻烦,另外就是时钟配置难以掌握,所以这种只适用于硬件SPI没有或者被用完的情况,在有硬件SPI外设的前提下,还是用硬件比较方便。
      这里以一个标准库下的SPI外设初始化为例理解一下SPI配置的方法:

    void SD_SPI_Init(void)
    {
         
        GPIO_InitTypeDef GPIO_InitStructure;
    
    //使能时钟——宏定义实现
        ENABLE_SD_SPI_GPIO_CLK();
        ENABLE_SD_SPI_CLK();
    
    //GPIO初始化
        GPIO_InitStructure.GPIO_Pin = SD_SPI_MOSI_PIN | SD_SPI_SCK_PIN;   //MOSI & SCK: AFIO,Output
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(SD_SPI_GPIO_PORT, &GPIO_InitStructure);
    
        GPIO_InitStructure.GPIO_Pin = SD_SPI_MISO_PIN;                    //MISO: Input
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  //输入
        GPIO_Init(SD_SPI_GPIO_PORT, &GPIO_InitStructure);
    
    //SPI外设初始化
        SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
        SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//设置SPI工作模式:设置为主SPI
        SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//设置SPI的数据大小:SPI发送接收8位帧结构
        SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//选择了串行时钟的稳态:时钟悬空高
        SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//数据捕获于第二个时钟沿
        SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
        SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;//定义波特率预分频的值:波特率预分频值为256
        SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
        SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC值计算的多项式
        SPI_Init(SD_SPI, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
    
        SPI_Cmd(SD_SPI, ENABLE); //使能SPI外设
    
        SD_SPI_ReadWriteByte(0xff);//启动传输
    }
    

    重点是GPIO口输出和输入要分别配置。

3 硬件连接

  SD卡电路设计如下图所示,在画电路板时,记得在几个sd卡的引脚上加上上拉电阻:

在这里插入图片描述

CD引脚全称是Card Detect,用于检测卡是否插入,在一些开发板的原理图中有类似的做法,但是软件其实也可以判断出来,所以必要性不强

4 代码实践【重点】

  在使用SD卡时,建议在充分理解上述展示的SD卡运行原理后先实现存储的访问,比如先写入一段,然后再去读取,串口输出读取的内容,对比一下是否一致。然后再考虑加上FATFS,实现基本的读写文件功能。
  很显然,我其实并没有按照这个流程学习,而是先找了网上的一个可运行的代码(已经带了FATFS),然后在此基础上不断尝试新的操作,在这个尝试的过程中对SD卡运行原理有了比较深刻的认识。

  言归正传,如果以实用为主,建议直接使用HAL库,如果愿意折腾,可以自己尝试在标准库实现,建议在HAL库的基础上再去移植标准库。由于这两个步骤我都实践了一遍,后文都有介绍。

参考链接:

4.1 HAL库移植

  这部分内容基本参考自上面的教程,只做了一些小的修改,让这个部分集成度更高。

  • 首先先设置一些系统参数,不设置其实问题也不大,但是设置全面,不留风险是编程开发的一个好习惯:
    在这里插入图片描述

    在这里插入图片描述

  • 然后使能SPI外设

    在这里插入图片描述
    这里简单介绍一下NSS,所谓硬件NSS类似于串口的硬件流控一样,即通过实际的引脚来实现片选,这样就可以直接调用SPI的函数来进行控制,而所谓软件(即下面 NSS Signal Type: Software)即是额外再初始化一个引脚来控制。

    这里其实个人觉得两者是差不多的,只是硬件是芯片指定的引脚,而软件则可以随便指定,相对自由一些。代码上其实差别不大,只是一个调SPI库的函数,一个调GPIO库的函数。但是网上相关的代码基本都是使用软件形式,所以这里也跟风一下。

  • 然后再添加FATFS,这里只改动两个设置:

    • USE_LFN:Enable with static working buffer on the BSS
    • MAX_SS:4096

    如下图所示
    在这里插入图片描述

  • 项目配置那块,需要把堆栈加大

    在这里插入图片描述
    分文件显示,模块化更容易理解:
    在这里插入图片描述

    私以为将不同外设分为不同文件是一个很好的习惯

最后,生成代码即可,代码方面主要修改3个文件:

fat_sd_card.c【额外添加的一个文件】

#define TRUE  1
#define FALSE 0
#define bool BYTE

#include "fatfs_sd_card.h"

static volatile DSTATUS Stat = STA_NOINIT;  /* Disk Status */
static uint8_t CardType;                    /* Type 0:MMC, 1:SDC, 2:Block addressing */
static uint8_t PowerFlag = 0;               /* Power flag */

/***************************************
 * SPI functions
 **************************************/

/* slave select */
static void SELECT(void)
{
   
    HAL_GPIO_WritePin(SD_CS_PORT, SD_CS_PIN, GPIO_PIN_RESET);
    HAL_Delay(1);
}

/* slave deselect */
static void DESELECT(void)
{
   
    HAL_GPIO_WritePin(SD_CS_PORT, SD_CS_PIN, GPIO_PIN_SET);
    HAL_Delay(1);
}

/* SPI transmit a byte */
static void SPI_TxByte(uint8_t data)
{
   
    while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE));
    HAL_SPI_Transmit(HSPI_SDCARD, &data, 1, SPI_TIMEOUT);
}

/* SPI transmit buffer */
static void SPI_TxBuffer(uint8_t* buffer, uint16_t len)
{
   
    while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE));
    HAL_SPI_Transmit(HSPI_SDCARD, buffer, len, SPI_TIMEOUT);
}

/* SPI receive a byte */
static uint8_t SPI_RxByte(void)
{
   
    uint8_t dummy, data;
    dummy = 0xFF;

    while(!__HAL_SPI_GET_FLAG(HSPI_SDCARD, SPI_FLAG_TXE));
    HAL_SPI_TransmitReceive(HSPI_SDCARD, &dummy, &data, 1, SPI_TIMEOUT);

    return data;
}

/* SPI receive a byte via pointer */
static void SPI_RxBytePtr(uint8_t* buff)
{
   
    *buff = SPI_RxByte();
}

/***************************************
 * SD functions
 **************************************/

/* wait SD ready */
static uint8_t SD_ReadyWait(void)
{
   
    uint8_t res;

    /* timeout 500ms */
    int32_t Timer2 = 0xffffff;

    /* if SD goes ready, receives 0xFF */
    do
    {
   
        res = SPI_RxByte();
        Timer2--;
    }
    while((res != 0xFF) && Timer2 > 0);

    return res;
}

/* power on */
static void SD_PowerOn(void)
{
   
    uint8_t args[6];
    uint32_t cnt = 0x1FFF;

    /* transmit bytes to wake up */
    DESELECT();
    for(int i = 0; i < 10; i++)
    {
   
        SPI_TxByte(0xFF);
    }

    /* slave select */
    SELECT();

    /* make idle state */
    args[0] = CMD0;   /* CMD0:GO_IDLE_STATE */
    args[1] = 0;
    args[2] = 0;
    args[3] = 0;
    args[4] = 0;
    args[5] = 0x95;   /* CRC */

    SPI_TxBuffer(args, sizeof(args));

    /* wait response */
    while((SPI_RxByte() != 0x01) && cnt)
    {
   
        cnt--;
    }

    DESELECT();
    SPI_TxByte(0XFF);

    PowerFlag = 1;
}

/* power off */
static void SD_PowerOff(void)
{
   
    PowerFlag = 0;
}

/* check power flag */
static uint8_t SD_CheckPower(void)
{
   
    return PowerFlag;
}

/* receive data block */
static bool SD_RxDataBlock(BYTE* buff, UINT len)
{
   
    uint8_t token;

    /* timeout 200ms */
    int32_t Timer1 = 0xffffff;

    /* loop until receive a response or timeout */
    do
    {
   
        token = SPI_RxByte();
        Timer1--;
    }
    while((token == 0xFF) && Timer1 > 0);

    /* invalid response */
    if(token != 0xFE) return FALSE;

    /* receive data */
    do
    {
   
        SPI_RxBytePtr(buff++);
    }
    while(len--);

    /* discard CRC */
    SPI_RxByte();
    SPI_RxByte();

    return TRUE;
}

/* transmit data block */
#if _USE_WRITE == 1
static bool SD_TxDataBlock(const uint8_t* buff, BYTE token)
{
   
    uint8_t resp;
    uint8_t i = 0;

    /* wait SD ready */
    if(SD_ReadyWait() != 0xFF) return FALSE;

    /* transmit token */
    SPI_TxByte(token);

    /* if it's not STOP token, transmit data */
    if(token != 0xFD)
    {
   
        SPI_TxBuffer((uint8_t*)buff, 512);

        /* discard CRC */
        SPI_RxByte();
        SPI_RxByte();

        /* receive response */
        while(i <= 64)
        {
   
            resp = SPI_RxByte();

            /* transmit 0x05 accepted */
            if((resp & 0x1F) == 0x05) break;
            i++;
        }

        /* recv buffer clear */
        while(SPI_RxByte() == 0);
    }

    /* transmit 0x05 accepted */
    if((resp & 0x1F) == 0x05) return TRUE;

    return FALSE;
}
#endif /* _USE_WRITE */

/* transmit command */
static BYTE SD_SendCmd(BYTE cmd, uint32_t arg)
{
   
    uint8_t crc, res;

    /* wait SD ready */
    if(SD_ReadyWait() != 0xFF) return 0xFF;

    /* transmit command */
    SPI_TxByte(cmd);          /* Command */
    SPI_TxByte((uint8_t)(arg >> 24));   /* Argument[31..24] */
    SPI_TxByte((uint8_t)(arg >> 16));   /* Argument[23..16] */
    SPI_TxByte((uint8_t)(arg >> 8));  /* Argument[15..8] */
    SPI_TxByte((uint8_t)arg);       /* Argument[7..0] */

    /* prepare CRC */
    if(cmd == CMD0) crc = 0x95; /* CRC for CMD0(0) */
    else if(cmd == CMD8) crc = 0x87;  /* CRC for CMD8(0x1AA) */
    else crc = 1;

    /* transmit CRC */
    SPI_TxByte(crc);

    /* Skip a stuff byte when STOP_TRANSMISSION */
    if(cmd == CMD12) SPI_RxByte();

    /* receive response */
    uint8_t n = 10;
    do
    {
   
        res = SPI_RxByte();
    }
    while((res & 0x80) && --n);

    return res;
}

/***************************************
 * user_diskio.c functions
 **************************************/

/* initialize SD */
DSTATUS SD_disk_initialize(BYTE drv)
{
   
    uint8_t n, type, ocr[4];

    /* single drive, drv should be 0 */
    if(drv) return STA_NOINIT;

    /* no disk */
    if(Stat & STA_NODISK) return Stat;

    /* power on */
    SD_PowerOn();

    /* slave select */
    SELECT()<

原文地址:https://blog.csdn.net/ZHOU_YONG915/article/details/137437634

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!