参考:
1.正点原子
背景:
很多单片机系统都需要大容量存储设备,以存储数据。目前常用的有 U 盘,FLASH 芯片,SD 卡等。他们各有优点,综合比较,最适合单片机系统的莫过于 SD 卡了,它不仅容量可以做到很大(32GB 以上),支持 SPI/SDIO 驱动,而且有多种体积的尺寸可供选择(标准的 SD 卡尺寸及 Micor SD 卡尺寸等),能满足不同应用的要求。
只需要少数几个 IO 口即可外扩一个高达 32GB 或以上的外部存储器,容量从几十 M 到几十 G 选择范围很大,更换也很方便,编程也简单,是单片机大容量外部存储器的首选。

50.1 SD 卡简介

50.1.1 SD 物理结构

SD 卡的规范由 SD 卡协会明确,可以访问 https://www.sdcard.org 查阅更多标准。SD 卡主要有 SD、Mini SD 和 microSD(原名 TF 卡,2004 年正式更名为 Micro SD Card,为方便本文用microSD 表示)三种类型,Mini SD 已经被 microSD 取代,使用得不多,根据最新的 SD 卡规格列出的参数如表 50.1.1.1 所示:
在这里插入图片描述

上述表格的“脚位数”,对应于实卡上的“金手指”数,不同类型的卡的触点数量不同,访问的速度也不相同。SD 卡允许了不同的接口来访问它的内部存储单元。最常见的是 SDIO 模式和SPI 模式,根据这两种接口模式,我们也列出 SD 卡引脚对应于这两种不同的电路模式的引脚功能定义,如表 50.1.1.2 所示。
在这里插入图片描述

对比的,我们来看一下 microSD 引脚,可见只比 SD 卡少了一个电源引脚 VSS2,其它的引脚功能类似。
在这里插入图片描述

SD 卡和 micorSD 只有引脚和形状大小不同,内部结构类似,操作时序完全相同,可以使用完全相同的代码驱动,下面以 9’Pin SD 卡的内部结构为为例,展示 SD 卡的存储结构,如图50.1.1.1 所示。
在这里插入图片描述

SD 卡有自己的寄存器,但它不能直接进行读写操作,需要通过命令来控制,SDIO 协议定义了一些命令用于实现某一特定功能,SD 卡根据收到的命令要求对内部寄存器进行修改。图50.1.1.4 中描述的 SD 卡的寄存器是我们和 SD 卡进行数据通讯的主要通道,如下:
在这里插入图片描述

关于 SD 卡的更多信息和硬件设计规范可以参考 SD 卡协议《Physical Layer Simplified Specification Version 2.00》的相关章节(注:因为 STM32 的 SDIO 匹配的是 SD 协议 2.0 版本,后续版本也兼容此旧协议版本,故本章仍以 2.0 版本为介绍对象)。

50.1.2 命令和响应

一个完整的 SD 卡操作过程是:主机(单片机等)发起“命令”,SD 卡根据命令的内容决定是否发送响应信息及数据等,如果是数据读/写操作,主机还需要发送停止读/写数据的命令来结束本次操作,这意味着主机发起命令指令后,SD 卡可以没有响应、数据等过程,这取决于命令的含义。这一过程如图 50.1.2.1 所示。
在这里插入图片描述

SD 卡有多种命令和响应,它们的格式定义及含义在《SD 卡协议 V2.0》的第三和第四章有详细介绍,发送命令时主机只能通过 CMD 引脚发送给 SD 卡,串行逐位发送时先发送最高位(MSB),然后是次高位这样类推……接下来,我们看看 SD 卡的命令格式,如表 50.1.2.1 所示:
在这里插入图片描述

SD 卡的命令固定为 48 位,由 6 个字节组成,字节 1 的最高 2 位固定为 01,低 6 位为命令号(比如 CMD16,为 10000B 即 16 进制的 0X10,完整的 CMD16,第一个字节为 01010000,即 0X10+0X40)。字节 2~5 为命令参数,有些命令是没有参数的。字节 6 的高七位为 CRC 值,最低位恒定为 1。
SD 卡的命令总共有 12 类,分为 Class0~Class11,本章,我们仅介绍几个比较重要的命令,如表 50.1.2.2 所示:
在这里插入图片描述
在这里插入图片描述

上表中,大部分的命令是初始化的时候用的,而表中的 R1、R1b、R2、R3、R6 和 R7 等是SD 卡的应答信号。在主机发送有响应的命令后,SD 卡都会给出相对应的应答,以告知主机该命令的执行情况,或者返回主机需要获取的数据,具体场景如图 50.1.2.2 所示:
在这里插入图片描述

SD 的响应大体分为短响应 48bit 和长响应 136bit,每个响应也有规定好的格式。R1、R1b、R3、R6 和 R7 属于短响应,而 R2 属于长响应,它们具体作用如下表 50.1.2.3 所示。
在这里插入图片描述

SD 卡的响应因使用接口不同,比如 SDIO 和 SPI 接口,它们的响应种类以及响应格式也是不同。这里以 SDIO 接口下的 R1 响应为例,其内容格式如下表 50.1.2.4 所示:
在这里插入图片描述

R2~R7 的响应,限于篇幅,我们就不介绍了,但需要注意的是除了 R2 响应是 128 位外,其它的响应都是 48 位,请大家参考 SD 卡 2.0 协议。

50.1.3 卡模式

SD 卡系统(包括主机和 SD 卡)定义了 SD 卡的工作模式,在每个操作模式下,SD 卡都有几种状态,参考表 50.1.3.1,状态之间通过命令控制实现卡状态的切换。
在这里插入图片描述

对于我们来说两种有效操作模式:卡识别模式和数据传输模式。在系统复位后,主机处于卡识别模式,寻找总线上可用的 SDIO 设备,对 SD 卡进行数据读写之前需要识别卡的种类:V1.0 标准卡、V2.0 标准卡、V2.0 高容量卡或者不被识别卡;同时,SD 卡也处于卡识别模式,直到被主机识别到,即当 SD 卡在卡识别状态接收到 CMD3(SEND_RCA)命令后,SD 卡就进入数据传输模式,而主机在总线上所有卡被识别后也进入数据传输模式。
在卡识别模式下,主机会复位所有处于“卡识别模式”的 SD 卡,确认其工作电压范围,识别 SD 卡类型,并且获取 SD 卡的相对地址(卡相对地址较短,便于寻址)。在卡识别过程中,要求 SD 卡工作在识别时钟频率 FOD 的状态下。卡识别模式下 SD 卡状态转换如图 50.1.3.1。
在这里插入图片描述

