OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 2253|回复: 0

《STM32F407 探索者开发指南》五十章 SD卡实验(下)

[复制链接]

1140

主题

1152

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4895
金钱
4895
注册时间
2019-5-8
在线时间
1248 小时
发表于 2023-9-1 14:54:53 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2023-8-31 17:31 编辑

五十章 SD卡实验

1)实验平台:正点原子探索者STM32F407开发板

2) 章节摘自【正点原子】STM32F407开发指南 V1.1

3)购买链接:https://detail.tmall.com/item.htm?id=609294673401

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)STM32技术交流QQ群:151941872


155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

50.3 SD卡初始化流程
50.3.1 SDIO模式下的SD卡初始化
这一节,我们来看看SD卡的初始化流程,要实现SDIO驱动SD卡,最重要的步骤就是SD卡的初始化,只要SD卡初始化完成了,那么剩下的(读写操作)就简单了,所以我们这里重点介绍SD卡的初始化。从《SD卡2.0协议》(见光盘资料)文档,我们得到SD卡初始化流程图如图50.3.1.1所示:                                 
image001.jpg
图50.3.1.1 SD卡初始化流程(Card Initialization and IdentificationFlow (SD mode))

从图中,我们看到,不管什么卡(这里我们将卡分为4类:SD2.0高容量卡(SDHC,最大32G),SDv2.0标准容量卡(SDSC,最大2G),SD1.x卡和MMC卡),首先我们要执行的是卡上电(需要设置SDIO_POWER[1:0]=11),上电后发送CMD0,对卡进行软复位,之后发送CMD8命令,用于区分SD卡2.0,只有2.0及以后的卡才支持CMD8命令,MMC卡和V1.x的卡,是不支持该命令的。CMD8的格式如表50.3.1.1所示:
QQ截图20230831165717.png
表50.3.1.1 CMD8命令格式

这里,我们需要在发送CMD8的时候,通过其带的参数我们可以设置VHS位,以告诉SD卡,主机的供电情况,VHS位定义如表50.3.1.2所示:
image003.png
表50.3.1.2 VHS位定义

这里我们使用参数0X1AA,即告诉SD卡,主机供电为2.7~3.6V之间,如果SD卡支持CMD8,且支持该电压范围,则会通过CMD8的响应(R7)将参数部分原本返回给主机,如果不支持CMD8,或者不支持这个电压范围,则不响应。

在发送CMD8后,发送ACMD41(注意发送ACMD41之前要先发送CMD55),来进一步确认卡的操作电压范围,并通过HCS位来告诉SD卡,主机是不是支持高容量卡(SDHC)。ACMD41的命令格式如表50.3.5.3所示:
image004.jpg
表50.3.1.3 ACMD41命令格式

ACMD41得到的响应(R3)包含SD卡OCR寄存器内容,OCR寄存器内容定义如表50.3.5.4所示:     
image006.png
表50.3.1.4 OCR寄存器定义

对于支持CMD8指令的卡,主机通过ACMD41的参数设置HCS位为1,来告诉SD卡主机支SDHC卡,如果设置为0,则表示主机不支持SDHC卡,SDHC卡如果接收到HCS为0,则永远不会反回卡就绪状态。对于不支持CMD8的卡,HCS位设置为0即可。

SD卡在接收到ACMD41后,返回OCR寄存器内容,如果是2.0的卡,主机可以通过判断OCR的CCS位来判断是SDHC还是SDSC;如果是1.x的卡,则忽略该位。OCR寄存器的最后一个位用于告诉主机SD卡是否上电完成,如果上电完成,该位将会被置1。

对于MMC卡,则不支持ACMD41,不响应CMD55,对MMC卡,我们只需要在发送CMD0后,在发送CMD1(作用同ACMD41),检查MMC卡的OCR寄存器,实现MMC卡的初始化。

至此,我们便实现了对SD卡的类型区分,图50.1.5.1中,最后发送了CMD2和CMD3命令,用于获得卡CID寄存器数据和卡相对地址(RCA)。CMD2,用于获得CID寄存器的数据,CID寄存器数据各位定义如表50.3.5.5所示:
image008.png
表50.3.1.5 卡CID寄存器位定义

SD卡在收到CMD2后,将返回R2长响应(136位),其中包含128位有效数据(CID寄存器内容),存放在SDIO_RESP1~4等4个寄存器里面。通过读取这四个寄存器,就可以获得SD卡的CID信息。

CMD3,用于设置卡相对地址(RCA,必须为非0),对于SD卡(非MMC卡),在收到CMD3后,将返回一个新的RCA给主机,方便主机寻址。RCA的存在允许一个SDIO接口挂多个SD卡,通过RCA来区分主机要操作的是哪个卡。而对于MMC卡,则不是由SD卡自动返回RCA,而是主机主动设置MMC卡的RCA,即通过CMD3带参数(高16位用于RCA设置),实现RCA设置。同样MMC卡也支持一个SDIO接口挂多个MMC卡,不同于SD卡的是所有的RCA都是由主机主动设置的,而SD卡的RCA则是SD卡发给主机的。

在获得卡RCA之后,我们便可以发送CMD9(带RCA参数),获得SD卡的CSD寄存器内容,从CSD寄存器,我们可以得到SD卡的容量和扇区大小等十分重要的信息。CSD寄存器我们在这里就不详细介绍了,关于CSD寄存器的详细介绍,请大家参考《SD卡2.0协议.pdf》。

至此,我们的SD卡初始化基本就结束了,最后通过CMD7命令,选中我们要操作的SD卡,即可开始对SD卡的读写操作了,SD卡的其他命令和参数,我们这里就不再介绍了,请大家参考《SD卡2.0协议.pdf》,里面有非常详细的介绍。

