三、数据断电存储,工程必备

8.SD卡任意地址的读写

        SD卡作为最常见的外设之一,本身也是一种基于半导体快闪记忆器的新一代记忆设备。它具有体积小、传输速度快、支持热插拔等优点,在便携式装置领域得到了非常广泛的应用,例如数码相机、多媒体播放器、笔记本电脑、行车记录仪等等都能看到它的身影,相比EEPROM和FLASH而言,SD卡具有断电存储大型数据的优势,同时对于.bmp、.wav等很多格式的数据会按照扇区进行存储,如图1所示是豌豆开发板Artix7上SD卡电路,为了在这款开发板上集成更多的外设资源,电路设计上节约了引脚,FPGA直接通过SPI总线和SD卡进行数据读写,但需要说明的是实际上SD卡有两种通信方式:SPI和SDIO,前者更节约芯片引脚、后者更提升读写速度,一般性地对于读写速度要求不是很苛刻地场合使用SPI通信即可。

图1 豌豆开发板Artix7上SD卡电路

       市面上除标准的SD卡外,还有MicroSD卡(原名TF卡),是一种极细小的快闪存储器卡,是由SanDisk(闪迪)公司发明,主要用于移动手机,在豌豆开发板上配有标准的MicroSD卡插槽,MicroSD卡在插入适配器后即可转换成SD卡,其操作时序和SD卡是完全相同的。MicroSD卡接口定义如下图2所示,如表1 MicroSD卡各个引脚功能说明。

 图2 MicroSD卡引脚定义示意图

引脚编号

引脚名称

SDIO模式下功能

SPI模式下功能

1

DAT2

数据线2

保留

2

DAT3/CS

数据线3

片选信号

3

CMD/MOSI

命令线

主机输出,从机输入

4

VDD

电源

电源

5

CLK

时钟

时钟

6

VSS

7

DAT0/MISO

数据线0

主机输入,从机输出

8

DAT1

数据线1

保留

表1 MicroSD卡各个引脚功能说明

       不同协议规范的SD卡也有着不同速度等级的表示方法。在SD1.0协议规范中,会使用“X”用以表示不同的速度等级;在SD2.0协议规范中,使用了SpeedClass表示不同的速度等级;SD3.0协议规范则使用UHS(Ultra High Speed)表示不同的速度等级。SD2.0规范中对SD卡的速度等级划分为普通卡(Class2、Class4、Class6)和高速(Class10);SD3.0规范对SD卡的速度等级划分为UHS速度等级1和3,而在SD4.0中则是UHS-II,不仅速度等级大大加快,接口方面也有所改变,SD2.0、3.0、4.0协议下不同等级的读写速度和应用场景如下表2所示。

表2 SD2.0、3.0、4.0协议下不同等级的读写速度和应用场景

       目前SD卡中使用最多的是2.0协议,虽然SD卡没有统一的芯片手册,但是却有一份官方手册SD2.0_Final_bookmark,在这个例程中我们使用SPI协议去和SD卡进行通信,同样地我们也尝试着来提取官方手册中的有效信息,如图3所示在SD2.0协议下SD卡SPI通信连接示意图,这里也是用到了4根线,即MOSI、MISO、CS和CLK。

 

