1)实验平台:正点原子MiniPro H750开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html
4)对正点原子STM32感兴趣的同学可以加群讨论:879133275

第四十五章 SD卡实验

很多单片机系统都需要大容量存储设备,以存储数据。目前常用的有U盘,FLASH芯片,SD卡等。他们各有优点,综合比较,最适合单片机系统的莫过于SD卡了,它不仅容量可以做到很大(32GB以上),支持SPI/SDIO驱动,而且有多种体积的尺寸可供选择(标准的SD卡尺寸及Micor SD卡尺寸等),能满足不同应用的要求。
只需要少数几个IO口即可外扩一个高达32GB或以上的外部存储器,容量从几十M到几十G选择范围很大,更换也很方便,编程也简单,是单片机大容量外部存储器的首选。
MiniPRO STM32H750开发板使用的接口是Micro SD卡接口,卡座带自锁功能,使用STM32H7自带的SDIO接口驱动,4位模式,最高通信速度可达48Mhz(分频器旁路时),最高每秒可传输数据24M字节,对于一般应用足够了。在本章中,我们将向大家介绍,如何在开发板上实现Micor SD卡的读取。
本章分为如下几个部分:
45.1 SD卡简介
45.2 SDIO接口简介
45.3 SD卡初始化流程
45.4 硬件设计
45.5 程序设计
45.6 下载验证

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

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

表45.1.1.2 SD卡引脚编号(注:S:电源 I:输入 O:推挽输出 PP:推挽)
对比的,我们来看一下microSD引脚,可见只比SD卡少了一个电源引脚VSS2,其它的引脚功能类似。

表45.1.1.3 microSD卡引脚编号(注:S:电源 I:输入 O:推挽输出 PP:推挽)
SD卡和micorSD只有引脚和形状大小不同,内部结构类似,操作时序完全相同,可以使用完全相同的代码驱动,下面以9’Pin SD卡的内部结构为为例,展示SD卡的存储结构。

图45.1.1.1 SD卡内部物理结构(RCA 寄存器在SPI模式下不可访问)
SD卡有自己的寄存器,但它不能直接进行读写操作,需要通过命令来控制,SDIO协议定义了一些命令用于实现某一特定功能,SD卡根据收到的命令要求对内部寄存器进行修改。图45.1.1.4中描述的SD卡的寄存器是我们和SD卡进行数据通讯的主要通道,如下:

表45.1.1.4 SD卡寄存器信息
关于SD卡的更多信息和硬件设计规范可以参考SD卡协议《Physical Layer Simplified Specification Version 2.00》的相关章节(注:因为STM32的SDIO匹配的是SD协议2.0版本,后续版本也兼容此旧协议版本,故本章仍以2.0版本为介绍对象)。
45.1.2 命令和响应
一个完整的SD卡操作过程是:主机(单片机等)发起“命令”,SD卡根据命令的内容决定是否发送响应信息及数据等,如果是数据读/写操作,主机还需要发送停止读/写数据的命令来结束本次操作,这意味着主机发起命令指令后,SD卡可以没有响应、数据等过程,这取决于命令的含义。这一过程如图45.1.2.1所示。

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

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

表45.1.2.2 SD卡部分命令
上表中,大部分的命令是初始化的时候用的。表中的R1、R3和R7等是SD卡的应答信号,每个响应也有规定好的格式,如图45.1.2.2所示:

图45.1.2.2 SD卡命令传输过程
在规定为有响应的命令下,每发送一个命令,SD卡都会给出一个应答,以告知主机该命令的执行情况,或者返回主机需要获取的数据,应答可以是R1~R7,R1的应答,各位描述如表45.1.2.3所示:
R1
响应 描述 起始位 传输位 命令号 卡状态 CRC7 终止位
Bit 47 46 [45:40] [39:8] [7:1] 0
位宽 1 1 6 32 7 1
值 “0” “0” x x x “1”
表45.1.2.3 R1响应
R2~R7的响应,限于篇幅,我们就不介绍了,但需要注意的是除了R2响应是128位外,其它的响应都是48位,请大家参考SD卡2.0协议。
45.1.3 卡模式
SD卡系统(包括主机和SD卡)定义了SD卡的工作模式,在每个操作模式下,SD卡都有几种状态,参考表45.1.3.1,状态之间通过命令控制实现卡状态的切换。

表45.1.3.1 SD卡状态与操作模式
对于我们来说两种有效操作模式:卡识别模式和数据传输模式。在系统复位后,主机处于卡识别模式,寻找总线上可用的SDIO设备,对SD卡进行数据读写之前需要识别卡的种类:V1.0标准卡、V2.0标准卡、V2.0高容量卡或者不被识别卡;同时,SD卡也处于卡识别模式,直到被主机识别到,即当SD卡在卡识别状态接收到CMD3 (SEND_RCA)命令后,SD卡就进入数据传输模式,而主机在总线上所有卡被识别后也进入数据传输模式。
在卡识别模式下,主机会复位所有处于“卡识别模式”的SD卡,确认其工作电压范围,识别SD卡类型,并且获取SD卡的相对地址(卡相对地址较短,便于寻址)。在卡识别过程中,要求SD卡工作在识别时钟频率FOD的状态下。卡识别模式下SD卡状态转换如图49.1.3.1。
主机上电后,所有卡处于空闲状态,包括当前处于无效状态的卡。主机也可以发送GO_IDLE_STATE(CMD0)让所有卡软复位从而进入空闲状态,但当前处于无效状态的卡并不会复位。
主机在开始与卡通信前,需要先确定双方在互相支持的电压范围内。SD卡有一个电压支持范围,主机当前电压必须在该范围可能才能与卡正常通信。SEND_IF_COND(CMD8)命令就是用于验证卡接口操作条件的(主要是电压支持)。卡会根据命令的参数来检测操作条件匹配性,如果卡支持主机电压就产生响应,否则不响应。而主机则根据响应内容确定卡的电压匹配性。CMD8是SD卡标准V2.0版本才有的新命令,所以如果主机有接收到响应,可以判断卡为V2.0或更高版本SD卡。
SD_SEND_OP_COND(ACMD41)命令可以识别或拒绝不匹配它的电压范围的卡。ACMD41命令的VDD电压参数用于设置主机支持电压范围,卡响应会返回卡支持的电压范围。对于对CMD8有响应的卡,把ACMD41命令的HCS位设置为1,可以测试卡的容量类型,如果卡响应的CCS位为1说明为高容量SD卡,否则为标准卡。卡在响应ACMD41之后进入准备状态,不响应ACMD41的卡为不可用卡,进入无效状态。ACMD41是应用特定命令,发送该命令之前必须先发CMD55。

