OpenEdv-开源电子网

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

[国产FPGA] 《ATK-DFPGL22G 之FPGA开发指南》第四十六章 SD卡读写测试实验

[复制链接]

1117

主题

1128

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4666
金钱
4666
注册时间
2019-5-8
在线时间
1224 小时
发表于 2023-12-27 17:43:16 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2023-12-26 15:58 编辑

第四十六章 SD卡读写测试实验

1)实验平台:正点原子 ATK-DFPGL22G开发板

2) 章节摘自【正点原子】ATK-DFPGL22G之FPGA开发指南_V1.0


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

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

6)FPGA技术交流QQ群:435699340

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

SD存储卡是一种基于半导体快闪记忆器的新一代记忆设备。它具有体积小、传输速度快、支持热插拔等优点,在便携式装置领域得到了广泛的应用,如数码相机、多媒体播放器、笔记本电脑等。本章我们将使用FPGA开发板学习如何对SD卡(本章用的是TF卡也就是MicroSD卡,以下简称SD卡)进行读写操作并进行SD卡的读写测试实验。
本章包括以下几个部分:     
1.1        简介
1.2        实验任务
1.3        硬件设计
1.4        程序设计
1.5        下载验证

1.1 简介
SD卡的英文全称是Secure Digital Card,即安全数字卡(又叫安全数码卡),是在MMC卡(Multimedia Card,多媒体卡)的基础上发展而来,具有两个突出的优点:更高的安全性和更快的读写速度。SD卡和MMC卡的长度和宽度都是32mm x 24mm,不同的是,SD卡的厚度为2.1mm,而MMC卡的厚度为1.4mm,SD卡比MMC卡略厚,以容纳更大容量的存贮单元,同时SD卡比MMC卡触点引脚要多,且在侧面多了一个写保护开关。SD卡与MMC卡保持着向上兼容,也就是说,MMC卡可以被新的SD设备存取,兼容性则取决于应用软件,但SD卡却不可以被MMC设备存取。SD卡和MMC卡可通过卡片上面的标注进行区分,如下图左侧图片上面标注为“MultiMediaCard”字母样式的为MMC卡,右侧图片上面标注为“SD”字母样式的为SD卡。
image001.png              image003.png
图46.1.1  MMC外观图(左)和SD卡外观图(右)
上图中右侧图片的SD卡实际上为SDHC卡,SD卡从存储容量上分为3个级别,分别为:SD卡、SDHC卡(Secure Digital High Capacity,高容量安全数字卡)和SDXC卡(SD eXtended Capacity,容量扩大化的安全存储卡)。SD卡在MMC卡的基础上发展而来,使用FAT12/FAT16文件系统,SD卡采用SD1.0协议规范,该协议规定了SD卡的最大存储容量为2GB;SDHC卡是大容量存储SD卡,使用FAT32文件系统,SDHC卡采用SD2.0协议规范,该协议规定了SDHC卡的存储容量范围为2GB至32GB;SDXC卡是新提出的标准,不同于SD卡和SDHC卡使用的FAT文件系统,SDXC卡使用exFAT文件系统,即扩展FAT文件系统。SDXC卡采用SD3.0协议规范,该协议规定了SDXC卡的存储容量范围为32GB至2TB(2048GB),一般用于中高端单反相机和高清摄像机。
下表为不同类型的SD卡采用的协议规范、容量等级及读写速度。

QQ截图20231226155207.png
表46.1.1 SD卡的类型、协议规范、容量等级及支持的文件系统


不同协议规范的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,它不仅速度等级大大加快,接口也有所变化。不同等级的读写速度和应用如下图所示。
image005.png
图46.1.2 SD卡不同速度等级表示法
SD卡共有9个引脚线,可以工作在SDIO模式或者SPI模式。在SDIO模式下,共用到CLK、CMD、DAT[3:0]六根信号线;在SPI模式下,共用到CS(SDIO_DAT[3])、CLK(SDIO_CLK)、MISO(SDIO_DAT[0])、MOSI(SDIO_CMD)四根信号线。SD卡接口定义以及各引脚功能说明如下图所示。
image008.png
图46.1.3 SD卡接口定义以及各引脚功能说明
市面上除标准SD卡外,还有MicroSD卡(原名TF卡,就是本次实验所使用的SD卡),是一种极细小的快闪存储器卡,是由SanDisk(闪迪)公司发明,主要用于移动手机。MicroSD卡插入适配器(Adapter)可以转换成SD卡,其操作时序和SD卡是一样的。MicroSD卡接口定义以及各引脚功能说明如下图所示。
image010.png
图46.1.4 MicroSD卡接口定义以及各引脚功能说明
标准SD卡2.0版本中,工作时钟频率可以达到50Mhz,在SDIO模式下采用4位数据位宽,理论上可以达到200Mbps(50Mx4bit)的传输速率;在SPI模式下采用1位数据位宽,理论上可以达到50Mbps的传输速率。因此SD卡在SDIO模式下的传输速率更快,同时其操作时序也更复杂。对于使用SD卡读取音乐文件和图片来说,SPI模式下的传输速度已经能够满足我们的需求,因此我们本章采用SD卡的SPI模式来对SD卡进行读写测试。
SD卡在正常读写操作之前,必须先对SD卡进行初始化,SD卡的初始化过程就是向SD中写入命令,使其工作在预期的工作模式。在对SD卡进行读写操作时同样需要先发送写命令和读命令,因此SD卡的命令格式是学习SD卡的重要内容。SD卡的命令格式由6个字节组成,发送数据时高位在前,SD卡的写入命令格式如下图所示:
image012.png
图46.1.5 SD卡命令格式
Byte1:命令字的第一个字节为命令号(如CMD0、CMD1等),格式为“0 1 x x x x x x”。命令号的最高位始终为0,是命令号的起始位;次高位始终为1,是命令号的发送位;低6位为具体的命令号(如CMD55,8’d55 = 8’b0011_0111,命令号为 0 1 1 1 0 1 1 1 = 0x77)。
Byte2~Byte5:命令参数,有些命令参数是保留位,没有定义参数的内容,保留位应设置为0。
Byte6:前7位为CRC(循环冗余校验)校验位,最后一位为停止位0。SD卡在SPI模式下默认不开启CRC校验,在SDIO模式下开启CRC校验。也就是说在SPI模式下,CRC校验位必须要发,但是SD卡会在读到CRC校验位时自动忽略它,所以校验位全部设置为1即可。需要注意的是,SD卡上电默认是SDIO模式,在接收SD卡返回CMD0的响应命令时,拉低片选CS,进入SPI模式。所以在发送CMD0命令的时候,SD卡处于SDIO模式,需要开启CRC校验。另外CMD8的CRC校验是始终启用的,也需要启用CRC校验。除了这两个命令,其它命令的CRC可以不做校验。
SD卡的命令分为标准命令(如CMD0)和应用相关命令(如ACMD41)。ACMD命令是特殊命令,发送方法同标准命令一样,但是在发送应用相关命令之前,必须先发送CMD55命令,告诉SD卡接下来的命令是应用相关命令,而非标准命令。发送完命令后,SD卡会返回响应命令的信息,不同的CMD命令会有不同类型的返回值,常用的返回值有R1类型、R3类型和R7类型(R7类型是CMD8命令专用)。SD卡的常用命令说明如下表所示。
QQ截图20231226155227.png
表46.1.2 SD卡常用命令说明

SD卡返回类型R1数据格式如下图所示:
image013.png
图46.1.6 SD卡返回类型R1数据格式
由上图可知,SD卡返回类型R1格式共返回1个字节,最高位固定为0,其它位分别表示对应状态的标志,高电平有效。
SD卡返回类型R3数据格式如下图所示:
image015.png
图46.1.7 SD卡返回类型R3数据格式
由上图可知,SD卡返回类型R3格式共返回5个字节,首先返回的第一个字节为前面介绍的R1的内容,其余字节为OCR(Operation Conditions Register,操作条件寄存器)寄存器的内容。
SD卡返回类型R7数据格式如下图所示:
image017.png
图46.1.8 SD卡返回类型R7数据格式
由上图可知,SD卡返回类型R7格式共返回5个字节,首先返回的第一个字节为前面介绍的R1的内容,其余字节包含SD卡操作电压信息和校验字节等内容。其中电压范围是一个比较重要的参数,其具体内容如下所示:
Bit[11:8]:操作电压反馈
          0:未定义
          1:2.7V~3.6V
          2:低电压
          4:保留位
          8:保留位
          其它:未定义