50.3.2 SPI模式下的SD卡初始化
STM32的SDIO驱动模式和SPI模式不兼容,二者使用时需要区分开来。《SD卡2.0协议.pdf》中提供了SD卡的SPI初始化时序,我们可以按它建议的流程进行SD卡的初始化,如图50.3.2.1所示。     
image010.png
图50.3.2.1 SD卡的SPI初始化流程(SPI Mode Initialization Flow)

要使用SPI模式驱动SD卡,先得让SD卡进入SPI模式。方法如下:在SD卡收到复位命令(CMD0)时,CS为有效电平(低电平)则SPI模式被启用。不过在发送CMD0之前,要发送>74个时钟,这是因为SD卡内部有个供电电压上升时间,大概为64个CLK,剩下的10个CLK用于SD卡同步,之后才能开始CMD0的操作,在卡初始化的时候,CLK时钟最大不能超过400Khz!

接着我们看看SD卡的初始化,由前面SD卡的基本介绍,我们知道SD卡是先发送数据高位的,SD卡的典型初始化过程如下:
1、初始化与SD卡连接的硬件条件(MCU的SPI配置,IO口配置);
2、拉低片选信号,上电延时(>74个CLK);
3、复位卡(CMD0),进入IDLE状态;
4、发送CMD8,检查是否支持2.0协议;
5、根据不同协议检查SD卡(命令包括:CMD55、ACMD41、CMD58和CMD1等);
6、取消片选,发多8个CLK,结束初始化

这样我们就完成了对SD卡的初始化,注意末尾发送的8个CLK是提供SD卡额外的时钟,完成某些操作。通过SD卡初始化,我们可以知道SD卡的类型(V1、V2、V2HC或者MMC),在完成了初始化之后,就可以开始读写数据了。

SD卡单扇区读取数据,这里通过CMD17来实现,具体过程如下:
1、发送CMD17;
2、接收卡响应R1;
3、接收数据起始令牌0XFE;
4、接收数据;
5、接收2个字节的CRC,如果不使用CRC,这两个字节在读取后可以丢掉。
6、禁止片选之后,发多8个CLK;

以上就是一个典型的读取SD卡数据过程,SD卡的写于读数据差不多,写数据通过CMD24来实现,具体过程如下:
1、发送CMD24;
2、接收卡响应R1;
3、发送写数据起始令牌0XFE;
4、发送数据;
5、发送2字节的伪CRC;
6、禁止片选之后,发多8个CLK;
以上就是一个典型的写SD卡过程。关于SD卡的介绍,我们就介绍到这里,更详细的介绍请参考光盘资料→7,硬件资料→SD卡资料→SD卡V2.0协议。

50.4硬件设计
1. 例程功能
本章实验功能简介:开机的时候先初始化SD卡,如果SD卡初始化完成,则提示LCD初始化成功。按下KEY0,读取SD卡扇区0的数据,然后通过串口发送到电脑。如果没初始化通过,则在LCD上提示初始化失败。同样用LED0来指示程序正在运行。

2. 硬件资源
1)LED灯
    LED0 – PF9
2)独立按键 KEY0 – PE4
3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
4)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面)
5)SD卡,通过SDIO(SDIO_D0~D4(PC8~PC11), SDIO_SCK(PC12), SDIO_CMD(PD2))连接

3. 原理图
前面介绍SD卡时我们已经介绍了SD卡对外的接口部分,实际上SD卡对于我们来说是可以灵活变更的部分,实际使用时,业界常用SD卡卡座用于专门放置SD卡。

下面我们介绍一下板载的SD卡接口和STM32的连接关系,如图50.4.1所示:     
image012.png
图50.4.1 SD卡接口与STM32连接原理图

microSD卡座在开发板正面(老版本探索者在背面),卡座和STM32开发板上是直接连接在一起的,硬件上不需要任何改动。

50.5 程序设计
50.4.1 SD卡的HAL库驱动
STM32的HAL库为SD卡操作封装了一些函数,主要存放在stm32f4xx_hal_sd.c/h下,下面我们来分析我们主要使用到的几个函数。

1. HAL_SD_Init函数
要使用一个外设首先要对它进行初始化,所以先看SD卡的初始化函数,其声明如下:
  1. HAL_StatusTypeDef HAL_SD_Init(SD_HandleTypeDef *hsd)
复制代码

l  函数描述:
根据SD参数,初始化SDIO外设以便后续操作SD卡。

l  函数形参:
形参1是SD卡的句柄,结构体类型是SD_HandleTypeDef,我们不使用USE_HAL_SD_REGISTER_CALLBACKS宏来拓展SD卡的自定义函数,精简后其定义如下:
  1. /**
  2.   *@brief  SD操作句柄结构体定义
  3.   */
  4. typedef struct
  5. {      
  6. SD_TypeDef                   *Instance;       /* SD相关寄存器基地址 */
  7. SD_InitTypeDef              Init;             /* SDIO初始化变量 */
  8. HAL_LockTypeDef             Lock;             /* 互斥锁,用于解决外设访问冲突 */
  9. uint8_t                      *pTxBuffPtr;     /* SD发送数据指针 */
  10. uint32_t                    TxXferSize;      /* SD发送缓存按字节数的大小 */
  11. uint8_t                      *pRxBuffPtr;     /* SD接收数据指针 */
  12. uint32_t                    RxXferSize;      /* SD接收缓存按字节数的大小 */
  13.   __IOuint32_t               Context;         /* HAL库对SD卡的操作阶段 */
  14.   __IOHAL_SD_StateTypeDef  State;           /* SD卡操作状态 */
  15.   __IOuint32_t               ErrorCode;      /* SD卡错误代码 */
  16. DMA_HandleTypeDef          *hdmatx;         /* SD DMA数据发送指针 */
  17. DMA_HandleTypeDef          *hdmarx;         /* SD DMA数据接收指针 */
  18. HAL_SD_CardInfoTypeDef    SdCard;          /* SD卡信息的 */
  19. uint32_t                     CSD[4];          /* 保存SD卡CSD寄存器信息 */
  20. uint32_t                     CID[4];          /* 保存SD卡CID寄存器信息 */
  21. }SD_HandleTypeDef;