图3 在SD2.0协议下SD卡SPI通信连接示意图

       如图4所示是在SD2.0协议下指令格式,这里每条指令均有6字节组成,如图5和图6所示是在SD2.0协议下指令发送和指令应答示意图,注意到指令发送第1字节的高2位均为01,即格式固定为“0 1 x x x x x x”,比如CMD55,8’d55 = 8’b 0011_0111,而发送指令号则应该是8’b 0111_0111;指令发送第2-5字节是指令参数,有些指令参数是保留位,没有定义参数的内容,保留位应全部设置为0;指令发送第6字节中前7位为CRC(循环冗余校验)校验位,最后一位为停止位1,SD卡在SPI模式下默认不开启CRC校验,在SDIO模式下开启CRC校验。换句话说在SPI模式下,虽然SD卡不开启CRC校验,但是CRC校验位还是需要发的,只是SD卡会在读到CRC校验位时自动忽略它,所以校验位全部设置为1即可。

       这里需要注意的是,SD卡上电默认是SDIO模式,如图7所示是SD卡上电工作初始化示意图手册上给出至少应该在上电74个时钟周期后,SD卡达到正常工作电压才可以对其进行操作,在接收SD卡返回CMD0的响应指令时,拉低片选CS,进入SPI模式。所以在发送CMD0指令时,SD卡仍处于SDIO模式,需要开启CRC校验,另外CMD8的CRC校验是始终启用的,也需要启用CRC校验,除了这两条指令,其它指令的CRC可以不用做校验。

        此外在SD2.0协议下SD卡的指令又被分为:标准指令比如CMD0、CMD8等,应用相关指令比如ACMD41等。ACMD指令是特殊指令,发送方法和标准指令是一样的,但是在发送应用相关指令之前,必须先发送CMD55指令,告诉SD卡接下来的指令是应用相关指令,而非标准指令。

       发送完指令后,SD卡会返回响应指令的信息,不同的CMD指令会有不同类型的返回值,常用的返回值有R1类型、R3类型和R7类型,其中R7类型是返回CMD8指令专用。SD卡的常用指令说明如下表3所示,这里是从图8在官方手册对SD2.0协议下SD卡常用指令汇总而来的,感兴趣的同学可以再结合图8对比下。

图4 SD2.0协议下指令格式

指令

索引

指令号

参数设置

返回类型

细节描述

CMD0

0x40

保留位

R1

复位SD卡进入默认状态,如果返回值为0x01,则表示SD卡复位成功

CMD8

0x48

Bit[31:12]:保留位

Bit[11:8]:主机电压范围(VHS)

0:未定义 

1:2.7V~3.6V

2:低电压 

4:保留位 

8:保留位 

Bit[7:0]:校验字节,该校验字节不是CRC校验位,而是此字节和返回的校验字节相同。如果该字节发送0xaa,那么当接收CMD8指令回复的数据时接收到的校验字节也应该是0xaa。

R7

发送主机的电压范围以及查询SD卡支持的电压范围,而V1.0版本的卡并不支持该指令,只有V2.0版本的卡才支持该指令。如果SD卡返回的值是0x01,则表示SD卡为V2.0,否则该卡为MMC卡或者为V1.0卡 

CMD17

0x51

Bit[31:0]:SD卡读扇区地址

R1

SD卡的读指令

CMD24

0x58

Bit[31:0]:SD卡写扇区地址

R1

SD卡的写指令

CMD55

0x77

Bit[31:16]:RCA(SD卡相对地址),在SPI模式下没有用到

Bit[15:0]:保留位

R1

告诉SD卡下一条指令是应用相关指令,而非标准指令

ACMD41

0x69

Bit[31]:保留位 

Bit[30]:HCS(OCR[30]),如果主机支持SDHC或SDXC的卡,此位设置为1

Bit[29:0]:保留位

R3

要求访问的SD卡发送它的操作条件寄存器(OCR)内容

表3 SD2.0协议下SD卡常用指令汇总