主机上电后,所有卡处于空闲状态,包括当前处于无效状态的卡。

  1. 主机也可以发送GO_IDLE_STATE(CMD0)让所有卡软复位从而进入空闲状态,但当前处于无效状态的卡并不会复位。
  2. 主机在开始与卡通信前,需要先确定双方在互相支持的电压范围内。SD 卡有一个电压支持范围,主机当前电压必须在该范围可能才能与卡正常通信。SEND_IF_COND(CMD8)命令就是用于验证卡接口操作条件的(主要是电压支持)。卡会根据命令的参数来检测操作条件匹配性,如果卡支持主机电压就产生响应,否则不响应。而主机则根据响应内容确定卡的电压匹配性。CMD8 是 SD 卡标准 V2.0 版本才有的新命令,所以如果主机有接收到响应,可以判断卡为 V2.0或更高版本 SD 卡。(用来识别2.0协议和之前协议,宽的电压范围)
  3. SD_SEND_OP_COND(ACMD41)命令可以识别或拒绝不匹配它的电压范围的卡。ACMD41命令的 VDD 电压参数用于设置主机支持电压范围,卡响应会返回卡支持的电压范围。对于对CMD8 有响应的卡,把 ACMD41 命令的 HCS 位设置为 1,可以测试卡的容量类型,如果卡响应的 CCS 位为 1 说明为高容量 SD 卡,否则为标准卡。卡在响应 ACMD41 之后进入准备状态,不响应 ACMD41 的卡为不可用卡,进入无效状态。ACMD41 是应用特定命令,发送该命令之前必须先发 CMD55。
  4. ALL_SEND_CID(CMD2)用来控制所有卡返回它们的卡识别号(CID),处于准备状态的卡在发送 CID 之后就进入识别状态。
  5. 之后主机就发送 SEND_RELATIVE_ADDR(CMD3)命令,让卡自己推荐一个相对地址(RCA)并响应命令。这个 RCA 是 16bit 地址,而 CID 是 128bit 地址,使用 RCA 简化通信。卡在接收到 CMD3 并发出响应后就进入数据传输模式,并处于待机状态,主机在获取所有卡 RCA 之后也进入数据传输模式。
  6. 只有 SD 卡系统处于数据传输模式下才可以进行数据读写操作。数据传输模式下可以将主机 SD 时钟频率设置为 FPP,默认最高为 25MHz,频率切换可以通过 CMD4 命令来实现。数据传输模式下,SD 卡状态转换过程见图 50.1.3.2。
    在这里插入图片描述

CMD7 用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是处于待机状态,必须选择一个 RCA 地址目标卡使其进入传输状态才可以进行数据通信。同时通过 CMD7 命令也可以让已经被选择的目标卡返回到待机状态。
数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。卡处于传输状态下可以通过命令对卡进行数据读写、擦除。CMD12 可以中断正在进行的数据通信,让卡返回到传输状态。CMD0 和 CMD15 会中止任何数据编程操作,返回卡识别模式,注意谨慎使用,不当操作可能导致卡数据被损坏。

在数据模式下我们可以对 SD 卡的存储块进行读写访问操作。SD 卡上电后默认以一位数据总线访问,可以通过指令设置为宽总线模式,可以同时使有 4 位总线并行读写数据,这样对于支持宽总线模式的接口(如:SDIO 和 SPI 等)都能加快数据操作速度。
SD 卡有两种数据模式,一种是常规的 8 位宽,即一次按一字节传输,另一种是一次按 512字节传输,我们只介绍前面一种。当按 8-bit 连续传输时,每次传输从最低字节开始,每字节从最高位(MSB)开始发送,当使用一条数据线时,只能通过 DAT0 进行数据传输,那它的数据传输结构如图 50.1.3.3 所示。
在这里插入图片描述

当使用 4 线模式传输 8-bit 结构的数据时,数据仍按 MSB 先发送的原则,DAT[3:0]的高位发送高数据位,低位发送低数据位。硬件支持的情况下,使用 4 线传输可以提升传输速率,其数据传输结构如图 50.1.3.4 所示。
在这里插入图片描述

至此,我们已经介绍了 SD 卡操作的一些知识,并知道了 SD 卡操作的命令、响应和数据传输等状态,接下来我们来分析实际的硬件接口如何向 SD 卡发送我们需要的数据。

50.2 SDIO 接口简介

前面提到 SD 卡的驱动方式之一是用 SDIO 接口通讯,正点原子探索者 STM32F407 自带SDIO 接口,本节,我们将简单介绍 STM32F4 的 SDIO 接口,包括:主要功能及框图、时钟、命令与响应和相关寄存器简介等。

50.2.1 SDIO 主要功能及框图

SDIO,全称为安全数字输入/输出接口,多媒体卡(MMC 卡)、SD 存储卡、SDI/O 卡和 CE-ATA 设备都有 SDIO 接口。SDIO 接口的设备整体概括如下图 50.2.1.1 所示。
在这里插入图片描述

STM32F4 的 SDIO 控制器支持多媒体卡(MMC 卡)、SD 存储卡、SDI/O 卡和 CE-ATA 设备等。SDIO 的主要功能如下:
➢ 与多媒体卡系统规格书版本 4.2 全兼容。支持三种不同的数据总线模式:1 位(默认)、4 位和 8 位。
➢ 与较早的多媒体卡系统规格版本全兼容(向前兼容)。
➢ 与 SD 存储卡规格版本 2.0 全兼容。SD 卡规范版本 2.0,包括 SD 和高容量 SDHC 标准卡,故不支持超大容量 SDXC/SDUC 标准卡,所以 STM32F4xx 的 SDIO 可以支持的最高卡容量是 32GB。
➢ 与 SDI/O 卡规格版本 2.0 全兼容:支持两种不同的数据总线模式:1 位(默认)和 4 位。
➢ 完全支持 CE-ATA 功能(与 CE-ATA 数字协议版本 1.1 全兼容)。8 位总线模式下数据传输速率可达 48MHz。
➢ 数据和命令输出使能信号,用于控制外部双向驱动器。
➢ SDIO 不具备兼容 SPI 的通信模式。
STM32F4 的 SDIO 控制器包含 2 个部分:SDIO 适配器模块和 APB2 总线接口,其功能框图如图 50.2.1.2 所示:
在这里插入图片描述

