14. sd卡数据读写控制

SD存储卡是一种基于半导体快闪记忆器的新一代记忆设备,由于它体积小、数据传输速度快、可热插拔等优良的特性,被广泛地于便携式装置上使用,例如数码相机、智能手机和多媒体播放器等。

本章节中我们要学习掌握SD卡相关知识,运用所学知识,结合前面学到的设计方法,设计一个SD卡数据读写控制器,实现SD卡数据读写操作,并上板验证。

14.1. 理论学习

14.1.1. sd卡简介

要说SD卡,就要先介绍一下MMC卡。MMC(Multi-Media Card,多媒体卡),由Siemens(西门子)和SanDisk(闪迪)于1997年推出。由于它的封装技术较为先进,7针引脚,体积小、重量轻、非常符合移动存储的需要。MMC支持1bit模式,20MHz时钟,采用总线结构。

SD卡是在MMC卡的基础上发展而来,全称Secure Digital Card,译为安全数字卡,简称SD卡。由松下电器、东芝和闪迪联合推出,1999年8月发布。SD卡的数据传送和物理规范与MMC相似,大小和MMC卡差不多,尺寸为32mm x 24mm x 2.1mm。长宽和MMC卡一样,只是比MMC卡厚了0.7mm,以容纳更大容量的存贮单元。SD卡与 MMC 卡保持着向上的兼容,MMC卡可以被新的SD设备存取,兼容性则取决于应用软件,但SD卡却不可以被MMC设备存取。

MMC卡、SD卡图片,具体见图 64‑1。

SD002

图 64‑1 MMC卡和SD卡

上图中的SD卡只是SD卡的一种,按照外形与尺寸,SD卡可分为三类:标准SD卡、MiniSD卡和MicroSD卡。

标准SD是规格最大的一种,今天市面上大多数消费级数字相机和摄像机均使用此标准的SD卡,这种卡有标准的“缺角”设计。

Mini SD卡,在性能上与标准SD卡并无太大区别,主要是尺寸更小。Mini SD卡特点是体积小巧、性能稳定,配合专用转接卡使用,可完全兼容标准SD卡插槽。而且Mini SD卡采用的是低耗电的设计,比标准SD卡更适用于移动通信设备,因此主要进攻手机、PDA、掌上电脑的信息终端。Mini SD卡已被更加小巧的Micro SD 卡取代,市面上已很难见到。大部分的标准SD卡和Mini SD卡卡身都有物理的写保护开关。

Micro SD 卡,原名Trans-flash Card(TF卡),在2004年正式更名为Micro SD 卡,由闪迪(SanDisk)公司发明,主要应用在移动手机。

三类SD卡的外形尺寸图,具体见图 64‑2。

SD003

图 64‑2 三类SD卡的外形尺寸图

常规的SD存储卡(非UHS-II卡),只有一排引脚,电压为3.3V,在总线接口中支持默认速度模式、高速模式和UHS-I模式。标准尺寸SD卡有9个引脚(两个VSS)而microSD有8个引脚(一个VSS)。

随后在第一排引脚的基础上增加了第二排引脚,能支持UHS-II、UHS-III总线接口。第二排引脚中包含了1.8V VDD,使UHS-II / UHS-III卡可选择两种不同的电压供应(3.3V或1.8V)。

SD004

图 64‑3 SD卡外观引脚图

其实我们经常谈论或使用的SD卡只是SD家族的一部分。SD作为一项标准,由SD协会制定。它的成功与普及已经造就无数的设备装置、应用及服务。SD协会提供了如图 64‑4所示的简便分类系统,用来分辨主机装置能搭配使用的各种SD标准之种类、尺寸以及其用途与特性。

须注意的是目前众所周知所使用的SD存储卡被分类为“可卸除式随机存取存储器”,拥有SD、SDHC、SDXC以及SDUC四种容量标准。在可卸除式随机存取存储器的分类中还包含了一个内容安全功能可选用,从而使SD存储卡能用来储存受著作权保护的声音、影像、书籍。

SD协会进一步延伸SD标准定义输入/输出(I/O)功能,使消费性电子中普遍采用的SD卡槽不仅只是插入记忆卡储存数据,还可应用SDIO接口来扩充装置添加如网络服务、卫星导航以及拍照等功能。

SD005

图 64‑4 SD家族示意图

14.1.2. SD卡容量标准和速度等级

除了前文提到的外观尺寸分类方式外,SD卡还可按照容量标准进行分类。若按照容量对SD卡进行等级划分,SD卡可分为4个等级,SD(Secure Digital Card,安全数字卡)卡、SDHC卡(Secure Digital High Capacity,高容量安全数字卡)、SDXC卡( SD eXtended Capacity,容量扩大化的安全数字卡)和SDUC(Secure Digital Ultra Capacity,超容量安全数字卡)。现今,市场的主流SD产品是SDHC和SDXC这两种较大容量的存储卡,SD卡因容量过小,已逐渐被市场淘汰,SDUC则是容量太大,预计会出现在未来市场。SD卡的四种容量标准,具体见图 64‑5。

SD006

图 64‑5 SD卡的四种容量标准

不同品牌和厂商生产的SD卡在对存取速度上的定义标准不同,这会使用户在选择SD卡时产生困扰。所以SD协会根据视频匀速写入到SD卡的最低持续速度来划分不同等级,每个等级的速率是以每秒传输多少MB来衡量的,单位为MB/S。

SD协会定义了三种速度等级:速度等级、UHS速度等级与视频速度等级。三种速度等级的具体传输速度如图 64‑6。

SD007

图 64‑6 SD卡速度等级具体传输速度

速度等级

从SD2.0的规范开始,对SD普通卡和高速卡的速度分级是:Class2、Class4、Class6 和Class 10 四个等级。

UHS速度等级

UHS(Ultra High Speed)是与SDXC同时推出的SD卡总线标准。UHS速度等级适用于SDHC和SDXC。

SD协会定义的UHS速度等级是UHS速度等级1(UHS-1)和UHS速度等级3(UHS-3)。U1和U3可应用于UHS总线IF产品系列(UHS-I,UHS-II和UHS-III)。厂商一般会直接在SD卡上标注速度,这可协助消费者透过读写标志来选择所需要的效能。

视频速度等级

视频速度等级定义为能满足高分辨率和高质量4K/8K视频录制的需求,它还具有支持下一代闪存类型(如3D NAND)的重要功能。此外,视频速度等级还涵盖了HD高清(2K)视频的速度。SD 协会定义的视频速度等级为V6, V10, V30,V60和V90。V6和V10可应用于高速和UHS总线IF产品系列。V30可应用于UHS总线IF产品系列。V60和V90可应用于UHS-II / UHS-III产品系列。

14.1.3. SD卡物理结构

SD卡从物理结构看包括5个部分,分别为存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动器,具体见图 64‑7。存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;电源检测单元保证 SD 卡工作在合适的电压下,如出现掉电或上电状态时,它会使控制单元和存储单元接口复位;卡及接口控制单元控制 SD 卡的运行状态,它包括有 8 个寄存器;接口驱动器控制 SD 卡引脚的输入输出。

SD008

图 64‑7 SD卡物理结构图

14.1.3.1. SD卡引脚及连接

常规的SD卡共有9个引脚接口,其中包括3根电源线、1根时钟线、1根命令线和4根数据线。工作模式有两种:SDIO模式和SPI模式(模式3)。各信号简介如下:

  • CLK:同步时钟线,由 SDIO 主机产生,即由 STM32 控制器输出; 使用 SPI 模式时,该引脚与 SPI 总线的 SCK 时钟信号相连;

  • CMD:命令控制线, SDIO 主机通过该线发送命令控制 SD 卡,如果命令要求 SD 卡提供应答(响应), SD 卡也是通过该线传输应答信息; 使用 SPI 模式时,该引脚与 SPI 总线的 MOSI 信号相连, SPI 主机通过它向 SD 卡发送命令及数据,但因为 SPI 总线的MOSI

仅用于主机向从机输出信号,所以 SD 卡返回应答信息时不使用该信号线;

  • DAT0-3:在 SDIO 模式下,它们均为数据线,传输读写数据, SD 卡可将 D0 拉低表示忙状态; 在 SPI 模式下, DAT0 与 SPI 总线的 MISO 信号相连, SD 卡通过该信号线向主机发送数据或响应, DAT3 与总线的 CS 信号相连, SPI 主机通过该信号线选择要通讯的

SD卡。

  • VDD、VSS1、VSS2:电源和地信号,其中 Micro SD 卡不包含 VSS2 信号,即 Micro SD卡仅有 8 根线。

SD卡有两种工作模式,在SDIO模式下,SD卡共使用到CLK、CMD、DAT[3:0]六根信号线;SDIO总线与多张SD卡连接时,可以共用CLK时钟信号线,对于CMD、DAT[3:0]信号线,每张SD卡都要独立连接。SDIO总线与SD卡连接方式,具体见图 64‑8。

SD009

图 64‑8 SDIO总线与SD卡连接方式

在SPI模式下,SD卡共使用到CS(DAT[3])、CLK、MISO(DAT[0])、MOSI(CMD)四根信号线;SPI总线与多张SD卡连接时,除CS片选信号线不可共用外,其他信号均可公用。SPI总线与SD卡连方式,具体见图 64‑9。

SD010

图 64‑9 SPI总线与SD卡连接方式

14.1.3.2. SD卡寄存器

SD 卡总共有 8 个寄存器,用于设定或表示 SD 卡信息,寄存器描述具体见图 64‑10。这些寄存器只能通过对应的命令访问,对 SD 卡的控制操作是通过命令来执行的, SD 卡定义了 64 个命令(部分命令不支持SPI 模式) ,每个命令都有特殊意义,可以实现某一特定功能, SD 卡接收到命令后,根据命令要求对 SD 卡内部寄存器进行修改,程序控制中只需要发送组合命令就可以实现 SD 卡的控制以及读写操作。

SD011

图 64‑10 SD卡寄存器寄存器简要描述

SD卡寄存器详细介绍,读者可参考SD简易规格文件《Part1_Physical_Layer_Simplified_Specification_Ver7.10》。

14.2. sd卡命令控制

14.2.1. sd卡控制时序

与 SD 卡的通信是基于命令和数据传输的。通讯由一个起始位(“0”)开始,由一个停止位(“1”)终止。SD 通信一般是主机发送一个命令(Command),从设备在接收到命令后作出响应(Response),如有需要会有数据(Data)传输参与。SD 卡的基本交互是命令与响应交互,见图 64‑11,图中的 DataIn 和 DataOut 线分别是SPI 的 MISO 及 MOSI 信号。

SD012

图 64‑11 命令与响应交互

SD 数据是以块(Block)形式传输的,SDHC 卡数据块长度一般为 512 字节,数据可以从主机到卡,也可以是从卡到主机。数据块需要 CRC位来保证数据传输成功, CRC 位由SD卡系统硬件生成。单个数据块的读写时序见图 64‑12、图 64‑13。

SD013

图 64‑12 单块读操作

SD014

图 64‑13 单块写操作

读写操作都是由主机发起的,主机发送不同的命令表示读或写, SD 卡接收到命令后先针对命令返回响应。在读操作中, SD 卡返回一个数据块,数据块中包含 CRC 校验码;在写操作中,主机接收到命令响应后需要先发送一个标志(TOKEN)然后紧跟一个要写入的数据块,卡接收完数据块后会返回一个数据响应及忙碌标志,当 SD 卡把接收到的数据写入到内部存储单元完成后,会停止发送忙碌标志,主机确认 SD 卡空闲后,可以发送下一个命令。

SD 数据传输支持单块和多块读写,它们分别对应不同的操作命令, 结束多块读写时需要使用命令来停止操作。

14.2.1.1. SD卡命令

命令格式

SD命令由主机发出,命令格式固定为 48bit,通过 CMD 信号线连续传输。SD命令格式,具体见图 64‑14。

SD015

图 64‑14 命令格式

SD 命令组成如下:

  • 起始位和终止位:命令的主体包含在起始位与终止位之间,它们都只包含一个数据位,起始位为 0,终止位为 1。

  • 传输标志:用于区分传输方向,该位为 1 时表示命令,方向为主机传输到 SD 卡,该位为 0 时表示响应,方向为 SD 卡传输到主机。命令主体内容包括命令、地址信息/参数和 CRC 校验三个部分。

  • 命令号:它固定占用 6bit,所以总共有 64 个命令,每个命令都有特定的用途,部分命令不适用于 SPI 总线,或不适用于 SD 卡操作,只是专门用于 MMC 卡或者 SD I/O 卡。

  • 地址/参数:每个命令有 32bit 地址信息/参数用于命令附加内容,例如,广播命令没有地址信息,这 32bit 用于指定参数,而寻址命令这 32bit 用于指定目标 SD 卡的地址,当使用 SDIO 驱动多张 SD 卡时,通过地址信息区分控制不同的卡,使用SPI

总线驱动时,通过片选引脚来选择不同的卡,所以使用这些命令时地址可填充任意值。

  • CRC7 校验:长度为 7bit 的校验位用于验证命令传输内容正确性,如果发生外部干扰导致传输数据个别位状态改变将导致校准失败,也意味着命令传输失败, SD卡不执行命令。 使用 SDIO 驱动时,命令中必须包含正确的 CRC7 校验值;而使用 SPI 驱动时,命令中的 CRC7