SD卡在正常读写操作之前,必须先对SD卡进行初始化,使其工作在预期的工作模式。SD卡1.0版本协议和2.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卡,接下来我们仅介绍SD2.0版本协议的初始化流程,以下提到的SD卡均代表基于SD2.0版本协议的SDHC卡,其详细初始化步骤如下:
1,SD卡完成上电后,主机FPGA先对从机SD卡发送至少74个以上的同步时钟,在上电同步期间,片选CS引脚和MOSI引脚必须为高电平(MOSI引脚除发送命令或数据外,其余时刻都为高电平);
2,拉低片选CS引脚,发送命令CMD0(0x40)复位SD卡,命令发送完成后等待SD卡返回响应数据;
3,SD卡返回响应数据后,先等待8个时钟周期再拉高片选CS信号,此时判断返回的响应数据。如果返回的数据为复位完成信号0x01,在接收返回信息期间片选CS为低电平,此时SD卡进入SPI模式,并开始进行下一步,如果返回的值为其它值,则重新执行第2步;
4,拉低片选CS引脚,发送命令CMD8(0x48)查询SD卡的版本号,只有SD2.0版本的卡才支持此命令,命令发送完成后等待SD卡返回响应数据;
5,SD卡返回响应数据后,先等待8个时钟周期再拉高片选CS信号,此时判断返回的响应数据。如果返回的电压范围为4’b0001即2.7V~3.6V,说明此SD卡为2.0版本,进行下一步,否则重新执行第4步;
拉低片选CS引脚,发送命令CMD55(0x77)告诉SD卡下一次发送的命令是应用相关命令,命令发送完成后等待SD卡返回响应数据;
SD卡返回响应数据后,先等待8个时钟周期再拉高片选CS信号,此时判断返回的响应数据。如果返回的数据为空闲信号0x01,开始进行下一步,否则重新执行第6步。
拉低片选CS引脚,发送命令ACMD41(0x69)查询SD卡是否初始化完成,命令发送完成后等待SD卡返回响应数据;
SD卡返回响应数据后,先等待8个时钟周期再拉高片选CS信号,此时判断返回的响应数据。如果返回的数据为0x00,此时初始化完成,否则重新执行第6步。
SD卡上电复位及初始化命令时序如下图所示:
image019.png
图46.1.9 SD卡复位时序图

image021.png
图46.1.10 SD卡初始化时序图
至此,SD卡完成了复位以及初始化操作,进入到SPI模式的读写操作。需要注意的是:SD卡在初始化的时候,SPI_CLK的时钟频率不能超过400KHz,在初始化完成之后,再将SPI_CLK的时钟频率切换至SD卡的最大时钟频率。尽管目前市面上的很多SD卡支持以较快的时钟频率进行初始化,为了能够兼容更多的SD卡,在SD卡初始化的时候时钟频率不能超过400KHz。
SD卡读写一次的数据量必须为512字节的整数倍,即对SD卡读写操作的最少数据量为512个字节。我们可以通过命令CMD16来配置单次读写操作的数据长度,使每次读写的数据量为(n*512)个字节(n≥1),本次SD卡的读写操作使用SD卡默认配置,即单次读写操作的数据量为512个字节。
SD卡初始化完成后,即可对SD卡进行读写测试,SD卡的读写测试是先向SD卡中写入数据,再从SD卡中读出数据,并验证数据的正确性。SD卡的写操作时序图如下图所示:
image023.png
图46.1.11 SD卡写操作时序图
SD卡的写操作流程如下:
1、拉低片选CS引脚,发送命令CMD24(0x58)读取单个数据块,命令发送完成后等待SD卡返回响应数据;
2、SD卡返回正确响应数据0x00后,等待至少8个时钟周期,开始发送数据头0xfe;
3、发送完数据头0xfe后,接下来开始发送512个字节的数据;
4、数据发送完成后,发送2个字节的CRC校验数据。由于SPI模式下不对数据进行CRC校验,直接发送两个字节的0xff即可;
5、校验数据发送完成后,等待SD卡响应;
6、SD卡返回响应数据后会进入写忙状态(MISO引脚为低电平),即此时不允许其它操作。当检测到MISO引脚为高电平时,SD卡此时退出写忙状态;
7、拉高CS引脚,等待8个时钟周期后允许进行其它操作。
SD卡的读操作时序图如下图所示:
image025.png
图46.1.12 SD卡读操作时序图
SD卡的读操作流程如下:
1、拉低片选CS引脚,发送命令CMD17(0x51)读取单个数据块,命令发送完成后等待SD卡返回响应数据;
2、SD卡返回正确响应数据0x00后,准备开始解析SD卡返回的数据头0xfe;
3、解析到数据头0xfe后,接下来接收SD卡返回的512个字节的数据;
4、数据解析完成后,接下来接收两个字节的CRC校验值。由于SPI模式下不对数据进行CRC校验,可直接忽略这两个字节;
5、校验数据接收完成后,等待8个时钟周期;
6、拉高片选CS引脚,等待8个时钟周期后允许进行其它操作。
在前面介绍的SD卡读写操作中,使用的是SD卡的SPI模式,即采用SPI协议进行读写操作。SPI和IIC都是芯片上常用的通信协议,SPI相比于IIC具有更高的通信速率,但同时占用更多的引脚线,接下来我们了解一下SPI的协议及传输时序。
SPI(Serial Peripheral interface)是由摩托罗拉公司定义的一种串行外围设备接口,是一种高速、全双工、同步的通信总线,只需要四根信号线即可,节约引脚,同时有利于PCB的布局。正是出于这种简单易用的特性,现在越来越多的芯片集成了SPI通信协议,如FLASH、AD转换器等。
SPI的通信原理比较简单,它以主从方式工作,通常有一个主设备(此处指FPGA)和一个或多个从设备(此处指SD卡)。SPI通信需要四根线,分别为SPI_CS、SPI_CLK、SPI_MOSI和SPI_MISO。其中SPI_CS、SPI_CLK和SPI_MOSI由主机输出给从机,而SPI_MISO由从机输出给主机。SPI_CS用于控制芯片是否被选中,也就是说只有片选信号有效时(对于SD卡来说是低电平有效),对芯片的操作才有效;SPI_CLK是由主机产生的同步时钟,用于同步数据;SPI_MOSI和SPI_MISO是主机发送和接收的数据脚。
一般而言,SPI通信有4种不同的模式,不同的从设备在出厂时被厂家配置为其中一种模式,模式是不允许用户修改的。主设备和从设备必须在同一模式下进行通信,否则数据会接收错误。SPI的通信模式是由CPOL(时钟极性)和CPHA(时钟相位)来决定的,四种通信模式如下:
模式0:CPOL = 0,CPHA = 0;
模式1:CPOL = 0,CPHA = 1;
模式2:CPOL = 1,CPHA = 0;
模式3:CPOL = 1,CPHA = 1。
CPOL控制着SPI_CLK的时钟极性,时钟极性变化如下图所示:
image027.png
图46.1.13 SPI_CLK时钟极性
由上图可知,当CPOL = 1时,SPI_CLK在空闲时为高电平,发起通信后的第一个时钟沿为下降沿;CPOL = 0时,SPI时钟信号SPI_CLK空闲时为低电平,发起通信后的第一个时钟沿为上升沿。
CPHA用于控制数据与时钟的对齐模式,其不同模式下的时序图如下图所示:
image029.png
图46.1.14 不同CPHA模式下的时序图
由上图可知,当CPHA=0时,数据在时钟的第一个变化沿之前就已经改变,并且保持稳定,也就意味着在时钟的第一个变化沿锁存数据;当CPHA=1时,时钟的第一个变化沿(上升沿或者下降沿)数据开始改变,那么也就意味着时钟的第2个变化沿(与第一个变化沿相反)锁存数据。
对于SD卡的SPI模式而言,采用的SPI的通信模式为模式3,即CPOL=1,CPHA=1,在SD卡2.0版本协议中,SPI_CLK时钟频率可达50Mhz。
以上是SD卡简介部分的全部内容,在这里还需要补充下FAT文件系统的知识。如果对SD卡的读写测试像EEPROM一样仅仅是写数据,读数据并验证数据的正确性的话,是不需要FAT文件系统的。而SD卡经常被用来在Windows操作系统上存取数据,必须使用Windows操作系统支持的FAT文件系统才能在电脑上正常使用。
FAT(File Allocation Table,文件分配表)是Windows操作系统所使用的一种文件系统,它的发展过程经历了FAT12、FAT16、FAT32三个阶段。FAT文件系统用“簇”作为数据单元,一个“簇”由一组连续的扇区组成,而一个扇区由512个字节组成。簇所包含的扇区数必须是2的整数次幂,其扇区个数最大为64,即32KB(512Byte * 64 = 32KB)。所有的簇从2开始进行编号,每个簇都有一个自己的地址编号,用户文件和目录都存储在簇中。
FAT文件系统的基本结构依次为:分区引导记录、文件分配表(FAT表1和FAT表2)、根目录和数据区。
分区引导记录:分区引导记录区通常占用分区的第一个扇区,共512个字节。包含四部分内容:BIOS参数记录块BPB(BIOS Parameter Block)、磁盘标志记录表、分区引导记录代码区和结束标志0x55AA。
文件分配表(FAT表1和FAT表2):文件在磁盘上以簇为单位存储,但是同一个文件的数据并不一定完整地存放在磁盘的一个连续的区域内,往往会分成若干簇,FAT表就是记录文件存储中簇与簇之间连接的信息,这就是文件的链式存储。对于FAT16文件系统来说,每个簇用16Bit来表示文件分配表,而对于FAT32文件系统,使用32Bit来表示文件分配表,这是两者之间的最重要区别。
根目录:根目录是文件或者目录的首簇号。在FAT32文件系统中,不再对根目录的位置做硬性规定,可以存储在分区内可寻址的任意簇内。不过通常根目录是最早建立的(格式化就生成了)目录表,所以我们看到的情况基本上都是根目录首簇紧邻FAT2,占簇区顺序上的第1个簇(即2号簇)。
数据区:数据区紧跟在根目录后面,是文件等数据存放的地方,占用大部分的磁盘空间。