复位后默认情况下 SDIO_D0 用于数据传输。初始化后主机可以改变数据总线的宽度(通过ACMD6 命令设置)。
如果一个多媒体卡接到了总线上,则 SDIO_D0、SDIO_D[3:0]或 SDIO_D[7:0]可以用于数据传输。MMC 版本 V3.31 和之前版本的协议只支持 1 位数据线,所以只能用 SDIO_D0(为了通用性考虑,在程序里面我们只要检测到是 MMC 卡就设置为 1 位总线数据)。
如果一个 SD 或 SD I/O 卡接到了总线上,可以通过主机配置数据传输使用 SDIO_D0 或SDIO_D[3:0]。所有的数据线都工作在推挽模式。
SDIO_CMD 有两种操作模式:
①用于初始化时的开路模式(仅用于 MMC 版本 V3.31 或之前版本)
②用于命令传输的推挽模式(SD/SD I/O 卡和 MMCV4.2 在初始化时也使用推挽驱动)

50.2.2 SDIO 的时钟

从图 50.2.1.1 我们可以看到 SDIO 总共有 3 个时钟,分别是:
① 卡时钟(SDIO_CK):每个时钟周期在命令和数据线上传输 1 位命令或数据。对于多媒体卡 V3.31 协议,时钟频率可以在 0MHz 至 20MHz 间变化;对于多媒体卡 V4.0/4.2 协议,时钟频率可以在 0MHz 至 48MHz 间变化;对于 SD 或 SDI/O 卡,时钟频率可以在 0MHz 至 25MHz间变化。
② SDIO 适配器时钟(SDIOCLK):该时钟用于驱动 SDIO 适配器,来自 OLL48CK,其频率一般为 48Mhz,并用于产生 SDIO_CK 时钟。
③ APB2 总线接口时钟(PCLK2):该时钟用于驱动 SDIO 的 APB2 总线接口,其频率为HCLK/2,一般为 84Mhz。
前面提到,我们的 SD 卡时钟(SDIO_CK),根据卡的不同,可能有好几个区间,这就涉及到时钟频率的设置,SDIO_CK 与 SDIOCLK 的关系为:
SDIO_CK = SDIOCLK / (2 + CLKDIV)
其中,SDIO CLK 为 PLL48CK,一般是 48Mhz,而 CLKDIV 则是分配系数,可以通过 SDIO的 SDIO_CLKCR 寄存器进行设置(确保 SDIO_CK 不超过卡的最大操作频率)。
这里要提醒大家,在 SD 卡刚刚初始化的时候,其时钟频率(SDIO_CK)是不能超过 400Khz的,否则可能无法完成初始化。在初始化以后,就可以设置时钟频率到最大了(但不可超过 SD卡的最大操作时钟频率)。

50.2.3 SDIO 的命令与响应

SDIO 的命令分为应用相关命令(ACMD)和通用命令(CMD)两部分,应用相关命令(ACMD)的发送,必须先发送通用命令(CMD55),然后才能发送应用相关命令(ACMD)。
SDIO 的所有命令和响应都只通过 SDIO_CMD 引脚传输的,任何命令的长度都是固定为 48位,SDIO 的命令格式如表 50.2.3.1 所示:
在这里插入图片描述

所有的命令都是由 STM32F4 发出,其中开始位、传输位、CRC7 和结束位由 SDIO 硬件控制,我们需要设置的就只有命令索引和参数部分。其中命令索引(如 CMD0,CMD1 之类的)在 SDIO_CMD 寄存器里面设置,命令参数则由寄存器 SDIO_ARG 设置。
一般情况下,选中的 SD 卡在接收到命令之后,都会回复一个应答(注意 CMD0 是无应答的),这个应答我们称之为响应,响应也是在 CMD 线上串行传输的。STM32F4 的 SDIO 控制器支持 2 种响应类型,即:短响应(48 位)和长响应(136 位),这两种响应类型都带 CRC 错误检测(注意不带 CRC 的响应应该忽略 CRC 错误标志,如 CMD1 的响应)。

短响应的格式如表 50.2.3.2 所示:
在这里插入图片描述

长响应的格式如表 50.2.3.3 所示:
在这里插入图片描述

同样,硬件为我们滤除了开始位、传输位、CRC7 以及结束位等信息,对于短响应,命令索引存放在 SDIO_RESPCMD 寄存器,参数则存放在 SDIO_RESP1 寄存器里面。对于长响应,则仅留 CID/CSD 位域,存放在 SDIO_RESP1~SDIO_RESP4 等 4 个寄存器。
SD 存储卡总共有 5 类响应(R1、R2、R3、R6、R7),我们这里以 R1 为例简单介绍一下。R1(普通响应命令)响应属于短响应,其长度为 48 位,R1 响应的格式如表 50.2.3.4 所示:
在这里插入图片描述

在收到 R1 响应后,我们可以从 SDIO_RESPCMD 寄存器和 SDIO_RESP1 寄存器分别读出命令索引和卡状态信息。关于其他响应的介绍,请大家参考光盘:《SD 卡 2.0 协议.pdf》或《STM32F4xx 中文参考手册》第 28 章。

最后,我们看看数据在 SDIO 控制器与 SD 卡之间的传输。对于 SDI/SDIO 存储器,数据是以数据块的形式传输的,而对于 MMC 卡,数据是以数据块或者数据流的形式传输。本节我们只考虑数据块形式的数据传输。SDIO(多)数据块读操作,如图 50.2.3.1 所示:
在这里插入图片描述

从上图,我们可以看出,从机在收到主机相关命令后,开始发送数据块给主机,所有数据块都带有 CRC 校验值(CRC 由 SDIO 硬件自动处理),单个数据块读的时候,在收到 1 个数据块以后即可以停止了,不需要发送停止命令(CMD12)。但是多块数据读的时候,SD 卡将一直发送数据给主机,直到接到主机发送的 STOP 命令(CMD12)。

SDIO(多)数据块写操作,如图 50.2.3.2 所示:
在这里插入图片描述