校验默认是关闭的,即这 CRC7 校验位中可以写入任意值而不影响通讯,仅在发送 CMD0 命令时需要强制带标准的 CRC7 校验。

命令说明

SD卡命令可分为标准命令 (如CMD0)和特殊应用命令 (如ACMD41),其中特殊应用命令只有在先写入CMD55命令后才能被识别。按照指令类型又可将SD卡命令分为基本命令、数据块写命令、数据块读命令、擦除命令等12种(class0 ~ class11),部分命令不适用于 SPI 总线,或不适用于 SD 卡操作,只是专门用于 MMC 卡或者 SD I/O 卡。

本章节实验工程将会使用SPI模式实现SD卡的数据读写操作,接下来我们只列举一下SPI模式下常用的SD卡命令,具体见表格 64‑1。

表格 64‑1 SPI模式下常用SD卡命令

命令 |

  • 命令号** |

参数* | ** |

应** | 缩写 | **类型 |

  • 描述 | |
| |

<table border=”1” class=”docutils”> <thead> <tr> <th>* - **基</th> <th>本命令(c</th> <th>lass0)**</th> </tr> </thead> <tbody> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> </tbody> </table>

    • CMD0

    • 0x00

    • [31:0] :填充位 |

    • R1 | LE

    • GO_ID TATE | 有的

    • 复位所 | | IDLE状态 |

    • CMD8

    • 0x08

    • [31:12] :保留位 |

      [11:8] :电源电 | 压(VHS) |

      0 :未定义 |

      1:2.7V | ~ 3.6V

      2 :低电压 |

      4 :保留位 |

      8 :保留位 |

      其它 | :未定义 |

      [7:0]: | 检查模式 |

    • R7 | _I

<pre><code> |

</code></pre>






<pre><code> |

</code></pre>



  • SEND COND | 存储

<pre><code> | 电源

</code></pre>

压。




<pre><code> |

</code></pre>

<pre><code> |

  • 发送sd | | 口条件, | 包括主机 | | 息,并 | 询问卡是 | 否支持电 | | 位应设置 | “0”。 |

</code></pre>




<pre><code> |

</code></pre>

<pre><code> |

</code></pre>

<table border=”1” class=”docutils”> <thead> <tr> <th>* - **读</th> <th>取命令(c</th> <th>lass2)**</th> </tr> </thead> <tbody> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> </tbody> </table>

    • CMD17

    • 0x11

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

    • R1 | E | LE

    • R _SING | 读 LOCK | 对于

    • SD卡数据 | , | | SD卡,读 | 取数据块 | 长度受C | MD16控制 | ;对于SD | HC、SDXC | 卡,读取 | 数据块为 | 固定的5 | 12字节, | 不受CMD | 16控制。 |

<table border=”1” class=”docutils”> <thead> <tr> <th>* - **写</th> <th>入命令(c</th> <th>lass4)**</th> </tr> </thead> <tbody> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> </tbody> </table>

    • CMD24

    • 0x18

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

    • R1 | T |

    • WRI BLOCK | 写 | 对于

    • SD卡数据 | , | | SD卡,写 | 入数据块 | 长度受C | MD16控制 | ;对于SD | HC、SDXC | 卡,写入 | 数据块为 | 固定的5 | 12字节, | 不受CMD | 16控制。 |

<table border=”1” class=”docutils”> <thead> <tr> <th>* - **特殊应</th> <th>用命令(c</th> <th>lass8)**</th> </tr> </thead> <tbody> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td>-</td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> </tbody> </table>

    • CMD55

    • 0x37

    • [31:16 ]:RCA(S | PI模式下 | 未使用) |

      [15:0] :保留位 |

    • R1 | | |


    • APP_CMD | | 命 | 定

      标准
    • 向卡指 | 下一个 | 特 | 用 | 程序的命 | 令而不是 | |

    • ACMD41

    • 0x29

    • [31] :保留位 |

      [30]: | HCS,若 | 主机支持 | SDHC、SD | XC卡,设 | 置为“1” |

      [29:0] :保留位 |

    • R3 | _O

<pre><code> |

| | |

</code></pre>


  • SD_SEND COND | 的卡

<pre><code> |

| | |

</code></pre>


  • 要求访问 | | 操作条件 | 存器( | R)内容 | | | | |


注:SPI模式下,上述各命令中,命令CMD0的CRC7校验为固定的1001_010;命令CMD8的CRC7校验为固定的1000_011;其他命令的CRC7校验在SPI模式下无作用,赋值为1111_111即可。

命令响应

当SD卡接收到命令时,会向SD卡回传命令响应。SD卡有5种类型的命令响应:R1、R1b、R2、R3、R7;SDIO卡还支持另外两种命令响应:R4、R5。

在这里我们只对部分响应做一下介绍,其他未说明的响应类型,读者若感兴趣可查阅SD简易规格文件《Part1_Physical_Layer_Simplified_Specification_Ver6.00》。

R1响应格式,具体见图 64‑15。

SD016

图 64‑15 R1响应类型说明

  • in idle state:当该位为 1 时,表示 SD 卡处于空闲状态;

  • erase reset:因为接收到无需擦除操作的命令,擦除操作被复位;

  • illegal command:接收到一个无效的命令代码 ;

  • com crc error:接收到的上一个命令的 CRC 校验错误;

  • erase sequence error:擦除命令的控制顺序错误;

  • address error:读写的数据地址不对齐(数据地址需要按块大小对齐);

  • parameter error:命令的参数错误;

R3响应格式,具体见图 64‑16。

SD017

图 64‑16 R3响应格式

R3响应包括5个字节,首先返回的第1个字节内容为R1,剩下的其余字节为OCR( Operation Conditions Register, 操作条件寄存器)寄存器的内容。

R7响应格式,具体见图 64‑17。

SD018

图 64‑17 R7响应格式

R7响应包括5个字节,首先返回的第1个字节内容为R1,R7 [31:28]位为命令版本,R7[27:12]为保留位,R7[11:8]为反馈的电压范围,最后1个字节为检查模式。

14.3. 实战演练

14.3.1. 实验目标

设计并实现SD卡读写控制器(SPI模式),通过PC机使用串口助手通过串口RS232向SD卡指定扇区写入512字节数据,随后读出写入数据回传到PC机,验证数据正确性。

14.3.2. 硬件资源

征途Pro开发板使用的是Micro SD 卡(TF卡)卡座,实物图与原理图,具体见图 64‑18、图 64‑19。

SD019

图 64‑18 TF卡座实物图

SD020

图 64‑19 TF卡座原理图

14.3.3. 程序设计

实验目标和硬件资源均已介绍完毕,在本小节我们开始程序设计的讲解。

14.3.3.1. 整体说明

在开始各子功能模块的设计与讲解之前,先对整个实验工程做一下整体说明。为了方便读者理解,我们分两个部分对模块进行整体说明。

本实验要实现SPI模式下的SD卡数据读写控制器,并通过串口RS232收发数据对SD卡读写控制器进行验证,我们第一部分就先说明一下SD卡数据读写控制器,第二部分讲解一下SD卡读写控制器的验证。

第一部分:SD卡控制器

SD卡数据读写控制器要实现控制SD卡数据读写操作,其整体框图和模块功能简介具体见图 64‑20、表格 64‑2。

SD021

图 64‑20 SD卡控制器整体框图

表格 64‑2 子模块功能描述

模块名称

功能描述

sd_init

SD卡初始化模块

sd_read

SD卡数据读操作模块

sd_write

SD卡数据写操作模块

sd_ctrl

SD卡读写控制器顶层模块

SD卡数据读写控制器部分包括4个子模块,sd_init为SD卡初始化模块,控制SD卡初始化,只有成功初始化的SD卡才能进行数据的读写操作;sd_read、sd_write分别为SD卡的数据读操作模块和数据写操作模块实现SD卡的数据读写操作;sd_ctrl模块为顶层模块,实例化上述3个子功能模块。对于 各子功能模块的详细内容,我们会在后文做具体介绍。

第二部分:SD卡读写控制器的验证

将SD卡数据读写控制器视为单独的子功能模块,使用串口RS232对SD卡数据读写控制器进行验证。整体框图和子模块简介具体见图 64‑21、表格 64‑3。

SD022

图 64‑21 串口读写SD卡整体框图

表格 64‑3 子模块功能描述

模块名称

功能描述

clk_gen

时钟生成模块

uart_rx

串口数据接收模块

data_rw_ctrl

数据读写控制模块

sd_ctrl

SD卡读写控制器

uart_tx

串口数据发送模块

uart_sd

串口读写SD卡顶层模块

将SD卡数据读写控制器sd_ctrl视为一个子功能模块,SD卡数据读写控制器验证部分分包括6个子功能模块。

时钟生成模块clk_gen负责模块工作时钟的生成,生成两路时钟信号,两路时钟信号时钟频率相同,一路未做相位偏移处理,作为各子模块工作时钟,一路时钟信号做相位偏移处理(理论偏移相位为180度,但也要结合具体情况做调整),作为SD卡工作时钟和sd_miso数据信号的采集时钟;uart_rx模块和uart _tx模块是串口数据的收发模块,负责接收串口发送的数据和向串口发送数据;数据读写控制模块data_rw_ctrl控制SD卡读写数据,主要是对串口传入的字节数据和读出SD卡的数据做缓存处理;SD卡读写控制器sd_ctrl实现SD卡的数据读写操作;最后是串口读写SD卡顶层模块uart_sd,作为顶层模块 就是例化个子功能模块,连接对应信号。

对于实验工程的整体框架已经做了简要说明,工程中的部分子功能模块在前面章节已经做了详细介绍,这里不再赘述;接下来会对部分未讲解的子功能模块做一下详细介绍。

14.3.3.2. SD卡初始化模块

SD卡初始化步骤

SD卡进行数据读写操作之前,初始化必不可少。SD1.0规范的SD卡与SD2.0规范的SD卡初始化略有区别,考虑到市面上流通较为广泛的是SD2.0规范的SD卡,本实验使用的也是此类卡片。接下来介绍的是SD2.0规范的SD卡的初始化相关知识,SD2.0规范SD卡详细初始化步骤如下。

  1. SD卡上电后, FPGA需要先向SD卡发送大于等于74个时钟脉冲,因为在上电初期,电压需要大约64个时钟周期才能达到SD卡的正常工作电压,其后的10个时钟周期是为了与SD卡同步。在此期间,片选信号CS_N和主输出从输入信号MOSI始终为高电平。时钟发送过后,SD卡做好接收命令的准备;

  2. 随后片选信号CS_N拉低,向SD卡写入命令CMD0(0x40),携带参数为0x00_00_00_00,CRC校验位为0x95。目的是对SD卡进行复位,命令发送完成后等待SD卡返回响应数据;

  3. SD卡返回响应数据后,等待8个时钟周期后拉高片选信号CS_N, 随后判断返回的响应数据R1。若返回的数据R1为0x01,表示空闲状态, SD卡进入SPI模式,并开始进行下一步, 若R1为其它值, 则重新执行第2步;

  4. 成功进入空闲状态后,再次拉低片选信号CS_N, 写入命令CMD8(0x48),命令携带参数为0x00_00_01_AA,目的是查询SD卡的版本号,命令发送完成后等待SD卡返回响应数据,不同版本的SD卡返回响应数据不同;

(5) SD卡返回响应数据后,等待8个时钟周期后拉高片选信号CS_N,随后判断返回的响应数据。若返回数据R1位0x05,表示SD卡非SD 2.0卡,若返回数据R1为0x01,32位参数为0x00_00_01_AA,判断SD卡为SD 2.0卡。本实验使用的SD卡规定为SD 2.0卡,若返回数据不正确,则重新执行第4步;

  1. 判断为SD 2.0卡后,再次拉低片选信号CS_N,写入命令CMD55(0x77),目的是告知SD卡下一次发送的命令是特殊应用命令,命令发送完成后等待SD卡返回响应数据;

  2. SD卡返回响应数据后,等待8个时钟周期拉高片选信号CS_N, 随后判断返回的响应数据。 若返回数据R1为空闲信号0x01,继续进行下一步,否则重新执行第6步。

  3. 进入空闲状态后,再次拉低片选信号CS_N,写入命令ACMD41(0x69),目的是查询SD卡初始化结果,命令发送完成后等待SD卡返回响应数据;

  4. SD卡返回响应数据后,等待8个时钟周期拉高片选CS信号,随后判断返回的响应数据。 若返回响应数据R3中的R1为0x00,表示初始化完成,否则重新执行第6步,直至初始化成功。

模块框图

SD卡初始化步骤讲解完毕后,开始初始化模块的设计。SD卡初始化模块框图如下图 64‑22。

SD023

图 64‑22 SD卡初始化模块框图

由模块框图可知,模块有4路输入信号、3路输出信号。输入信号包含2路时钟信号sys_clk,sys_clk_shift、复位信号sys_rst_n以及主输入从输出信号miso。2路时钟信号频率相同,相位不同,是因为传入SD卡的工作时钟为sys_clk_shift时钟信号,SD卡通过此时钟信号的上升沿来 接收FPGA发送的命令、数据信号和发送响应数据,SD卡时钟信号做相位偏移处理可以更好的采集到命令、数据的稳定状态,理论上相位偏移180度最佳,但在在实际工程中可略做调整,同样FPGA也需要使用此时钟信号读取SD卡返回的响应数据miso。