复制代码
上面的初始化结构体中HAL_SD_CardInfoTypeDef用于初始化后提取卡信息,包括卡类型、容量等参数。
  1. /**
  2. *@brief  SD 卡信息结构定义
  3. */
  4. typedef struct
  5. {
  6. uint32_t CardType;                   /* 存储卡类型标记:标准卡、高速卡 */
  7. uint32_t CardVersion;               /* 存储卡版本 */
  8. uint32_t Class;                      /* 卡类型 */
  9. uint32_t RelCardAdd;                /* 卡相对地址 */
  10. uint32_t BlockNbr;                   /* 卡存储块数 */
  11. uint32_t BlockSize;                 /* SD卡每个存储块大小 */
  12. uint32_t LogBlockNbr;               /* 以块表示的卡逻辑容量 */
  13. uint32_t LogBlockSize;              /* 以字节为单位的逻辑块大小 */
  14. }HAL_SD_CardInfoTypeDef;
复制代码

l  函数返回值:
HAL_StatusTypeDef枚举类型的值,有4个,分别是HAL_OK表示成功,HAL_ERROR表示错误,HAL_BUSY表示忙碌,HAL_TIMEOUT超时。后续遇到该结构体也是一样的。只有返回HAL_OK才是正常的卡初始化状态,遇到其它状态则需要结合硬件分析一下代码。

2. HAL_SD_ConfigWideBusOperation函数
SD卡上电后默认使用1 位数据总线进行数据传输,卡如果允许,可以在初始化完成后重新设置SD卡的数据位宽以加快数据传输过程:
  1. HAL_StatusTypeDefHAL_SD_ConfigWideBusOperation(SD_HandleTypeDef *hsd,uint32_t WideMode);
复制代码

l  函数描述:
这个函数用于设置数据总线格式的数据宽度,用于加快卡的数据访问速度,当然前提是硬件连接和卡本身能支持这样操作。

l  函数形参:
形参1是SD卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO初始化结束后才能使用,我们需要通过使用初始化后的SDIO结构体的句柄访问外设。
形参2是总线宽度,根据函数的形参检查规则我们可知它实际上只有三个可选值:
  1. #define SDIO_BUS_WIDE_1B                      ((uint32_t)0x00000000U)
  2. #define SDIO_BUS_WIDE_4B                      SDIO_CLKCR_WIDBUS_0
  3. #define SDIO_BUS_WIDE_8B                      SDIO_CLKCR_WIDBUS_1
复制代码
l  函数返回值:
HAL_StatusTypeDef类型的函数,返回值同样是需要获取到HAL_OK表示成功。

3. HAL_SD_ReadBlocks函数
SD卡初始化后从SD卡的指定扇区读数据:
  1. HAL_StatusTypeDef HAL_SD_ReadBlocks (SD_HandleTypeDef *hsd, uint8_t *pData,
  2. uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout);
复制代码
这个函数是直接读取,不使用硬件中断。

l  函数描述:
从SD卡的指定扇区读取一定数量的数据。

l  函数形参:
形参1是SD卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO初始化结束后才能使用,我们需要通过使用初始化后的SDIO结构体的句柄访问外设。
形参2 pData是一个指向8位类型的数据指针缓冲,它用于接收我们需要的数据。
形参3BlockAdd指向我们需要访问的数据扇区,对于任意的存储都是类似的,像SD卡这样的大存储块也同样是通过位置标识来访问不同的数据。
形参4 NumberOfBlocks对应的是我们本次要从指定扇区读取的字节数。
形参5Timeout表示读的超时时间。HAL库驱动在达到超时时间前还没读到数据会进行重试和等待,达到超时时间后或者本次读取成功才退出本次操作。

l  函数返回值:
HAL_StatusTypeDef类型的函数,返回值同样是需要获取到HAL_OK表示成功。
类似功能的函数还有,我们的例程没有使用DMA和中断方式,故不使用以下两个接口:
  1. HAL_StatusTypeDef HAL_SD_ReadBlocks_IT  (SD_HandleTypeDef *hsd, uint8_t *pData,
  2. uint32_t BlockAdd, uint32_t NumberOfBlocks);
  3. HAL_StatusTypeDef HAL_SD_ReadBlocks_DMA (SD_HandleTypeDef *hsd, uint8_t *pData,
  4. uint32_t BlockAdd, uint32_t NumberOfBlocks);
复制代码
它们分别使用了中断方式和DMA方式来实现类似的功能,它们的调用非常相似

4. HAL_SD_WriteBlocks函数
SD卡初始化后,在SD卡的指定扇区写入数据:
  1. HAL_StatusTypeDef HAL_SD_WriteBlocks (SD_HandleTypeDef *hsd, uint8_t *pData,uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout);
复制代码
l  函数描述:
从SD卡的指定扇区读取一定数量的数据。

l  函数形参:
形参1是SD卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO初始化结束后才能使用,我们需要通过使用初始化后的SDIO结构体的句柄访问外设。
形参2 pData是一个指向8位类型的数据指针缓冲,它用于接收我们需要的数据。
形参3 BlockAdd指向我们需要访问的数据扇区,对于任意的存储都是类似的,像SD卡这样的大存储块也同样是通过位置标识来访问不同的数据。
形参4NumberOfBlocks对应的是我们本次要从指定扇区读取的字节数。
形参5Timeout表示写动作的超时时间。HAL库驱动在达到超时时间前还没读到数据会进行重试和等待,达到超时时间后或者本次写入成功才退出本次操作。