图5 SD2.0协议下指令发送示意图

 图6 SD2.0协议下指令应答示意图

 图7 SD卡上电工作初始化示意图

       如图8所示是在SD2.0协议下SD卡常用指令报文和功能详细描述,当然大家结合表3看起来则更为直观,注意到SD卡的每条发送指令后,都需要SD卡返回响应数据,其中用到了返回类型R1、R3、R7三种数据格式,笔者也把官方手册的相关说明做了截图以便于大家查阅。

       

 

 

 

 

 图8 SD2.0协议下SD卡常用指令报文和功能详细描述

       如图9所示是SD卡返回类型R1的数据格式,其中从高位到低位依次是起始位0、参数错误标志位、地址错误标志位、擦除序列错误标志位、CRC校验错误标志位、无效指令标志位、擦除复位标志位、空闲状态标志位。

       如图10所示是SD卡返回类型R3的数据格式共返回5个字节,返回的第1个字节为R1的内容,其余字节为OCR(操作条件寄存器)的内容。

       如图11所示是SD卡返回类型R7的数据格式,SD卡返回类型R7格式共返回5个字节,返回的第1个字节也为R1的内容,其余字节包含SD卡操作电压信息和校验字节等相关内容。

 图9 SD卡返回类型R1数据格式

 图10 SD卡返回类型R3数据格式

 图11 SD卡返回类型R7数据格式

         SD卡在正常读写操作之前,必须先对SD卡进行初始化,使其能够工作在预期的SPI模式下,但需要注意的是SD1.0协议和SD2.0协议在初始化的过程中仍然有所区别:只有SD2.0协议的SD卡才支持CMD8指令,所以能响应这条指令的SD卡可以判断为SD2.0协议的卡,否则为SD1.0协议的SD卡或者MMC卡;而对于CMD8无响应的情况,可以发送CMD55 + ACMD41指令,如果返回0,则表示SD1.0协议卡初始化成功,如果返回错误,则判定为MMC卡;在确定为MMC卡后,继续向卡发送CMD1指令,如果返回0,表示MMC卡初始化成功,否则判断为错误卡。

       下面为大家详细介绍SD2.0协议下SD卡复位、初始化、读写操作的时序逻辑,也是这个例程中程序设计的关键点。

        如图12所示是SD卡复位时序逻辑图,SD卡完成上电以后,主机FPGA需要先对从机SD卡发送至少74个周期以上的同步时钟,在上电同步期间,片选CS引脚和MOSI引脚必须是高电(MOSI引脚除发送指令或者数据之外,均默认为高电平),接着拉低片选CS引脚,发送指令CMD0用来复位SD卡,指令发送完成以后需要等SD卡返回响应数据,这时主机FPGA从MISO总线上逐个字节地接收SD卡返回的响应数据,判断如果返回的数据类型R1是复位完成信号0x01,则此时SD卡进入SPI模式,当然如果返回的值为其它值,则继续接收MISO总线上的下个字节数据再做判断,因为未必恰好刚发送完CMD0指令后即可接收到0x01信号,所以程序设计上最好做一个超时判断。

 图12 SD卡复位时序逻辑图

       如图13所示是SD卡初始化时序逻辑图,在主机FPGA成功复位从机SD卡后,这里以常用的SD2.0协议SD卡举例,拉低片选CS引脚,发送指令CMD8以查询SD卡的版本号,只有SD2.0协议SD卡才支持这条指令,指令发送完成以后需要等SD卡返回响应数据0x01信号,说明此SD卡为2.0协议,当然在程序设计的时候也完全可以省略这个步骤所以示意图中并没有画出来。

       在SD卡成功地响应CMD8指令判定其为SD2.0协议SD卡,主机FPGA等待至少8个时钟周期后,发送指令CMD55即告诉SD卡下一次发送的指令是应用相关指令,指令发送完成后等待SD卡返回响应数据0x01信号,等待至少8个时钟周期后,发送指令ACMD41查询SD卡是否初始化完成,指令发送完成后需要等待SD卡返回响应数据0x00,则此时SD卡初始化成功,到这里SD卡已经成功完成了上电复位和初始化配置操作,进入到SPI模式。