输出信号包含片选信号cs_n、主输出从输入信号mosi和初始化完成信号init_end;FPGA通过主输出从输入信号mosi向SD卡写入初始化相关指令,初始化完成后,初始化完成信号init_end拉高,告知其他模块SD卡初始化完成,可执行其他操作。

波形图绘制

本小节我们开始SD卡初始化模块波形图的绘制,通过模块波形图的绘制为读者讲解初始化模块的设计思路以及各信号波形的设计与实现。SD卡初始化模块整体波形图具体见图 64‑23、图 64‑24、图 64‑25和图 64‑26。

SD024

图 64‑23 SD卡初始化模块整体波形图(一)

SD025

图 64‑24 SD卡初始化模块整体波形图(二)

SD026

图 64‑25 SD卡初始化模块整体波形图(三)

SD027

图 64‑26 SD卡初始化模块整体波形图(四)

上面列举的4张图片为SD卡初始化模块整体波形图,接下来我们会对图中各信号做详细讲解,为读者说明各信号波形的设计思路与实现方法。

由前文SD卡初始化步骤可知,要实现SD卡的初始化我们需要向SD卡分步骤写入4条命令:CMD0、CMD8、CMD55和ACMD41。每条命令写入后需要接收到SD卡返回的正确响应才可继续后续操作。结合之前学习的相关知识,我们发现使用状态机作为主要思路来实现SD卡的初始化较为方便,且容易理解。

声明状态机状态变量state,定义状态机状态:IDLE(初始状态)、SEND_CMD0(发送命令CMD0)、CMDO_ACK(CMD0响应)、SEND_CMD8(发送命令CMD8)、CMD8_ACK(CMD8响应)、SEND_CMD55(发送命令CMD55)、CMD55_ACK(CMD55响应)、S END_ACMD41(发送特殊应用命令ACMD41)、ACMD41ACK(ACMD41 响应)、INIT_END(初始化完成状态)。

结合SD卡初始化步骤,绘制SD卡初始化状态机状态转移图如图 64‑27所示。

SD028

图 64‑27 SD卡初始化模块状态机状态转移图

结合SD卡初始化步骤和状态转移图,按照状态机跳转顺序对模块涉及的各信号的设计与实现进行详细讲解。

系统上电后,初始化模块始终处于初始化状态(IDLE),只有当FPGA向SD卡发送大于等于74个时钟周期,实现时钟同步后,状态机由初始状态跳转到发送命令CMD0状态(SEND_CMD0)。

在初始状态中,为确定FPGA向SD卡发送时钟周期个数,我们声明同步时钟计数器cnt_wait。同步时钟计数器cnt_wait,计数时钟为系统时钟sys_clk,每个时钟周期自加1,计数范围为0-100,计数到最大值100保持不变,当计数器cnt_wait计数到最大值100后,状态机实现初始状态到发送 命令CMD0状态的跳转。同步时钟计数器cnt_wait波形如下。

SD029

图 64‑28 cnt_wait信号波形图

状态跳转到发送命令CMD0状态后,在此状态我们需要通过mosi信号向SD卡写入命令CMD0,CMD0命令组成格式为:命令号0x40(1字节)+命令参数0x00_00_00_00(4字节)+CRC校验0x95(2字节)。

为了保证命令的完整且正确写入,声明命令比特计数器cnt_cmd_bit对写入SD卡的命令进行比特计数,当状态机处于发送命令CMD0状态时,计数器进行计数,计数时钟为系统时钟系统时钟sys_clk,计数范围0-48,其他状态保持为0。当计数器cnt_cmd_bit在1-48计数范围时,按位向mosi写 入命令CMD0。发送命令CMD0状态下,cnt_cmd_bit、mosi信号波形图如下图 64‑29。

SD030

图 64‑29 cnt_cmd_bit、mosi信号波形图(一)

同样,当状态机处于发送命令CMD8状态、发送命令CMD55状态和发送命令AMD41状态时,我们可以以使用同样的方法分别写入命令CMD8、CMD55和ACMD41。各发送命令状态下,cnt_cmd_bit、mosi信号波形图如图 64‑30、图 64‑31、图 64‑32所示。

CMD8命令组成格式为:命令号0x48(1字节)+命令参数0x00_00_01_aa(4字节)+CRC校验0x87(2字节);CMD55命令组成格式为:命令号0x77(1字节)+命令参数0x00_00_00_00(4字节)+CRC校验0xFF(2字节);ACMD41命令组成格式为:命令号0x69(1 字节)+命令参数0x40_00_00_00(4字节)+CRC校验0xFF(2字节)。

注:各命令及命令参数的相关介绍,读者可参阅本章“理论学习”小节的相关内容。

SD031

图 64‑30 cnt_cmd_bit、mosi信号波形图(二)

SD032

图 64‑31 cnt_cmd_bit、mosi信号波形图(三)

SD033

图 64‑32 cnt_cmd_bit、mosi信号波形图(四)

在发送命令CMD0状态下,写入CMD0命令后,状态机跳转到命令CMD0响应状态,在此状态下,FPGA要接收SD卡返回的CMD0响应数据,只有返回的响应数据正确,继续进行下一步操作,否者重新写入本条命令。对于其他三条命令的响应也是如此。存在区别的一点是,初始化模块写入SD卡的4条命令,它们的响应数据类 型和内容存在区别,但响应数据的第一个字节都为R1。

CMD0响应数据类型为R1,1字节,正确响应数据为0x01;CMD8响应数据类型为R7,5字节,正确响应数据为0x01_00_00_01_AA;CMD55响应数据类型为R1,1字节,正确响应数据为0x01;ACMD41响应数据类型为R3,5字节,正确响应数据为0x01(R1) +OCR (4字节)。

命令响应数据所占字节不同,为了便于工程的实现,每条命令响应同一接收5个字节,在做响应数据正误判断时,只截取有效响应数据。因为4条命令对应的相应数据的首字节均为R1,且R1最高位固定为0。我们可以以miso下降沿作为响应开始接收标志。

读者还需注意的是,片选信号自上电后始终保持高电平,只有在命令写入和响应接收时保持低电平,且在响应数据接收完成后,需要等待8个时钟周期才能拉高片选信号。

综上所述,我们需要声明若干信号来保证响应数据的正确接收。首先声明信号miso_dly对SD卡输入的miso进行打拍,使用两信号判断miso信号下降沿,确定返回数据接收时刻;其次,声明响应数据比特计数器cnt_ack_bit,对返回数据进行比特计数,只有在响应使能信号ack_en为有效高电平时进行计数 ,计数范围0-48,0-39为5个字节返回数据的比特计数,40-47为片选信号拉高前所需的8个时钟周期的等待,计数值为48时,片选信号cs_n保持一个时钟周期的高电平;然后,声明响应使能信号ack_en和响应数据ack_data,使能信号ack_en负责规定响应数据比特计数器cnt_ack_bit的 计数范围,使能信号ack_en初值为低电平,当检测到miso下降沿后,拉高使能信号ack_en,当计数器cnt_ack_bit计数到47时拉低使能信号ack_en,响应数据ack_data只有在计数器技术在0-39时对返回数据miso_dly进行数据拼接,其他时刻保持不变。要注意的是,上述声明的若干 信号的同步时钟均为SD卡工作时钟sys_clk_shift。

输出至SD卡的片选信号cs_n在初始状态、初始化完成状态以及在计数器cnt_ack_bit计数值为48时保持高电平,其他其他时刻保持低电平。上述各信号波形图如下。

SD034

图 64‑33 SD卡响应相关信号波形图(一)

SD035

图 64‑34 SD卡响应相关信号波形图(二)

SD036

图 64‑35 SD卡响应相关信号波形图(三)

SD037

图 64‑36 SD卡响应相关信号波形图(四)

当写入的ACMD41命令返回正确响应数据,状态机跳转到初始化完成状态,SD卡只有完成初始化后才能进行后续数据读写操作,在此我们需要声明初始化完成信号init_end作为信号输出,告知其他模块SD卡初始化完成可进行后续操作。初始化完成信号init_end只有在初始化完成状态保持高电平,表示初始化完成, 其他时刻均为低电平,init_end信号波形图如下。

SD038

图 64‑37 init_end信号波形图

模块各信号均已介绍完毕,将各信号波形进行整合,就能得到模块整体波形图。本小节中的波形图仅供参考,读者也可按照自己的理解进行波形图的绘制。

代码编写

参照模块整体波形图编写模块参考代码,SD卡初始化模块参考代码具体见代码清单 64‑1。