l  函数返回值:
HAL_StatusTypeDef类型的函数,返回值同样是需要获取到HAL_OK表示成功。
类似于读函数,写函数同样有中断版本,我们的例程没有使用DMA和中断方式,故不使用以下两个接口:
  1. HAL_StatusTypeDef HAL_SD_WriteBlocks_IT (SD_HandleTypeDef *hsd, uint8_t *pData,
  2. uint32_t BlockAdd, uint32_t NumberOfBlocks);
  3. HAL_StatusTypeDef HAL_SD_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData,
  4. uint32_t BlockAdd, uint32_t NumberOfBlocks);
复制代码
它们分别使用了中断方式和DMA方式来实现类似的功能,它们的调用非常相似,这里就不重复介绍了,大家查看对应的函数实现即可。

5. HAL_SD_GetCardInfo函数
SD卡初始化后,根据设备句柄读SD卡的相关状态信息:
  1. <font face="Tahoma">HAL_StatusTypeDef HAL_SD_GetCardInfo(SD_HandleTypeDef *hsd,HAL_SD_CardInfoTypeDef *pCardInfo);</font>
复制代码
l  函数描述:
从SD卡的指定扇区读取一定数量的数据。

l  函数形参:
形参1是SD卡的句柄,结构体类型是SD_HandleTypeDef,此函数需要在SDIO初始化结束后才能使用,我们需要通过使用初始化后的SDIO结构体的句柄访问外设。
形参2 pData是一个指向8位类型的数据指针缓冲,它用于接收我们需要的数据。
形参3 BlockAdd指向我们需要访问的数据扇区,对于任意的存储都是类似的,像SD卡这样的大存储块也同样是通过位置标识来访问不同的数据。
形参4 NumberOfBlocks对应的是我们本次要从指定扇区读取的字节数。
形参5Timeout表示读的超时时间。HAL库驱动在达到超时时间前还没读到数据会进行重试和等待,达到超时时间后才退出本次操作。

l  函数返回值:
HAL_StatusTypeDef类型的函数,返回值同样是需要获取到HAL_OK表示成功。类似的函数还有:
HAL_StatusTypeDef HAL_SD_SendSDStatus(SD_HandleTypeDef *hsd, uint32_t *pSDstatus);
HAL_SD_CardStateTypeDef HAL_SD_GetCardState(SD_HandleTypeDef *hsd);
HAL_StatusTypeDef HAL_SD_GetCardCID(SD_HandleTypeDef *hsd,HAL_SD_CardCIDTypeDef *pCID);
HAL_StatusTypeDef HAL_SD_GetCardCSD(SD_HandleTypeDef *hsd,HAL_SD_CardCSDTypeDef *pCSD);
HAL_StatusTypeDef HAL_SD_GetCardStatus(SD_HandleTypeDef *hsd,HAL_SD_CardStatusTypeDef *pStatus);
HAL_StatusTypeDef HAL_SD_GetCardInfo(SD_HandleTypeDef *hsd,HAL_SD_CardInfoTypeDef *pCardInfo);
它们分别使用了中断方式和DMA方式来实现类似的功能,它们的调用非常相似。

SDIO驱动SD卡配置步骤
1)使能SDIO和相关GPIO时钟,并设置好GPIO工作模式
我们通过SDIO读写SD卡,所以先需要使能SDIO以及相关GPIO口的时钟,并设置好GPIO的工作模式。

2)初始化SDIO
HAL库通过SDIO_Init完成对SDIO的初始化,不过我们并不直接调用该函数,而是通过:HAL_SD_InitàHAL_SD_InitCardàSDIO_Init的调用关系,来完成对SDIO的初始化。我们只需要配置好SDIO相关工作参数,然后调用HAL_SD_Init函数即可,详见本例程源码。

3)初始化SD
HAL库通过HAL_SD_InitCard函数完成对SD卡的初始化,如上可知,我们也只需要调用HAL_SD_Init函数,即可完成对SD卡的初始化。

4)实现SD卡读取&写入函数
在初始化SDMMC和SD卡完成以后,我们就可以访问SD卡了,HAL库提供了两个基本的SD卡读写函数:HAL_SD_ReadBlocks和HAL_SD_WriteBlocks,用于读取和写入SD卡。我们对这两个函数再进行一次封装,以便更好的适配文件系统,再封装后,我们使用:sd_read_disk来读取SD卡,使用:sd_write_disk来写入SD卡,详见本例程源码。

50.4.2 程序流程图
QQ截图20230831173001.png
图50.4.2.1 SD读写实验程序流程图

50.4.3 程序解析
1. SDIO驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SDIO驱动源码包括两个文件:sdio_sdcard.c和sdio_sdcard.h。