数据块写操作同数据块读操作基本类似,只是数据块写的时候,多了一个繁忙判断,新的数据块必须在 SD 卡非繁忙的时候发送。这里的繁忙信号由 SD 卡拉低 SDIO_D0,以表示繁忙,SDIO 硬件自动控制,不需要我们软件处理。
SDIO 的命令与响应就为大家介绍到这里。

50.2.4 SDIO 相关寄存器介绍

这部分将结合《STM32F4xx 参考手册_V4(中文版).pdf》的内容和大家一起分析使用 SDIO时我们主要用的一些寄存器的情况。

⚫ SDIO 电源控制寄存器(SDIO_POWER)
SDIO 电源控制寄存器(SDIO_POWER),该寄存器定义如图 50.2.4.1 所示:
在这里插入图片描述

该寄存器复位值为 0,所以 SDIO 的电源是关闭的,我们要启用 SDIO,第一步就是要设置该寄存器最低 2 个位均为 1,让 SDIO 上电,开启卡时钟。

⚫ SDIO 时钟控制寄存器(SDIO_CLKCR)
SDIO 时钟控制寄存器(SDIO_CLKCR),该寄存器主要用于设置 SDIO_CK 的分配系数,开关等,并可以设置 SDIO 的数据位宽,该寄存器的定义如图 50.2.4.2 所示:
在这里插入图片描述

上图仅列出了部分我们要用到的位设置,WIDBUS 用于设置 SDIO 总线位宽,正常使用的时候,设置为 1,即 4 位宽度。BYPASS 用于设置分频器是否旁路,我们一般要使用分频器,所以这里设置为 0,禁止旁路。CLKEN 则用于设置是否使能 SDIO_CK,我们设置为 1。最后,CLKDIV 则用于控制 SDIO_CK 的分频,设置为 1,即可得到 24Mhz 的 SDIO_CK 频率。

⚫ SDIO 参数寄存器(SDIO_ARG)
SDIO 参数寄存器(SDIO_ARG),该寄存器包含一个 32 位命令参数,该参数作为命令消息的一部分发送到卡,如果命令包含参数,则在将命令写入到命令寄存器之前,必须将参数加载到此寄存器中。

⚫ SDIO 命令响应寄存器(SDIO_RESPCMD)
SDIO 命令响应寄存器(SDIO_RESPCMD),该寄存器为 32 位,但只有低 6 位有效,比较简单,用于存储最后收到的命令响应中的命令索引。如果传输的命令响应不包含命令索引(长响应或 OCR 响应),则该寄存器的内容不可预知。

⚫ SDIO 响应寄存器组(SDIO_RESP1~SDIO_RESP4)
SDIO 响应寄存器组(SDIO_RESP1~SDIO_RESP4),该寄存器组总共由 4 个 32 位寄存器组成,用于存放接收到的卡响应部分信息。如果收到短响应,则数据存放在 SDIO_RESP1 寄存器里面,其他三个寄存器没有用到 。 而如果收到长响应 ,则依次存放在SDIO_RESP1~SDIO_RESP4 里面,如表 50.2.4.3 所示:
在这里插入图片描述

⚫ SDIO 命令寄存器(SDIO_CMD)
SDIO 命令寄存器(SDIO_CMD)各位定义如图 50.2.4.3 所示:
在这里插入图片描述

图中只列出了部分位的描述,其中低 6 位为命令索引,也就是我们要发送的命令索引号(比如发送 CMD1,其值为 1,索引就设置为 1)。位[7:6],用于设置等待响应位,用于指示 CPSM是否需要等待,以及等待类型等。这里的 CPSM,即命令通道状态机,我们就不详细介绍了,请参阅《STM32F4xx 参考手册_V4(中文版).pdf》第 807 页,有详细介绍。命令通道状态机我们一般都是开启的,所以位 10 要设置为 1。

⚫ SDIO 数据定时器寄存器(SDIO_DTIMER)
SDIO 数据定时器寄存器(SDIO_DTIMER)用于存储以卡总线时钟(SDIO_CK)为周期的数据超时时间,一个计数器将从 SDIO_DTIMER 寄存器加载数值,并在数据通道状态机(DPSM)进入 Wait_R 或繁忙状态时进行递减计数,当 DPSM 处在这些状态时,如果计数器减为 0,则设置超时标志。这里的 DPSM,即数据通道状态机,类似 CPSM,详细请参考《STM32F4xx 参考手册_V4(中文版).pdf》第 809 页。注意:在写入数据控制寄存器,进行数据传输之前,必须先写入该寄存器(SDIO_DTIMER)和数据长度寄存器(SDIO_DLEN)!

⚫ SDIO 数据长度寄存器(SDIO_DLEN)
SDIO 数据长度寄存器(SDIO_DLEN)低 25 位有效,用于设置需要传输的数据字节长度。对于块数据传输,该寄存器的数值,必须是数据块长度(通过 SDIO_DCTRL 设置)的倍数。

⚫ SDIO 数据控制寄存器(SDIO_DCTRL)
SDIO 数据控制寄存器(SDIO_DCTRL)各位定义如图 50.2.4.4 所示:
在这里插入图片描述
在这里插入图片描述

该寄存器,用于控制数据通道状态机(DPSM),包括数据传输使能、传输方向、传输模式、DMA 使能、数据块长度等信息,都是通过该寄存器设置。我们需要根据自己的实际情况,来配置该寄存器,才可正常实现数据收发。

⚫ SDIO 状态寄存器(STA)/ 清除中断寄存器(ICR)/ 中断屏蔽寄存器(MASK)
接下来,我们介绍几个位定义十分类似的寄存器,他们是:状态寄存器(SDIO_STA)、清除中断寄存器(SDIO_ICR)和中断屏蔽寄存器(SDIO_MASK),这三个寄存器每个位的定义都相同,只是功能各有不同。所以可以一起介绍,以状态寄存器(SDIO_STA)为例,该寄存器各位定义如图 50.2.4.5 所示:
在这里插入图片描述
在这里插入图片描述

状态寄存器可以用来查询 SDIO 控制器的当前状态,以便处理各种事务。比如 SDIO_STA的位 2 表示命令响应超时,说明 SDIO 的命令响应出了问题。我们通过设置 SDIO_ICR 的位 2则可以清除这个超时标志,而设置 SDIO_MASK 的位 2,则可以开启命令响应超时中断,设置为 0 关闭。其他位我们就不一一介绍了,请大家自行学习。