图13 SD卡初始化时序逻辑图

        如图14所示是SD卡写操作时序逻辑图,在主机FPGA成功复位和初始化从机SD卡后,拉低片选CS引脚,发送指令CMD24写入单个数据块,指令发送完成后等待SD卡返回响应数据,SD卡返回正确的响应数据0x00,主机FPGA等待至少8个时钟周期后,开始发送数据头0xfe,接下来开始发送512个字节的数据,因为SPI模式下不对数据进行CRC校验,所以直接发送2字节的0xff即可,SD卡响应后会进入写忙碌的状态,即MISO引脚为低电平,此时不允许其他的操作,当检测到MISO引脚为高电平时,SD卡就退出了写忙碌状态,同样地SD卡响应完成整条指令后等待8个时钟周期后允许进行其它操作。

 图14 SD卡写操作时序逻辑图

       如图15所示是SD卡读操作时序逻辑图,在主机FPGA成功复位和初始化从机SD卡后,拉低片选CS引脚,发送指令CMD17读取单个数据块,指令发送完成后等待SD卡返回响应数据,SD卡返回正确的响应数据0x00,主机FPGA等待至少8个时钟周期后,开始准备解析SD卡返回的数据头0xfe,解析到数据头0xfe后,接下来接收SD卡返回的512个字节的数据,数据解析完成后,接下来接收2个字节的CRC校验值,因为SPI模式下不对数据进行CRC校验,所以能直接忽略这2个字节,同样地SD卡响应完成整条指令后等待8个时钟周期后允许进行其它操作。

 图15 SD卡读操作时序逻辑图

       因为市面上大部分的MicroSD卡均为SD2.0协议的SD卡,所以在这个例程中默认去判断SD2.0协议的SD卡,下面仅去介绍SD2.0协议下的初始化流程,具体的初始化步骤如下: 

1. CMD0复位SD卡,返回值0x01;

2. CMD8检测是不是SD2.0卡,返回值0x01;

3. CMD55告诉SD卡下一条指令是应用相关指令而非标准指令,返回值0x01;

4. ACMD41查询SD卡是否初始化完成,返回值0x00;

5. CMD24写SD卡扇区指令,返回值0x00,再依次发送数据头0xfe、512个字节的扇区数据、2字节的0xffffCRC校验数据,返回值0xXXX00101,代表SD卡退出写忙碌状态;

6. CMD17读SD卡扇区指令,返回值0x00,再等待SD卡发送数据头0xfe后,依次接收512个字节的扇区数据、2字节的CRC校验数据。

       这里有几个地方需要特别注意,笔者实现这个例程的功能时,对照SD2.0协议官方手册和一些开源说明,代码很快写出来了,但调试花了好几天都没有成功,所以想把容易大意出错的地方总结给大家少走弯路:

1. SD卡在初始化的时候,时钟频率不能超过400Khz否则初始化不会通过,笔者刚开始在这里吃了很多亏,即读写SD卡和初始化SD卡的时钟都用了同一个时钟,手册给出对于SD2.0协议的SD卡初始化时钟不超过400Khz,正常工作时钟不超过50Mhz,大家可以在顶层信号例化时,给出两个模块不同的时钟频率;

2. 需要等待上电后,主机FPGA需要先对从机SD卡发送至少74个周期以上的同步时钟,再拉低片选CS后发送CMD0指令进行复位操作;

3. 每次主机FPGA发送完一条指令后,需要等待SD卡响应回复,程序上可以做一个超时等待计数器,如果等待超时可以重复发送该指令;

4. 对于SD卡的SPI通信,其CPOL时钟极性和CPHA时钟相位均为1;

5. 对于SD卡每次响应一个指令后,需要间隔至少8个时钟周期才能发送下一条指令;