代码清单 64‑1 SD卡初始化模块参考代码(sd_init.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
module sd_init
(
input wire sys_clk , //输入工作时钟,频率50MHz
input wire sys_clk_shift , //输入工作时钟,频率50MHz,相位偏移90度
input wire sys_rst_n , //输入复位信号,低电平有效
input wire miso , //主输入从输出信号

output reg cs_n , //输出片选信号
output reg mosi , //主输出从输入信号
output reg init_end //初始化完成信号
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter CMD0 = {8'h40,8'h00,8'h00,8'h00,8'h00,8'h95}, //复位指令
CMD8 = {8'h48,8'h00,8'h00,8'h01,8'haa,8'h87}, //查询电压指令
CMD55 = {8'h77,8'h00,8'h00,8'h00,8'h00,8'hff},//应用指令告知指令
ACMD41= {8'h69,8'h40,8'h00,8'h00,8'h00,8'hff}; //应用指令
parameter CNT_WAIT_MAX = 8'd100; //上电后同步过程等待时钟计数最大值
parameter IDLE = 4'b0000, //初始状态
SEND_CMD0 = 4'b0001, //CMD0发送状态
CMD0_ACK = 4'b0011, //CMD0响应状态
SEND_CMD8 = 4'b0010, //CMD8发送状态
CMD8_ACK = 4'b0110, //CMD8响应状态
SEND_CMD55 = 4'b0111, //CMD55发送状态
CMD55_ACK = 4'b0101, //CMD55响应状态
SEND_ACMD41 = 4'b0100, //ACMD41发送状态
ACMD41_ACK = 4'b1100, //ACMD41响应状态
INIT_END = 4'b1101; //初始化完成状态

//reg define
reg [7:0] cnt_wait ; //上电同步时钟计数器
reg [3:0] state ; //状态机状态
reg [7:0] cnt_cmd_bit ; //指令比特计数器
reg miso_dly ; //主输入从输出信号打一拍
reg ack_en ; //响应使能信号
reg [39:0] ack_data ; //响应数据
reg [7:0] cnt_ack_bit ; //响应数据字节计数

////
//\* Main Code \//
////
//cnt_wait:上电同步时钟计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_wait <= 8'd0;
else if(cnt_wait >= CNT_WAIT_MAX)
cnt_wait <= CNT_WAIT_MAX;
else
cnt_wait <= cnt_wait + 1'b1;

//state:状态机状态跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE:
if(cnt_wait == CNT_WAIT_MAX)
state <= SEND_CMD0;
else
state <= state;
SEND_CMD0:
if(cnt_cmd_bit == 8'd48)
state <= CMD0_ACK;
else
state <= state;
CMD0_ACK:
if(cnt_ack_bit == 8'd48)
if(ack_data[39:32] == 8'h01)
state <= SEND_CMD8;
else
state <= SEND_CMD0;
else
state <= state;
SEND_CMD8:
if(cnt_cmd_bit == 8'd48)
state <= CMD8_ACK;
else
state <= state;
CMD8_ACK:
if(cnt_ack_bit == 8'd48)
if(ack_data[11:8] == 4'b0001)
state <= SEND_CMD55;
else
state <= SEND_CMD8;
else
state <= state;
SEND_CMD55:
if(cnt_cmd_bit == 8'd48)
state <= CMD55_ACK;
else
state <= state;
CMD55_ACK:
if(cnt_ack_bit == 8'd48)
if(ack_data[39:32] == 8'h01)
state <= SEND_ACMD41;
else
state <= SEND_CMD55;
else
state <= state;
SEND_ACMD41:
if(cnt_cmd_bit == 8'd48)
state <= ACMD41_ACK;
else
state <= state;
ACMD41_ACK:
if(cnt_ack_bit == 8'd48)
if(ack_data[39:32] == 8'h00)
state <= INIT_END;
else
state <= SEND_CMD55;
else
state <= state;
INIT_END:
state <= state;
default:
state <= IDLE;
endcase

//cs_n,mosi,init_end,cnt_cmd_bit
//片选信号,主输出从输入信号,初始化结束信号,指令比特计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
cs_n <= 1'b1;
mosi <= 1'b1;
init_end <= 1'b0;
cnt_cmd_bit <= 8'd0;
end
else
case(state)
IDLE:
begin
cs_n <= 1'b1;
mosi <= 1'b1;
init_end <= 1'b0;
cnt_cmd_bit <= 8'd0;
end
SEND_CMD0:
if(cnt_cmd_bit == 8'd48)
cnt_cmd_bit <= 8'd0;
else
begin
cs_n <= 1'b0;
mosi <= CMD0[8'd47 - cnt_cmd_bit];
init_end <= 1'b0;
cnt_cmd_bit <= cnt_cmd_bit + 8'd1;
end
CMD0_ACK:
if(cnt_ack_bit == 8'd47)
cs_n <= 1'b1;
else
cs_n <= 1'b0;
SEND_CMD8:
if(cnt_cmd_bit == 8'd48)
cnt_cmd_bit <= 8'd0;
else
begin
cs_n <= 1'b0;
mosi <= CMD8[8'd47 - cnt_cmd_bit];
init_end <= 1'b0;
cnt_cmd_bit <= cnt_cmd_bit + 8'd1;
end
CMD8_ACK:
if(cnt_ack_bit == 8'd47)
cs_n <= 1'b1;
else
cs_n <= 1'b0;
SEND_CMD55:
if(cnt_cmd_bit == 8'd48)
cnt_cmd_bit <= 8'd0;
else
begin
cs_n <= 1'b0;
mosi <= CMD55[8'd47 - cnt_cmd_bit];
init_end <= 1'b0;
cnt_cmd_bit <= cnt_cmd_bit + 8'd1;
end
CMD55_ACK:
if(cnt_ack_bit == 8'd47)
cs_n <= 1'b1;
else
cs_n <= 1'b0;
SEND_ACMD41:
if(cnt_cmd_bit == 8'd48)
cnt_cmd_bit <= 8'd0;
else
begin
cs_n <= 1'b0;
mosi <= ACMD41[8'd47 - cnt_cmd_bit];
init_end <= 1'b0;
cnt_cmd_bit <= cnt_cmd_bit + 8'd1;
end
ACMD41_ACK:
if(cnt_ack_bit < 8'd47)
cs_n <= 1'b0;
else
cs_n <= 1'b1;
INIT_END:
begin
cs_n <= 1'b1;
mosi <= 1'b1;
init_end <= 1'b1;
end
default:
begin
cs_n <= 1'b1;
mosi <= 1'b1;
end
endcase

//miso_dly:主输入从输出信号打一拍
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
miso_dly <= 1'b0;
else
miso_dly <= miso;

//ack_en:响应使能信号
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ack_en <= 1'b0;
else if(cnt_ack_bit == 8'd47)
ack_en <= 1'b0;
else if((miso == 1'b0)&&(miso_dly == 1'b1) && (cnt_ack_bit == 8'd0))
ack_en <= 1'b1;
else
ack_en <= ack_en;

//ack_data:响应数据
//cnt_ack_bit:响应数据字节计数
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
ack_data <= 8'b0;
cnt_ack_bit <= 8'd0;
end
else if(ack_en == 1'b1)
begin
cnt_ack_bit <= cnt_ack_bit + 8'd1;
if(cnt_ack_bit < 8'd40)
ack_data <= {ack_data[38:0],miso_dly};
else
ack_data <= ack_data;
end
else
cnt_ack_bit <= 8'd0;

endmodule

Signal Tap信号波形

代码编写完成后需要对参考代码进行仿真验证,由于我们没有找到合适的SD卡仿真模型,不能进行代码的仿真验证。我们直接上板验证,使用SignalTap对信号进行抓取,SD卡初始化模块信号抓取波形如下。与绘制波形图相对比,各信号波形吻合,模块正常工作。

SD039

图 64‑38 SD卡初始化模块信号抓取整体波形图

SD040

图 64‑39 SD卡初始化模块信号抓取局部波形图(一)

SD041

图 64‑40 SD卡初始化模块信号抓取局部波形图(二)

SD042

图 64‑41 SD卡初始化模块信号抓取局部波形图(三)

SD043

图 64‑42 SD卡初始化模块信号抓取局部波形图(四)

SD044

图 64‑43 SD卡初始化模块信号抓取局部波形图(五)

SD045

图 64‑44 SD卡初始化模块信号抓取局部波形图(六)

SD046

图 64‑45 SD卡初始化模块信号抓取局部波形图(七)

SD047

图 64‑46 SD卡初始化模块信号抓取局部波形图(八)

14.3.3.3. SD卡数据写模块

SD卡数据写步骤

本实验工程要实现的是SD卡数据的读写验证,SD卡完成初始化后,先进行数据的写入操作,随后将写入数据读出并验证。SD卡数据写步骤如下:

  1. 拉低片选信号CS_N,向SD卡写入命令CMD24,命令号为0x58,携带参数为4字节的SD卡写扇区地址,CRC校验字节未使用直接写入0xFF,命令发送完成后等待SD卡返回响应数据;

  2. 若SD卡返回正确响应数据R1为0x00,等待8个时钟周期,向SD卡写入令牌0xFE,紧随其后写入512个字节的数据;

  3. 数据发送完成后,再向SD卡写入2个字节的CRC校验字节。SPI模式下不对数据进行CRC校验,直接写入两个字节的0xFF;

  4. 校验数据发送完成后, SD卡会有响应数据返回,随后SD卡将miso信号拉低进入写忙状态;

  5. miso信号再次拉高后SD卡退出写忙状态,等待8个时钟周期后拉高片选信号,SD卡数据写操作完成,可以执行其它操作。

模块框图

上一部分我们说明了SD卡数据写操作的详细步骤,接下来开始SD卡数据写模块的设计。SD卡数据写模块框图具体见图 64‑47。

SD048

图 64‑47 SD卡数据写模块框图

由图可知,SD卡数据写模块包含输入输出信号共11路,其中输入信号7路,输出信号4路。输入信号包括2路时钟信号sys_clk、sys_clk_shift,两路信号时钟频率相同,相位不同,时钟sys_clk为模块工作时钟,时钟sys_clk_shift用于读取自SD卡传回的响应数据;1路复位信号,低电平 有效;主输入从输出信号miso为SD卡传入FPGA的响应信号;此外还有数据写使能信号wr_en、写地址信号wr_addr和写数据信号wr_data,这三路信号由SD卡控制器外部传入。

输出信号中包含片选信号cs_n;主输出从输入信号mosi,由FPGA传入SD卡,负责向SD卡写入命令和数据;写忙信号wr_busy,除初始状态外的其他状态均保持高电平;写数据请求信号wr_req,作为请求信号,请求数据的写入。

波形图绘制

本小节我们开始SD卡数据写模块波形图的绘制,通过模块波形图的绘制为读者讲解数据写模块的设计思路以及各信号波形的设计与实现。SD卡数据写模块整体波形图具体见图 64‑48、图 64‑49。

SD049

图 64‑48 SD卡数据写模块整体波形图(一)

SD050

图 64‑49 SD卡数据写模块整体波形图(二)

SD卡数据写模块整体波形图如上图所示,为了便于读者理解,接下来我们会对模块所涉及各信号的设计思路与实现方法做一下详细介绍。

我们沿用SD卡初始化模块的设计思路,结合SD卡数据写步骤,使用状态机来实现数据写模块。声明状态机状态变量state,定义状态机状态:IDLE(初始状态)、SEND_CMD24(发送命令CMD24)、CMD24_ACK(CMD24响应)、WR_DATA(写数据)、WR_BUSY(写忙状态)、WR_EN D(写结束状态)。

绘制SD卡数据写模块状态转移图如图 64‑50所示。

SD051

图 64‑50 SD卡数据写模块状态转移图

系统上电后,数据写模块处于IDLE (初始状态)状态,检测到写使能信号wr_en下降沿后,状态机跳转到SEND_CMD24(发送命令CMD24)状态,在这一状态下要进行命令CMD24的写入。为此我们要声明若干信号实现这一状态跳转和命令的写入。

声明信号wr_en_dly1和wr_en_dly2对输入的写使能信号wr_en进行打拍,用以检测写使能信号wr_en下降沿,实现状态跳转。

对于命令的写入,我们参照SD卡初始化模块的处理方法,声明命令比特计数器cnt_cmd_bit对写入SD卡命令进行比特计数,状态机处于CMD24命令写入状态,计数器进行计数,计数时钟为系统时钟系统时钟sys_clk,计数范围0-48,其他状态保持为0。当计数器在1-48计数范围时,按位向mosi写入命 令。

上述各信号波形图如下。

SD052

图 64‑51 wr_en_dly1、wr_en_dly2、cnt_cmd_bit、mosi信号波形图

状态机在SEND_CMD24状态下写入CMD24命令完毕后,状态机跳转到CMD24_ACK状态接收SD卡返回响应数据。

命令写入SD卡后,SD卡会通过miso信号返回对应响应,只有响应正确,才会执行后续操作,否者重新写入本条命令。SD卡在接收到FPGA写入的指令后,会等待若若干个时钟周期开始返回命令响应。

参照SD卡初始化模块处理方法,以miso下降沿作为开始响应接收标志。SD卡数据写模块只有一条命令CMD24写入,返回数据响应为R1,宽度为1个字节。

我们需要声明若干信号来保证响应数据接收正确,首先声明信号miso_dly对SD卡输入的miso进行打拍,使用两信号判断miso信号下降沿,确定返回数据接收时刻;其次,声明响应数据比特计数器cnt_ack_bit,对返回数据进行比特计数,只有在响应使能信号ack_en为有效高电平时进行计数,计数范围0 -16,0-7为1个字节返回数据R1的比特计数,8-15为所需的8个时钟周期的等待;然后,声明响应使能信号ack_en和响应数据ack_data,使能信号ack_en负责规定响应数据比特计数器cnt_ack_bit的计数范围,使能信号ack_en初值为低电平,当检测到miso下降沿后,拉高使能信号a ck_en,当计数器cnt_ack_bit计数到15时拉低使能信号ack_en,响应数据ack_data只有在计数器计数在0-7时对返回数据miso_dly进行数据拼接,其他时刻保持不变。要注意的是,上述声明的若干信号的同步时钟均为SD卡工作时钟sys_clk_shift。上述各信号波形图如下。

SD053

图 64‑52 SD卡响应数据相关信号波形图

在接收到正确的响应数据后,状态机跳转到写数据状态(WR_DATA),在此状态,我们先要向SD卡写入1个字节的数据头0xFE,目的是告知SD卡准备接收写入数据,随后写入512个字节数据,数据写入完成后写入2个字节的CRC校验数据0xFF_FF。

读者要注意的是,写入SD卡的512字节数据是由SD卡控制器外部传入的,且一次传入数据量为2个字节。之所以一次传入2个字节是因为,后续章节我们要实现SD卡存储图片的VGA和TFT显示,显示图片的色彩格式为RGB565,每个像素点需要2个字节表示,为提高SD卡控制器的复用性,一次传入SD卡控制器的数据量 默认为2字节。

为了实现512字节数据的正确写入,我们需要声明若干变量。

写入SD卡的数据是通过mosi串行传入的,为确定写入SD卡的数据量我们需要声明两个计数器对写入SD卡的数据量进行计数。声明数据比特计数器cnt_data_bit对写入SD卡数据进行比特计数,声明数据字节计数器cnt_data_num对写入SD卡数据进行字节计数。数据比特计数器cnt_data_bit ,在状态机处于写数据状态时进行循环计数,计数范围为0-15,表示2个字节;数据字节计数器cnt_data_num,初值为0,当数据字节计数器cnt_data_num计数到最大值15,cnt_data_num自加1。

在写数据状态首先写入SD卡的为1字节数据头0xFE,而计数器cnt_data_bit一个循环计数为2字节,想到在无命令或数据输出时,mosi始终保持高电平,我们在数据头前补充一个字节的0xFF,即在数据字节计数器cnt_data_num等于0,数据比特计数器cnt_data_bit计数范围为0-15 时,向SD卡写入0xFF_FE作为数据头;随后数据字节计数器cnt_data_num计数范围为1-256,数据比特计数器cnt_data_bit计数范围为0-15时,写入外部传入的512字节数据;最后,数据字节计数器cnt_data_num等于257,数据比特计数器cnt_data_bit计数范围为 0-15时,写入2个字节的CRC校验数据0xFF_FF。

SD卡读写控制器外部输入的512字节数据,是暂存在数据读写控制模块data_rw_ctrl的FIFO中,只有接收到有效的数据写请求信号才会通过输入端口wr_data写入2字节数据。需要声明数据写请求信号wr_req,当cnt_data_num计数范围为0-255,数据比特计数器cnt_data_bi t计数值为15时,wr_req保持1个时钟高电平,读取的2字节数据在下一个时钟周期通过端口wr_data传入,符合时序要求。

上述各信号波形图如下。

SD054

图 64‑53 SD卡写数据相关信号波形图

在写数据状态(WR_DATA),数据写入完毕后,状态机跳转到写忙状态(WR_BUSY),此状态下miso信号会回传响应信号,随后保持低电平,此时SD卡处于写忙状态,待miso信号再次拉高,SD卡跳出写忙状态,数据写入完成。

在状态机写忙状态下,为确定SD卡是否跳出写忙状态,声明变量busy_data。当状态机处于写忙状态时,busy_data赋值方式为{busy_data[6:0],miso},数据不断更新,当busy_data数值为0xFF,表示SD卡跳出写忙状态,状态机也可以由写忙状态跳转到写完成状态(WR_END )。

状态机跳转到写完成状态后,需要等待8个时钟周期,状态机跳回初始状态(IDLE)。之所以等待8个时钟周期,是因为片选信号cs_n在SD卡跳出写忙状态后,要等待8个时钟周期才能拉高片选信号。在此声明计数器cnt_end,状态机处于写完成状态时计数器cnt_end开始计数,计数范围为0-7,计数到最大值7 ,清0保持不变,状态机跳转到初始状态,等待下次数据写操作。

上述各信号波形图如下。

SD055

图 64‑54 busy_data、cnt_end信号波形图

综上所述,SD卡数据写模块各信号波形均已介绍完毕,各信号整合后可得到可得到数据写模块整体波形图。

代码编写

参照SD卡数据写模块波形图,编写SD卡数据写模块参考代码。模块参考代码,具体见代码清单 64‑2。

代码清单 64‑2 SD卡数据写模块参考代码(sd_write.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
module sd_write
(
input wire sys_clk , //输入工作时钟,频率50MHz
input wire sys_clk_shift , //输入工作时钟,频率50MHz,相位偏移90度
input wire sys_rst_n , //输入复位信号,低电平有效
input wire miso , //主输入从输出信号
input wire wr_en , //数据写使能信号
input wire [31:0] wr_addr , //写数据扇区地址
input wire [15:0] wr_data , //写数据

output reg cs_n , //输出片选信号
output reg mosi , //主输出从输入信号
output wire wr_busy , //写操作忙信号
output wire wr_req //写数据请求信号
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter IDLE = 3'b000 , //初始状态
SEND_CMD24 = 3'b001 , //写命令CMD24发送状态
CMD24_ACK = 3'b011 , //CMD24响应状态
WR_DATA = 3'b010 , //写数据状态
WR_BUSY = 3'b110 , //SD卡写忙状态
WR_END = 3'b111 ; //写结束状态
parameter DATA_NUM = 12'd256 ; //待写入数据字节数
parameter BYTE_HEAD = 16'hfffe; //写数据字节头

//wire define
wire [47:0] cmd_wr ; //数据写指令

//reg define
reg [2:0] state ; //状态机状态
reg [7:0] cnt_cmd_bit ; //指令比特计数器
reg ack_en ; //响应使能信号
reg [7:0] ack_data ; //响应数据
reg [7:0] cnt_ack_bit ; //响应数据字节计数
reg [11:0] cnt_data_num; //写入数据个数计数
reg [3:0] cnt_data_bit; //写数据比特计数器
reg [7:0] busy_data ; //忙状态数据
reg [2:0] cnt_end ; //结束状态时钟计数
reg miso_dly ; //主输入从输出信号打一拍

////
//\* Main Code \//
////
//wr_busy:写操作忙信号
assign wr_busy = (state != IDLE) ? 1'b1 : 1'b0;

//wr_req:写数据请求信号
assign wr_req = ((cnt_data_num <= DATA_NUM - 1'b1) && (cnt_data_bit == 4'd15))
? 1'b1 : 1'b0;

//cmd_wr:数据写指令
assign cmd_wr = {8'h58,wr_addr,8'hff};

//miso_dly:主输入从输出信号打一拍
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
miso_dly <= 1'b0;
else
miso_dly <= miso;

//ack_en:响应使能信号
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ack_en <= 1'b0;
else if(cnt_ack_bit == 8'd15)
ack_en <= 1'b0;
else if((state == CMD24_ACK) && (miso == 1'b0)
&& (miso_dly == 1'b1) && (cnt_ack_bit == 8'd0))
ack_en <= 1'b1;
else
ack_en <= ack_en;

//ack_data:响应数据
//cnt_ack_bit:响应数据字节计数
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
ack_data <= 8'b0;
cnt_ack_bit <= 8'd0;
end
else if(ack_en == 1'b1)
begin
cnt_ack_bit <= cnt_ack_bit + 8'd1;
if(cnt_ack_bit < 8'd8)
ack_data <= {ack_data[6:0],miso_dly};
else
ack_data <= ack_data;
end
else
cnt_ack_bit <= 8'd0;

//busy_data:忙状态数据
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
busy_data <= 8'd0;
else if(state == WR_BUSY)
busy_data <= {busy_data[6:0],miso};
else
busy_data <= 8'd0;

//state:状态机状态跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE:
if(wr_en == 1'b1)
state <= SEND_CMD24;
else
state <= state;
SEND_CMD24:
if(cnt_cmd_bit == 8'd47)
state <= CMD24_ACK;
else
state <= state;
CMD24_ACK:
if(cnt_ack_bit == 8'd15)
if(ack_data == 8'h00)
state <= WR_DATA;
else
state <= SEND_CMD24;
else
state <= state;
WR_DATA:
if((cnt_data_num == (DATA_NUM + 1'b1))
&& (cnt_data_bit == 4'd15))
state <= WR_BUSY;
else
state <= state;
WR_BUSY:
if(busy_data == 8'hff)
state <= WR_END;
else
state <= state;
WR_END:
if(cnt_end == 3'd7)
state <= IDLE;
else
state <= state;
default:state <= IDLE;
endcase

//cs_n:输出片选信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cs_n <= 1'b1;
else if(cnt_end == 3'd7)
cs_n <= 1'b1;
else if(wr_en == 1'b1)
cs_n <= 1'b0;
else
cs_n <= cs_n;

//cnt_cmd_bit:指令比特计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_cmd_bit <= 8'd0;
else if(state == SEND_CMD24)
cnt_cmd_bit <= cnt_cmd_bit + 8'd1;
else
cnt_cmd_bit <= 8'd0;

//mosi:主输出从输入信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
mosi <= 1'b1;
else if(state == SEND_CMD24)
mosi <= cmd_wr[8'd47 - cnt_cmd_bit];
else if(state == WR_DATA)
if(cnt_data_num == 12'd0)
mosi <= BYTE_HEAD[15 - cnt_data_bit];
else if((cnt_data_num >= 12'd1) && (cnt_data_num <= DATA_NUM))
mosi <= wr_data[15 - cnt_data_bit];
else
mosi <= 1'b1;
else
mosi <= 1'b1;

//cnt_data_bit:写数据比特计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data_bit <= 4'd0;
else if(state == WR_DATA)
cnt_data_bit <= cnt_data_bit + 4'd1;
else
cnt_data_bit <= 4'd0;

//cnt_data_num:写入数据个数计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data_num <= 12'd0;
else if(state == WR_DATA)
if(cnt_data_bit == 4'd15)
cnt_data_num <= cnt_data_num + 12'd1;
else
cnt_data_num <= cnt_data_num;
else
cnt_data_num <= 12'd0;

//cnt_end:结束状态时钟计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_end <= 3'd0;
else if(state == WR_END)
cnt_end <= cnt_end + 3'd1;
else
cnt_end <= 3'd0;

endmodule

Signal Tap信号波形

使用SignalTap对信号进行抓取,SD卡数据写模块信号抓取波形如下。与绘制波形图相对比,各信号波形吻合,模块正常工作。

SD056

图 64‑55 SD卡数据写模块信号抓取局部波形图(一)

SD057

图 64‑56 SD卡数据写模块信号抓取局部波形图(二)

SD058

图 64‑57 SD卡数据写模块信号抓取局部波形图(三)

SD059

图 64‑58 SD卡数据写模块信号抓取局部波形图(四)

SD060

图 64‑59 SD卡数据写模块信号抓取局部波形图(五)

SD061

图 64‑60 SD卡数据写模块信号抓取局部波形图(六)

SD062

图 64‑61 SD卡数据写模块信号抓取局部波形图(七)

14.3.3.4. SD卡数据读模块

SD卡数据读步骤

本实验工程要实现的是SD卡数据的读写验证,SD卡完成初始化后,先进行数据的写入操作,随后将写入数据读出并验证。SD卡数据读步骤如下:

  1. 拉低片选信号CS_N, 向SD卡写入命令CMD17,命令号为0x51,携带参数为4字节的SD卡读扇区地址,CRC校验字节未使用直接写入0xFF,命令发送完成后等待SD卡返回响应数据;

  2. 若SD卡返回正确响应数据R1为0x00,以SD卡返回的数据头0xFE为标志,接收自SD卡读出的512字节数据和2字节的CRC校验字节;

  3. 解析到数据头0xFE后,接下来接收SD卡返回的512个字节的数据;

  4. 数据解析完成后,接下来接收两个字节的CRC校验值。 由于SPI模式下不对数据进行CRC校验,可直接忽略这两个字节;

  5. CRC校验字节接收完毕,等待8个时钟周期,拉高片选信号CS_N,一次数据读操作完成;

模块框图

上一部分我们说明了SD卡数据写操作的详细步骤,接下来开始SD卡数据写模块的设计。SD卡数据写模块框图具体见图 64‑62。

SD063

图 64‑62 SD卡数据读模块框图

由图可知,SD卡数据读模块包含输入输出信号共11路,其中输入信号6路,输出信号5路。输入信号包括2路时钟信号sys_clk、sys_clk_shift,两路信号时钟频率相同,相位不同,时钟sys_clk为模块工作时钟,时钟sys_clk_shift用于读取自SD卡传回的响应数据;1路复位信号,低电平 有效;主输入从输出信号miso为SD卡传入FPGA的响应信号;此外还有数据读使能信号rd_en和读地址信号rd_addr,这两路信号由SD卡控制器外部传入。

输出信号中包含片选信号cs_n;主输出从输入信号mosi,由FPGA传入SD卡,负责向SD卡写入命令和数据;读忙信号wr_busy,除初始状态外的其他状态均保持高电平;读数据rd_data,表示自SD卡读出的字节数据;rd_data_en为与读数据匹配的读数据标志信号。

波形图绘制

本小节我们开始SD卡数据写模块波形图的绘制,通过模块波形图的绘制为读者讲解数据读模块的设计思路以及各信号波形的设计与实现。SD卡数据写模块整体波形图具体见图 64‑63、图 64‑64。

SD064

图 64‑63 SD卡数据读模块整体波形图(一)

SD065

图 64‑64 SD卡数据读模块整体波形图(二)

SD卡数据读模块整体波形图如上图所示,为了便于读者理解,接下来我们会对模块所涉及各信号的设计思路与实现方法做一下详细介绍。

我们沿用SD卡初始化模块的设计思路,结合SD卡数据读步骤,使用状态机来实现数据写模块。声明状态机状态变量state,定义状态机状态:IDLE(初始状态)、SEND_CMD17(发送命令CMD17)、CMD17_ACK(CMD17响应)、RD_DATA(读数据)、RD_END(读结束状态)。

绘制SD卡数据读模块状态转移图如图 64‑65所示。

SD066

图 64‑65 SD卡数据读模块状态转移图

系统上电后,数据写模块处于IDLE (初始状态)状态,检测到读使能信号rd_en下降沿后,状态机跳转到SEND_CMD17(发送命令CMD17)状态,在这一状态下要进行命令CMD17的写入。为此我们要声明若干信号实现这一状态跳转和命令的写入。

声明信号rd_en_dly对输入的读使能信号rd_en进行打拍,用以检测读使能信号rd_en下降沿,实现状态跳转。对于命令的写入,我们参照SD卡初始化模块的处理方法,声明命令比特计数器cnt_cmd_bit对写入SD卡命令进行比特计数,状态机处于CMD17命令写入状态,计数器进行计数,计数时钟为系统 时钟系统时钟sys_clk,计数范围0-48,其他状态保持为0。当计数器在1-48计数范围时,按位向mosi写入命令。

上述各信号波形图如下。

SD067

图 64‑66 rd_en_dly、cnt_cmd_bit、mosi信号波形图

状态机在SEND_CMD17状态下写入CMD17命令完毕后,状态机跳转到CMD17_ACK状态接收SD卡返回响应数据。

命令写入SD卡后,SD卡会通过miso信号返回对应响应,只有响应正确,才会执行后续操作,否者重新写入本条命令。SD卡在接收到FPGA写入的指令后,会等待若若干个时钟周期开始返回命令响应。

参照SD卡初始化模块处理方法,以miso下降沿作为开始响应接收标志。SD卡数据写模块只有一条命令CMD17写入,返回数据响应为R1,宽度为1个字节。

我们需要声明若干信号来保证响应数据接收正确,首先声明信号miso_dly对SD卡输入的miso进行打拍,使用两信号判断miso信号下降沿,确定返回数据接收时刻;其次,声明响应数据比特计数器cnt_ack_bit,对返回数据进行比特计数,只有在响应使能信号ack_en为有效高电平时进行计数,计数范围0 -16,0-7为1个字节返回数据R1的比特计数,8-15为所需的8个时钟周期的等待;然后,声明响应使能信号ack_en和响应数据ack_data,使能信号ack_en负责规定响应数据比特计数器cnt_ack_bit的计数范围,使能信号ack_en初值为低电平,当检测到miso下降沿后,拉高使能信号a ck_en,当计数器cnt_ack_bit计数到15时拉低使能信号ack_en,响应数据ack_data只有在计数器技术在0-7时对返回数据miso_dly进行数据拼接,其他时刻保持不变。要注意的是,上述声明的若干信号的同步时钟均为SD卡工作时钟sys_clk_shift。上述各信号波形图如下。

SD068

图 64‑67 SD卡响应数据相关信号波形图

在接收到正确的响应数据后,状态机跳转到读数据状态(RD_DATA),在此状态,SD卡会返回1个字节的数据头0xFE,目的是告知FPGA准备接收读取数据,随后512个字节数据和2个字节的CRC校验数据0xFF_FF自SD卡传入。

在读数据状态首先读取的为1字节数据头0xFE,为正确解析数据头,需要声明两个变量。声明数据头使能信号byte_data_en和数据头变量byte_data,当状态机处于读数据状态时拉高使能信号byte_data_en,读取miso信号赋值给byte_data,赋值方式为{byte_data[6:0] ,miso},当byte_data数据刷新为0xFE,表示检测到数据头,使能信号拉低,byte_data清0,开始进行字节数据读取。

读出SD卡的数据是通过miso串行传入的,为确定读出SD卡的数据量我们需要声明两个计数器对写入SD卡的数据量进行计数。声明数据比特计数器cnt_data_bit对读出SD卡数据进行比特计数,声明数据字节计数器cnt_data_num对读出SD卡数据进行字节计数。数据比特计数器cnt_data_bit ,在状态机处于读数据状态且解析到SD卡回传的数据头0xFE后进行循环计数,计数范围为0-15,表示2个字节;数据字节计数器cnt_data_num,初值为0,当数据字节计数器cnt_data_num计数到最大值15或byte_data检测到数据头0xFE,cnt_data_num自加1。

通过miso端口读取SD卡中的字节数据,同步时钟使用的是sys_clk_shift时钟信号,声明变量rd_data_reg暂存读取的字节数据,位宽为16bit,2字节。

自SD卡读取的字节数据需要暂存在数据读写控制模块data_rw_ctrl的FIFO中,随后按照串口RS232发送时序向PC机发送读取的字节数据。数据写入FIFO中使能信号必不可少,声明读使能信号rd_data_en和读数据rd_data,作为FIFO的写使能和写数据输出至数据读写控制模块。

上述各信号波形图如下。

SD069

图 64‑68 SD卡读数据相关信号波形图

数据读取完毕后,状态机跳转到读完成状态(WR_END), 在此状态需要等待8个时钟周期,状态机跳回初始状态(IDLE)。之所以等待8个时钟周期,是因为片选信号cs_n在SD卡数据读取完成后,要等待8个时钟周期才能拉高片选信号。在此声明计数器cnt_end,状态机处于读完成状态时计数器cnt_end开 始计数,计数范围为0-7,计数到最大值7,清0保持不变,状态机跳转到初始状态,等待下次数据读操作。

上述各信号波形图如下。

SD070

图 64‑69 cnt_end信号波形图

综上所述,SD卡数据读模块各信号波形均已介绍完毕,各信号整合后可得到可得到数据写模块整体波形图。

代码编写

参照SD卡数据读模块波形图,编写SD卡数据读模块参考代码。模块参考代码,具体见代码清单 64‑3。

代码清单 64‑3 SD卡数据读模块(sd_read.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
module sd_read
(
input wire sys_clk , //输入工作时钟,频率50MHz
input wire sys_clk_shift , //输入工作时钟,频率50MHz,相位偏移90度
input wire sys_rst_n , //输入复位信号,低电平有效
input wire miso , //主输入从输出信号
input wire rd_en , //数据读使能信号
input wire [31:0] rd_addr , //读数据扇区地址

output wire rd_busy , //读操作忙信号
output reg rd_data_en , //读数据标志信号
output reg [15:0] rd_data , //读数据
output reg cs_n , //片选信号
output reg mosi //主输出从输入信号
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter IDLE = 3'b000 , //初始状态
SEND_CMD17 = 3'b001 , //读命令CMD17发送状态
CMD17_ACK = 3'b011 , //CMD17响应状态
RD_DATA = 3'b010 , //读数据状态
RD_END = 3'b110 ; //读结束状态
parameter DATA_NUM = 12'd256 ; //待读取数据字节数

//wire define
wire [47:0] cmd_rd ; //数据读指令

//reg define
reg [2:0] state ; //状态机状态
reg [7:0] cnt_cmd_bit ; //指令比特计数器
reg ack_en ; //响应使能信号
reg [7:0] ack_data ; //响应数据
reg [7:0] cnt_ack_bit ; //响应数据字节计数
reg [11:0] cnt_data_num; //读出数据个数计数
reg [3:0] cnt_data_bit; //读数据比特计数器
reg [2:0] cnt_end ; //结束状态时钟计数
reg miso_dly ; //主输入从输出信号打一拍
reg [15:0] rd_data_reg ; //读出数据寄存
reg [15:0] byte_head ; //读数据字节头
reg byte_head_en; //读数据字节头使能

////
//\* Main Code \//
////
//rd_busy:读操作忙信号
assign rd_busy = (state != IDLE) ? 1'b1 : 1'b0;

//cmd_rd:数据读指令
assign cmd_rd = {8'h51,rd_addr,8'hff};

//miso_dly:主输入从输出信号打一拍
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
miso_dly <= 1'b0;
else
miso_dly <= miso;

//ack_en:响应使能信号
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ack_en <= 1'b0;
else if(cnt_ack_bit == 8'd15)
ack_en <= 1'b0;
else if((state == CMD17_ACK) && (miso == 1'b0)
&& (miso_dly == 1'b1) && (cnt_ack_bit == 8'd0))
ack_en <= 1'b1;
else
ack_en <= ack_en;

//ack_data:响应数据
//cnt_ack_bit:响应数据字节计数
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
ack_data <= 8'b0;
cnt_ack_bit <= 8'd0;
end
else if(ack_en == 1'b1)
begin
cnt_ack_bit <= cnt_ack_bit + 8'd1;
if(cnt_ack_bit < 8'd8)
ack_data <= {ack_data[6:0],miso_dly};
else
ack_data <= ack_data;
end
else
cnt_ack_bit <= 8'd0;

//state:状态机状态跳转
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE:
if(rd_en == 1'b1)
state <= SEND_CMD17;
else
state <= state;
SEND_CMD17:
if(cnt_cmd_bit == 8'd47)
state <= CMD17_ACK;
else
state <= state;
CMD17_ACK:
if(cnt_ack_bit == 8'd15)
if(ack_data == 8'h00)
state <= RD_DATA;
else
state <= SEND_CMD17;
else
state <= state;
RD_DATA:
if((cnt_data_num == (DATA_NUM + 1'b1))
&& (cnt_data_bit == 4'd15))
state <= RD_END;
else
state <= state;
RD_END:
if(cnt_end == 3'd7)
state <= IDLE;
else
state <= state;
default:state <= IDLE;
endcase

//cs_n:输出片选信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cs_n <= 1'b1;
else if(cnt_end == 3'd7)
cs_n <= 1'b1;
else if(rd_en == 1'b1)
cs_n <= 1'b0;
else
cs_n <= cs_n;

//cnt_cmd_bit:指令比特计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_cmd_bit <= 8'd0;
else if(state == SEND_CMD17)
cnt_cmd_bit <= cnt_cmd_bit + 8'd1;
else
cnt_cmd_bit <= 8'd0;

//mosi:主输出从输入信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
mosi <= 1'b1;
else if(state == SEND_CMD17)
mosi <= cmd_rd[8'd47 - cnt_cmd_bit];
else
mosi <= 1'b1;

//byte_head:读数据字节头
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
byte_head <= 16'b0;
else if(byte_head_en == 1'b0)
byte_head <= 16'b0;
else if(byte_head_en == 1'b1)
byte_head <= {byte_head[14:0],miso};
else
byte_head <= byte_head;

//byte_head_en:读数据字节头使能
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
byte_head_en <= 1'b0;
else if(byte_head == 16'hfffe)
byte_head_en <= 1'b0;
else if((state == RD_DATA) && (cnt_data_num == 12'd0)
&& (cnt_data_bit == 4'd0))
byte_head_en <= 1'b1;
else
byte_head_en <= byte_head_en;

//cnt_data_bit:读数据比特计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data_bit <= 4'd0;
else if((state == RD_DATA) && (cnt_data_num >= 12'd1))
cnt_data_bit <= cnt_data_bit + 4'd1;
else
cnt_data_bit <= 4'd0;

//cnt_data_num:读出数据个数计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data_num <= 12'd0;
else if(state == RD_DATA)
if((cnt_data_bit == 4'd15) \|\| (byte_head == 16'hfffe))
cnt_data_num <= cnt_data_num + 12'd1;
else
cnt_data_num <= cnt_data_num;
else
cnt_data_num <= 12'd0;

//rd_data_reg:读出数据寄存
always@(posedge sys_clk_shift or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_data_reg <= 16'd0;
else if((state == RD_DATA) && (cnt_data_num >= 12'd1)
&& (cnt_data_num <= DATA_NUM))
rd_data_reg <= {rd_data_reg[14:0],miso};
else
rd_data_reg <= 16'd0;

//rd_data_en:读数据标志信号
//rd_data:读数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
rd_data_en <= 1'b0;
rd_data <= 16'd0;
end
else if(state == RD_DATA)
begin
if((cnt_data_bit == 4'd15) && (cnt_data_num <= DATA_NUM))
begin
rd_data_en <= 1'b1;
rd_data <= rd_data_reg;
end
else
begin
rd_data_en <= 1'b0;
rd_data <= rd_data;
end
end
else
begin
rd_data_en <= 1'b0;
rd_data <= 16'd0;
end

//cnt_end:结束状态时钟计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_end <= 3'd0;
else if(state == RD_END)
cnt_end <= cnt_end + 3'd1;
else
cnt_end <= 3'd0;

endmodule

Signal Tap信号波形

使用SignalTap对信号进行抓取,SD卡数据读模块信号抓取波形如下。与绘制波形图相对比,各信号波形吻合,模块正常工作。

SD071

图 64‑70 SD卡数据读模块信号抓取局部波形图(一)

SD072

图 64‑71 SD卡数据读模块信号抓取局部波形图(二)

SD073

图 64‑72 SD卡数据读模块信号抓取局部波形图(三)

SD074

图 64‑73 SD卡数据读模块信号抓取局部波形图(四)

SD075

图 64‑74 SD卡数据读模块信号抓取局部波形图(五)

14.3.3.5. SD卡控制模块

模块框图

上面几个小节对SD卡初始化模块、数据写模块和数据读模块做了详细说明,SD卡控制器的主要内容已经介绍完毕,我们开始SD卡控制模块的介绍。

SD卡控制模块其实就是SD卡控制器的顶层模块,将SD卡初始化模块、数据写模块和数据读模块实例化其中,将各子功能模块对应信号相连接。SD卡控制模块框图和输入输出信号功能描述具体见图 64‑75、表格 64‑4。

SD076

图 64‑75 SD卡控制模块框图

表格 64‑4 输入输出端口功能描述

信号 |

宽 | 类型

功能描述

sys_clk

1Bit

Input

工作时钟,频率50MHz,无相位偏移 |

sys_clk_shift

1Bit

Input

SD卡工作时钟,频率50MHz,做相位 | 偏移处理,也作为miso信号采集时钟 |

sys_rst_n

1Bit

Input

复位信号,低电平有效 |

sd_miso

1Bit

Input

SD卡主输入从 | 输出信号miso,数据由SD卡传入FPGA |

wr_en

1Bit

Input

数据写使能信号 |

wr_addr

32Bit

Input

数据写扇区地址 |

wr_data

16Bit

Input

SD卡待写入数据 |

rd_en

1Bit

Input

数据读使能信号 |

rd_addr

32Bit

Input

数据读扇区地址 |

sd_clk

1Bit

Output

SD卡工作时钟 | ,频率50MHz,由sys_clk_shift赋值 |

sd_cs_n

1Bit

Output

SD卡片选信号 |

sd_mosi

1Bit

Output

SD卡主输出从 | 输入信号mosi,数据由FPGA传入SD卡 |

wr_busy

1Bit

Output

数据写操作忙信号 |

wr_req

1Bit

Output

写数据请求信号 |

rd_busy

1Bit

Output

数据读操作忙信号 |

rd_data_en

1Bit

Output

SD卡读出数据标志信号 |

rd_data

16Bit

Output

SD卡读出数据 |

init_end

1Bit

Output

SD卡初始化完成信号 |

代码编写

SD卡控制模块为SD卡控制器的顶层模块,内部为各子功能模块的实例化,无需波形图的绘制,模块代码较为简单可直接编写。SD卡控制模块参考代码具体见代码清单 64‑4。

代码清单 64‑4 SD卡控制模块参考代码(sd_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
module sd_ctrl
(
input wire sys_clk , //输入工作时钟,频率50MHz
input wire sys_clk_shift , //输入工作时钟,频率50MHz,相位偏移90度
input wire sys_rst_n , //输入复位信号,低电平有效
//SD卡接口
input wire sd_miso , //主输入从输出信号
output wire sd_clk , //SD卡时钟信号
output reg sd_cs_n , //片选信号
output reg sd_mosi , //主输出从输入信号
//写SD卡接口
input wire wr_en , //数据写使能信号
input wire [31:0] wr_addr , //写数据扇区地址
input wire [15:0] wr_data , //写数据
output wire wr_busy , //写操作忙信号
output wire wr_req , //写数据请求信号
//读SD卡接口
input wire rd_en , //数据读使能信号
input wire [31:0] rd_addr , //读数据扇区地址
output wire rd_busy , //读操作忙信号
output wire rd_data_en , //读数据标志信号
output wire [15:0] rd_data , //读数据

output wire init_end //SD卡初始化完成信号
);

////
//\* Parameter and Internal Signal \//
////
//wire define
wire init_cs_n ; //初始化阶段片选信号
wire init_mosi ; //初始化阶段主输出从输入信号
wire wr_cs_n ; //写数据阶段片选信号
wire wr_mosi ; //写数据阶段主输出从输入信号
wire rd_cs_n ; //读数据阶段片选信号
wire rd_mosi ; //读数据阶段主输出从输入信号

////
//\* Main Code \//
////
//sd_clk:SD卡时钟信号
assign sd_clk = sys_clk_shift;

//SD卡接口信号选择
always@(*)
if(init_end == 1'b0)
begin
sd_cs_n <= init_cs_n;
sd_mosi <= init_mosi;
end
else if(wr_busy == 1'b1)
begin
sd_cs_n <= wr_cs_n;
sd_mosi <= wr_mosi;
end
else if(rd_busy == 1'b1)
begin
sd_cs_n <= rd_cs_n;
sd_mosi <= rd_mosi;
end
else
begin
sd_cs_n <= 1'b1;
sd_mosi <= 1'b1;
end

////
//\* Instantiation \//
////
//------------- sd_init_inst -------------
sd_init sd_init_inst
(
.sys_clk (sys_clk ), //输入工作时钟,频率50MHz
.sys_clk_shift (sys_clk_shift ), //输入工作时钟,频率50MHz,相位偏移90度
.sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效
.miso (sd_miso ), //主输入从输出信号

.cs_n (init_cs_n ), //输出片选信号
.mosi (init_mosi ), //主输出从输入信号
.init_end (init_end ) //初始化完成信号
);

//------------- sd_write_inst -------------
sd_write sd_write_inst
(
.sys_clk (sys_clk ), //输入工作时钟,频率50MHz
.sys_clk_shift (sys_clk_shift ), //输入工作时钟,频率50MHz,相位偏移90度
.sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效
.miso (sd_miso ), //主输入从输出信号
.wr_en (wr_en && init_end ), //数据写使能信号
.wr_addr (wr_addr ), //写数据扇区地址
.wr_data (wr_data ), //写数据

.cs_n (wr_cs_n ), //输出片选信号
.mosi (wr_mosi ), //主输出从输入信号
.wr_busy (wr_busy ), //写操作忙信号
.wr_req (wr_req ) //写数据请求信号
);

//------------- sd_read_inst -------------
sd_read sd_read_inst
(
.sys_clk (sys_clk ), //输入工作时钟,频率50MHz
.sys_clk_shift (sys_clk_shift ), //输入工作时钟,频率50MHz,相位偏移90度
.sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效
.miso (sd_miso ), //主输入从输出信号
.rd_en (rd_en & init_end ), //数据读使能信号
.rd_addr (rd_addr ), //读数据扇区地址

.rd_busy (rd_busy ), //读操作忙信号
.rd_data_en (rd_data_en ), //读数据标志信号
.rd_data (rd_data ), //读数据
.cs_n (rd_cs_n ), //片选信号
.mosi (rd_mosi ) //主输出从输入信号
);

endmodule

在前文中,我们对SD卡控制器内部各子功能模块均进行了SignalTap信号抓取,对SD卡控制模块不再进行信号抓取。

14.3.3.6. 数据读写控制模块

模块框图

SD卡数据读写控制器已经设计完成,在本实验中我们使用串口RS232对SD卡数据读写控制器进行验证。串口RS232的相关知识在前面章节介绍过,此处直接调用之前模块即可,不再赘述。

本小节主要详细介绍数据读写控制模块,因为串口RS232读写数据的时序与SD卡数据读写时序不同,不能直接进行数据交互,所以在此处设计数据读写控制模块模块data_rw_ctrl。数据读写控制模块的作用有三,一是使用FIFO暂存串口接收模块写入的512字节数据;二是使用FIFO暂存自SD卡读出的512字 节数据;三是为SD卡数据读写控制器提供读写使能、读写地址和写数据。

同时,读者还要注意的是串口传入和写入串口的数据为单字节数据,位宽8比特,而写入和读出SD卡控制器的数据均为2字节,位宽为16比特,在数据读写控制模块中使用FIFO实现位宽转换。

数据读写控制模块框图和输入输出端口功能描述具体见图 64‑76、表格 64‑5。

SD077

图 64‑76 数据读写控制模块框图

表格 64‑5 输入输出端口功能功能描述

信号 |

宽 | 类型

功能描述

sys_clk

1Bit

Input

工作时钟,频率50MHz,无相位偏移 |

sys_rst_n

1Bit

Input

复位信号,低电平有效 |

init_end

1Bit

Input

SD卡初始化完成信号 |

rx_flag

1Bit

Input

串口发送数据标志信号,作为读fifo的写请求信号 |

rx_data

8Bit

Input

串口发送模块写入的单字节数据,作为读fifo的写入信号 |

wr_req

1Bit

Input

写数据请求信号,作为读fifo的读请求信号 |

wr_busy

1Bit

Input

数据写操作忙信号 |

rd_data_en

1Bit

Input

SD卡读出数据标志信号,作为写fifo的写请求信号 |

rd_data

16Bit

Input

SD卡读出数据,作为写fifo的写数据 |

rd_busy

1Bit

Input

数据读操作忙信号 |

wr_en

1Bit

Output

SD卡控制器写使能信号 |

wr_addr

32Bit

Output

SD卡写数据扇区地址 |

wr_data

16Bit

Output

待写入SD卡数据,由读fifo读数据传出 |

rd_en

1Bit

Output

SD卡控制器写使能信号 |

rd_addr

32Bit

Output

SD卡读数据扇区地址 |

tx_flag

1Bit

Output

串口接收数据标志信号,作为写fifo的读请求信号 |

tx_data

8Bit

Output

出入串口接收模块的单字节数据,作为写fifo的读出数据 |

波形图绘制

本小节我们开始数据读写控制模块波形图的绘制,通过模块波形图的绘制为读者讲解数据读写控制模块的设计思路以及各信号波形的设计与实现。数据读写控制模块整体波形图具体见图 64‑77、图 64‑78。

SD078

图 64‑77 数据读写控制模块整体波形图(一)

SD079

图 64‑78 数据读写控制模块整体波形图(二)

SD卡数据读模块整体波形图如上图所示,为了便于读者理解,接下来我们会对模块所涉及各信号的设计思路与实现方法做一下详细介绍。

前文我们也说到,本模块的作用是做数据缓存,以及为SD卡控制器提供读写使能、读写地址和写数据。模块内部我们实例化了两个FIFO IP核,一个FIFO用于缓存串口接收模块传入的512字节数据;另一个FIFO用以缓存自SD卡读出的512字节数据。同时,这两个FIFO还有实现位宽转换的作用。

待写入SD卡的512字节数据由端口rx_data传入,与其同步传入的还有数据标志信号rx_flag,rx_flag信号作为模块内部读FIFO的写使能信号将数据rx_data写入写FIFO,写入时钟信号为系统时钟sys_clk,与串口数据接收模块系统时钟相同。

为了判断写FIFO内缓存的数据量,声明变量wr_fifo_data_num将读FIFO的读端口剩余数据量rdusedw引出。

声明SD卡数据写使能信号wr_en,输出到SD卡读写控制器,控制SD卡数据写操作,以变量wr_fifo_data_num和SD卡初始化完成信号init_end为约束条件,当写FIFO内数据量达到设定值且SD卡初始化完成,拉高写使能信号wr_en。上述个信号波形如下。

SD080

图 64‑79 写使能wr_en及相关信号波形

SD卡写使能信号wr_en传入SD卡读写控制器,wr_en信号有效,SD卡开始执行数据写操作,回传写忙信号wr_busy和写请求信号wr_req。

以写请求信号作为写FIFO的读请求信号,对写FIFO进行数据读取,将读出的待写入SD卡数据wr_data传回SD卡读写控制器,同时传出的还有SD卡写扇区初始地址wr_addr。各信号波形如下。

SD081

图 64‑80 写数据wr_data、写地址wr_addr信号波形

当SD卡完成一个扇区,即512字节数据写入后,我们可以开始SD卡数据读操作。

当SD卡完成数据写操作后,写忙信号wr_busy拉低,我们可以利用wr_busy信号的下降沿生成并传出读使能信号rd_en。

声明信号wr_busy_dly对写忙信号wr_busy打一拍,检测wr_busy信号下降沿wr_busy_fall,使用wr_busy_fall信号作为约束条件,检测到wr_busy信号下降时,拉高SD卡读使能信号rd_en,只拉高一个时钟周期,其他时刻保持低电平。同时与读使能信号rd_en传入SD 卡读写控制器的还有SD卡读扇区初始地址rd_addr。各信号波形如下。

SD082

图 64‑81 读使能rd_en、读地址rd_addr信号波形

SD卡读使能信号rd_en传入SD卡读写控制器,rd_en信号有效,SD卡开始执行数据读操作,回传读忙信号rd_busy、写数据rd_data,以及与读数据同步的读数据使能信号rd_data_en。

以读数据使能信号rd_data_en作为读FIFO的写请求信号,将读出SD卡的数据rd_data暂存到读FIFO中。各信号波形如下。

SD083

图 64‑82 读数据rd_data及相关信号波形

SD卡数据读操作完成后,读取的全部数据已暂存到读FIFO中,接下来要做的就是按照串口RS232的时序将数据回传到PC机。

使用读忙信号rd_busy打一拍得到信号rd_busy_dly,两信号作为约束条件检测rd_busy信号下降沿,生成信号rd_busy_fall。

之所以生成下降沿信号rd_busy_fall,是为了使用下降沿信号rd_busy_fall作为约束条件,控制串口发送数据使能信号send_data_en电平变化。当信号rd_busy_fall有效拉高信号send_data_en,当读FIFO内数据均已读取完成,拉低信号send_data_en,只有 信号send_data_en位高电平时,通过串口向PC机发送数据,为低电平时发送数据无效。

为了满足串口RS232的时序要求,我们需要考虑两次数据发送之间的时间间隔,时间间隔必须大于等于串口RS232完整发送8比特数据所需时间。在本次实验中,我们使用的串口RS232波特率为9600、时钟为50MHz,完成一次单字节数据的发送需要52080个时钟周期。所以两次数据发送之间的时间间隔要大约等于 52080个时钟周期。如果使用其他波特率,自行计算时间间隔,计算方法在串口RS232章节有详细介绍。

为了对时间间隔进行计数,声明计数器cnt_wait,初值为0,每个时钟周期自加1,计数最大值大于等于52080,只有在使能信号send_data_en为高电平时进行计数,其他时刻保持为0。

想要将读FIFO中的数据读出,读时钟信号要与串口时钟信号相同,还需要写入读使能信号。声明读FIFO读使能信号rd_fifo_rd_en,当计数器cnt_wait计数到最大值时,保持一个时钟周期高电平,其他时刻保持低电平,将信号输入读FIFO读请求端口,读取FIFO内数据。

读使能信号输入读FIFO中,使能信号有效时,读取数据输出,定义为tx_data,输出至串口数据发送模块。

为确定读出数据个数,我们还要声明计数器对读出数据个数计数,同时此计数器也作为约束条件控制使能信号send_data_en。声明计数器send_data_num,使能信号send_data_en为无效的低电平时,计数器保持为0,当使能信号为高电平,且时间间隔计数器cnt_wait计数到最大值,计数器s end_data_numsend_data_num自加1,其他时刻保持不变。当读出数据个数与写入SD卡数据个数相同,拉低使能信号send_data_en

读者还要注意的一点是,有效的读请求信号写入FIFO后,数据在请求信号的下一个时钟周期才会输出;同时,输入串口数据发送模块的数据必须有与数据同步的标志信号一起传入。所以我们可以利用读请求信号打一拍生成读出数据标志信号tx_flag,这样标志信号tx_flag与读出数据tx_data同步传入串口数据发送 模块。

上述各信号波形如下。

SD084

图 64‑83 串口发送数据tx_data及相关信号波形

各信号波形讲解完毕,整合后可得到模块整体波形图。小节中波形图仅供参考,读者也可根据自己理解自行设计波形图。

代码编写

参照绘制的模块波形图,编写模块参考代码。模块参考代码具体见。

代码清单 64‑5 数据读写控制模块参考代码(data_rw_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
module data_rw_ctrl
(
input wire sys_clk , //输入工作时钟,频率50MHz
input wire sys_rst_n , //输入复位信号,低电平有效
input wire init_end , //SD卡初始化完成信号

input wire rx_flag , //写fifo写入数据标志信号
input wire [7:0] rx_data , //写fifo写入数据
input wire wr_req , //sd卡数据写请求
input wire wr_busy , //sd卡写数据忙信号

output wire wr_en , //sd卡数据写使能信号
output wire [31:0] wr_addr , //sd卡写数据扇区地址
output wire [15:0] wr_data , //sd卡写数据

input wire rd_data_en , //sd卡读出数据标志信号
input wire [15:0] rd_data , //sd卡读出数据
input wire rd_busy , //sd卡读数据忙信号
output reg rd_en , //sd卡数据读使能信号
output wire [31:0] rd_addr , //sd卡读数据扇区地址
output reg tx_flag , //读fifo读出数据标志信号
output wire [7:0] tx_data //读fifo读出数据
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter DATA_NUM = 12'd256 ; //读写数据个数
parameter SECTOR_ADDR = 32'd1000 ; //读写数据扇区地址
parameter CNT_WAIT_MAX= 16'd60000 ; //读fifo输出数据时间间隔计数最大值

//wire define
wire [11:0] wr_fifo_data_num ; //写fifo内数据个数
wire wr_busy_fall ; //sd卡写数据忙信号下降沿
wire rd_busy_fall ; //sd卡读数据忙信号下降沿
//wire rd_fifo_rd_en ; //读fifo读使能信号

//reg define
reg wr_busy_dly ; //sd卡写数据忙信号打一拍
reg rd_busy_dly ; //sd卡读数据忙信号打一拍
reg send_data_en ; //串口发送数据使能信号
reg [15:0] cnt_wait ; //读fifo输出数据时间间隔计数
reg [11:0] send_data_num ; //串口发送数据字节数计数
reg rd_fifo_rd_en ;

////
//\* Main Code \//
////
//wr_en:sd卡数据写使能信号
assign wr_en = ((wr_fifo_data_num == (DATA_NUM)) && (init_end == 1'b1))
? 1'b1 : 1'b0;

//wr_addr:sd卡写数据扇区地址
assign wr_addr = SECTOR_ADDR;

//wr_busy_dly:sd卡写数据忙信号打一拍
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wr_busy_dly <= 1'b0;
else
wr_busy_dly <= wr_busy;

//wr_busy_fall:sd卡写数据忙信号下降沿
assign wr_busy_fall = ((wr_busy == 1'b0) && (wr_busy_dly == 1'b1))
? 1'b1 : 1'b0;

//rd_en:sd卡数据读使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_en <= 1'b0;
else if(wr_busy_fall == 1'b1)
rd_en <= 1'b1;
else
rd_en <= 1'b0;

//rd_addr:sd卡读数据扇区地址
assign rd_addr = SECTOR_ADDR;

//rd_busy_dly:sd卡读数据忙信号打一拍
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_busy_dly <= 1'b0;
else
rd_busy_dly <= rd_busy;

//rd_busy_fall:sd卡读数据忙信号下降沿
assign rd_busy_fall = ((rd_busy == 1'b0) && (rd_busy_dly == 1'b1))
? 1'b1 : 1'b0;

//send_data_en:串口发送数据使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
send_data_en <= 1'b0;
else if((send_data_num == (DATA_NUM \* 2) - 1'b1)
&& (cnt_wait == CNT_WAIT_MAX - 1'b1))
send_data_en <= 1'b0;
else if(rd_busy_fall == 1'b1)
send_data_en <= 1'b1;
else
send_data_en <= send_data_en;

//cnt_wait:读fifo输出数据时间间隔计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_wait <= 16'd0;
else if(send_data_en == 1'b1)
if(cnt_wait == CNT_WAIT_MAX)
cnt_wait <= 16'd0;
else
cnt_wait <= cnt_wait + 1'b1;
else
cnt_wait <= 16'd0;

//send_data_num:串口发送数据字节数计数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
send_data_num <= 12'd0;
else if(send_data_en == 1'b1)
if(cnt_wait == CNT_WAIT_MAX)
send_data_num <= send_data_num + 1'b1;
else
send_data_num <= send_data_num;
else
send_data_num <= 12'd0;

//rd_fifo_rd_en:读fifo读使能信号
//assign rd_fifo_rd_en = (cnt_wait == CNT_WAIT_MAX) ? 1'b1 : 1'b0;
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_fifo_rd_en <= 1'b0;
else if(cnt_wait == (CNT_WAIT_MAX - 1'b1))
rd_fifo_rd_en <= 1'b1;
else
rd_fifo_rd_en <= 1'b0;

//tx_flag:读fifo读出数据标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tx_flag <= 1'b0;
else
tx_flag <= rd_fifo_rd_en;

////
//\* Instantiation \//
////
//------------- fifo_wr_data_inst -------------
fifo_wr_data fifo_wr_data_inst
(
.wrclk (sys_clk ), //数据写时钟
.wrreq (rx_flag ), //数据写使能
.data (rx_data ), //写入数据

.rdclk (sys_clk ), //数据读时钟
.rdreq (wr_req ), //数据读使能
.q (wr_data ), //读出数据
.rdusedw (wr_fifo_data_num ) //fifo内剩余数据个数
);

//------------- fifo_rd_data_inst -------------
fifo_rd_data fifo_rd_data_inst
(
.wrclk (sys_clk ), //数据写时钟
.wrreq (rd_data_en ), //数据写使能
.data (rd_data ), //写入数据

.rdclk (sys_clk ), //数据读时钟
.rdreq (rd_fifo_rd_en ), //数据读使能
.q (tx_data ) //读出数据
);

endmodule

Signal Tap信号波形

使用SignalTap对信号进行抓取,SD卡数据读模块信号抓取波形如下。与绘制波形图相对比,各信号波形吻合,模块正常工作。

SD085

图 64‑84 数据读写控制模块信号抓取局部波形图(一)

SD086

图 64‑85 数据读写控制模块信号抓取局部波形图(二)

SD087

图 64‑86数据读写控制模块信号抓取局部波形图(三)

SD088

图 64‑87 数据读写控制模块信号抓取局部波形图(四)

SD089

图 64‑88 数据读写控制模块信号抓取局部波形图(五)

SD090

图 64‑89 数据读写控制模块信号抓取局部波形图(六)

SD091

图 64‑90 数据读写控制模块信号抓取局部波形图(七)

SD092

图 64‑91 数据读写控制模块信号抓取局部波形图(八)

14.3.3.7. 顶层模块

讲到这里,实验工程涉及的子功能模块均已介绍完毕,接下来说明整个实验工程的顶层模块。

模块框图

顶层模块将各子功能模块实例化其中,连接各自对应信号,顶层模块模块框图如图 64‑92所示。

SD093

图 64‑92 顶层模块模块框图

由图可知,顶层模块有输入输出信号各4路,4路输入信号有时钟信号、复位信号、串行数据输入信号rx和SD卡传入的主输入从输出信号sd_miso;4路输出信号中有3路输出到SD卡,包括SD卡时钟信号sd_clk、片选信号sd_cs_n和主输出从输入信号sd_mosi,还有1路串行数据发送信号tx。

代码编写

顶层模块的代码较为较为简单,只是对各模块进行实例化以及连接各自对应信号,无需波形图的绘制。编写顶层模块参考代码,具体见代码清单 64‑6。

代码清单 64‑6 顶层模块参考代码(uart_sd.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
module uart_sd
(
input wire sys_clk , //输入工作时钟,频率50MHz
input wire sys_rst_n , //输入复位信号,低电平有效
input wire rx , //串口发送数据
input wire sd_miso , //主输入从输出信号

output wire sd_clk , //SD卡时钟信号
output wire sd_cs_n , //片选信号
output wire sd_mosi , //主输出从输入信号
output wire tx //串口接收数据
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter UART_BPS = 14'd9600 , //比特率
CLK_FREQ = 26'd50_000_000 ; //时钟频率

//wire define
wire rx_flag ; //写fifo写入数据标志信号
wire [7:0] rx_data ; //写fifo写入数据
wire wr_req ; //sd卡数据写请求
wire wr_busy ; //sd卡写数据忙信号
wire wr_en ; //sd卡数据写使能信号
wire [31:0] wr_addr ; //sd卡写数据扇区地址
wire [15:0] wr_data ; //sd卡写数据
wire rd_data_en ; //sd卡读出数据标志信号
wire [15:0] rd_data ; //sd卡读出数据
wire rd_busy ; //sd卡读数据忙信号
wire rd_en ; //sd卡数据读使能信号
wire [31:0] rd_addr ; //sd卡读数据扇区地址
wire tx_flag ; //读fifo读出数据标志信号
wire [7:0] tx_data ; //读fifo读出数据
wire clk_50m ; //生成50MHz时钟
wire clk_50m_shift ; //生成50MHz时钟,相位偏移180度
wire locked ; //时钟锁定信号
wire rst_n ; //复位信号
wire init_end ; //SD卡初始化完成信号

//rst_n:复位信号,低有效
assign rst_n = sys_rst_n && locked;

////
//\* Instantiation \//
////
//------------- clk_gen_inst -------------
clk_gen clk_gen_inst
(
.areset (~sys_rst_n ), //复位信号,高有效
.inclk0 (sys_clk ), //输入系统时钟,50MHz

.c0 (clk_50m ), //生成50MHz时钟
.c1 (clk_50m_shift ), //生成50MHz时钟,相位偏移180度
.locked (locked ) //时钟锁定信号
);

//------------- uart_rx_inst -------------
uart_rx
#(
.UART_BPS (UART_BPS ), //串口波特率
.CLK_FREQ (CLK_FREQ ) //时钟频率
)
uart_rx_inst
(
.sys_clk (clk_50m ), //系统时钟50MHz
.sys_rst_n (rst_n ), //全局复位
.rx (rx ), //串口接收数据

.po_data (rx_data ), //串转并后的数据
.po_flag (rx_flag ) //串转并后的数据有效标志信号
);

//------------- data_rw_ctrl_inst -------------
data_rw_ctrl data_rw_ctrl_inst
(
.sys_clk (clk_50m ), //输入工作时钟,频率50MHz
.sys_rst_n (rst_n ), //输入复位信号,低电平有效
.init_end (init_end ), //SD卡初始化完成信号

.rx_flag (rx_flag ), //写fifo写入数据标志信号
.rx_data (rx_data ), //写fifo写入数据
.wr_req (wr_req ), //sd卡数据写请求
.wr_busy (wr_busy ), //sd卡写数据忙信号

.wr_en (wr_en ), //sd卡数据写使能信号
.wr_addr (wr_addr ), //sd卡写数据扇区地址
.wr_data (wr_data ), //sd卡写数据

.rd_data_en (rd_data_en), //sd卡读出数据标志信号
.rd_data (rd_data ), //sd卡读出数据
.rd_busy (rd_busy ), //sd卡读数据忙信号
.rd_en (rd_en ), //sd卡数据读使能信号
.rd_addr (rd_addr ), //sd卡读数据扇区地址
.tx_flag (tx_flag ), //读fifo读出数据标志信号
.tx_data (tx_data ) //读fifo读出数据
);

//------------- sd_ctrl_inst -------------
sd_ctrl sd_ctrl_inst
(
.sys_clk (clk_50m ), //输入工作时钟,频率50MHz
.sys_clk_shift (clk_50m_shift ), //输入工作时钟,频率50MHz,相位偏移180度
.sys_rst_n (rst_n ), //输入复位信号,低电平有效

.sd_miso (sd_miso ), //主输入从输出信号
.sd_clk (sd_clk ), //SD卡时钟信号
.sd_cs_n (sd_cs_n ), //片选信号
.sd_mosi (sd_mosi ), //主输出从输入信号

.wr_en (wr_en ), //数据写使能信号
.wr_addr (wr_addr ), //写数据扇区地址
.wr_data (wr_data ), //写数据
.wr_busy (wr_busy ), //写操作忙信号
.wr_req (wr_req ), //写数据请求信号

.rd_en (rd_en ), //数据读使能信号
.rd_addr (rd_addr ), //读数据扇区地址
.rd_busy (rd_busy ), //读操作忙信号
.rd_data_en (rd_data_en ), //读数据标志信号
.rd_data (rd_data ), //读数据

.init_end (init_end ) //SD卡初始化完成信号
);

//------------- uart_tx_inst -------------
uart_tx
#(
.UART_BPS (UART_BPS ), //串口波特率
.CLK_FREQ (CLK_FREQ ) //时钟频率
)
uart_tx_inst
(
.sys_clk (clk_50m ), //系统时钟50MHz
.sys_rst_n (rst_n ), //全局复位
.pi_data (tx_data ), //并行数据
.pi_flag (tx_flag ), //并行数据有效标志信号

.tx (tx ) //串口发送数据
);

endmodule

RTL视图

顶层代码编写完成后,使用Quartus软件对实验工程进行编译,编译通过后查看RTL视图,如图 64‑93、图 64‑94所示。RTL视图与实验工程框图一致,各信号连接正确。

SD094

图 64‑93 RTL视图(一)

SD095

图 64‑94 RTL视图(二)

14.4. 上板调试

14.4.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 64‑6所示。

表格 64‑6 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

时钟

sys_rst_n

Input

M15

复位

rx

input

N6

串口接收数据

tx

output

N5

串口发送数据

sd_miso

input

J16

SD卡主输入从输出信号

sd_clk

output

J12

SD卡时钟信号

sd_cs_n

output

K12

SD卡片选信号

sd_mosi

output

J14

SD卡主输出从输入信号

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 64‑95所示。

SD096

图 64‑95 管脚分配

14.4.1.1. 结果验证

如图 64‑96所示,开发板连接12V直流电源、USB-Blaster下载器JTAG端口、USB数据线、插入SD卡、连接好短路帽。线路正确连接后,打开开关为板卡上电。

SD097

图 64‑96 程序下载连线图

如图 64‑97所示,使用“Programmer”为开发板下载程序。

SD098

图 64‑97 程序下载图

程序下载完成后,使用串口助手向开发板发送连续的512字节数据,通过SD卡读写控制器将数据写入SD卡,随后从SD卡中读出写入数据,并通过串口助手打印出来,如图 64‑98所示。

SD099 SD100

图 64‑98 串口助手读写SD卡

14.5. 章末总结

本章节我们学习了SD卡在SPI模式下的初始化、数据写操作以及数据读操作的流程;结合SD卡相关理论知识,设计了SD卡数据读写控制器,并通过实验加以验证。读者要认真理解相关理论知识,切实掌握SD卡数据读写控制器的设计与实现。

14.6. 拓展训练

  1. 修改代码,尝试实现SD卡SPI模式下多数据块的读写操作;

  2. 自行查阅资料,学习SD卡SDIO模式下的数据读出的相关理论知识,尝试实现SDIO模式下的SD卡数据读写控制器。