⚫ SDIO 数据 FIFO 寄存器(SDIO_FIFO)
数据 FIFO 寄存器包括接收和发送 FIFO,他们由一组连续的 32 个地址上的 32 个寄存器组成,CPU 可以使用 FIFO 读写多个操作数。例如我们要从 SD 卡读数据,就必须读 SDIO_FIFO寄存器,要写数据到 SD 卡,则要写 SDIO_FIFO 寄存器。SDIO 将这 32 个地址分为 16 个一组,发送接收各占一半。而我们每次读写的时候,最多就是读取发送 FIFO 或写入接收 FIFO 的一半大小的数据,也就是 8 个字(32 个字节),这里特别提醒,我们操作 SDIO_FIFO(不论读出还是写入)必须是以 4 字节对齐的内存进行操作,否则将导致出错!
至此,SDIO 的相关寄存器介绍,我们就介绍完了。还有几个不常用的寄存器,我们没有介绍到,请大家参考《STM32F4xx 参考手册_V4(中文版).pdf》第 28 章相关章节。

50.2.5 SDIO 模式下的 SD 卡初始化

这一节,我们来看看 SD 卡的初始化流程,要实现 SDIO 驱动 SD 卡,最重要的步骤就是SD 卡的初始化,只要 SD 卡初始化完成了,那么剩下的(读写操作)就简单了,所以我们这里重点介绍 SD 卡的初始化。从《SD 卡 2.0 协议》(见光盘资料)文档,我们得到 SD 卡初始化流程图如图 50.2.5.1 所示:
在这里插入图片描述

Non compatible voltage rangeor check pattem is not correct:不兼容的电压范围或检查模式不正确

从图中,我们看到,不管什么卡(这里我们将卡分为 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.2.5.1 所示:
在这里插入图片描述

这里,我们需要在发送 CMD8 的时候,通过其带的参数我们可以设置 VHS 位,以告诉 SD卡,主机的供电情况,VHS 位定义如表 50.2.5.2 所示:
在这里插入图片描述

这里我们使用参数 0X1AA,即告诉 SD 卡,主机供电为 2.7~3.6V 之间,如果 SD 卡支持CMD8,且支持该电压范围,则会通过 CMD8 的响应(R7)将参数部分原本返回给主机,如果不支持 CMD8,或者不支持这个电压范围,则不响应。
在发送 CMD8 后,发送 ACMD41(注意发送 ACMD41 之前要先发送 CMD55),来进一步确认卡的操作电压范围,并通过 HCS 位来告诉 SD 卡,主机是不是支持高容量卡(SDHC)。ACMD41 的命令格式如表 50.2.5.3 所示:
在这里插入图片描述

Sends host capacity support information(HCS) and asks the accessed card to send its operating condition register(OCR) content in the response on the CMD line.HCS is effective when card receives SEND_IF_COND command. Reserved bit shall be set to ‘0’. CCS bit is assigned to OCR[30]
发送主机容量支持信息(HCS),并要求被访问的卡在CMD线上的响应中发送其操作条件寄存器(OCR)内容。当板卡收到SEND_IF_COND命令时,HCS有效。保留位应设置为“0”(是指响应的OCR里面的 reserved应该设置为0)。CCS位分配给OCR[30]
ACMD41 得到的响应(R3)包含 SD 卡 OCR 寄存器内容,OCR 寄存器内容定义如表 50.2.5.4所示:
在这里插入图片描述

对于支持 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.2.5.1 中,最后发送了 CMD2 和 CMD3 命令,用于获得卡 CID 寄存器数据和卡相对地址(RCA)。发送 CMD2,用于获得 CID 寄存器的数据,而 CID 寄存器数据各位定义如表 50.2.5.5 所示:
在这里插入图片描述

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 硬件设计

1. 例程功能
本章实验功能简介:开机的时候先初始化 SD 卡,如果 SD 卡初始化完成,则串口输出SD卡相关信息。通过串口输入指令调用sd卡的读写函数向sd卡读写数据,验证相关函数是否能工作。
像SD卡这样的大容量存储器件,使用读写扇区的方式是不方便的,这篇笔记主要是介绍sd的相关知识,实际使用需要借助fatfs这样的文件系统来管理sd卡这样的大容量器件。
2. 硬件资源
1)串口 1 (PA9/PA10 连接在板载 USB 转串口芯片 CH340 上面)
2)microSD Card
3. 原理图
在这里插入图片描述

SD 卡座在 JTAG 插座附近,SD 卡座与开发板的连接在开发板上是直接连接在一起的,硬件上不需要任何改动。大家准备好 SD 卡就可以开始我们的程序设计和验证了。

50.4 程序设计

50.4.1 SD 卡的 HAL 库驱动

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

1. HAL_SD_Init 函数
要使用一个外设首先要对它进行初始化,所以先看 SD 卡的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_SD_Init(SD_HandleTypeDef *hsd)
⚫ 函数描述:
根据 SD 参数,初始化 SDIO 外设以便后续操作 SD 卡。
⚫ 函数形参:
形参1是SD 卡的句柄,结构体类型是 SD_HandleTypeDef ,我们不使用USE_HAL_SD_REGISTER_CALLBACKS 宏来拓展 SD 卡的自定义函数,精简后其定义如下:

/** 
* @brief SD 操作句柄结构体定义
*/
typedef struct
{
    SD_TypeDef *Instance; /* SD 相关寄存器基地址 */
    SD_InitTypeDef Init; /* SDIO 初始化变量 */
    HAL_LockTypeDef Lock; /* 互斥锁,用于解决外设访问冲突 */
    uint8_t *pTxBuffPtr; /* SD 发送数据指针 */
    uint32_t TxXferSize; /* SD 发送缓存按字节数的大小 */
    uint8_t *pRxBuffPtr; /* SD 接收数据指针 */
    uint32_t RxXferSize; /* SD 接收缓存按字节数的大小 */
    __IO uint32_t Context; /* HAL 库对 SD 卡的操作阶段 */
    __IO HAL_SD_StateTypeDef State; /* SD 卡操作状态 */
    __IO uint32_t ErrorCode; /* SD 卡错误代码 */
    DMA_HandleTypeDef *hdmatx; /* SD DMA 数据发送指针 */
    DMA_HandleTypeDef *hdmarx; /* SD DMA 数据接收指针 */
    HAL_SD_CardInfoTypeDef SdCard; /* SD 卡信息的 */
    uint32_t CSD[4]; /* 保存 SD 卡 CSD 寄存器信息 */
    uint32_t CID[4]; /* 保存 SD 卡 CID 寄存器信息 */
}SD_HandleTypeDef;