6. 对于SD卡上电默认是SDIO模式,初始化配置成SPI模式后,才可以进行后续的扇区读写操作。 

      在这里还要补充FAT文件系统知识,如果对SD卡的读写测试就像EEPROM、FLASH一样是读写数据的话,则不需要FAT文件系统的支持,但SD卡却经常被用来在Windows操作系统上存取数据,所以必须使用Windows操作系统支持的FAT文件系统才能在电脑上正常使用。

       FAT是Windows操作系统所使用的一种文件系统,文件系统的作用则是:负责为用户建立文件、读出、修改,控制文件的读取,在这里为大家简单介绍FAT文件系统,一方面对于后期FPGA从SD卡读取.wav音乐播放,从SD卡读取.bmp图片显示等例程做好前期铺垫,另一方面对于一名嵌入式工程师而言,FAT是非常关键的知识点,典型地在STM32或者ARM端移植FAT32,使其可以支持U盘插拔读写数据,比如读取U盘bin文件以实现在线升级程序,写入U盘人机界面截图jpg文件等等,也是嵌入式开发当中常见的应用情景。

        FAT文件系统用“簇”作为数据单元,一个“簇”是由一组连续的扇区所组成,一个扇区由512个字节组成,“簇”中所包含的扇区数必须是2的整数次幂,所有的“簇”从2开始进行编号,每个“簇”都有一个自己的地址编号,用户文件和目录都应存储在“簇”中,FAT文件系统的基本结构分为:分区引导记录、文件分配表、根目录和数据区。 

        分区引导记录:分区引导记录区通常占用分区的第一个扇区,即包含512个字节。其中包括了四个部分内容:BIOS参数记录块、磁盘标志记录表、分区引导记录代码区和结束标志符0x55aa。 

        文件分配表:文件在磁盘上以“簇”为单位存储,但同一个文件的数据并不一定完整地存放在磁盘的一个连续的区域内,往往会分成若干“簇”,FAT表也就是记录文件存储中“簇”与“簇”之间连接的信息,即文件的链式存储。对于FAT32文件系统,使用32Bit来表示文件分配表。 

       根目录:根目录是文件或者目录的首“簇”号。在FAT32文件系统中,不针对根目录的位置做硬性规定,可以存储在分区内可寻址的任意“簇”内。 

       数据区:数据区紧跟在根目录后面,是文件等数据存放的地方,占用大部分的磁盘空间。 

       分析总结完SD2.0协议的SD卡初始化和读写时序逻辑,下面我们开始动手还原整个设计,其实在理清楚思路以后再进行编码,仅仅只是工作量问题,朋友们还记得在第四个例程中“串行DAC输出模拟电压控制LED亮度”对于SPI底层驱动的设计吗,当时为了模块化设计,我们把CPOL时钟极性和CPHA时钟相位均作为输入信号,不过例程中只实现了MOSI总线的数据输出,并未实现MISO总线的数据输入,而在这个例程,我们需要完善该模块,以应用在SD卡底层SPI通信协议的数据输出和输入上,模块里使用两个移位寄存器把MOSI总线的输出数据逐位移出出去,同时也把MISO总线的输入数据逐位移入进来,但在这里不过多赘述了,朋友们对照spi_driver模块的代码,很容易就可以看得很明白,其中模块里用spi_en输入信号作为触发SPI总线协议1字节数据读写的使能信号,用spi_done输出信号作为SPI总线协议读写完成1字节数据读写的结束信号。

       在这个例程中,主要详细介绍sdcard_init和sdcard_driver两个模块的设计,即SD卡初始化和扇区读写模块,如表4所示,是sdcard_init模块的信号列表。

        到这里可能有朋友会存在疑问,同样都是SPI总线通信,为什么不采用第七个例程中“FLASH读写断电存储“的设计思路,把驱动层、控制层和逻辑层,层层分割再用模块化设计?因为这里SD卡存在初始化的操作,初始化时钟又和扇区读写时钟不一致,且两者 有着先后顺序,即初始化完成后才能扇区读写,所以如果再按照上一个例程的设计思路,原理上可以实现,但代码设计上显然会增加麻烦,而把spi_driver模块单独模块化出来,再在sdcard_init和sdcard_driver两个模块中调用,则很好地规避了上面的问题。

       对于sdcard_init即SD卡初始化模块,按照前面所分析的依次发送CMD0、CMD8、CMD55、ACMD41,等待SD卡依次返回响应数据0x01、0x01、0x01、0x00即可,但这个模块的设计中需要注意三个地方:1. 主机至少74个时钟周期再发送CMD0(程序里笔者给出了80个时钟周期的等待);2. 主机发送每条指令都要SD卡做出响应回复,主机再收到回复后,需要至少等待8个时钟周期再发送下一条指令;3. spi_en是触发下游spi_driver模块在SPI总线协议读写1字节数据的使能信号,本模块需要在收到spi_driver模块传来的在SPI总线协议读写1字节数据的完成信号spi_done后,拉高一个时钟的spi_en信号继续触发下游模块在SPI总线上读写1字节数据,如图16是SD卡初始化模块的代码设计。