1.2 实验任务
本节实验任务是使用FPGA开发板向SD卡指定的扇区地址中写入512个字节的数据,写完后将数据读出,并验证数据是否正确。

1.3 硬件设计
我们的ATK-DFPGL22G开发板上有一个SD卡插槽,用于插入SD卡,其原理图如图 46.3.1所示:
image031.png
图 46.3.1SD卡接口原理图
本次实验,我们使用SD卡的SPI模式,只用到了SDIO_D3(SPI_CS)、SDIO_CMD(SPI_MOSI),SDIO_SCK(SPI_SCK)和SDIO_D0(SPI_MISO)引脚,而其它两个引脚是在SD卡的SDIO模式下用到的。
本实验中各端口信号的管脚分配如下表所示。
QQ截图20231226155239.png
表 46.3.1 SD卡读写测试实验管脚分配

1.4 程序设计
通过前面介绍的SD卡初始化、写操作以及读操作可知,SD卡的这个三个操作是相互独立且不能同时进行的,因此我们可以将SD卡的初始化、写操作以及读操作分别划分为三个独立的模块,最后将这三个模块例化在SD卡的控制器模块中,便于在其它工程项目中使用。下图是实验的系统框图,时钟模块为各个模块提供驱动时钟,SD卡测试数据产生模块产生测试数据写入SD卡,写完后从SD卡中读出数据,最终读写测试结果由LED显示模块通过控制LED灯的显示状态来指示。
image033.png
图46.4.1 SD卡读写测试系统框图
顶层模块的原理图如下图所示:
image035.png
图46.4.2 顶层模块原理图
FPGA顶层模块(top_sd_rw)例化了以下四个模块:时钟模块(pll_clk)、SD卡测试数据产生模块(data_gen)、SD卡控制器模块(sd_ctrl_top)和LED显示模块(led_alarm)。
顶层模块(top_sd_rw):顶层模块完成了对其它四个模块的例化,SD卡测试数据产生模块产生的开始写入信号及数据连接至SD卡控制器模块,数据写完后从SD卡控制器中读出数据,并验证数据的正确性,将验证的结果连接至LED显示模块。
时钟模块(pll_clk):时钟模块通过调用时钟IP核来实现,总共输出两个时钟,频率都是50Mhz,但两个时钟相位相差180度。我们知道,SD卡的SPI通信模式为CPOL=1,CPHA=1;即SPI_CLK在空闲时为高电平,数据发送是在时钟的第一个边沿,也就是SPI_CLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。为了在程序代码中统一使用上升沿,我们使用两个相位相差180度的时钟来对SD卡进行操作。
SD卡测试数据产生模块(data_gen):SD卡测试数据产生模块产生的开始写入信号和数据写入SD卡控制器模块中,数据写完后从SD卡控制器中读出数据,并验证数据的正确性,将验证的结果发送给LED显示模块。
SD卡控制器模块(sd_ctrl_top):SD卡控制器模块例化了SD卡初始化模块(sd_init)、SD卡写数据模块(sd_write)和SD卡读数据模块(sd_read)。SD卡初始化模块完成对SD卡的上电初始化操作;SD卡写数据模块完成对SD卡的写操作;SD卡读数据模块完成对SD卡的读操作。由于这三个模块都操作了SD卡的引脚信号,且这三个模块在同一时间内不会同时操作,所以此模块实现了对其它三个模块的例化以及选择SD卡的引脚连接至其中某一个模块。
LED显示模块(led_alarm):LED显示模块将SD卡测试数据产生模块输出的验证结果值,通过控制LED灯的显示状态来指示。
顶层模块的代码如下:
  1. 1   module top_sd_rw(
  2. 2       input               sys_clk     ,  //系统时钟
  3. 3       input               sys_rst_n   ,  //系统复位,低电平有效
  4. 4      
  5. 5       //SD卡接口
  6. 6       input               sd_miso     ,  //SD卡SPI串行输入数据信号
  7. 7       output              sd_clk      ,  //SD卡SPI时钟信号
  8. 8       output              sd_cs       ,  //SD卡SPI片选信号
  9. 9       output              sd_mosi     ,  //SD卡SPI串行输出数据信号
  10. 10     
  11. 11      //LED
  12. 12      output      [3:0   led             //LED灯
  13. 13      );
  14. 14  
  15. 15  //wire define
  16. 16  wire            clk_ref        ;
  17. 17  wire            clk_ref_180deg ;
  18. 18  wire            rst_n          ;
  19. 19  wire            locked         ;
  20. 20  wire            wr_start_en    ;      //开始写SD卡数据信号
  21. 21  wire     [31:0 wr_sec_addr    ;      //写数据扇区地址   
  22. 22  wire     [15:0  wr_data        ;      //写数据            
  23. 23  wire            rd_start_en    ;      //开始写SD卡数据信号
  24. 24  wire     [31:0 rd_sec_addr    ;      //读数据扇区地址   
  25. 25  wire            error_flag     ;      //SD卡读写错误的标志
  26. 26  wire             wr_busy        ;      //写数据忙信号
  27. 27  wire            wr_req         ;      //写数据请求信号
  28. 28  wire            rd_busy        ;      //读忙信号
  29. 29  wire            rd_val_en      ;      //数据读取有效使能信号
  30. 30  wire     [15:0 rd_val_data    ;      //读数据
  31. 31  wire            sd_init_done   ;      //SD卡初始化完成信号
  32. 32  
  33. 33  //*****************************************************
  34. 34  //**                   main code
  35. 35  //*****************************************************
  36. 36  
  37. 37  assign  rst_n = sys_rst_n & locked;
  38. 38  
  39. 39  //时钟IP核
  40. 40  pll_clk pll_clk_inst (
  41. 41      .areset     (1'b0),
  42. 42      .inclk0     (sys_clk),
  43. 43      .c0         (clk_ref),
  44. 44      .c1         (clk_ref_180deg),
  45. 45      .locked     (locked)
  46. 46      );
  47. 47      
  48. 48  //产生SD卡测试数据  
  49. 49  data_gen u_data_gen(
  50. 50      .clk            (clk_ref),
  51. 51      .rst_n          (rst_n),
  52. 52      .sd_init_done   (sd_init_done),
  53. 53      .wr_busy        (wr_busy),
  54. 54      .wr_req         (wr_req),
  55. 55      .wr_start_en    (wr_start_en),
  56. 56      .wr_sec_addr    (wr_sec_addr),
  57. 57      .wr_data        (wr_data),
  58. 58      .rd_val_en      (rd_val_en),
  59. 59      .rd_val_data    (rd_val_data),
  60. 60      .rd_start_en    (rd_start_en),
  61. 61      .rd_sec_addr    (rd_sec_addr),
  62. 62      .error_flag      (error_flag)
  63. 63      );     
  64. 64  
  65. 65  //SD卡顶层控制模块
  66. 66  sd_ctrl_top u_sd_ctrl_top(
  67. 67      .clk_ref          (clk_ref),
  68. 68      .clk_ref_180deg   (clk_ref_180deg),
  69. 69      .rst_n            (rst_n),
  70. 70      //SD卡接口
  71. 71      .sd_miso          (sd_miso),
  72. 72      .sd_clk           (sd_clk),
  73. 73      .sd_cs            (sd_cs),
  74. 74      .sd_mosi          (sd_mosi),
  75. 75      //用户写SD卡接口
  76. 76      .wr_start_en      (wr_start_en),
  77. 77      .wr_sec_addr      (wr_sec_addr),
  78. 78      .wr_data          (wr_data),
  79. 79      .wr_busy          (wr_busy),
  80. 80      .wr_req           (wr_req),
  81. 81      //用户读SD卡接口
  82. 82      .rd_start_en      (rd_start_en),
  83. 83      .rd_sec_addr      (rd_sec_addr),
  84. 84      .rd_busy          (rd_busy),
  85. 85      .rd_val_en        (rd_val_en),
  86. 86      .rd_val_data      (rd_val_data),   
  87. 87      
  88. 88      .sd_init_done     (sd_init_done)
  89. 89      );
  90. 90  
  91. 91  //led警示
  92. 92  led_alarm #(
  93. 93      .L_TIME      (25'd25_000_000)
  94. 94      )  
  95. 95     u_led_alarm(
  96. 96      .clk            (clk_ref),
  97. 97      .rst_n          (rst_n),
  98. 98      .led            (led),
  99. 99      .error_flag     (error_flag)
  100. 100     );
  101. 101
  102. 102 endmodule
复制代码
SD卡控制器模块输出的sd_init_done(SD卡初始化完成信号)连接至SD卡测试数据产生模块,只有在SD卡初始化完成之后(sd_init_done为高电平),才能对SD卡进行读写测试。SD卡控制器模块将SD卡的初始化以及读写操作封装成方便用户调用的接口,SD卡测试数据产生模块只需对SD卡控制器模块的用户接口进行操作即可完成对SD卡的读写操作。
在代码的第93行定义了一个参数(L_TIME),用于在读写测试错误时控制LED闪烁的时间,其单位是1个时钟周期。因为输入的时钟频率为50Mhz,周期为20ns,所以20 * 25'd25_000_000 = 500ms,因此LED灯在读写错误时每500ms闪烁一次。
SD卡控制器模块的代码如下:
  1. 1   module sd_ctrl_top(
  2. 2       input                clk_ref       ,  //时钟信号
  3. 3       input                clk_ref_180deg,  //时钟信号,与clk_ref相位相差180度
  4. 4       input                rst_n         ,  //复位信号,低电平有效
  5. 5       //SD卡接口
  6. 6       input                sd_miso       ,  //SD卡SPI串行输入数据信号
  7. 7       output               sd_clk        ,  //SD卡SPI时钟信号   
  8. 8       output reg          sd_cs         ,  //SD卡SPI片选信号
  9. 9       output reg          sd_mosi       ,  //SD卡SPI串行输出数据信号
  10. 10      //用户写SD卡接口
  11. 11      input                wr_start_en   ,  //开始写SD卡数据信号
  12. 12      input        [31:0 wr_sec_addr   ,  //写数据扇区地址
  13. 13      input        [15:0  wr_data       ,  //写数据                  
  14. 14      output               wr_busy       ,  //写数据忙信号
  15. 15      output               wr_req        ,  //写数据请求信号   
  16. 16      //用户读SD卡接口
  17. 17      input                rd_start_en   ,  //开始读SD卡数据信号
  18. 18      input        [31:0 rd_sec_addr   ,  //读数据扇区地址
  19. 19      output               rd_busy       ,  //读数据忙信号
  20. 20      output               rd_val_en     ,  //读数据有效信号
  21. 21      output       [15:0 rd_val_data   ,  //读数据   
  22. 22      
  23. 23      output               sd_init_done      //SD卡初始化完成信号
  24. 24      );
  25. 25  
  26. 26  //wire define
  27. 27  wire               init_sd_clk   ;        //初始化SD卡时的低速时钟
  28. 28  wire               init_sd_cs    ;        //初始化模块SD片选信号
  29. 29  wire               init_sd_mosi  ;       //初始化模块SD数据输出信号
  30. 30  wire               wr_sd_cs      ;       //写数据模块SD片选信号   
  31. 31  wire                wr_sd_mosi    ;       //写数据模块SD数据输出信号
  32. 32  wire               rd_sd_cs      ;       //读数据模块SD片选信号   
  33. 33  wire               rd_sd_mosi    ;       //读数据模块SD数据输出信号
  34. 34  
  35. 35  //*****************************************************
  36. 36  //**                   main code
  37. 37  //*****************************************************
  38. 38  
  39. 39  //SD卡的SPI_CLK  
  40. 40  assign  sd_clk = (sd_init_done==1'b0)  ?  init_sd_clk  :  clk_ref_180deg;
  41. 41  
  42. 42  //SD卡接口信号选择
  43. 43  always @(*) begin
  44. 44      //SD卡初始化完成之前,端口信号和初始化模块信号相连
  45. 45      if(sd_init_done == 1'b0)begin     
  46. 46          sd_cs = init_sd_cs;
  47. 47          sd_mosi = init_sd_mosi;
  48. 48      end   
  49. 49      else if(wr_busy) begin
  50. 50          sd_cs = wr_sd_cs;
  51. 51          sd_mosi = wr_sd_mosi;   
  52. 52      end   
  53. 53      else if(rd_busy) begin
  54. 54          sd_cs = rd_sd_cs;
  55. 55          sd_mosi = rd_sd_mosi;      
  56. 56      end   
  57. 57      else begin
  58. 58          sd_cs = 1'b1;
  59. 59          sd_mosi = 1'b1;
  60. 60      end   
  61. 61  end   
  62. 62  
  63. 63  //SD卡初始化
  64. 64  sd_init u_sd_init(
  65. 65      .clk_ref           (clk_ref),
  66. 66      .rst_n             (rst_n),
  67. 67      
  68. 68      .sd_miso           (sd_miso),
  69. 69      .sd_clk            (init_sd_clk),
  70. 70      .sd_cs             (init_sd_cs),
  71. 71      .sd_mosi           (init_sd_mosi),
  72. 72      
  73. 73      .sd_init_done      (sd_init_done)
  74. 74      );
  75. 75  
  76. 76  //SD卡写数据
  77. 77  sd_write u_sd_write(
  78. 78      .clk_ref           (clk_ref),
  79. 79      .clk_ref_180deg    (clk_ref_180deg),
  80. 80      .rst_n             (rst_n),
  81. 81      
  82. 82      .sd_miso           (sd_miso),
  83. 83      .sd_cs             (wr_sd_cs),
  84. 84      .sd_mosi           (wr_sd_mosi),
  85. 85      //SD卡初始化完成之后响应写操作   
  86. 86      .wr_start_en       (wr_start_en & sd_init_done),  
  87. 87      .wr_sec_addr       (wr_sec_addr),
  88. 88      .wr_data           (wr_data),
  89. 89      .wr_busy           (wr_busy),
  90. 90      .wr_req            (wr_req)
  91. 91      );
  92. 92  
  93. 93  //SD卡读数据
  94. 94  sd_read u_sd_read(
  95. 95      .clk_ref           (clk_ref),
  96. 96      .clk_ref_180deg    (clk_ref_180deg),
  97. 97      .rst_n             (rst_n),
  98. 98      
  99. 99      .sd_miso           (sd_miso),
  100. 100     .sd_cs             (rd_sd_cs),
  101. 101     .sd_mosi           (rd_sd_mosi),   
  102. 102     //SD卡初始化完成之后响应读操作
  103. 103     .rd_start_en       (rd_start_en & sd_init_done),  
  104. 104     .rd_sec_addr       (rd_sec_addr),
  105. 105     .rd_busy           (rd_busy),
  106. 106     .rd_val_en         (rd_val_en),
  107. 107     .rd_val_data       (rd_val_data)
  108. 108     );
  109. 109
  110. 110 endmodule
复制代码
SD卡控制器模块例化了SD卡初始化模块(sd_init)、SD卡写数据模块(sd_write)和SD卡读数据模块(sd_read)。由于这三个模块都驱动着SD卡的引脚,因此在代码的第43行开始的always块中,用于选择哪一个模块连接至SD卡的引脚。
在代码的第40行,init_sd_clk用于初始化SD卡时提供较慢的时钟,在SD卡初始化完成之后,再将较快的时钟clk_ref_180deg赋值给sd_clk。当sd_clk上电之后,是一直都有时钟的,而我们在前面说过SPI_CLK的时钟在空闲时为高电平或者低电平。事实上,为了简化设计,sd_clk在空闲时提供时钟也是可以的,是否有效主要由片选信号来控制。
在这里主要介绍下SD卡控制器模块的使用方法。当外部需要对SD卡进行读写操作时,首先要判断sd_init_done(SD卡初始化完成)信号,该信号拉高之后才能对SD卡进行读写操作;在对SD卡进行写操作时,只需给出wr_start_en(开始写SD卡数据信号)和wr_sec_addr(写数据扇区地址),此时SD卡控制器模块会拉高wr_busy信号,开始对SD卡发起写入命令;在命令发起成功后SD卡控制器模块会输出wr_req(写数据请求)信号,此时我们给出wr_data(写数据)即可将数据写入SD卡中;待所有数据写入完成后,wr_busy信号拉低,即可再次发起读写操作。SD卡的读操作是给出rd_start_en(rd_start_en)和rd_sec_addr(读数据扇区地址),此时SD卡控制器会拉高rd_busy(读数据忙)信号,开始对SD卡发起读出命令;在命令发起成功后SD卡控制器模块会输出rd_val_en(读数据有效)信号和rd_val_data(读数据),待所有数据读完之后,拉低rd_busy信号。需要注意的是,SD卡单次写入和读出的数据量为512个字节,因为接口封装为16位数据,所以单次读写操作会有256个16位数据。
SD卡初始化模块完成对SD卡的上电初始化操作,我们在SD卡的简介部分已经详细的介绍了SD卡的初始化流程,我们只需要按照SD卡的初始化步骤即可完成SD卡的初始化。由SD卡的初始化流程可知,其步骤非常适合状态机编写,其状态跳转图如图 46.4.3所示。
image038.png
图 46.4.3SD卡初始化状态跳转图
由上图可知,我们把SD卡初始化过程定义为7个状态,分别为st_idle(初始状态)、st_send_cmd0(发送软件复位命令)、st_wait_cmd0(等待SD卡响应)、st_send_cmd8(发送CMD8命令)、st_send_cmd55(发送CMD55命令)、st_send_acmd41(发送ACMD41命令)以及st_init_done(SD卡初始化完成)。因为SD卡的初始化只需要上电后执行一次,所以在初始化完成之后,状态机一直处于st_init_done状态。
SD卡初始化模块的部分代码如下所示:
  1. 91  //接收sd卡返回的响应数据
  2. 92  //在div_clk_180deg(sd_clk)的上升沿锁存数据
  3. 93  always @(posedge div_clk_180deg or negedge rst_n) begin
  4. 94      if(!rst_n) begin
  5. 95          res_en <= 1'b0;
  6. 96          res_data <= 48'd0;
  7. 97          res_flag <= 1'b0;
  8. 98          res_bit_cnt <= 6'd0;
  9. 99      end   
  10. 100     else begin
  11. 101         //sd_miso = 0 开始接收响应数据
  12. 102         if(sd_miso == 1'b0&& res_flag == 1'b0) begin
  13. 103             res_flag <= 1'b1;
  14. 104             res_data <= {res_data[46:0,sd_miso};
  15. 105             res_bit_cnt <= res_bit_cnt + 6'd1;
  16. 106             res_en <= 1'b0;
  17. 107         end   
  18. 108         else if(res_flag) begin
  19. 109            //R1返回1个字节,R3 R7返回5个字节
  20. 110            //在这里统一按照6个字节来接收,多出的1个字节为NOP(8个时钟周期的延时)
  21. 111             res_data <= {res_data[46:0,sd_miso};     
  22. 112             res_bit_cnt <= res_bit_cnt + 6'd1;
  23. 113             if(res_bit_cnt == 6'd47) begin
  24. 114                 res_flag <= 1'b0;
  25. 115                 res_bit_cnt <= 6'd0;
  26. 116                 res_en <= 1'b1;
  27. 117             end               
  28. 118         end  
  29. 119         else
  30. 120             res_en <= 1'b0;         
  31. 121     end
  32. 122 end
复制代码
在上述代码的always语句块中,我们使用div_clk_180deg(同sd_clk)的上升沿采集SD卡返回的信号,而其它语句块使用div_clk时钟(sd_clk相位偏差180度的时钟)来操作,这是因为SD卡的SPI模式下SD卡的上升沿锁存(采集)数据,在下降沿的时候更新(发送)数据,所以在SD卡的上升沿采集数据是数据保持稳定的时刻,以确保采集的数据不会发送错误。我们知道,SD卡在初始化过程中共返回三种响应类型,分别为R1、R3和R7,其中返回的R3类型和R7类型中包含R1类型,R1类型最高位固定为0,而SD_MISO在空闲时是为高电平状态,因此我们可以通过判断SD_MISO引脚拉低作为开始接收响应信号的条件。
图 46.4.4 和图 46.4.5为SD卡初始化过程中Fabric Debugger抓取的波形图,从图中我们可以清晰的看到在SD卡正在初始化过程中,其中cur_state是初始化过程的状态机跳变,其中1,2,4,8,16,32分别对应代码中的st_idle,st_send_cmd0,st_wait_cmd0,st_send_cmd8,st_send_cmd55,st_send_acmd41这6个状态,是根据SD卡的数据手册进行的初始化操作(其中16和32两个状态可能会重复多次,直到SD卡给出正确应答),在没有初始化完成时sd_init_done信号是处于低电平的,当SD卡的状态机进入64也就是st_init_done时SD卡就算初始化成功了,此时sd_init_done(初始化完成标志)拉高说明SD卡初始化完成,sd_cs信号拉高,SD卡进入空闲状态,等待下一步操作。
image039.png
图 46.4.4SD卡初始化前的Debugger波形图

image041.png
图 46.4.5SD卡初始化完成的Debugger波形图
SD卡写操作模块的代码如下:
  1. 1   module sd_write(
  2. 2       input                clk_ref       ,  //时钟信号
  3. 3       input                clk_ref_180deg,  //时钟信号,与clk_ref相位相差180度
  4. 4       input                rst_n         ,  //复位信号,低电平有效
  5. 5       //SD卡接口
  6. 6       input                sd_miso       ,  //SD卡SPI串行输入数据信号
  7. 7       output reg          sd_cs         ,  //SD卡SPI片选信号  
  8. 8       output reg          sd_mosi       ,  //SD卡SPI串行输出数据信号
  9. 9       //用户写接口   
  10. 10      input                wr_start_en   ,  //开始写SD卡数据信号
  11. 11      input        [31:0 wr_sec_addr   ,  //写数据扇区地址
  12. 12      input        [15:0  wr_data       ,  //写数据                          
  13. 13      output reg          wr_busy       ,  //写数据忙信号
  14. 14      output reg          wr_req            //写数据请求信号
  15. 15      );
  16. 16  
  17. 17  //parameter define
  18. 18  parameter  HEAD_BYTE = 8'hfe    ;         //数据头
  19. 19                                 
  20. 20  //reg define                    
  21. 21  reg           wr_en_d0         ;         //wr_start_en信号延时打拍
  22. 22  reg           wr_en_d1         ;   
  23. 23  reg           res_en           ;         //接收SD卡返回数据有效信号      
  24. 24  reg    [7:0  res_data         ;         //接收SD卡返回数据                 
  25. 25  reg           res_flag         ;         //开始接收返回数据的标志
  26. 26  reg    [5:0  res_bit_cnt      ;         //接收位数据计数器                  
  27. 27                                 
  28. 28  reg    [3:0  wr_ctrl_cnt      ;         //写控制计数器
  29. 29  reg    [47:0  cmd_wr           ;         //写命令
  30. 30  reg    [5:0  cmd_bit_cnt      ;         //写命令位计数器
  31. 31  reg    [3:0   bit_cnt          ;         //写数据位计数器
  32. 32  reg    [8:0  data_cnt         ;         //写入数据数量
  33. 33  reg    [15:0 wr_data_t        ;         //寄存写入的数据,防止发生改变
  34. 34  reg           detect_done_flag ;         //检测写空闲信号的标志
  35. 35  reg    [7:0  detect_data      ;         //检测到的数据
  36. 36  
  37. 37  //wire define
  38. 38  wire          pos_wr_en        ;         //开始写SD卡数据信号的上升沿
  39. 39  
  40. 40  //*****************************************************
  41. 41  //**                   main code
  42. 42  //*****************************************************
  43. 43  
  44. 44  assign  pos_wr_en = (~wr_en_d1) & wr_en_d0;
  45. 45  
  46. 46  //wr_start_en信号延时打拍
  47. 47  always @(posedge clk_ref or negedge rst_n)begin
  48. 48      if(!rst_n) begin
  49. 49          wr_en_d0 <= 1'b0;
  50. 50          wr_en_d1 <= 1'b0;
  51. 51      end   
  52. 52      else begin
  53. 53          wr_en_d0 <= wr_start_en;
  54. 54          wr_en_d1 <= wr_en_d0;
  55. 55      end        
  56. 56  end
  57. 57  
  58. 58  //接收sd卡返回的响应数据
  59. 59  //在clk_ref_180deg(sd_clk)的上升沿锁存数据
  60. 60  always @(posedge clk_ref_180deg or negedge rst_n)begin
  61. 61      if(!rst_n) begin
  62. 62          res_en <= 1'b0;
  63. 63          res_data <= 8'd0;
  64. 64          res_flag <= 1'b0;
  65. 65          res_bit_cnt <= 6'd0;
  66. 66      end   
  67. 67      else begin
  68. 68          //sd_miso = 0 开始接收响应数据
  69. 69          if(sd_miso == 1'b0 && res_flag == 1'b0)begin
  70. 70              res_flag <= 1'b1;
  71. 71              res_data <= {res_data[6:0,sd_miso};
  72. 72              res_bit_cnt <= res_bit_cnt + 6'd1;
  73. 73              res_en <= 1'b0;
  74. 74          end
  75. 75          else if(res_flag) begin
  76. 76              res_data <= {res_data[6:0,sd_miso};
  77. 77              res_bit_cnt <= res_bit_cnt + 6'd1;
  78. 78              if(res_bit_cnt == 6'd7)begin
  79. 79                  res_flag <= 1'b0;
  80. 80                  res_bit_cnt<= 6'd0;
  81. 81                  res_en <= 1'b1;
  82. 82              end               
  83. 83          end
  84. 84          else
  85. 85              res_en <= 1'b0;
  86. 86      end
  87. 87  end
复制代码
SD卡写数据模块主要完成对SD卡的写操作,在程序的第18定义了一个参数HEAD_BYTE(SD卡写数据头),在开始写入有效数据之前,必须先发送数据头8’hfe。在代码的第60行开始的always语句块中,同样使用clk_ref_180deg(sd_clk)的上升沿采集数据,其原因同SD初始化模块一样,clk_ref_180deg的上升沿是数据保持稳定的时刻,采集的数据不会发生错误。
  1. 89  //写完数据后检测SD卡是否空闲
  2. 90  always @(posedge clk_ref or negedge rst_n)begin
  3. 91      if(!rst_n)
  4. 92          detect_data <= 8'd0;
  5. 93      else if(detect_done_flag)
  6. 94          detect_data <= {detect_data[6:0,sd_miso};
  7. 95      else
  8. 96          detect_data <= 8'd0;
  9. 97  end
  10. 98  
  11. 99  //SD卡写入数据
  12. 100 always @(posedge clk_ref or negedge rst_n)begin
  13. 101     if(!rst_n) begin
  14. 102         sd_cs <= 1'b1;
  15. 103         sd_mosi <= 1'b1;
  16. 104         wr_ctrl_cnt <= 4'd0;
  17. 105         wr_busy <= 1'b0;
  18. 106         cmd_wr <= 48'd0;
  19. 107         cmd_bit_cnt <= 6'd0;
  20. 108         bit_cnt <= 4'd0;
  21. 109         wr_data_t <= 16'd0;
  22. 110         data_cnt <= 9'd0;
  23. 111         wr_req <= 1'b0;
  24. 112         detect_done_flag <= 1'b0;
  25. 113     end
  26. 114     else begin
  27. 115         wr_req <= 1'b0;
  28. 116         case(wr_ctrl_cnt)
  29. 117             4'd0 : begin
  30. 118                 wr_busy <= 1'b0;                            //写空闲
  31. 119                 sd_cs <= 1'b1;                                 
  32. 120                 sd_mosi <= 1'b1;                              
  33. 121                 if(pos_wr_en)begin                          
  34. 122                     cmd_wr <= {8'h58,wr_sec_addr,8'hff};  //写入单个命令块CMD24
  35. 123                    wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;      //控制计数器加1
  36. 124                     //开始执行写入数据,拉高写忙信号
  37. 125                     wr_busy <= 1'b1;
  38. 126                 end
  39. 127             end
  40. 128             4'd1 : begin
  41. 129                 if(cmd_bit_cnt <= 6'd47) begin            //开始按位发送写命令
  42. 130                    cmd_bit_cnt <= cmd_bit_cnt + 6'd1;
  43. 131                     sd_cs <= 1'b0;
  44. 132                     sd_mosi <= cmd_wr[6'd47 - cmd_bit_cnt]; //先发送高字节                 
  45. 133                 end
  46. 134                 else begin
  47. 135                     sd_mosi <= 1'b1;
  48. 136                     if(res_en)begin                      //SD卡响应
  49. 137                        wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;  //控制计数器加1
  50. 138                        cmd_bit_cnt <= 6'd0;
  51. 139                        bit_cnt <= 4'd1;
  52. 140                     end
  53. 141                 end
  54. 142             end                                                                                                   
  55. 143             4'd2 : begin
  56. 144                 bit_cnt <= bit_cnt + 4'd1;
  57. 145                 //bit_cnt = 0~7 等待8个时钟周期
  58. 146                 //bit_cnt = 8~15,写入命令头8'hfe
  59. 147                 if(bit_cnt>=4'd8 && bit_cnt <= 4'd15) begin
  60. 148                     sd_mosi <= HEAD_BYTE[4'd15-bit_cnt];    //先发送高字节
  61. 149                     if(bit_cnt == 4'd14)                       
  62. 150                        wr_req <= 1'b1;                     //提前拉高写数据请求信号
  63. 151                     else if(bit_cnt == 4'd15)                  
  64. 152                        wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;  //控制计数器加1
  65. 153                 end                                            
  66. 154             end                                               
  67. 155             4'd3 : begin                                    //写入数据
  68. 156                 bit_cnt <= bit_cnt + 4'd1;                    
  69. 157                 if(bit_cnt == 4'd0)begin                    
  70. 158                     sd_mosi <= wr_data[4'd15-bit_cnt];      //先发送数据高位
  71. 159                    wr_data_t <= wr_data;                  //寄存数据
  72. 160                 end                                            
  73. 161                 else                                          
  74. 162                     sd_mosi <= wr_data_t[4'd15-bit_cnt];    //先发送数据高位
  75. 163                 if((bit_cnt == 4'd14)&& (data_cnt <= 9'd255))
  76. 164                     wr_req <= 1'b1;
  77. 165                 if(bit_cnt == 4'd15)begin
  78. 166                     data_cnt<= data_cnt + 9'd1;
  79. 167                     //写入单个BLOCK共512个字节 = 256 *16bit
  80. 168                     if(data_cnt == 9'd255)begin
  81. 169                        data_cnt <= 9'd0;
  82. 170                         //写入数据完成,控制计数器加1         
  83. 171                        wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;     
  84. 172                     end                                       
  85. 173                 end                                            
  86. 174             end      
  87. 175             //写入2个字节CRC校验,由于SPI模式下不检测校验值,此处写入两个字节的8'hff                                         
  88. 176             4'd4 : begin                                       
  89. 177                 bit_cnt <= bit_cnt + 4'd1;
  90. 178                 sd_mosi <= 1'b1;
  91. 179                 //crc写入完成,控制计数器加1
  92. 180                 if(bit_cnt == 4'd15)
  93. 181                    wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;
  94. 182             end
  95. 183             4'd5 : begin
  96. 184                 if(res_en)                               //SD卡响应
  97. 185                    wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;
  98. 186             end
  99. 187             4'd6 : begin                                    //等待写完成
  100. 188                detect_done_flag <= 1'b1;
  101. 189                 //detect_data = 8'hff时,SD卡写入完成,进入空闲状态
  102. 190                 if(detect_data == 8'hff)begin
  103. 191                    wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;
  104. 192                    detect_done_flag <= 1'b0;
  105. 193                 end
  106. 194             end
  107. 195             default : begin
  108. 196                 //进入空闲状态后,拉高片选信号,等待8个时钟周期
  109. 197                 sd_cs <= 1'b1;   
  110. 198                 wr_ctrl_cnt <= wr_ctrl_cnt + 4'd1;
  111. 199             end     
  112. 200         endcase
  113. 201     end
  114. 202 end            
  115. 203
  116. 204 endmodule
复制代码
在代码第100行开始的always语句块中,使用写计数控制器(wr_ctrl_cnt)控制写入的流程。其流程为首先检测开始写入数据信号(wr_start_en)的上升沿,检测到上升沿之后开始发送写命令(CMD24);写命令发送完成等待SD卡返回响应信号;SD卡返回响应命令后,等待8个时钟周期,随后写入数据头和数据,注意写数据之前写请求信号要提前拉高,以保证写入数据时刻数据是有效的;发送完数据之后,再次发送两个字节的CRC校验值,由于SPI模式下不对数据做校验,这里发送两个字节的8’hff,然后等待SD卡返回响应数据。在接收完SD卡的响应之后给出标志detect_done_flag,以检测SD卡是否进入空闲状态。当SD卡进入空闲状态后等待8个时钟周期即可重新检测开始写入数据信号(wr_start_en)的上升沿。
图 46.4.6为SD卡写数据过程中Debugge抓取的波形图。从图中可以看出,在检测到wr_start_en信号为高电平时开始写数据状态(wr_start_en只拉高一次作为写开始标志),此时wr_busy(写忙信号)开始拉高,sd_cs片选信号拉低,开始对SD卡写命令和数据,当SD卡准备好写入数据时wr_req拉高,每拉高一次写入一个数据,共写入数据256次。在数据及CRC校验写完后detect_done_flag信号拉高,开始等待SD卡空闲。
image043.png
图 46.4.6SD卡写数据Fabric Debugger波形图
图 46.4.7为数据写完后抓取到的Fabric Debugger波形图。从图中可以看出,SD卡写数据完成wr_data(写数据线)锁定在256,片选信号sd_cs拉高,wr_busy(写忙信号)拉低,detect_done_flag信号由高拉低,说明SD卡再次处于空闲状态此时可以进行下一次写操作。
image045.png
图 46.4.7SD卡数据写完成Fabric Debugger波形图
SD卡读操作模块的代码如下:
  1. 1   module sd_read(
  2. 2       input                clk_ref       ,  //时钟信号
  3. 3       input                clk_ref_180deg,  //时钟信号,与clk_ref相位相差180度
  4. 4       input                rst_n         ,  //复位信号,低电平有效
  5. 5       //SD卡接口
  6. 6       input                sd_miso       ,  //SD卡SPI串行输入数据信号
  7. 7       output reg          sd_cs         ,  //SD卡SPI片选信号
  8. 8       output reg          sd_mosi       ,  //SD卡SPI串行输出数据信号
  9. 9       //用户读接口   
  10. 10      input                rd_start_en   ,  //开始读SD卡数据信号
  11. 11      input        [31:0 rd_sec_addr   ,  //读数据扇区地址                        
  12. 12      output reg          rd_busy       ,  //读数据忙信号
  13. 13      output reg          rd_val_en     ,  //读数据有效信号
  14. 14      output reg  [15:0 rd_val_data       //读数据
  15. 15      );
  16. 16  
  17. 17  //reg define
  18. 18  reg           rd_en_d0      ;            //rd_start_en信号延时打拍
  19. 19  reg           rd_en_d1      ;                                
  20. 20  reg           res_en        ;            //接收SD卡返回数据有效信号      
  21. 21  reg    [7:0  res_data      ;            //接收SD卡返回数据                  
  22. 22  reg           res_flag      ;            //开始接收返回数据的标志            
  23. 23  reg    [5:0  res_bit_cnt   ;            //接收位数据计数器                  
  24. 24                              
  25. 25  reg           rx_en_t       ;            //接收SD卡数据使能信号
  26. 26  reg    [15:0 rx_data_t     ;            //接收SD卡数据
  27. 27  reg           rx_flag       ;            //开始接收的标志
  28. 28  reg    [3:0  rx_bit_cnt    ;            //接收数据位计数器
  29. 29  reg    [8:0  rx_data_cnt   ;            //接收的数据个数计数器
  30. 30  reg           rx_finish_en  ;            //接收完成使能信号
  31. 31                              
  32. 32  reg    [3:0  rd_ctrl_cnt   ;            //读控制计数器
  33. 33  reg    [47:0  cmd_rd        ;            //读命令
  34. 34  reg    [5:0  cmd_bit_cnt   ;            //读命令位计数器
  35. 35  reg           rd_data_flag  ;            //准备读取数据的标志
  36. 36  
  37. 37  //wire define
  38. 38  wire          pos_rd_en     ;            //开始读SD卡数据信号的上升沿
  39. 39  
  40. 40  //*****************************************************
  41. 41  //**                   main code
  42. 42  //*****************************************************
  43. 43  
  44. 44  assign  pos_rd_en = (~rd_en_d1) & rd_en_d0;
  45. 45  
  46. 46  //rd_start_en信号延时打拍
  47. 47  always @(posedge clk_ref or negedge rst_n)begin
  48. 48      if(!rst_n) begin
  49. 49          rd_en_d0 <= 1'b0;
  50. 50          rd_en_d1 <= 1'b0;
  51. 51      end
  52. 52      else begin
  53. 53          rd_en_d0 <= rd_start_en;
  54. 54          rd_en_d1 <= rd_en_d0;
  55. 55      end
  56. 56  end
  57. 57  
  58. 58  //接收sd卡返回的响应数据
  59. 59  //在clk_ref_180deg(sd_clk)的上升沿锁存数据
  60. 60  always @(posedge clk_ref_180deg or negedge rst_n)begin
  61. 61      if(!rst_n) begin
  62. 62          res_en <= 1'b0;
  63. 63          res_data <= 8'd0;
  64. 64          res_flag <= 1'b0;
  65. 65          res_bit_cnt <= 6'd0;
  66. 66      end   
  67. 67      else begin
  68. 68          //sd_miso = 0 开始接收响应数据
  69. 69          if(sd_miso == 1'b0 && res_flag == 1'b0)begin
  70. 70              res_flag <= 1'b1;
  71. 71              res_data <= {res_data[6:0,sd_miso};
  72. 72              res_bit_cnt <= res_bit_cnt + 6'd1;
  73. 73              res_en <= 1'b0;
  74. 74          end
  75. 75          else if(res_flag) begin
  76. 76              res_data <= {res_data[6:0,sd_miso};
  77. 77              res_bit_cnt <= res_bit_cnt + 6'd1;
  78. 78              if(res_bit_cnt == 6'd7)begin
  79. 79                  res_flag <= 1'b0;
  80. 80                  res_bit_cnt<= 6'd0;
  81. 81                  res_en <= 1'b1;
  82. 82              end
  83. 83          end
  84. 84          else
  85. 85              res_en <= 1'b0;
  86. 86      end
  87. 87  end
  88. 88  
  89. 89  //接收SD卡有效数据
  90. 90  //在clk_ref_180deg(sd_clk)的上升沿锁存数据
  91. 91  always @(posedge clk_ref_180deg or negedge rst_n)begin
  92. 92      if(!rst_n) begin
  93. 93          rx_en_t <= 1'b0;
  94. 94          rx_data_t <= 16'd0;
  95. 95          rx_flag <= 1'b0;
  96. 96          rx_bit_cnt <= 4'd0;
  97. 97          rx_data_cnt <= 9'd0;
  98. 98          rx_finish_en <= 1'b0;
  99. 99      end   
  100. 100     else begin
  101. 101         rx_en_t <= 1'b0;
  102. 102         rx_finish_en <= 1'b0;
  103. 103         //数据头0xfe 8'b1111_1110,所以检测0为起始位
  104. 104         if(rd_data_flag && sd_miso == 1'b0 && rx_flag == 1'b0)   
  105. 105             rx_flag <= 1'b1;   
  106. 106         else if(rx_flag) begin
  107. 107             rx_bit_cnt <= rx_bit_cnt + 4'd1;
  108. 108             rx_data_t <= {rx_data_t[14:0,sd_miso};
  109. 109             if(rx_bit_cnt == 4'd15)begin
  110. 110                 rx_data_cnt <= rx_data_cnt + 9'd1;
  111. 111                 //接收单个BLOCK共512个字节 = 256 *16bit
  112. 112                 if(rx_data_cnt <= 9'd255)
  113. 113                     rx_en_t <= 1'b1;
  114. 114                 else if(rx_data_cnt == 9'd257) begin   //接收两个字节的CRC校验值
  115. 115                     rx_flag <= 1'b0;
  116. 116                    rx_finish_en <= 1'b1;                //数据接收完成
  117. 117                    rx_data_cnt <= 9'd0;               
  118. 118                    rx_bit_cnt <= 4'd0;
  119. 119                 end   
  120. 120             end               
  121. 121         end      
  122. 122         else
  123. 123             rx_data_t <= 16'd0;
  124. 124     end   
  125. 125 end
复制代码
SD卡读数据模块主要完成对SD卡的读操作。在代码的第60行开始的always语句块和第91行开始的always语句块中,同样采用clk_ref_180deg的上升沿采集数据,其原因同SD初始化模块一样,clk_ref_180deg的上升沿是数据保持稳定的时刻,采集的数据不会发生错误。
  1. 127 //寄存输出数据有效信号和数据
  2. 128 always @(posedge clk_ref or negedge rst_n)begin
  3. 129     if(!rst_n) begin
  4. 130         rd_val_en <= 1'b0;
  5. 131         rd_val_data <= 16'd0;
  6. 132     end
  7. 133     else begin
  8. 134         if(rx_en_t)begin
  9. 135             rd_val_en <= 1'b1;
  10. 136             rd_val_data <= rx_data_t;
  11. 137         end
  12. 138         else
  13. 139             rd_val_en <= 1'b0;
  14. 140     end
  15. 141 end              
  16. 142
  17. 143 //读命令
  18. 144 always @(posedge clk_ref or negedge rst_n)begin
  19. 145     if(!rst_n) begin
  20. 146         sd_cs <= 1'b1;
  21. 147         sd_mosi <= 1'b1;        
  22. 148         rd_ctrl_cnt <= 4'd0;
  23. 149         cmd_rd <= 48'd0;
  24. 150         cmd_bit_cnt <= 6'd0;
  25. 151         rd_busy <= 1'b0;
  26. 152         rd_data_flag <= 1'b0;
  27. 153     end   
  28. 154     else begin
  29. 155         case(rd_ctrl_cnt)
  30. 156             4'd0 : begin
  31. 157                 rd_busy <= 1'b0;
  32. 158                 sd_cs <= 1'b1;
  33. 159                 sd_mosi <= 1'b1;
  34. 160                 if(pos_rd_en)begin
  35. 161                     cmd_rd <= {8'h51,rd_sec_addr,8'hff};  //写入单个命令块CMD17
  36. 162                    rd_ctrl_cnt <= rd_ctrl_cnt + 4'd1;      //控制计数器加1
  37. 163                     //开始执行读取数据,拉高读忙信号
  38. 164                     rd_busy <= 1'b1;                     
  39. 165                 end   
  40. 166             end
  41. 167             4'd1 : begin
  42. 168                 if(cmd_bit_cnt <= 6'd47) begin            //开始按位发送读命令
  43. 169                    cmd_bit_cnt <= cmd_bit_cnt + 6'd1;
  44. 170                     sd_cs <= 1'b0;
  45. 171                     sd_mosi <= cmd_rd[6'd47 - cmd_bit_cnt]; //先发送高字节
  46. 172                 end   
  47. 173                 else begin                                 
  48. 174                     sd_mosi <= 1'b1;
  49. 175                     if(res_en)begin                      //SD卡响应
  50. 176                        rd_ctrl_cnt <= rd_ctrl_cnt + 4'd1;  //控制计数器加1
  51. 177                        cmd_bit_cnt <= 6'd0;
  52. 178                     end   
  53. 179                 end   
  54. 180             end   
  55. 181             4'd2 : begin
  56. 182                 //拉高rd_data_flag信号,准备接收数据
  57. 183                 rd_data_flag<= 1'b1;                       
  58. 184                 if(rx_finish_en)begin                    //数据接收完成
  59. 185                    rd_ctrl_cnt <= rd_ctrl_cnt + 4'd1;
  60. 186                    rd_data_flag <= 1'b0;
  61. 187                     sd_cs <= 1'b1;
  62. 188                 end
  63. 189             end        
  64. 190             default : begin
  65. 191                 //进入空闲状态后,拉高片选信号,等待8个时钟周期
  66. 192                 sd_cs <= 1'b1;   
  67. 193                 rd_ctrl_cnt <= rd_ctrl_cnt + 4'd1;
  68. 194             end   
  69. 195         endcase
  70. 196     end         
  71. 197 end
  72. 198
  73. 199 endmodule
复制代码
在代码第144行开始的always语句块中,使用读计数控制器(rd_ctrl_cnt)控制读取数据的流程。其流程为检测开始读取数据信号(rd_start_en)的上升沿,检测到上升沿之后开始发送读命令(CMD17);读命令发送完成之后等待SD卡返回响应信号;SD卡返回响应命令后,准备接收SD卡的数据头,因为SD卡的数据头为8’hfe = 8’b1111_1110,所以我们只需要检测SD_MISO输入引脚的第一个低电平即可检测到数据头;检测到数据头之后,紧跟后面的就是256个16位数据和两个字节的CRC校验值,我们只需接收有效数据,CRC的校验值可不用关心;CRC校验接收完成后等待8个时钟周期即可检测开始读取数据信号(rd_start_en)的上升沿,再次对SD卡进行读操作。
图 46.4.8为SD卡读数据过程中Fabric Debugger逻辑分析仪抓取的波形图。从图中可以看出,在检测到rd_start_en后读数据开始,rd_busy(读忙信号)开始拉高,sd_cs片选信号拉低,开始对SD卡发送读命令但是此时rd_val_data读数据没有值,是因为我们一次接收256个16bit数据,此时数据未完全接收,读数据有效信号没有被拉高;当数据接收完成后,读数据有效信号rd_val_en拉高,此时我们可以看到rd_val_data读数据与wr_data写入的数据完全一致(如图 46.4.9所示),所以可以确定我们的读写工程功能是正常可以工作的。
image047.png
图 46.4.8SD卡开始读数据过程Fabric Debugger波形图

image049.png
图 46.4.9SD卡读数据有效波形图
SD卡测试数据产生模块的代码如下:
  1. 1   module data_gen(
  2. 2       input                clk           ,  //时钟信号
  3. 3       input                rst_n         ,  //复位信号,低电平有效
  4. 4       input                sd_init_done  ,  //SD卡初始化完成信号
  5. 5       //写SD卡接口
  6. 6       input                wr_busy       ,  //写数据忙信号
  7. 7       input                wr_req        ,  //写数据请求信号
  8. 8       output reg          wr_start_en   ,  //开始写SD卡数据信号
  9. 9       output reg  [31:0 wr_sec_addr   ,  //写数据扇区地址
  10. 10      output       [15:0  wr_data       ,  //写数据
  11. 11      //读SD卡接口
  12. 12      input                rd_val_en     ,  //读数据有效信号
  13. 13      input        [15:0 rd_val_data   ,  //读数据
  14. 14      output reg          rd_start_en   ,  //开始写SD卡数据信号
  15. 15      output reg  [31:0 rd_sec_addr   ,  //读数据扇区地址
  16. 16      
  17. 17      output               error_flag        //SD卡读写错误的标志
  18. 18      );
  19. 19  
  20. 20  //reg define
  21. 21  reg             sd_init_done_d0  ;       //sd_init_done信号延时打拍
  22. 22  reg             sd_init_done_d1  ;      
  23. 23  reg             wr_busy_d0       ;       //wr_busy信号延时打拍
  24. 24  reg             wr_busy_d1       ;
  25. 25  reg    [15:0   wr_data_t        ;   
  26. 26  reg    [15:0   rd_comp_data     ;       //用于对读出数据作比较的正确数据
  27. 27  reg    [8:0    rd_right_cnt     ;       //读出正确数据的个数
  28. 28  
  29. 29  //wire define
  30. 30  wire            pos_init_done    ;       //sd_init_done信号的上升沿,用于启动写入信号
  31. 31  wire            neg_wr_busy      ;       //wr_busy信号的下降沿,用于判断数据写入完成
  32. 32  
  33. 33  //*****************************************************
  34. 34  //**                   main code
  35. 35  //*****************************************************
  36. 36  
  37. 37  assign  pos_init_done= (~sd_init_done_d1) & sd_init_done_d0;
  38. 38  assign  neg_wr_busy = wr_busy_d1 & (~wr_busy_d0);
  39. 39  //wr_data_t变化范围0~256;wr_data范围:0~255
  40. 40  assign  wr_data = (wr_data_t > 16'd0)  ?  (wr_data_t - 1'b1) : 16'd0;
  41. 41  //读256次正确的数据,说明读写测试成功,error_flag = 0
  42. 42  assign  error_flag = (rd_right_cnt == (9'd256))  ?  1'b0 : 1'b1;
  43. 43  
  44. 44  //sd_init_done信号延时打拍
  45. 45  always @(posedge clk or negedge rst_n)begin
  46. 46      if(!rst_n) begin
  47. 47          sd_init_done_d0 <= 1'b0;
  48. 48          sd_init_done_d1 <= 1'b0;
  49. 49      end
  50. 50      else begin
  51. 51          sd_init_done_d0 <= sd_init_done;
  52. 52          sd_init_done_d1 <= sd_init_done_d0;
  53. 53      end        
  54. 54  end
  55. 55  
  56. 56  //SD卡写入信号控制
  57. 57  always @(posedge clk or negedge rst_n)begin
  58. 58      if(!rst_n) begin
  59. 59          wr_start_en <= 1'b0;
  60. 60          wr_sec_addr <= 32'd0;
  61. 61      end   
  62. 62      else begin
  63. 63          if(pos_init_done)begin
  64. 64              wr_start_en <= 1'b1;
  65. 65              wr_sec_addr <= 32'd20000;         //任意指定一块扇区地址
  66. 66          end   
  67. 67          else
  68. 68              wr_start_en <= 1'b0;
  69. 69      end   
  70. 70  end
  71. 71  
  72. 72  //SD卡写数据
  73. 73  always @(posedge clk or negedge rst_n)begin
  74. 74      if(!rst_n)
  75. 75          wr_data_t <= 16'b0;
  76. 76      else if(wr_req)
  77. 77          wr_data_t <= wr_data_t + 16'b1;
  78. 78      
  79. 79  end
  80. 80  
  81. 81  //wr_busy信号延时打拍
  82. 82  always @(posedge clk or negedge rst_n)begin
  83. 83      if(!rst_n) begin
  84. 84          wr_busy_d0 <= 1'b0;
  85. 85          wr_busy_d1 <= 1'b0;
  86. 86      end   
  87. 87      else begin
  88. 88          wr_busy_d0 <= wr_busy;
  89. 89          wr_busy_d1 <= wr_busy_d0;
  90. 90      end
  91. 91  end
  92. 92  
  93. 93  //SD卡读出信号控制
  94. 94  always @(posedge clk or negedge rst_n)begin
  95. 95      if(!rst_n) begin
  96. 96          rd_start_en <= 1'b0;
  97. 97          rd_sec_addr <= 32'd0;   
  98. 98      end
  99. 99      else begin
  100. 100         if(neg_wr_busy)begin
  101. 101             rd_start_en <= 1'b1;
  102. 102             rd_sec_addr <= 32'd20000;
  103. 103         end   
  104. 104         else
  105. 105             rd_start_en <= 1'b0;         
  106. 106     end   
  107. 107 end   
  108. 108
  109. 109 //读数据错误时给出标志
  110. 110 always @(posedge clk or negedge rst_n)begin
  111. 111     if(!rst_n) begin
  112. 112         rd_comp_data <= 16'd0;
  113. 113         rd_right_cnt <= 9'd0;
  114. 114     end     
  115. 115     else begin
  116. 116         if(rd_val_en)begin
  117. 117             rd_comp_data <= rd_comp_data + 16'b1;
  118. 118             if(rd_val_data == rd_comp_data)
  119. 119                 rd_right_cnt<= rd_right_cnt + 9'd1;  
  120. 120         end   
  121. 121     end        
  122. 122 end
  123. 123
  124. 124 endmodule
复制代码
在代码的第45行开始的always语句块中,对SD卡控制器的初始化完成信号(sd_init_done)打两拍,以检测SD卡的上升沿,用于发起SD卡的写入操作。在代码的第57行开始的always块中,当检测到SD卡的上升沿之后,开始发起写入操作,即wr_start_en(开始写SD卡数据信号)和wr_sec_addr(wr_sec_addr)。这里需要注意的是,写扇区地址并没有从0开始写,0扇区地址为SD卡的分区引导记录区,如果从0扇区地址开始写入测试数据会破坏SD卡的FAT文件系统,因此我们随意指定SD卡的中间数据区域开始测试,这里是从扇区地址20000开始写入,读出扇区地址和写扇区地址保持一致。
测试数据写完后(wr_busy写忙信号的下降沿代表写完数据),就开始从SD卡中读出数据。需要注意的是代码中的error_flag默认是高电平,即错误状态,正确读取到256次(单个扇区为512个字节,因为接口封装成16位,因此需要读取256次)时才会变成低电平,目的是在未插入SD卡时,SD卡无法完成初始化,也就无法对SD卡进行读写测试,如果此时默认低电平的话,会误以为读写测试正确。

1.5 下载验证
首先我们将SD卡(TF卡)插入开发板的SD卡插槽,注意带有金属引脚的一面朝下,接下来将下载器一端连接电脑,另一端与开发板上对应端口连接,最后连接电源线并打开电源开关。
注意:本次实验是基于市面上常用的SD2.0版本协议的MicroSD卡,存储容量为2GB至32GB。
打开工程,下载完成后开发板上的LED灯常亮,说明从SD卡读出的512个字节(256个16位数据)与写入的数据相同,SD卡读写测试程序下载验证成功。
image051.png
图 46.5.1 实物图
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

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

本版积分规则



关闭

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

正点原子公众号

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

GMT+8, 2024-11-22 17:42

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

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