首先是sdio_sdcard.h文件,根据我们STM32的复用功能和我们的硬件设计,我们把用到的管脚用宏定义,需要更换其它的引脚时也可以通过修改宏实现快速移植,它们列出如下:
  1. <font face="Tahoma">/*****************************************************************************/
  2. /* SDIO的信号线: SD1_D0 ~ SD1_D3/SD1_CLK/SD1_CMD 引脚 定义
  3. * 如果你使用了其他引脚做SDIO的信号线,修改这里写定义即可适配.
  4. */
  5. #define SD1_D0_GPIO_PORT                GPIOC
  6. #define SD1_D0_GPIO_PIN                 GPIO_PIN_8
  7. #define SD1_D0_GPIO_CLK_ENABLE()      
  8.   do{__HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)   /* 所在IO口时钟使能 */
  9. #define SD1_D1_GPIO_PORT                GPIOC
  10. #define SD1_D1_GPIO_PIN                 GPIO_PIN_9
  11. #define SD1_D1_GPIO_CLK_ENABLE()        
  12. do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)    /* 所在IO口时钟使能 */
  13. #define SD1_D2_GPIO_PORT                GPIOC
  14. #define SD1_D2_GPIO_PIN                 GPIO_PIN_10
  15. #define SD1_D2_GPIO_CLK_ENABLE()        
  16. do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)    /* 所在IO口时钟使能 */
  17. #define SD1_D3_GPIO_PORT                GPIOC
  18. #define SD1_D3_GPIO_PIN                 GPIO_PIN_11
  19. #define SD1_D3_GPIO_CLK_ENABLE()        
  20. do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)    /* 所在IO口时钟使能 */
  21. #define SD1_CLK_GPIO_PORT               GPIOC
  22. #define SD1_CLK_GPIO_PIN                GPIO_PIN_12
  23. #define SD1_CLK_GPIO_CLK_ENABLE()      
  24. do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)    /* 所在IO口时钟使能 */
  25. #define SD1_CMD_GPIO_PORT               GPIOD
  26. #define SD1_CMD_GPIO_PIN                GPIO_PIN_2
  27. #define SD1_CMD_GPIO_CLK_ENABLE()      
  28. do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)    /* 所在IO口时钟使能 */
  29. /*****************************************************************************/
  30. #define SD_TIMEOUT           ((uint32_t)100000000)        /* 超时时间 */
  31. #define SD_TRANSFER_OK      ((uint8_t)0x00)              /* 传输完成 */
  32. #define SD_TRANSFER_BUSY    ((uint8_t)0x01)              /* 卡正忙 */
  33. /* 根据 SD_HandleTypeDef 定义的宏,用于快速计算容量 */
  34. #define SD_TOTAL_SIZE_BYTE(__Handle__)  (((uint64_t)((__Handle__)->SdCard.LogBlockNbr)*
  35. ((__Handle__)->SdCard.LogBlockSize))>>0)
  36. #define SD_TOTAL_SIZE_KB(__Handle__)   (((uint64_t)((__Handle__)->SdCard.LogBlockNbr)*
  37. ((__Handle__)->SdCard.LogBlockSize))>>10)
  38. #define SD_TOTAL_SIZE_MB(__Handle__)    (((uint64_t)((__Handle__)->SdCard.LogBlockNbr)*
  39. ((__Handle__)->SdCard.LogBlockSize))>>20)
  40. #define SD_TOTAL_SIZE_GB(__Handle__)   (((uint64_t)((__Handle__)->SdCard.LogBlockNbr)*
  41. ((__Handle__)->SdCard.LogBlockSize))>>30)
  42. /*  
  43. *  SD传输时钟分频,由于HAL库运行效率低,很容易产生上溢(读SD卡时)/下溢错误(写SD卡时)
  44. *  使用4bit模式时,需降低SDIO时钟频率,将该宏改为 1,SDIO时钟频率:
  45. *  48/(SDIO_TRANSF_CLK_DIV + 2 ) = 16M * 4bit = 64Mbps
  46. *  使用1bit模式时,该宏SDIO_TRANSF_CLK_DIV改为 0,SDIO时钟频率:
  47. *  48/(SDIO_TRANSF_CLK_DIV + 2 ) = 24M * 1bit = 24Mbps
  48. */
  49. #define  SDIO_TRANSF_CLK_DIV        1</font>
复制代码
主要注意SDIO_TRANSF_CLK_DIV这个宏,该宏用于设置SDIO的传输时钟分频,由于由于HAL库运行效率较低,很容易产生上溢(读SD卡时)/下溢错误(写SD卡时),所以在使用不同的模式时,该宏的值需要做相应修改。

sdio_sdcard.c我们主要介绍三个函数:sd_init、sd_read_disk和sd_write_disk。