图45.1.3.2 卡识别模式状态转换图
ALL_SEND_CID(CMD2)用来控制所有卡返回它们的卡识别号(CID),处于准备状态的卡在发送CID之后就进入识别状态。之后主机就发送SEND_RELATIVE_ADDR(CMD3)命令,让卡自己推荐一个相对地址(RCA)并响应命令。这个RCA是16bit地址,而CID是128bit地址,使用RCA简化通信。卡在接收到CMD3并发出响应后就进入数据传输模式,并处于待机状态,主机在获取所有卡RCA之后也进入数据传输模式。
45.1.4 数据模式
在数据模式下我们可以对SD卡的存储块进行读写访问操作。SD卡上电后默认以一位数据总线访问,可以通过指令设置为宽总线模式,可以同时使有4位总线并行读写数据,这样对于支持宽总线模式的接口(如:SDIO和QSPI等)都能加快数据操作速度。

图45.1.4.1 1位数据线传输8bit的数据流格式
SD卡有两种数据模式,一种是常规的8位宽,即一次按一字节传输,另一种是一次按512字节传输,我们只介绍前面一种。当按8-bit连续传输时,每次传输从最低字节开始,每字节从最高位(MSB)开始发送,当使用一条数据线时,只能通过DAT0进行数据传输,那它的数据传输结构如图49.1.4.1 1所示。
当使用4线模式传输8-bit结构的数据时,数据仍按MSB先发送的原则,DAT[3:0]的高位发送高数据位,低位发送低数据位。硬件支持的情况下,使用4线传输可以提升传输速率。

图45.1.4.2 4位数据线传输8bit格式的数据流格式
只有SD卡系统处于数据传输模式下才可以进行数据读写操作。数据传输模式下可以将主机SD时钟频率设置为FPP,默认最高为25MHz,频率切换可以通过CMD4命令来实现。数据传输模式下,SD卡状态转换过程见图45.1.4.3。

图45.1.4.3 数据传输模式卡状态转换
CMD7用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是出于待机状态,必须选择一个RCA地址目标卡使其进入传输状态才可以进行数据通信。同时通过CMD7命令也可以让已经被选择的目标卡返回到待机状态。
数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。卡处于传输状态下可以通过命令对卡进行数据读写、擦除。CMD12可以中断正在进行的数据通信,让卡返回到传输状态。CMD0和CMD15会中止任何数据编程操作,返回卡识别模式,注意谨慎使用,不当操作可能导致卡数据被损坏。
至此,我们已经介绍了SD卡操作的一些知识,并知道了SD卡操作的命令、响应和数据传输等状态,接下来我们来分析实际的硬件接口如何向SD卡发送我们需要的数据。
45.2 SDMMC简介
下面简单介绍STM32H750的SDMMC接口,包括:主要功能及框图、时钟、命令与响应和相关寄存器简介等,最后,我们将介绍SD卡的初始化流程。
45.2.1 SDMMC主要功能及框图
STM32H750的SDMMC控制器支持多媒体卡(MMC卡)、SD存储卡、SDIO卡等设备。SDMMC的主要功能如下:
与多媒体卡系统规格书版本4.51全兼容。支持三种不同的数据总线模式:1位(默认)、4位和8位。
与较早的多媒体卡系统规格版本全兼容(向前兼容)。
与SD存储卡规格版本4.1全兼容。
与SD I/O卡规格版本4.0全兼容:支持良种不同的数据总线模式:1位(默认)和4位。
8位总线模式下数据传输速率可达208MHz(但受限于IO口速度)。
数据和命令输出使能信号,用于控制外部双向驱动器。
STM32H750的SDMMC控制器包含4个部分:AHB总线接口、SDMMC适配器模块、内部DMA模块(IDMA)和延迟模块(DLYB),其功能框图如图45.2.1.1所示:

图45.2.1.1 STM32H750的SDMMC控制器功能框图
图中,右侧的信号为SDMMC对外引脚(信号),如表45.2.1.1所示:

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

表45.2.1.2 SDMMC在SD/SDIO模式下的速度模式
上表中:SDR表示单倍速率(时钟单边沿采样),DDR表示双倍速率(时钟双边沿采样),由表可知,STM32H7支持最高的SDR104总线速度模式,理论上传输速度最高可达104MB/s(实际上由于IO口速度无法达到208Mhz,所以,是达不到104MB/s的传输速度的)。
这里的DS、HS、SDR12、SDR25、DDR50、SDR50、SDR104等速度模式,实际上是超高速I卡(UHS-I,详见光盘 7,硬件资料6,SD卡资料 SD4.0协议标准完整版[中文版].pdf)所支持的一些速度等级,SD卡需要通过CMD6指令来设置相应的速度等级(仅SDHC和SDXC卡支持CMD6命令)。
我们的SD卡接口是3.3V供电的,所以,仅支持DS和HS模式(最高25MB/s的传输速度)。接下来的SDMMC相关介绍,我们也仅以这两个模式为基础进行介绍。
45.2.2 SDMMC的时钟
从图45.2.1.1我们可以看到SDMMC总共有3个时钟,分别是:
卡时钟(SDMMC_CK):每个时钟周期在命令和数据线上传输1位命令或数据。在3.3V信号电平下,该时钟频率最大可以达到50Mhz。
SDMMC适配器内核时钟(sdmmc_ker_ck):该时钟用于驱动SDMMC适配器,来自pll1_q_ck/pll2_r_ck(由RCC_D1CCIPR寄存器的SDMMCSEL位选择),默认选择来自pll1_q_ck,其频率一般为240Mhz,并用于产生SDMMC_CK时钟。
AHB3总线接口时钟(sdmmc_hclk):该时钟用于驱动SDMMC的AHB总线接口,来自AHB3,其频率一般为240Mhz。
SD卡时钟(SDMMC_CK),根据卡类型的不同,可能有好几个区间,这就涉及到时钟频率的设置,SDMMC_CK与sdmmc_ker_ck的关系(CLKDIV不等于0时)为:
SDMMC_CK = sdmmc_ker_ck / ( 2*CLKDIV )
其中,sdmmc_ker_ck一般来自pll1_q_ck,为240Mhz,而CLKDIV则是分频系数,可以通过SDMMC的SDMMC_CLKCR寄存器进行设置(确保SDMMC_CK不超过卡的最大操作频率)。注意,以上公式,是时钟分频器不旁路时的计算公式,当CLKDIV等于0时,SDMMC_CK直接等于sdmmc_ker_ck。
这里要提醒大家,在SD卡刚刚初始化的时候,其时钟频率(SDMMC_CK)是不能超过400Khz的,否则可能无法完成初始化。在初始化以后,就可以设置时钟频率到最大了(但不可超过SD卡的最大操作时钟频率)。
45.2.3 SDMMC的命令与响应
SDMMC的命令分为应用相关命令(ACMD)和通用命令(CMD)两部分,应用相关命令(ACMD)的发送,必须先发送通用命令(CMD55),然后才能发送应用相关命令(ACMD)。
SDMMC的所有命令和响应都是通过SDMMC_CMD引脚传输的,任何命令的长度都是固定为48位,SDMMC的命令格式如表45.2.3.1所示:

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