上面的初始化结构体中 HAL_SD_CardInfoTypeDef 用于初始化后提取卡信息,包括卡类型、容量等参数。

/** 
* @brief SD 卡信息结构定义
*/
typedef struct
{
    uint32_t CardType;      /* 存储卡类型标记:标准卡、高速卡 */
    uint32_t CardVersion;   /* 存储卡版本 */
    uint32_t Class;         /* 卡类型 */
    uint32_t RelCardAdd;    /* 卡相对地址 */
    uint32_t BlockNbr;      /* 卡存储块数 */
    uint32_t BlockSize;     /* SD 卡每个存储块大小 */
    uint32_t LogBlockNbr;   /* 以块表示的卡逻辑容量 */
    uint32_t LogBlockSize;  /* 以字节为单位的逻辑块大小 */
}HAL_SD_CardInfoTypeDef;

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

2. HAL_SD_ConfigWideBusOperation 函数
SD 卡上电后默认使用 1 位数据总线进行数据传输,卡如果允许,可以在初始化完成后重新设置 SD 卡的数据位宽以加快数据传输过程:
HAL_StatusTypeDef HAL_SD_ConfigWideBusOperation(SD_HandleTypeDef *hsd,uint32_t WideMode);
⚫ 函数描述:
这个函数用于设置数据总线格式的数据宽度,用于加快卡的数据访问速度,当然前提是硬件连接和卡本身能支持这样操作。
⚫ 函数形参:
形参 1 是 SD 卡的句柄,结构体类型是 SD_HandleTypeDef,此函数需要在 SDIO 初始化结束后才能使用,我们需要通过使用初始化后的 SDIO 结构体的句柄访问外设。
形参 2 是总线宽度,根据函数的形参检查规则我们可知它实际上只有三个可选值:

#define SDIO_BUS_WIDE_1B ((uint32_t)0x00000000U)
#define SDIO_BUS_WIDE_4B SDIO_CLKCR_WIDBUS_0
#define SDIO_BUS_WIDE_8B SDIO_CLKCR_WIDBUS_1

⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到 HAL_OK 表示成功。

3. HAL_SD_ReadBlocks 函数
SD 卡初始化后从 SD 卡的指定扇区读数据:
HAL_StatusTypeDef HAL_SD_ReadBlocks (SD_HandleTypeDef *hsd, uint8_t *pData,uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout);
这个函数是直接读取,不使用硬件中断。
⚫ 函数描述:
从 SD 卡的指定扇区读取一定数量的数据。
⚫ 函数形参:

/**
  * @brief  Reads block(s) from a specified address in a card. The Data transfer
  *         is managed by polling mode.
  * @note   This API should be followed by a check on the card state through
  *         HAL_SD_GetCardState().
  * @param  hsd: Pointer to SD handle
  * @param  pData: pointer to the buffer that will contain the received data
  * @param  BlockAdd: Block Address from where data is to be read
  * @param  NumberOfBlocks: Number of SD blocks to read
  * @param  Timeout: Specify timeout value
  * @retval HAL status
  */
  /**
  * @brief  从卡中的指定地址读取一个或多个块。数据传输通过轮询模式管理。
  * @note   此API调用后应通过 `HAL_SD_GetCardState()` 检查卡的状态。
  * @param  hsd: 指向SD句柄的指针
  * @param  pData: 指向将存储接收数据的缓冲区的指针
  * @param  BlockAdd: 要读取数据的块地址
  * @param  NumberOfBlocks: 要读取的SD块数量
  * @param  Timeout: 指定超时值
  * @retval HAL状态
  */

形参 1 是 SD 卡的句柄,结构体类型是 SD_HandleTypeDef,此函数需要在 SDIO 初始化结束后才能使用,我们需要通过使用初始化后的 SDIO 结构体的句柄访问外设。
形参 2 pData 是一个指向 8 位类型的数据指针缓冲,它用于接收我们需要的数据。
形参 3 BlockAdd 是我们需要访问的数据扇区序号(地址),对于任意的存储都是类似的,像 SD 卡这样的大存储块也同样是通过位置标识来访问不同的数据。
形参 4 NumberOfBlocks 是我们本次要从指定扇区序号(地址)启始的读取扇区数量。
形参 5 Timeout 表示读的超时时间。HAL 库驱动在达到超时时间前还没读到数据会进行重试和等待,达到超时时间后或者本次读取成功才退出本次操作。
⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到 HAL_OK 表示成功。类似功能的函数还有,我们的例程没有使用 DMA 和中断方式,故不使用以下两个接口:

HAL_StatusTypeDef HAL_SD_ReadBlocks_IT (SD_HandleTypeDef *hsd, uint8_t *pData,uint32_t BlockAdd, uint32_t NumberOfBlocks);
HAL_StatusTypeDef HAL_SD_ReadBlocks_DMA (SD_HandleTypeDef *hsd, uint8_t *pData,uint32_t BlockAdd, uint32_t NumberOfBlocks);

它们分别使用了中断方式和 DMA 方式来实现类似的功能,它们的调用非常相似

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

/**
  * @brief  Allows to write block(s) to a specified address in a card. The Data
  *         transfer is managed by polling mode.
  * @note   This API should be followed by a check on the card state through
  *         HAL_SD_GetCardState().
  * @param  hsd: Pointer to SD handle
  * @param  pData: pointer to the buffer that will contain the data to transmit
  * @param  BlockAdd: Block Address where data will be written
  * @param  NumberOfBlocks: Number of SD blocks to write
  * @param  Timeout: Specify timeout value
  * @retval HAL status
  */
  /**
  * @brief  允许将数据块写入卡中的指定地址。数据传输通过轮询模式管理。
  * @note   此API调用后应通过 `HAL_SD_GetCardState()` 检查卡的状态。
  * @param  hsd: 指向SD句柄的指针
  * @param  pData: 指向包含要传输数据的缓冲区的指针
  * @param  BlockAdd: 数据将被写入的块地址
  * @param  NumberOfBlocks: 要写入的SD块数量
  * @param  Timeout: 指定超时值
  * @retval HAL状态
  */