1) sd_init函数
sd_init的设计就比较简单了,我们只需要填充SDIO结构体的控制句柄,然后使用HAL库的HAL_SD_Init初始化函数即可,在此过程中HAL_SD_Init会调用HAL_SD_MspInit函数回调函数,根据外设的情况,我们可以设置数据总线宽度为4位:
  1. <font face="Tahoma">/**
  2. * @brief        初始化SD卡
  3. * @param        无
  4. * @retval       返回值:0 初始化正确;其他值,初始化错误
  5. */
  6. uint8_t sd_init(void)
  7. {
  8.     uint8_t SD_Error;
  9.     /* 初始化时的时钟不能大于400KHZ */
  10.    g_sdcard_handler.Instance = SDIO;
  11.     g_sdcard_handler.Init.ClockEdge =SDIO_CLOCK_EDGE_RISING;  /* 上升沿 */
  12.     /* 不使用bypass模式,直接用HCLK进行分频得到SDIO_CK */
  13.     g_sdcard_handler.Init.ClockBypass =SDIO_CLOCK_BYPASS_DISABLE;
  14.     /* 空闲时不关闭时钟电源 */
  15.     g_sdcard_handler.Init.ClockPowerSave =SDIO_CLOCK_POWER_SAVE_DISABLE;
  16.     g_sdcard_handler.Init.BusWide = SDIO_BUS_WIDE_1B;            /* 1位数据线 */
  17.     g_sdcard_handler.Init.HardwareFlowControl =
  18.                                 SDIO_HARDWARE_FLOW_CONTROL_ENABLE; /* 开启硬件流控 */
  19.     /* SD传输时钟频率最大25MHZ */
  20.     g_sdcard_handler.Init.ClockDiv = SDIO_TRANSFER_CLK_DIV;
  21.     SD_Error = HAL_SD_Init(&g_sdcard_handler);
  22.     if (SD_Error != HAL_OK)
  23.     {
  24.         return 1;
  25.     }
  26.     /* 使能宽总线模式,即4位总线模式,加快读取速度 */
  27.     SD_Error=HAL_SD_ConfigWideBusOperation(&g_sdcard_handler,SDIO_BUS_WIDE_4B);
  28.     if (SD_Error != HAL_OK)
  29.     {
  30.         return 2;
  31.     }
  32.     return 0;
  33. }
  34. /**
  35. *@brief        SDMMC底层驱动,时钟使能,引脚配置,DMA配置
  36. *@brief        SDMMC底层驱动,时钟使能,引脚配置,DMA配置
  37. *@param        hsd:SD卡句柄
  38. *@note         此函数会被HAL_SD_Init()调用
  39. *@retval       无
  40. */
  41. voidHAL_SD_MspInit(SD_HandleTypeDef*hsd)
  42. {
  43.    GPIO_InitTypeDef gpio_init_struct;
  44.    __HAL_RCC_SDMMC1_CLK_ENABLE();  /* 使能SDMMC1时钟 */
  45.    SD1_D0_GPIO_CLK_ENABLE();        /* D0引脚IO时钟使能 */
  46.    SD1_D1_GPIO_CLK_ENABLE();        /* D1引脚IO时钟使能 */
  47.    SD1_D2_GPIO_CLK_ENABLE();        /* D2引脚IO时钟使能 */
  48.    SD1_D3_GPIO_CLK_ENABLE();        /* D3引脚IO时钟使能 */
  49.    SD1_CLK_GPIO_CLK_ENABLE();       /* CLK引脚IO时钟使能 */
  50.    SD1_CMD_GPIO_CLK_ENABLE();       /* CMD引脚IO时钟使能 */
  51.    gpio_init_struct.Pin = SD1_D0_GPIO_PIN;
  52.    gpio_init_struct.Mode = GPIO_MODE_AF_PP;                  /* 推挽复用 */
  53.    gpio_init_struct.Pull = GPIO_PULLUP;                      /* 上拉 */
  54.    gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;     /* 高速 */
  55.    gpio_init_struct.Alternate = GPIO_AF12_SDIO1;            /* 复用为SDIO */
  56.    HAL_GPIO_Init(SD1_D0_GPIO_PORT, &gpio_init_struct);     /* 初始化D0引脚 */
  57.    gpio_init_struct.Pin = SD1_D1_GPIO_PIN;
  58.    HAL_GPIO_Init(SD1_D1_GPIO_PORT, &gpio_init_struct);     /* 初始化D1引脚 */
  59.    gpio_init_struct.Pin = SD1_D2_GPIO_PIN;
  60.    HAL_GPIO_Init(SD1_D2_GPIO_PORT, &gpio_init_struct);     /* 初始化D2引脚 */
  61.    gpio_init_struct.Pin = SD1_D3_GPIO_PIN;
  62.    HAL_GPIO_Init(SD1_D3_GPIO_PORT, &gpio_init_struct);     /* 初始化D3引脚 */
  63.    gpio_init_struct.Pin = SD1_CLK_GPIO_PIN;
  64.    HAL_GPIO_Init(SD1_CLK_GPIO_PORT, &gpio_init_struct);   /* 初始化CLK引脚 */
  65.    gpio_init_struct.Pin = SD1_CMD_GPIO_PIN;
  66.    HAL_GPIO_Init(SD1_CMD_GPIO_PORT, &gpio_init_struct);   /* 初始化CMD引脚 */
  67. }</font>
复制代码
2) sd_read_disk函数
这个函数比较简单,实际上我们使用它来对HAL库的读函数HAL_SD_ReadBlocks进行了二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据读函数返回值自己作其它处理。为了保护SD卡的数据操作,我们在进行操作时暂时关闭了中断以防止数据读过程发生意外。
  1. <font face="Tahoma">uint8_t sd_read_disk(uint8_t *pbuf, uint32_t saddr, uint32_t cnt)
  2. {
  3.     uint8_t sta = HAL_OK;
  4.     uint32_t timeout = SD_TIMEOUT;
  5.     long long lsector = saddr;
  6.     __disable_irq();     /* 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!) */
  7. sta = HAL_SD_ReadBlocks(&g_sdcard_handler, (uint8_t *)pbuf, lsector,
  8. cnt, SD_TIMEOUT);   /* 多个sector的读操作 */
  9.     /* 等待SD卡读完 */
  10.     while (get_sd_card_state() != SD_TRANSFER_OK)
  11.     {
  12.         if (timeout-- == 0)
  13.         {
  14.             sta = SD_TRANSFER_BUSY;
  15.         }
  16.     }
  17.     __enable_irq(); /* 开启总中断 */
  18.     return sta;
  19. }</font>
复制代码
3) sd_write_disk函数
这个函数比较简单,实际上我们使用它来对HAL库的读函数HAL_SD_WriteBlocks进行了二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据读函数返回值自己作其它处理。为了保护SD卡的数据操作,我们在进行操作时暂时关闭了中断以防止数据写过程发生意外。
  1. <font face="Tahoma">uint8_t sd_write_disk(uint8_t *pbuf, uint32_t saddr, uint32_t cnt)
  2. {
  3.     uint8_t sta = HAL_OK;
  4.     uint32_t timeout = SD_TIMEOUT;
  5.     long long lsector = saddr;
  6.     __disable_irq();    /* 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!) */
  7.     sta = HAL_SD_WriteBlocks(&g_sdcard_handler, (uint8_t *)pbuf, lsector,
  8. cnt, SD_TIMEOUT);   /* 多个sector的写操作 */
  9.     /* 等待SD卡写完 */
  10.     while (get_sd_card_state() != SD_TRANSFER_OK)
  11.     {
  12.         if (timeout-- == 0)
  13.         {
  14.             sta = SD_TRANSFER_BUSY;
  15.         }
  16.     }
  17.     __enable_irq(); /* 开启总中断 */
  18.     return sta;
  19. }</font>