信号列表

信号名

I/O

位宽

clk

I

1

rst_n

I

1

sd_data_vld

I

1

sd_dout

I

8

sd_din

O

8

sd_cs

O

1

spi_en

O

1

sd_init_done

O

1

表4 sdcard_init模块信号列表

图16 SD卡初始化模块的代码设计

       在这个例程中我们去实现以下功能,通过串口助手向豌豆开发板发送简单的报文数据,控制SD卡读写第0扇区的512字节数据,和前面EERPOM报文同样的思路,报文的具体格式是:8’hAA(固定报头)、8’h00(写操作)/8’h01(读操作)、写入SD卡的初始值(此后扇区内512字节数据会按输入的初始值再做加一操作),如报文:8’hAA 8’h00 8’h1F,代表向SD卡的0扇区写入初始值为1F的数据,报文:8’hAA 8’h01,代表向SD卡的0扇区读取512字节数据,因为command_detect模块和前面例程中的设计大同小异,所以不在赘述,感兴趣的同学也可以把读写扇区的地址做到报文里实现控制不同扇区读写的效果。

       如表5所示是sdcard_driver模块信号列表,在SD卡扇区读写模块设计中,其实和前面读写FLASH例程相似,但也有几个地方需要注意:1. 主机发送CMD24写SD卡扇区指令,返回值0x00,再依次发送数据头0xfe、512个字节的扇区数据、2字节的0xffffCRC校验数据,返回值0xXXX00101,代表SD卡退出写忙碌状态;2. 主机发送CMD17读SD卡扇区指令,返回值0x00,再等待SD卡发送数据头0xfe后,依次接收512个字节的扇区数据、2字节的CRC校验数据;3. 本模块需要一个FIFO去存储从SD卡扇区内读出的512字节数据;4. 本模块需要上游command_detect模块的sec_wr_addr、sec_rd_addr、wr_start_byte、wrl_rdh_mode信号来进行读写扇区操作;5. 同样地spi_en是触发下游spi_driver模块在SPI总线协议读写1字节数据的使能信号,本模块需要在收到spi_driver模块传来的在SPI总线协议读写1字节数据的完成信号spi_done后,拉高一个时钟的spi_en信号继续触发下游模块在SPI总线上读写1字节数据,如图17所示是SD卡扇区读写模块的代码设计。

信号列表

信号名

I/O

位宽

clk

I

1

rst_n

I

1

uart_fifo_rdy

I

1

sd_data_vld

I

1

sdcard_en

I

1

sec_wr_addr

I

32

sec_rd_addr

I

32

wr_start_byte

I

8

wrl_rdh_mode

I

1

sd_dout

I

8

sd_din

O

8

sd_cs

O

1

spi_en

O

1

sd_rd_dout

O

8

sd_rd_dout_vld

O

1

表5 sdcard_driver模块信号列表

图17 SD卡扇区读写模块的代码设计

       如图18所示是SD卡任意地址的读写顶层文件的例化设计,这里只需要把串口发送uart_transfer模块、串口接收uart_receive模块、指令解析command_detect模块、SD卡初始化sdcard_init模块、SD卡扇区读写sdcard_driver模块、SPI总线底层驱动spi_driver模块的相关信号例化到一起即可,因为sdcard_driver模块需要在sdcard_init模块初始化后才能正常工作,且两个模块的SPI总线时钟不同,所以用sd_init_done来作为标志信号,作为判断两个模块SPI总线控制的标志,且SD卡完成初始化后用LED点灯方便观察。

图18 SD卡任意地址的读写顶层文件的例化

        如图19和20所示,通过串口助手先发送写SD卡扇区报文指令,即写入初始字节值为1F,再发送读SD卡扇区报文指令,可以看到512个字节从扇区0地址中读出。

图19 串口助手发送写SD卡扇区报文指令

 图20 串口助手发送读SD卡扇区报文指令