表45.2.3.2 SDMMC短响应格式
长响应的格式如表45.2.3.3所示:

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

表45.2.3.4 R1响应格式
在收到R1响应后,我们可以从SDMMC_RESPCMD寄存器和SDMMC_RESP1寄存器分别读出命令索引和卡状态信息。关于其他响应的介绍,请大家参考光盘:《SD卡2.0协议.pdf》或《STM32H7xx参考手册_V7(英文版).pdf》第54章。
最后,我们看看数据在SDMMC控制器与SD卡之间的传输。对于SDI/SDMMC存储器,数据是以数据块的形式传输的,而对于MMC卡,数据是以数据块或者数据流的形式传输。本节我们只考虑数据块形式的数据传输。
SDMMC(多)数据块读操作,如图45.2.3.1所示:

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

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

45.2.4 SDMMC寄存器
SDMMC电源控制寄存器 (SDMMC_POWER)
首先,我们来看SDMMC电源控制寄存器(SDMMC_POWER),该寄存器定义如图45.2.4.1所示:

图45.2.4.1 SDMMC_POWER寄存器
该寄存器我们只关心PWRCTRL[1:0]这两个位:00,掉电,停止为卡提供时钟;01,保留;10,上电周期;11,上电,为卡提供时钟。复位后,PWRCTRL[1:0]的值默认为00,所以SDMMC的电源是关闭的,我们要启用SDMMC,第一步就是要设置该寄存器最低2个位均为1,让SDMMC上电,开启卡时钟。
SDMMC时钟控制寄存器 (SDMMC_CLKCR)
第二个是SDMMC时钟控制寄存器(SDMMC_CLKCR),该寄存器主要用于设置SDMMC_CK的分配系数,开关等,并可以设置SDMMC的数据位宽,该寄存器的定义如图45.2.4.2所示:

图45.2.4.2 SDMMC_CLKCR寄存器
CLKDIV[9:0]位,用于控制时钟分频系数,SDMMC_CK的时钟频率由这10个位控制,其计算公式为:SDMMC_CK = sdmmc_ker_ck / ( 2*CLKDIV[9:0])。
WIDBUS[1:0]位,用于控制总线宽度。00,1位宽度;01,4位宽度;10,8位宽度。我们一般使用4位宽度通信(WIDBUS[1:0]=01)。
DDR位,用于控制数据采样率。0,表示SDR单倍采样(1个时钟传输1个位);1,表示DDR双倍采样(1个时钟传输2个位)。我们一般设置为0。
BUSSPEED位,用于设置总线速度模式。0,表示使用DS/HS/SDR12/SDR25等总线速度模式;1,表示使用SDR50/DDR50/SDR104等总线速度模式。我们一般设置为0。
该寄存器的其他位,详见《STM32H7xx参考手册_V7(英文版).pdf》第2458页,55.9.2节。
SDMMC参数寄存器 (SDMMC_ARGR)
第三个是SDMMC参数制寄存器(SDMMC_ARGR),该寄存器的定义如图45.2.4.3所示:

图45.2.4.3 SDMMC_ARGR寄存器
该寄存器比较简单,就是一个32位寄存器,用于存储命令参数,不过需要注意的是,必须在写命令之前先写这个参数寄存器!
SDMMC命令响应寄存器 (SDMMC_RESPCMDR)
第四个是SDMMC命令响应寄存器(SDMMC_RESPCMD),该寄存器的定义如图45.2.4.4所示:

图45.2.4.4 SDMMC_RESPCMDR寄存器
该寄存器为32位,但只有低6位有效,比较简单,用于存储最后收到的命令响应中的命令索引。如果传输的命令响应不包含命令索引,则该寄存器的内容不可预知。
SDMMC响应1…4寄存器(SDMMC_RESPxR) (x = 1…4)
第五个是SDMMC响应1…4寄存器 (SDMMC_RESPxR) (x = 1…4),该寄存器的定义如图45.2.4.5所示:

图45.2.4.5 SDMMC_RESPxR寄存器
该寄存器组总共由4个32位寄存器组成,用于存放接收到的卡响应部分信息。如果收到短响应,则数据存放在SDMMC_RESP1寄存器里面,其他三个寄存器没有用到。而如果收到长响应,则依次存放在SDMMC_RESP1~ SDMMC_RESP4里面,如表45.2.4.1所示:

表45.2.4.1 响应类型和SDMMC_RESPx寄存器
SDMMC命令寄存器 (SDMMC_CMDR)
第六个是SDMMC命令寄存器(SDMMC_CMDR),该寄存器各位定义如图45.2.4.6所示:

图45.2.4.6 SDMMC_CMDR寄存器
CMDINDEX[5:0]位,用于表示命令索引号,比如发送CMD1,其值为1,索引就设置为1。
WAITRESP[1:0]位,用于设置等待响应,指示CPSM是否需要等待,以及等待类型等。这里的CPSM,即命令通道状态机,我们就不详细介绍了,请参阅《STM32H7xx参考手册_V7(英文版).pdf》 ,有详细介绍。
WAITINT位,用于设置是否等待中断,我们一般不用等待中断,因此设置该位为0。
CPSMEN位,用于设置是否使能CPSM命令通道状态机,我们设置为1,使能CPSM。
该寄存器的其他位介绍,详见《STM32H7xx参考手册_V7(英文版).pdf》。
SDMMC数据定时器寄存器 (SDMMC_DTIMER)
第七个是SDMMC数据定时器寄存器(SDMMC_DTIMER),寄存器各位定义如下图所示:

图45.2.4.7 SDMMC_DTIMER寄存器
该寄存器用于存储以卡总线时钟(SDMMC_CK)为周期的数据超时时间,计数器将从SDMMC_DTIMER寄存器加载数值,并在数据通道状态机(DPSM)进入Wait_R或繁忙状态时进行递减计数,当DPSM处在这些状态时,如果计数器减为0,则设置超时标志。这里的DPSM,即数据通道状态机,类似CPSM,详细请参考《STM32H7xx参考手册_V7(英文版).pdf》。注意:在写入数据控制寄存器,进行数据传输之前,必须先写入该寄存器(SDMMC_DTIMER)和数据长度寄存器(SDMMC_DLEN)!
SDMMC数据长度寄存器 (SDMMC_DLENR)
第八个是SDMMC数据长度寄存器(SDMMC_DLEN),该寄存器各位定义如图45.2.4.8所示:

图45.2.4.8 SDMMC_DLENR寄存器
该寄存器低25位有效,用于设置需要传输的数据字节长度。对于块数据传输,该寄存器的数值,必须是数据块长度(通过SDMMC_DCTRL设置)的倍数。
SDMMC数据控制寄存器 (SDMMC_DCTRL)
第九个是SDMMC数据控制寄存器(SDMMC_DCTRL),该寄存器各位定义如图45.2.4.9所示:

图45.2.4.9 SDMMC_DCTRL寄存器
DTEN位,用于使能数据传输。0,禁止数据传输;1,开始数据传输。
DTDIR位,用于控制数据传输方向。0,数据从主机传到卡;1,数据从卡传到主机。
DTMODE[1:0]位,用于设置数据传输模式;00,指定块数的块传输;01,SDIO多数据传输;10,eMMC数据流传输(仅支持1位数据线);11,不限数块传输(由STOP_TRANSMISSION命令停止传输)。一般我们使用00模式即可(指定块数块传输)。
DBLOCKSIZE[3:0]位,用于设置数据块大小。取值范围为:0~14,表示单个数据块的长度为:2^DBLOCKSIZE[3:0]字节。我们一般设置DBLOCKSIZE[3:0]=9,表示块大小为512字节。
该寄存器的其他位介绍,详见《STM32H7xx参考手册_V7(英文版).pdf》。
接下来,我们介绍几个位定义十分类似的寄存器,他们是:状态寄存器(SDMMC_ STAR)、清除中断寄存器(SDMMC_ICR)和中断屏蔽寄存器(SDMMC_MASKR),这三个寄存器每个位的定义都相同,只是功能各有不同。所以可以一起介绍,以状态寄存器(SDMMC_STAR)为例,该寄存器各位定义如图45.2.4.10所示:

图45.2.4.10 SDMMC_STAR寄存器位定义
CCRCFAIL位,已接收命令CRC检验错误。
DCRCFAIL位,已发送/接收数据块CRC检验错误。
CTIMEOUT位,命令响应超时。
DTIMEOUT位,数据超时。
TXUNDERR位,发送FIFO下溢错误。
RXUNDERR位,接收FIFO上溢错误。
CMDREND位,已接受命令响应。
CMDSENT位,命令已发送。
DATAEND位,数据接收。
DHOLD位,数据发送保持。
DBCKEND位,已发送/接收数据块。
DABORT位,被CMD12退出数据传输。
DPSMACT位,数据通道状态机启动。
CPSMACT位,命令通道状态机启动。
TXFIFOHE位,发送FIFO半空。
RXFIFOHF位,接收FIFO半满。
TXFIFOF位,发送FIFO满。
RXFIFOF位,接收FIFO满。
TXFIFOE位,发送FIFO空。
RXFIFOE位,接收FIFO空。
BUSYD0位,SDMMC_D0信号忙。
BUSYD0END位,SDMMC_D0忙结束(非忙)。
SDIOIT位,接收到SDIO中断。
ACKFAIL位,接收启动应答失败。
ACKTIMEOUT位,接收启动应答超时。
VSWEND位,电压切换电路切换完成。
CKSTOP位,在电压切换的时候,SDMMC_CK停止输出。
IDMATE位,IDMA发送错误。
IDMABTC位:IDMA缓冲区传输完成。
状态寄存器可以用来查询SDMMC控制器的当前状态,以便处理各种事务。比如SDMMC_STA的位2表示命令响应超时,说明SDMMC的命令响应出了问题。我们通过设置SDMMC_ICR的位2则可以清除这个超时标志,而设置SDMMC_MASK的位2,则可以开启命令响应超时中断,设置为0关闭。
状态寄存器(SDMMC_STA)、清除中断寄存器(SDMMC_ICR)和中断屏蔽寄存器(SDMMC_MASK)等三个寄存器的详细介绍,请参考:《STM32H7xx参考手册_V7(英文版).pdf》。
最后,我们向大家介绍SDMMC的数据FIFO寄存器(SDMMC_FIFO),数据FIFO寄存器包括接收和发送FIFO,他们由一组连续的32个地址上的32个寄存器组成,CPU可以使用FIFO读写多个操作数。例如我们要从SD卡读数据,就必须读SDMMC_FIFO寄存器,要写数据到SD卡,则要写SDMMC_FIFO寄存器。SDMMC将这32个地址分为16个一组,发送接收各占一半。而我们每次读写的时候,最多就是读取发送FIFO或写入接收FIFO的一半大小的数据,也就是8个字(32个字节),这里特别提醒,我们操作SDMMC_FIFO(不论读出还是写入)必须是以4字节对齐的内存进行操作,否则将导致出错!
至此,SDMMC的相关寄存器介绍,我们就介绍完了。还有几个不常用的寄存器,我们没有介绍到,请大家参考《STM32H7xx参考手册_V7(英文版).pdf》第55章相关章节。
45.3 SD卡初始化流程
45.3.1 SDIO模式下的SD卡初始化
这一节,我们来看看SD卡的初始化流程,要实现SDIO驱动SD卡,最重要的步骤就是SD卡的初始化,只要SD卡初始化完成了,那么剩下的(读写操作)就简单了,所以我们这里重点介绍SD卡的初始化。从《SD卡2.0协议》(见光盘资料)文档,我们得到SD卡初始化流程图如图45.3.1.1所示:

图45.3.1.1 SD卡初始化流程(Card Initialization and Identification Flow (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的格式如表45.3.1.1所示:

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

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

表45.3.5.3 ACMD41命令格式
ACMD41得到的响应(R3)包含SD卡OCR寄存器内容,OCR寄存器内容定义如表45.3.5.4所示:

表45.3.5.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卡的类型区分,图45.1.5.1中,最后发送了CMD2和CMD3命令,用于获得卡CID寄存器数据和卡相对地址(RCA)。
CMD2,用于获得CID寄存器的数据,CID寄存器数据各位定义如表45.3.5.5所示:

表45.3.5.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》,里面有非常详细的介绍。
45.3.2 SPI模式下的SD卡初始化
STM32的SDIO驱动模式和SPI模式不兼容,二者使用时需要区分开来。《SD卡2.0协议.pdf》中提供了SD卡的SPI初始化时序,我们可以按它建议的流程进行SD卡的初始化,如图45.3.2.1所示。

图45.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协议。
45.4硬件设计

  1. 例程功能
    本章实验功能简介:开机的时候先初始化SD卡,如果SD卡初始化完成,则提示LCD初始化成功。按下KEY0,读取SD卡扇区0的数据,然后通过串口发送到电脑。如果没初始化通过,则在LCD上提示初始化失败。同样用LED0来指示程序正在运行。
  2. 硬件资源
    1)RGB灯
    RED :LED0 - PB4
    2)独立按键 KEY0 - PA1
    3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面)
    5)SD卡,通过SDMMC1(SDMMC_D0D4(PC8PC11),SDMMC_SCK(PC12),
    SDMMC_CMD(PD2))连接
  3. 原理图
    前面介绍SD卡时我们已经介绍了SD卡对外的接口部分,实际上SD卡对于我们来说是可以灵活变更的部分,实际使用时,业界常用SD卡卡座用于专门放置SD卡。
    下面我们介绍一下板载的SD卡接口和STM32的连接关系,如图45.4.1所示:

图45.4.1 SD卡接口与STM32连接原理图
microSD卡座在开发板背面近,卡座和STM32开发板上是直接连接在一起的,硬件上不需要任何改动。
45.5 程序设计
45.4.1 SD卡的HAL库驱动
STM32的HAL库为SD卡操作封装了一些函数,主要存放在stm32h7xx_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卡错误代码 */  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;              /* 以字节为单位的逻辑块大小 */
uint32_t CardSpeed;                     /* 指定卡的速度 */
}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 SDMMC_BUS_WIDE_1B                      ((uint32_t)0x00000000U)
#define SDMMC_BUS_WIDE_4B                      SDMMC_CLKCR_WIDBUS_0
#define SDMMC_BUS_WIDE_8B                      SDMMC_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卡的指定扇区读取一定数量的数据。
函数形参:
形参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卡的指定扇区读取一定数量的数据。
函数形参:
形参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 pData是一个指向8位类型的数据指针缓冲,它用于接收我们需要的数据。
形参3 BlockAdd指向我们需要访问的数据扇区,对于任意的存储都是类似的,像SD卡这样的大存储块也同样是通过位置标识来访问不同的数据。
形参4 NumberOfBlocks对应的是我们本次要从指定扇区读取的字节数。
形参5 Timeout表示读的超时时间。HAL库驱动在达到超时时间前还没读到数据会进行重试和等待,达到超时时间后才退出本次操作。
函数返回值:
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方式来实现类似的功能,它们的调用非常相似。
SDMMC驱动SD卡配置步骤
1)使能SDMMC和相关GPIO时钟,并设置好GPIO工作模式
我们通过SDMMC读写SD卡,所以先需要使能SDMMC以及相关GPIO口的时钟,并设置好GPIO的工作模式。
2)初始化SDMMC
HAL库通过SDMMC_Init完成对SDMMC的初始化,不过我们并不直接调用该函数,而是通过: HAL_SD_InitHAL_SD_InitCardSDMMC_Init的调用关系,来完成对SDMMC的初始化。我们只需要配置好SDMMC相关工作参数,然后调用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卡,详见本例程源码。
45.4.2 程序流程图

图45.4.2.1 SD读写实验程序流程图
45.4.3 程序解析

  1. SDMMC驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SDMMC驱动源码包括两个文件:sdmmc_sdcard.c和sdmmc_sdcard.h。
    首先是sdmmc_sdcard.h文件,根据我们STM32的复用功能和我们的硬件设计,我们把用到的管脚用宏定义,需要更换其它的引脚时也可以通过修改宏实现快速移植,它们列出如下:
/*****************************************************************************/
/* SDMMC1的信号线: SD1_D0 ~ SD1_D3/SD1_CLK/SD1_CMD 引脚 定义 * 如果你使用了其他引脚做SDMMC1的信号线,修改这里写定义即可适配.* 注意, 这里仅支持SDMMC1, 我们使用的也是SDMMC1. 不支持SDMMMC2.* 另外, 由于SDMMC1的IO口复用功能都是AF12, 这里就不独立再定义每个IO口的AF功能了*/
#define SD1_D0_GPIO_PORT                GPIOC
#define SD1_D0_GPIO_PIN                 GPIO_PIN_8
#define SD1_D0_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)      /* 所在IO口时钟使能 */#define SD1_D1_GPIO_PORT                GPIOC
#define SD1_D1_GPIO_PIN                 GPIO_PIN_9
#define SD1_D1_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)     /* 所在IO口时钟使能 */#define SD1_D2_GPIO_PORT                GPIOC
#define SD1_D2_GPIO_PIN                 GPIO_PIN_10
#define SD1_D2_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)     /* 所在IO口时钟使能 */#define SD1_D3_GPIO_PORT                GPIOC
#define SD1_D3_GPIO_PIN                 GPIO_PIN_11
#define SD1_D3_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)     /* 所在IO口时钟使能 */#define SD1_CLK_GPIO_PORT               GPIOC
#define SD1_CLK_GPIO_PIN                GPIO_PIN_12
#define SD1_CLK_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)     /* 所在IO口时钟使能 */#define SD1_CMD_GPIO_PORT               GPIOD
#define SD1_CMD_GPIO_PIN                GPIO_PIN_2
#define SD1_CMD_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)     /* 所在IO口时钟使能 */
/*****************************************************************************/
#define SD_TIMEOUT          ((uint32_t)100000000)           /* 超时时间 */
#define SD_TRANSFER_OK      ((uint8_t)0x00)                 /* 传输完成 */
#define SD_TRANSFER_BUSY    ((uint8_t)0x01)             /* 卡正忙 */
sdio_sdcard.c我们主要介绍三个函数:sd_init、sd_read_disk和sd_write_disk。
1) sd_init函数
sd_init的设计就比较简单了,我们只需要填充SDMMC结构体的控制句柄,然后使用HAL库的HAL_SD_Init初始化函数即可,在此过程中HAL_SD_Init 会调用HAL_SD_MspInit函数回调函数,根据外设的情况,我们可以设置数据总线宽度为4位:
/*** @brief       初始化SD卡* @param       无* @retval      返回值:0 初始化正确;其他值,初始化错误*/
uint8_t sd_init(void)
{uint8_t SD_Error;/* 初始化时的时钟不能大于400KHZ */g_sd_handle.Instance = SDMMC1;
g_sd_handle.Init.ClockEdge = SDMMC_CLOCK_EDGE_RISING;   /* 上升沿 */
/* 空闲时不关闭时钟电源 */g_sd_handle.Init.ClockPowerSave = SDMMC_CLOCK_POWER_SAVE_DISABLE;
g_sd_handle.Init.BusWide = SDMMC_BUS_WIDE_4B;     /* 4位数据线 */
/* 关闭硬件流控 */g_sd_handle.Init.HardwareFlowControl = SDMMC_HARDWARE_FLOW_CONTROL_DISABLE; g_sd_handle.Init.ClockDiv = SDMMC_NSpeed_CLK_DIV;/* SD传输时钟频率最大25MHZ */SD_Error = HAL_SD_Init(&g_sd_handle);if (SD_Error != HAL_OK){return 1;}HAL_SD_GetCardInfo(&g_sd_handle, &g_sd_card_info_handle);  /* 获取SD卡信息 */return 0;
}/*** @brief       SDMMC底层驱动,时钟使能,引脚配置,DMA配置* @param       hsd:SD卡句柄* @note        此函数会被HAL_SD_Init()调用* @retval      无*/
void HAL_SD_MspInit(SD_HandleTypeDef *hsd)
{GPIO_InitTypeDef gpio_init_struct;__HAL_RCC_SDMMC1_CLK_ENABLE(); /* 使能SDMMC1时钟 */SD1_D0_GPIO_CLK_ENABLE();       /* D0引脚IO时钟使能 */SD1_D1_GPIO_CLK_ENABLE();       /* D1引脚IO时钟使能 */SD1_D2_GPIO_CLK_ENABLE();       /* D2引脚IO时钟使能 */SD1_D3_GPIO_CLK_ENABLE();       /* D3引脚IO时钟使能 */SD1_CLK_GPIO_CLK_ENABLE();      /* CLK引脚IO时钟使能 */SD1_CMD_GPIO_CLK_ENABLE();      /* CMD引脚IO时钟使能 */gpio_init_struct.Pin = SD1_D0_GPIO_PIN;gpio_init_struct.Mode = GPIO_MODE_AF_PP;                   /* 推挽复用 */gpio_init_struct.Pull = GPIO_PULLUP;                         /* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;    /* 高速 */gpio_init_struct.Alternate = GPIO_AF12_SDIO1;              /* 复用为SDIO */HAL_GPIO_Init(SD1_D0_GPIO_PORT, &gpio_init_struct);    /* 初始化D0引脚 */gpio_init_struct.Pin = SD1_D1_GPIO_PIN;HAL_GPIO_Init(SD1_D1_GPIO_PORT, &gpio_init_struct);    /* 初始化D1引脚 */gpio_init_struct.Pin = SD1_D2_GPIO_PIN;HAL_GPIO_Init(SD1_D2_GPIO_PORT, &gpio_init_struct);    /* 初始化D2引脚 */gpio_init_struct.Pin = SD1_D3_GPIO_PIN;HAL_GPIO_Init(SD1_D3_GPIO_PORT, &gpio_init_struct);    /* 初始化D3引脚 */gpio_init_struct.Pin = SD1_CLK_GPIO_PIN;HAL_GPIO_Init(SD1_CLK_GPIO_PORT, &gpio_init_struct);      /* 初始化CLK引脚 */gpio_init_struct.Pin = SD1_CMD_GPIO_PIN;HAL_GPIO_Init(SD1_CMD_GPIO_PORT, &gpio_init_struct);     /* 初始化CMD引脚 */
}

2) sd_read_disk函数
这个函数比较简单,实际上我们使用它来对HAL库的读函数HAL_SD_ReadBlocks进行了二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据读函数返回值自己作其它处理。为了保护SD卡的数据操作,我们在进行操作时暂时关闭了中断以防止数据读过程发生意外。

/*** @brief       读SD卡数据* @param       buf:读数据缓存区* @param       sector:扇区地址* @param       cnt:扇区个数* @retval      返回值:0,正常;其他,错误;*/
uint8_t sd_read_disk(uint8_t* buf, uint32_t sector, uint32_t cnt)
{uint8_t sta = HAL_OK;uint32_t timeout = SD_TIMEOUT;long long lsector = sector;INTX_DISABLE();  /* 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!) */
sta = HAL_SD_ReadBlocks(&g_sd_handle, (uint8_t*)buf,lsector, cnt,
SD_TIMEOUT); /* 多个sector的读操作 *//* 等待SD卡读完 */while (get_sd_card_state() != SD_TRANSFER_OK){if (timeout-- == 0){sta = SD_TRANSFER_BUSY;break;}}INTX_ENABLE();  /* 开启总中断 */return sta;
}

3) sd_write_disk函数
这个函数比较简单,实际上我们使用它来对HAL库的读函数HAL_SD_WriteBlocks进行了二次封装,并在最后加入了状态判断以使后续操作(实际上这部分代码也可以省略),直接根据读函数返回值自己作其它处理。为了保护SD卡的数据操作,我们在进行操作时暂时关闭了中断以防止数据写过程发生意外。

/*** @brief       写数据到SD卡* @param       buf:写数据缓存区* @param       sector:扇区地址* @param       cnt:扇区个数* @retval      返回值:0,正常;其他,错误;*/
uint8_t sd_write_disk(uint8_t *buf, uint32_t sector, uint32_t cnt)
{uint8_t sta = HAL_OK;uint32_t timeout = SD_TIMEOUT;long long lsector = sector;INTX_DISABLE();  /* 关闭总中断(POLLING模式,严禁中断打断SDIO读写操作!!!) */
sta = HAL_SD_WriteBlocks(&g_sd_handle, (uint8_t*)buf, lsector, cnt,
SD_TIMEOUT); /* 多个sector的写操作 *//* 等待SD卡写完 */while (get_sd_card_state() != SD_TRANSFER_OK){if (timeout-- == 0){sta = SD_TRANSFER_BUSY;break;}}INTX_ENABLE(); /* 开启总中断 */return sta;
}
  1. main.c代码
    在main.c就比较简单了,按照我们的流程图的思路,为了方便测试,我们编写了sd_test_read()\sd_test_write()\show_sdcard_info ()三个函数分别用于读写测试和卡信息打印,也都是基于对前面HAL库的代码进行简单地调用,代码也比较容易看懂,这里就不单独介绍这几个函数了,大家查看光盘中的源代码即可。
    最后,我们在main.c编写的程序如下:
/*** @brief       通过串口打印SD卡相关信息* @param       无* @retval      无*/
void show_sdcard_info(void)
{uint64_t card_capacity;       /* SD卡容量 */HAL_SD_CardCIDTypeDef sd_card_cid;HAL_SD_GetCardCID(&g_sd_handle, &sd_card_cid);      /* 获取CID */get_sd_card_info(&g_sd_card_info_handle);            /* 获取SD卡信息 */switch (g_sd_card_info_handle.CardType){case CARD_SDSC:{if (g_sd_card_info_handle.CardVersion == CARD_V1_X){printf("Card Type:SDSC V1\r\n");}else if (g_sd_card_info_handle.CardVersion == CARD_V2_X){printf("Card Type:SDSC V2\r\n");}}break;case CARD_SDHC_SDXC:printf("Card Type:SDHC\r\n");break;}card_capacity = (uint64_t)(g_sd_card_info_handle.LogBlockNbr) *
(uint64_t)(g_sd_card_info_handle.LogBlockSize); /* 计算SD卡容量 */
/* 制造商ID */
printf("Card ManufacturerID:%d\r\n", sd_card_cid.ManufacturerID);
/* 卡相对地址 */               printf("Card RCA:%d\r\n", g_sd_card_info_handle.RelCardAdd);
printf("LogBlockNbr:%d \r\n",
(uint32_t)(g_sd_card_info_handle.LogBlockNbr));   /* 显示逻辑块数量 */
printf("LogBlockSize:%d \r\n",
(uint32_t)(g_sd_card_info_handle.LogBlockSize));  /* 显示逻辑块大小 */
/* 显示容量 */
printf("Card Capacity:%d MB\r\n", (uint32_t)(card_capacity >> 20));
/* 显示块大小 */         printf("Card BlockSize:%d\r\n\r\n", g_sd_card_info_handle.BlockSize);
}/*** @brief       测试SD卡的读取*   @note      从secaddr地址开始,读取seccnt个扇区的数据* @param       secaddr : 扇区地址* @param       seccnt  : 扇区数* @retval      无*/
void sd_test_read(uint32_t secaddr, uint32_t seccnt)
{uint32_t i;uint8_t *buf;uint8_t sta = 0;buf = mymalloc(SRAMIN, seccnt * 512);       /* 申请内存,从SDRAM申请内存 */sta = sd_read_disk(buf, secaddr, seccnt);  /* 读取secaddr扇区开始的内容 */if (sta == 0){printf("SECTOR %d DATA:\r\n", secaddr);for (i = 0; i < seccnt * 512; i++){printf("%x ", buf[i]);  /* 打印secaddr开始的扇区数据 */}printf("\r\nDATA ENDED\r\n");}else printf("err:%d\r\n", sta);myfree(SRAMIN, buf);    /* 释放内存 */
}/*** @brief       测试SD卡的写入*   @note      从secaddr地址开始,写入seccnt个扇区的数据*               慎用!! 最好写全是0XFF的扇区,否则可能损坏SD卡.** @param       secaddr : 扇区地址* @param       seccnt  : 扇区数* @retval      无*/
void sd_test_write(uint32_t secaddr, uint32_t seccnt)
{uint32_t i;uint8_t *buf;uint8_t sta = 0;buf = mymalloc(SRAMIN, seccnt * 512);   /* 从SDRAM申请内存 */for (i = 0; i < seccnt * 512; i++)      /* 初始化写入的数据,是3的倍数. */{buf[i] = i * 3;}
/* 从secaddr扇区开始写入seccnt个扇区内容 */sta = sd_write_disk(buf, secaddr, seccnt); if (sta == 0){printf("Write over!\r\n");}else printf("err:%d\r\n", sta);myfree(SRAMIN, buf);    /* 释放内存 */
}int main(void)
{uint8_t key;uint32_t sd_size;uint8_t t = 0;uint8_t *buf;uint64_t card_capacity;                   /* SD卡容量 */sys_cache_enable();                      /* 打开L1-Cache */HAL_Init();                                 /* 初始化HAL库 */sys_stm32_clock_init(240, 2, 2, 4);    /* 设置时钟, 480Mhz */delay_init(480);                          /* 延时初始化 */usart_init(115200);                      /* 串口初始化为115200 */usmart_dev.init(240);                     /* 初始化USMART */mpu_memory_protection();                 /* 保护相关存储区域 */led_init();                                   /* 初始化LED */lcd_init();                                     /* 初始化LCD */key_init();                                     /* 初始化按键 */my_mem_init(SRAMIN);                         /* 初始化内部内存池(AXI) */my_mem_init(SRAM12);                         /* 初始化SRAM12内存池(SRAM1+SRAM2) */my_mem_init(SRAM4);                         /* 初始化SRAM4内存池(SRAM4) */my_mem_init(SRAMDTCM);                      /* 初始化DTCM内存池(DTCM) */my_mem_init(SRAMITCM);                    /* 初始化ITCM内存池(ITCM) */lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);lcd_show_string(30, 70, 200, 16, 16, "SD  TEST", RED);lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);lcd_show_string(30, 110, 200, 16, 16, "KEY0:Read Sector 0", RED);while (sd_init())    /* 检测不到SD卡 */{lcd_show_string(30, 150, 200, 16, 16, "SD Card Error!", RED);delay_ms(500);lcd_show_string(30, 150, 200, 16, 16, "Please Check! ", RED);delay_ms(500);LED0_TOGGLE();  /* 红灯闪烁 */}/* 打印SD卡相关信息 */show_sdcard_info(); /* 检测SD卡成功 */lcd_show_string(30, 150, 200, 16, 16, "SD Card OK    ", BLUE);lcd_show_string(30, 170, 200, 16, 16, "SD Card Size:     MB", BLUE);
card_capacity = (uint64_t)(g_sd_card_info_handle.LogBlockNbr) *
(uint64_t)(g_sd_card_info_handle.LogBlockSize); /* 计算SD卡容量 */
/* 显示SD卡容量 */lcd_show_num(30 + 13 * 8, 170, card_capacity >> 20, 5, 16, BLUE); while (1){key = key_scan(0);if (key == KEY0_PRES)        /* KEY0按下了 */{buf = mymalloc(0, 512); /* 申请内存 */key = sd_read_disk(buf, 0, 1);if (key == 0)             /* 读取0扇区的内容 */{lcd_show_string(30, 190, 200, 16, 16,
"USART1 Sending Data...", BLUE);printf("SECTOR 0 DATA:\r\n");for (sd_size = 0; sd_size < 512; sd_size++){printf("%x ", buf[sd_size]);    /* 打印0扇区数据 */}printf("\r\nDATA ENDED\r\n");lcd_show_string(30, 190, 200, 16, 16,
"USART1 Send Data Over!", BLUE);}else printf("err:%d\r\n", key);myfree(0, buf); /* 释放内存 */}t++;delay_ms(10);if (t == 20){LED0_TOGGLE();  /* 红灯闪烁 */t = 0;}}
}