复制代码
2. main.c代码
在main.c就比较简单了,按照我们的流程图的思路,为了方便测试,我们编写了sd_test_read()\sd_test_write()\show_sdcard_info()三个函数分别用于读写测试和卡信息打印,也都是基于对前面HAL库的代码进行简单地调用,代码也比较容易看懂,这里就不单独介绍这几个函数了,大家查看光盘中的源代码即可。

最后,我们在main.c编写的程序如下:
  1. <font face="Tahoma">/**
  2. *@brief        通过串口打印SD卡相关信息
  3. *@param        无
  4. *@retval       无
  5. */
  6. voidshow_sdcard_info(void)
  7. {
  8.    HAL_SD_CardCIDTypeDef sd_card_cid;
  9.    HAL_SD_GetCardCID(&g_sd_handle, &sd_card_cid);      /* 获取CID */
  10.    get_sd_card_info(&g_sd_card_info_handle);           /* 获取SD卡信息 */
  11.     switch (g_sd_card_info_handle.CardType)
  12.     {
  13.        case CARD_SDSC:
  14.        {
  15.            if (g_sd_card_info_handle.CardVersion == CARD_V1_X)
  16.            {
  17.                 printf("Card Type:SDSC V1\r\n");
  18.            }
  19.            else if (g_sd_card_info_handle.CardVersion == CARD_V2_X)
  20.            {
  21.                 printf("Card Type:SDSC V2\r\n");
  22.            }
  23.        }
  24.            break;
  25.       
  26.        caseCARD_SDHC_SDXC:
  27.            printf("CardType:SDHC\r\n");
  28.            break;
  29.     }
  30. /* 制造商ID */
  31. printf("Card ManufacturerID:%d\r\n", sd_card_cid.ManufacturerID);
  32. /* 卡相对地址 */              
  33.    printf("CardRCA:%d\r\n", g_sd_card_info_handle.RelCardAdd);
  34. printf("LogBlockNbr:%d\r\n",
  35. (uint32_t)(g_sd_card_info_handle.LogBlockNbr));      /* 显示逻辑块数量 */
  36. printf("LogBlockSize:%d\r\n",
  37. (uint32_t)(g_sd_card_info_handle.LogBlockSize));     /* 显示逻辑块大小 */
  38. /* 显示容量 */
  39. printf("CardCapacity:%d MB\r\n",
  40.         (uint32_t)( SD_TOTAL_SIZE_MB(&g_sdcard_handler));
  41. /* 显示块大小 */
  42.    printf("CardBlockSize:%d\r\n\r\n", g_sd_card_info_handle.BlockSize);
  43. }
  44. /**
  45. *@brief        测试SD卡的读取
  46. *  @note       从secaddr地址开始,读取seccnt个扇区的数据
  47. *@param        secaddr : 扇区地址
  48. *@param        seccnt  : 扇区数
  49. *@retval       无
  50. */
  51. void sd_test_read(uint32_t secaddr, uint32_t seccnt)
  52. {
  53.     uint32_t i;
  54.     uint8_t *buf;
  55.     uint8_t sta = 0;
  56.     buf= mymalloc(SRAMIN, seccnt * 512);       /* 申请内存,从SDRAM申请内存 */
  57.     sta= sd_read_disk(buf, secaddr, seccnt);   /* 读取secaddr扇区开始的内容 */
  58.     if (sta == 0)
  59.     {
  60.        lcd_show_string(30, 170, 200, 16, 16, "USART1Sending Data...", BLUE);
  61.        printf("SECTOR%d DATA:\r\n", secaddr);
  62.        for (i = 0; i < seccnt * 512; i++)
  63.        {
  64.            printf("%x", buf);                 /* 打印secaddr开始的扇区数据 */
  65.        }
  66.        printf("\r\nDATAENDED\r\n");
  67.        lcd_show_string(30, 170, 200, 16, 16, "USART1 SendData Over!", BLUE);
  68.     }
  69.     else
  70.     {
  71.        printf("err:%d\r\n", sta);
  72.        lcd_show_string(30, 170, 200, 16, 16, "SD readFailure!      ", BLUE);
  73.     }
  74.    myfree(SRAMIN, buf);    /* 释放内存 */
  75. }
  76. /**
  77. *@brief        测试SD卡的写入
  78. *  @note       从secaddr地址开始,写入seccnt个扇区的数据
  79. *               慎用!! 最好写全是0XFF的扇区,否则可能损坏SD卡.
  80. *
  81. *@param        secaddr : 扇区地址
  82. *@param        seccnt  : 扇区数
  83. *@retval       无
  84. */
  85. voidsd_test_write(uint32_t secaddr, uint32_t seccnt)
  86. {
  87.     uint32_t i;
  88.     uint8_t *buf;
  89.     uint8_t sta = 0;
  90.     buf= mymalloc(SRAMIN, seccnt * 512);   /* 从SDRAM申请内存 */
  91.     for (i = 0; i < seccnt * 512; i++)      /* 初始化写入的数据,是3的倍数. */
  92.     {
  93.        buf = i * 3;
  94.     }
  95. /* 从secaddr扇区开始写入seccnt个扇区内容 */
  96.     sta=sd_write_disk(buf, secaddr, seccnt);
  97.     if (sta == 0)
  98.     {
  99.        printf("Writeover!\r\n");
  100.     }
  101.     else printf("err:%d\r\n", sta);
  102.    myfree(SRAMIN, buf);    /* 释放内存 */
  103. }
  104. int main(void)
  105. {
  106.     uint8_t key;
  107.     uint32_t sd_size;
  108.     uint8_t t = 0;
  109.     uint8_t *buf;
  110.     uint64_t card_capacity;                   /* SD卡容量 */
  111.    HAL_Init();                                  /* 初始化HAL库 */
  112.    sys_stm32_clock_init(336, 8, 2, 7);     /* 设置时钟, 168Mhz */
  113.    delay_init(168);                            /* 延时初始化 */
  114.    usart_init(115200);                        /* 串口初始化为115200 */
  115.    usmart_dev.init(84);                       /* 初始化USMART */
  116.    led_init();                                 /* 初始化LED */
  117.    lcd_init();                                 /* 初始化LCD */
  118.    key_init();                                 /* 初始化按键 */
  119.     sram_init();                                /* SRAM初始化 */
  120.    my_mem_init(SRAMIN);                       /* 初始化内部SRAM内存池 */
  121.    my_mem_init(SRAMEX);                       /* 初始化外部SRAM内存池) */
  122.    my_mem_init(SRAMCCM);                     /* 初始化CCM内存池) */
  123.    lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  124.    lcd_show_string(30, 70, 200, 16, 16, "SD  TEST", RED);
  125.    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
  126.    lcd_show_string(30, 110, 200, 16, 16, "KEY0:ReadSector 0", RED);
  127.     while (sd_init())    /* 检测不到SD卡 */
  128.     {
  129.        lcd_show_string(30, 150, 200, 16, 16, "SD CardError!", RED);
  130.        delay_ms(500);
  131.        lcd_show_string(30, 150, 200, 16, 16, "PleaseCheck! ", RED);
  132.        delay_ms(500);
  133.        LED0_TOGGLE();   /* 红灯闪烁 */
  134.     }
  135.     /* 打印SD卡相关信息 */
  136.    show_sdcard_info();
  137.    
  138.     /* 检测SD卡成功 */
  139.    lcd_show_string(30, 150, 200, 16, 16, "SD CardOK    ", BLUE);
  140.    lcd_show_string(30, 170, 200, 16, 16, "SD CardSize:     MB", BLUE);
  141. /* 显示SD卡容量 */
  142. lcd_show_num(30 + 13 * 8, 170, SD_TOTAL_SIZE_MB(&g_sdcard_handler),
  143.                 5, 16, BLUE);
  144.     while (1)
  145.     {
  146.        key = key_scan(0);
  147.        if (key == KEY0_PRES)    /* KEY0按下了 */
  148.        {
  149.            sd_test_read(0, 1);
  150.        }
  151.        t++;
  152.        delay_ms(10);
  153.        if (t == 20)
  154.        {
  155.            LED0_TOGGLE();        /* 红灯闪烁 */
  156.            t = 0;
  157.        }
  158.     }
  159. }</font>
复制代码
这里总共4个函数:
1show_sdcard_info函数
该函数用于从串口输出SD卡相关信息,包括:卡类型、制造商ID、卡相对地址、容量和块大小等信息。

2sd_test_read
该函数用于测试SD卡的读取,通过USMART调用,可以指定SD卡的任何地址,读取指定个数的扇区数据,将读到的数据,通过串口打印出来,从而验证SD卡数据的读取。

3sd_test_write函数
该函数用于测试SD卡的写入,通过USMART调用,可以指定SD卡的任何地址,写入指定个数的扇区数据,写入数据自动生成(都是3的倍数),写入完成后,在串口打印写入结果。我们可以通过sd_test_read函数,来检验写入数据是否正确。注意:千万别乱写,否则可能把卡写成砖头/数据丢失!!写之前,先读取该地址的数据,最好全部是0XFF才写(全部0X00也行),其他情况最好别写!
4main函数函数
该函数,先初化相关外设和SD卡,初始化成功,则调用show_sdcard_info函数,输出SD卡相关信息,并在LCD上面显示SD卡容量。然后进入死循环,如果有按键KEY0按下,则通过SD_ReadDisk读取SD卡的扇区0(物理磁盘,扇区0),并将数据通过串口打印出来。这里,我们对上一章学过的内存管理小试牛刀,稍微用了下,以后我们会尽量使用内存管理来设计。

最后,我们将sd_test_read和sd_test_write函数加入USMART控制,这样,我们就可以通过串口调试助手,测试SD卡的读写了,方便测试。

50.6 下载验证
将程序下载到开发板后,我们测试使用的是16Gb标有“SDHC”标志的卡,安装方法如图50.6.1所示:     
image016.png
图50.6.1 测试用的microSD卡与开发板的连接方式

SD卡成功初始化后,LCD显示本程序的一些必要信息,如图50.6.2:     
image018.png
图50.6.2 程序运行效果图

在进入测试的主循环前,我们如果已经通过USB连接开发板的串口1和电脑,可以看到串口端打印出SD卡的相关信息(也可以在接好SD卡后按Reset复位开发板),我们测试使用的是16Gb标有“SDHC”标志的卡,SD卡成功初始化后的信息,如图50.6.3:     
image020.png
图50.6.3 测试用的microSD卡

可见我们用程序读到的SD卡信息与我们使用的SD卡一致。伴随LED0的不停闪烁,提示程序在运行。此时,我们按下KEY0,调用我们编写的SD卡测试函数,这里我们只用到了读函数,写函数的测试大家可以添加代码进行演示。按下后LCD显示按下,信息如图50.6.4,数量量较多的情况我们用串口打印,得到的SD卡扇区0存储的512个字节的信息如图50.6.5所示:     
image022.png
图50.6.4 按下KEY1的开发板界面

image024.png
图50.6.5 串口调试助手显示按下KEY0后读取到的信息

对SD卡的使用的简单介绍就到这里了,另外,利用USMART测试的部分,我们这里就不做介绍了,大家可自行验证下。
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-2-24 06:22

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表