形参 1 是 SD 卡的句柄,结构体类型是 SD_HandleTypeDef,此函数需要在 SDIO 初始化结束后才能使用,我们需要通过使用初始化后的 SDIO 结构体的句柄访问外设。
形参 2 pData 是一个指向 8 位类型的数据指针缓冲,它用于接收我们需要的数据。
形参 3 BlockAdd 指向我们需要访问的数据扇区,对于任意的存储都是类似的,像 SD 卡这样的大存储块也同样是通过位置标识来访问不同的数据。
形参 4 NumberOfBlocks 是我们本次要从指定扇区序号(地址)启始的写入扇区数量。。
形参 5 Timeout 表示写动作的超时时间。HAL 库驱动在达到超时时间前还没读到数据会进行重试和等待,达到超时时间后或者本次写入成功才退出本次操作。
⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到 HAL_OK 表示成功。
类似于读函数,写函数同样有中断版本,我们的例程没有使用 DMA 和中断方式,故不使用以下两个接口:

HAL_StatusTypeDef HAL_SD_WriteBlocks_IT (SD_HandleTypeDef *hsd, uint8_t *pData,uint32_t BlockAdd, uint32_t NumberOfBlocks);
HAL_StatusTypeDef HAL_SD_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData,uint32_t BlockAdd, uint32_t NumberOfBlocks);

它们分别使用了中断方式和 DMA 方式来实现类似的功能,它们的调用非常相似,这里就不重复介绍了,大家查看对应的函数实现即可。

5. HAL_SD_GetCardInfo 函数
SD 卡初始化后,根据设备句柄读 SD 卡的相关状态信息:
HAL_StatusTypeDef HAL_SD_GetCardInfo(SD_HandleTypeDef *hsd,HAL_SD_CardInfoTypeDef *pCardInfo);
⚫ 函数描述:
获取 SD 卡信息。
⚫ 函数形参:
形参 1 是 SD 卡的句柄,结构体类型是 SD_HandleTypeDef,此函数需要在 SDIO 初始化结束后才能使用,我们需要通过使用初始化后的 SDIO 结构体的句柄访问外设。
形参 2 pCardInfo 是一个指向 HAL_SD_CardInfoTypeDef 类型的数据指针缓冲,它用于接收卡信息,包括卡类型、容量等参数。
⚫ 函数返回值:
HAL_StatusTypeDef 类型的函数,返回值同样是需要获取到 HAL_OK 表示成功。

SDIO 驱动 SD 卡配置步骤
1)初始化 SD 卡相关 GPIO 口
具体用到哪些 GPIO 口可以参考原理图,特别注意:IO 口的模式要设置为复用推挽。
2)SD 卡初始化
这里需要使能 SDIO 时钟并且设置 SDIO 的工作参数,使用 HAL_SD_Init 函数进行初始化。
3)重新设置 SDIO 总线位宽
这个步骤是可选的,HAL 库通过 HAL_SD_WideBusOperation_config 函数实现 SDIO 总线位宽改变。通过 HAL_SD_Init 函数初始化后,总线位宽默认为 1 位,所以要想获得比较快的速度,就需要改变位宽。
4)实现 SD 卡读取&写入函数
在初始化 SDIO 和 SD 卡完成以后,我们就可以访问 SD 卡了,HAL 库提供了两个基本的SD 卡读写函数:HAL_SD_ReadBlocks 和 HAL_SD_WriteBlocks,用于读取和写入 SD 卡。我们对这两个函数再进行一次封装,以便更好的适配文件系统,再封装后,我们使用:sd_read_disk来读取 SD 卡,使用:sd_write_disk 来写入 SD 卡,详见本例程源码。

50.4.2 程序解析

我们主要介绍三个函数:MX_SDIO_SD_Init、write_sd,read_sd。
1) MX_SDIO_SD_Init 函数
MX_SDIO_SD_Init 填充 SDIO 结构体的控制句柄,然后使用 HAL 库的 HAL_SD_Init 初始化函数即可,在此过程中 HAL_SD_Init 会调用 HAL_SD_MspInit 函数回调函数,开始的时候速率比较慢一般需要1位操作,稳定之后设置为4位:

void MX_SDIO_SD_Init(void)
{

  /* USER CODE BEGIN SDIO_Init 0 */

  /* USER CODE END SDIO_Init 0 */

  /* USER CODE BEGIN SDIO_Init 1 */

  /* USER CODE END SDIO_Init 1 */
  hsd.Instance = SDIO;
  hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING; /* 上升沿 */
  hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE; /* 不使用 bypass 模式,直接用 HCLK 进行分频得到 SDIO_CK */
  hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
  hsd.Init.BusWide = SDIO_BUS_WIDE_1B;/* 1 位数据线 */
  hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
  hsd.Init.ClockDiv = 0;
  if (HAL_SD_Init(&hsd) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_1B) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN SDIO_Init 2 */
  
  if (HAL_SD_ConfigWideBusOperation(&hsd, SDIO_BUS_WIDE_4B) != HAL_OK)/* 使能宽总线模式,即 4 位总线模式,加快读取速度 */
  {
    Error_Handler();
  }

  /* USER CODE END SDIO_Init 2 */

}