这里总共4个函数:
1、show_sdcard_info函数
该函数用于从串口输出SD卡相关信息,包括:卡类型、制造商ID、卡相对地址、容量和块大小等信息。
2、sd_test_read
该函数用于测试SD卡的读取,通过USMART调用,可以指定SD卡的任何地址,读取指定个数的扇区数据,将读到的数据,通过串口打印出来,从而验证SD卡数据的读取。
3、sd_test_write函数
该函数用于测试SD卡的写入,通过USMART调用,可以指定SD卡的任何地址,写入指定个数的扇区数据,写入数据自动生成(都是3的倍数),写入完成后,在串口打印写入结果。我们可以通过sd_test_read函数,来检验写入数据是否正确。注意:千万别乱写,否则可能把卡写成砖头/数据丢失!!写之前,先读取该地址的数据,最好全部是0XFF才写(全部0X00也行),其他情况最好别写!
4、main函数函数
该函数,先初化相关外设和SD卡,初始化成功,则调用show_sdcard_info函数,输出SD卡相关信息,并在LCD上面显示SD卡容量。然后进入死循环,如果有按键KEY0按下,则通过SD_ReadDisk读取SD卡的扇区0(物理磁盘,扇区0),并将数据通过串口打印出来。这里,我们对上一章学过的内存管理小试牛刀,稍微用了下,以后我们会尽量使用内存管理来设计。
最后,我们将sd_test_read和sd_test_write函数加入USMART控制,这样,我们就可以通过串口调试助手,测试SD卡的读写了,方便测试。
45.6 下载验证
将程序下载到开发板后,我们测试使用的是16Gb标有“SDHC”标志的卡,安装方法如图45.6.1所示:

图45.6.1 测试用的microSD卡与开发板的连接方式
SD卡成功初始化后,LCD显示本程序的一些必要信息,如图45.6.2:

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

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

图45.6.4 按下KEY1的开发板界面

图46.6.5 串口调试助手显示按下KEY0后读取到的信息
对SD卡的使用的简单介绍就到这里了,另外,利用USMART测试的部分,我们这里就不做介绍了,大家可自行验证下。

【正点原子STM32连载】第四十五章 SD卡实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1相关推荐

  1. 【正点原子STM32连载】 第十五章 按键输入实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

    1)实验平台:正点原子stm32f103战舰开发板V4 2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420 3)全套实验源码+手册+视 ...

  2. bmp文件头_「正点原子FPGA连载」第十九章SD卡读BMP图片LCD显示

    1)摘自[正点原子]领航者 ZYNQ 之嵌入式开发指南 2)实验平台:正点原子领航者ZYNQ开发板 3)平台购买地址:https://item.taobao.com/item.htm?&id= ...

  3. 简述sd卡2.0协议_【正点原子FPGA连载】第十二章SD卡读写TXT文本实验-领航者 ZYNQ 之嵌入式开发指南...

    1)实验平台:正点原子领航者ZYNQ开发板 2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761 3)全套实验源码+手册+视频下 ...

  4. zynqsd的读写数据_【正点原子FPGA连载】 第十二章SD卡读写TXT文本实验-摘自【正点原子】领航者 ZYNQ 之嵌入式开发指南 (amobbs.com 阿莫电子论坛)...

    本帖最后由 正点原子 于 2020-10-24 10:25 编辑 QQ群头像.png (1.78 KB) 2020-10-24 10:25 上传5)关注正点原子公众号,获取最新资料 100846rel ...

  5. 【正点原子STM32连载】 第二十五章 TFTLCD(MCU屏)实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

  6. 【正点原子STM32连载】 第二十五章 TFT-LCD(MCU屏)实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

    1)实验平台:正点原子stm32f103战舰开发板V4 2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420 3)全套实验源码+手册+视 ...

  7. 【正点原子MP157连载】第十六章 基本定时器实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  8. 【正点原子MP157连载】第十二章 按键输入实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  9. 【正点原子FPGA连载】第十二章 呼吸灯实验 -摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0

    1)实验平台:正点原子领航者ZYNQ开发板 2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761 3)全套实验源码+手册+视频下 ...

最新文章

  1. Error:java: 无效的源发行版: 11
  2. Jenkins之构建Maven项目的多种方式
  3. 面试官:你说一下Redis使用场景都有哪些
  4. winform调用websocket_C#基于websocket的前台及后台实时推送
  5. 重拾web开发-DIV+CSS基础(总结)
  6. Python爬虫采集网易云音乐热评实战
  7. matlab 命令打开ANSYS,matlab如何调用ansys
  8. Python下载Wyoming怀俄明大学探空数据(数据网址更新)
  9. Representation Learning with Contrastive Predictive Coding 论文阅读笔记
  10. 虾神的csdn技术博客
  11. 中国总部经济园市场发展策略分析及市场十四五前景展望报告2022-2028年版
  12. html显示汉字音调,汉语拼音音调符号标法的规则
  13. 支持指纹模块的服务器,指纹模块
  14. 28 网络文件共享服务
  15. 如何制作Windows10屏保?简单呀--Windows batch
  16. 研究生阶段应该如何度过?
  17. 5、TORCH.RANDOM
  18. 苹果macbook系列中哪款比较好?
  19. [计算机基础与编程综合实验]计费管理系统
  20. EVO轨迹评估工具显示界面设置

热门文章

  1. 新一代DaaS产品:数聚核DataN产品社区交流会 | 华坤道威专访
  2. 云计算之概念——IaaS、SaaS、PaaS、Daas
  3. OCR识别软件(uTools)→{个人笔记记录}
  4. db9接口(db9接口详细接线图)
  5. 【C语言】字符个数统计 笔试常见题型
  6. python socket 编程之三:长连接、短连接以及心跳(转药师Aric的文章)...
  7. 《深入理解计算机系统》读书笔记
  8. linux之shell语言
  9. 计算机二级c++考试
  10. Spring 中的Advice类型介绍