void HAL_SD_MspInit(SD_HandleTypeDef* sdHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(sdHandle->Instance==SDIO)
  {
  /* USER CODE BEGIN SDIO_MspInit 0 */

  /* USER CODE END SDIO_MspInit 0 */
    /* SDIO clock enable */
    __HAL_RCC_SDIO_CLK_ENABLE();

    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOD_CLK_ENABLE();
    /**SDIO GPIO Configuration
    PC8     ------> SDIO_D0
    PC9     ------> SDIO_D1
    PC10     ------> SDIO_D2
    PC11     ------> SDIO_D3
    PC12     ------> SDIO_CK
    PD2     ------> SDIO_CMD
    */
    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11
                          |GPIO_PIN_12;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;/* 推挽复用 */ 
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;/* 高速 */ 
    GPIO_InitStruct.Alternate = GPIO_AF12_SDIO;/* 复用为 SDIO */
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_2;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF12_SDIO;
    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

    /* SDIO DMA Init */
    /* SDIO_RX Init */
    hdma_sdio_rx.Instance = DMA2_Stream3;
    hdma_sdio_rx.Init.Channel = DMA_CHANNEL_4;
    hdma_sdio_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_sdio_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_sdio_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_sdio_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
    hdma_sdio_rx.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    hdma_sdio_rx.Init.Mode = DMA_PFCTRL;
    hdma_sdio_rx.Init.Priority = DMA_PRIORITY_LOW;
    hdma_sdio_rx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
    hdma_sdio_rx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
    hdma_sdio_rx.Init.MemBurst = DMA_MBURST_INC4;
    hdma_sdio_rx.Init.PeriphBurst = DMA_PBURST_INC4;
    if (HAL_DMA_Init(&hdma_sdio_rx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(sdHandle,hdmarx,hdma_sdio_rx);

    /* SDIO_TX Init */
    hdma_sdio_tx.Instance = DMA2_Stream6;
    hdma_sdio_tx.Init.Channel = DMA_CHANNEL_4;
    hdma_sdio_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_sdio_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_sdio_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_sdio_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
    hdma_sdio_tx.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    hdma_sdio_tx.Init.Mode = DMA_PFCTRL;
    hdma_sdio_tx.Init.Priority = DMA_PRIORITY_LOW;
    hdma_sdio_tx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
    hdma_sdio_tx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
    hdma_sdio_tx.Init.MemBurst = DMA_MBURST_INC4;
    hdma_sdio_tx.Init.PeriphBurst = DMA_PBURST_INC4;
    if (HAL_DMA_Init(&hdma_sdio_tx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(sdHandle,hdmatx,hdma_sdio_tx);

    /* SDIO interrupt Init */
    HAL_NVIC_SetPriority(SDIO_IRQn, 3, 0);
    HAL_NVIC_EnableIRQ(SDIO_IRQn);
  /* USER CODE BEGIN SDIO_MspInit 1 */

  /* USER CODE END SDIO_MspInit 1 */
  }
}

2) read_disk 函数
这个函数比较简单,实际上我们使用它来对 HAL 库的读函数 HAL_SD_ReadBlocks(HAL_SD_WriteBlocks_DMA也行) 进行了二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据读函数返回值自己作其它处理。

int32_t read_sd(uint8_t* out_p, uint32_t out_len) 
{
    if ((NULL == out_p) && (BLOCK_SIZE < out_len))
    {
        return -1;
    }

    //HAL_StatusTypeDef HAL_SD_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)

    if (HAL_SD_ReadBlocks_DMA(&hsd, (uint8_t*)buffer_RX, 0, 1) == HAL_OK)
    //if (HAL_SD_ReadBlocks(&hsd, (uint8_t*)buffer_RX, 0, 1, 0xFFFF) == HAL_OK)            
    {
        while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER);  //处于传输状态退出
        printf("ReadBlocks Successfully\r\n");

        memcpy((void*)out_p, buffer_RX, out_len);
    }
    else
    {
        printf("ReadBlocks fail\r\n");
    }
   
    return 0;

}

3) write_disk 函数
这个函数比较简单,实际上我们使用它来对 HAL 库的读函数 HAL_SD_WriteBlocks(HAL_SD_WriteBlocks_DMA()也行) 进行了二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据读函数返回值自己作其它处理。

int32_t write_sd(uint8_t* input_p, uint32_t input_len) 
{
    if ((NULL == input_p) && (BLOCK_SIZE < input_len))
    {
        return -1;
    }

    memcpy((void*)buffer_WX, input_p, input_len);

    //HAL_StatusTypeDef HAL_SD_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)
    //HAL_StatusTypeDef HAL_SD_WriteBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout)

    if (HAL_SD_WriteBlocks_DMA(&hsd, (uint8_t*)buffer_WX, 0, 1) == HAL_OK) 
    //if (HAL_SD_WriteBlocks(&hsd, (uint8_t*)buffer_WX, 0, 1, 0xFFFF) == HAL_OK)             
    {
        while(HAL_SD_GetCardState(&hsd) != HAL_SD_CARD_TRANSFER);  //处于传输状态退出
        printf("WriteBlocks Successfully\r\n");
    }
    else
    {
        printf("WriteBlocks fail\r\n");
    }

    return 0;
}

2. main.c 代码
main.c 里面的代码比较简单。为了方便测试,我们编写了 SD_test ()函数用于卡信息打印。

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_SDIO_SD_Init();
  /* USER CODE BEGIN 2 */
  HAL_GPIO_WritePin(LED_0_GPIO_Port, LED_0_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(LED_1_GPIO_Port, LED_1_Pin, GPIO_PIN_RESET);

  cc();
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    uart_debug_task();

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

main 函数先初始化相关外设和 SD 卡,初始化成功后,通过调用 SD_test() 函数输出 SD 卡相关信息(卡类型、制造商 ID、卡相对地址、容量和块大小等),并在通过串口输出。然后进入死循环,如果有串口输入"cmd_sd 1",则通过 read_sd 函数读取 SD 卡的扇区0(物理磁盘,扇区 0),并将数据通过串口打印出来。如果有串口输入"cmd_sd 2 xx",则通过 write_sd 函数把"xx"写入 SD 卡的扇区0(物理磁盘,扇区 0)。

50.5 STM32CubeMX

在这里插入图片描述
备注:
STM32CubeMX配置是4位数据位,但是在SD初始化的时候只能是1位400K内的速率进行通讯,初始化完成之后,才能设置为4位,全速率通讯,所以生成的初始化代码需要进行调整,把4位修改为1位,在初始化结束的时候再修改为4位。

50.6 下载验证

1.在代码编译成功之后,我们通过下载代码到正点原子探索者 STM32F407 上,我们测试使用的是 32GB 标有“SDHC”标志的卡:
在这里插入图片描述

2.SD 卡成功初始化后,可以看到串口端打印出 SD 卡的相关信息(也可以在接好 SD 卡后按 Reset 复位开发板),我们测试使用的是 32GB 标有“SDHC”标志的卡,SD 卡成功初始化后的信息,如图:
在这里插入图片描述
3.在串口里面输入“cmd_sd 2 500”写入数据500, 手动断开电源,重启开发板,输入“cmd_sd 1”读取数据为500.验证数据写入了。
在这里插入图片描述

50.7 源码

git clone git@gitee.com:xiaoliangliangcong/stm32.git
STM32F407ZGT6/18.SD

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