之前挖了个MicroSD的坑,当时以为是个小坑,做了才发现是个天坑,光是介绍就要写一整篇,因此打算把它分成两部分,第一部分讲SD卡的初始化流程,第二部分做具体的数据传送
其实在介绍开发板的几种烧写方式时,有略微提到了怎么用MicroSD卡来烧写,但没有深入介绍是怎么使用这种小巧轻便,而且容量不小,还比较通用的存储方式,对于嵌入式攻城狮们挺有用的
FPGA基础入门【18】开发板MicroSD接口控制--初始化
MicroSD介绍
在介绍UART串口控制的时候有提到过板载的PIC24芯片,它除了控制串口和USB,还控制MicroSD。在NEXYS 4开发板文档中看到它和FPGA的连接如下(省去了PIC24的部分)。引脚定义下面会讲,只是这个SD_RESET复位信号接了一个PNP三极管,是低电平连通的,用来保证为SD卡的电流供应,因为FPGA的输出电流,也就是驱动能力很低:
开始找和SD卡接口相关的时序才发现这是个大坑,内容多且杂乱,连英文文档都不容易读,希望通过这一篇和实际开发板有关的教程,帮助理解SD的驱动流程
和SD卡有关的官方定义是由SD association做的,链接如下:
SD association simplified specification
我们主要读的是Physical Layer Simplified Specification简化物理层文档
结构和引脚
SD卡的形状和内部结构如下,这里放的是大的卡,和微型卡差不多。可以看出卡中包含一个接口控制器、多个控制寄存器以及存储核心
各个引脚的定义如下:
SD卡根据初始化时候的寄存器配置,有SD和SPI两种模式。SPI模式只需要修改一些SPI借口逻辑就可以,很多人用这种方式避免额外的工作,这里我们选择做出完整的SD接口
- DAT3是数据线第3位,也当做卡识别位,以及SPI中的片选位
- CMD是命令/回复位,在SPI中是数据输入位
- VSS1、VSS2和VDD在两种模式中都是电源和接地位
- CLK在两种模式中作为时钟位
- DAT0是数据线第0位,在SPI中是数据输出位
- DAT1和DAT2分别是数据线第1位和第2位,在SPI中不使用
在卡中各个控制寄存器定义如下,暂时不介绍具体细节,下面的指令中需要的时候再找它的具体定义
- CID,ID寄存器
- RCA,相关地址寄存器
- DSR,驱动状态寄存器,表示SD卡的输出状态
- CSD,指定数据寄存器,包含SD卡的操作状态
- SCR,配置寄存器
- OCR,操作状态寄存器
- SSR,SD状态寄存器
- CSR,卡片状态寄存器
时钟时序
关于时钟频率,找到最基础的UHS-I SD卡时钟频率表,我们使用最基础的25MHz。SD卡的时钟是可以实时改变的,如果需要中断一个指令,甚至可以把时钟停一段时间。需要注意的是,这个只是在数据传输模式的时钟频率,在待机模式和卡片识别模式中,要保持时钟在100kHz到400kHz之间,为了保险我们选择中间的200kHz:
SD卡有三种模式如下,每种模式都包含自己的状态,无卡时处于Inactive待机模式,侦测到卡后进入card identification卡片识别模式,识别并配置SD卡,之后进入数据传输模式。这篇教程先处理待机模式和卡片识别模式,进入数据传输模式之后的事情到下一篇再解决:
跟其他的接口比较相似的是,SD卡的输入与输出都是上升沿有效,因此沿用之前的设计,时钟下降沿输出,上升沿读入。下面两张图是SD接口的时序,但是对于CMD引脚是一样的
在SD模式下,和SD之间的通信由CMD和DAT引脚完成,初始化完全由CMD引脚完成,大致时序如上所示,方块里所有数据都是时钟上升沿有效,因此时钟被省略了
数据结构
在初始化流程中不需要回复或者数据的命令传送格式如下:
命令结构如下,头2位01,加上6位命令,32位内容,7位CRC循环校验位,最后1位终结
CRC7循环校验位的计算和举例如下,从一个指令的最高位01开始,一直输入到第40位,生成7位的CRC校验,加在指令或者回复的后端:
在Verilog代码中,每当一位数准备好时,可以用如下语句生成:
CRC <= {CRC[5:3], data_in^CRC[6]^CRC[2],CRC[1:0],data_in^CRC[6]};
Response回复结构如下:
回复分为R1普通回复、R1b包含忙碌位的普通回复、R2 CID和CSD寄存器回复、R3 OCR回复、R6 RCA回复、R7 卡片接口状态回复:
其中32位的card status卡片状态每一位对应如下:
R1b和R1结构相同,不过同时会在DAT信号线传输忙碌信号,host应该检查忙碌信号是否结束,再传输下一个指令
指令集
SD卡的各个模式以及状态的切换是由指令Command完成的,上面有指令的大致架构,其具体架构如下。在物理层文档86页开始,有各个指令的细节,这里摘取初始化流程中需要的部分:
基础指令
用户指定指令
ACMD是用户指令,在执行普通指令CMD55后,SD卡就了解下一个接收到的指令是用户指令ACMD了:
初始化流程
初始化模式和数据传输模式之间的具体切换如下:
- 探测到卡片后先CMD0从待机模式引入卡片识别模式,在这个过程中如果将SPI模式中的CS引脚,也就是SD模式中的DAT3引脚拉低,则进入SPI模式,否则使用SD模式。这里我们使用SD模式
- 使用CMD8探测卡片是否可以接受目前的3.3V电压模式,FPGA的IO一般使用3.3V
- 如果SD卡可以接受3.3V,则发送用户指令ACMD41,看SD卡的初始化是否完成,如果没有完成就持续发送ACMD41,并按需求送出切换到1.8V的指令。由于我们使用FPGA,如果切换到1.8V会无法探测到高电平,从而带来一大堆问题,因此我们保持在3.3V,有可能会牺牲一定的速度
- 如果需要切换到1.8V,则送出CMD11指令,否则跳过这步
- 发送CMD2读取卡片识别寄存器,会返回和SD卡相关的信息
- 发送CMD3读取卡片的相对地址寄存器。这一步完成后,SD卡就正式进入了数据传输模式
看起来比较眼花缭乱,在文档的第32页也对初始化步骤作出简化,这篇教程也以这个流程为主,和初始化相关的是CMD3及以前,后面的步骤到下一篇博客中完成:
ACMD和普通CMD的区别是要通过特殊指令CMD55才能输入,有点类似于键盘上的shift键。ACMD41的指令流程:
再梳理一次这个流程的简化版本:
- CMD0卡片复位到IDLE
- CMD8配置电压(我们用3.3V)
- ACMD41配置初始化,持续送出直到收到初始化完成的回复,过程大约需要1秒
- CMD11切换电压(如果需要的话)
- CMD2读取CID寄存器
- CMD3读取RCA相对地址寄存器
下面这张图更好的解释了为什么要这么配置:
控制寄存器
在初始化过程中读取了CID和RCA两个寄存器,它们的结构分别如下
而RCA就是16位的地址,默认值为0x0000
具体发送计划
从CMD指令的结构中可以看出,除了头两位是01,最后八位中,七位是循环校验码,最后一位是1,48位的指令中有38位是有效的。为了凑个整,把头两位固定的01也放进指令中,变成40位,而CRC循环冗余校验码交给代码自动生成。
z
这篇教程是详细介绍初始化的细节,因此不过多深入自动化,而是尽量手动完成,能看到更多信息
从上面的初始化流程得到如下具体步骤:
- CMD0,发送0x40_0000_0000,无回复
- CMD8,发送0x48_0000_0155,其中[11:8]=4’h1是VHS,根据下图的CMD8的VHS格式,我们选择0001,也就是3.3V在的区间;[7:0]=8’h55是选择的测试格式,预期回复中会有同样的信息表示这个接口正常工作。预期回复R7:0x08_0000_0155_E1
- CMD55,发送0x77_0000_0000,[37:32]=6’h37=6’d55,是指令索引号。预期回复R1:0x37_0000_0120_83,其中[39:8]是卡片状态寄存器,下图是CSR后半部分的定义,0x0120意味着第8位和第5位是1,显示卡片准备好接收数据,并且下一个输入的CMD序列号将作为ACMD处理
- ACMD41,发送0x69_40FF_8000,其中[37:32]=6’h29=6’d41,查阅文档中ACMD41的具体定义如下所示,0x40表示HCS为1,选择SDHC或SDXC,处于省电状态,并且使用当前3.3V而不是切换到1.8V,原因是FPGA引脚的IO电压不是实时变化的,不方便从3.3V到1.8V,至于OCR寄存器23位到8位,按照定义表示可以接收的电压范围,虽然说是3.3V,但考虑电压波动还是把所有范围都拉高。预期回复R3:0x3F_C1_FF80_00FF,文档中同样有回复的格式,在第二张图中,主要是看第39位是否拉高
- CMD2,发送0x42_0000_0000,预期回复0x3F1B_534D_4743_64A2_A860_B29C_5F3C_04A8_1F,比较特殊的是回复中CMD索引部分都置1,当中的128位是如下所示的卡片信息,我用的是一张三星的SDXC卡,没有具体查过细节
- CMD3,发送0x43_0000_0000,预期回复0x03_XXXX_0520_{CRC},它的结构如下所示,0xXXXX是返回的RCA相对地址,0x0520是讲CSR寄存器中23、22、19和12:0结合起来的16位,可以查前面的定义,23、22、19都是错误指示位,[12:0]=0x520意味着卡片处于数据传输模式
逻辑设计
从顶层来看逻辑设计很简单,串口输入到FPGA的数据转换成40位的SD指令,经过一定转换后送到SD接口通信模块,SD通信模块发送到SD卡,如果有回复则通过相反方向传回串口模块输出
从细节看,分成三个部分,串口通信,SD通信,和SD转串口接口。
串口通信代码之前的博客有写过,但缺点是要写两个字节才会在电脑端显示,有滞后,并且不显示回车,发送出去和收回来的数据缩成一团看不清;
SD接口通信模块比较新,但在初始化流程中,时钟锁定在200kHz,其实跟I2C区别不大,使用状态机设计,之后需要加上时钟频率控制的逻辑;
SD转串口接口比较简单,需要把SD的十六进制数和串口的UTF-8互相转换翻译,并缓存存放,另外识别串口发送的回车来决定发送指令
顶层代码
下面是MicroSD.v,该工程的顶层代码:
module MicroSD(
input clk,
input rst,
output reg led,
// SD port
output SD_RESET,
input SD_CD,
inout SD_DAT3,
inout SD_DAT2,
inout SD_DAT1,
inout SD_DAT0,
inout SD_CMD,
inout SD_SCK,
// UART port
output TXD,
input RXD,
output CTS,
input RTS
);
接口定义,时钟复位和LED不说了;SD接口中基本上都是双向口;串口前面有介绍过
// If a card detected, then LED light on
always @(posedge clk or posedge rst) begin
if(rst) begin
led <= 1'b0;
end
else begin
led <= ~SD_CD;
end
end
将card detection卡片侦测位加非门连到LED,只要探测到卡就点亮LED
// SD ports control
wire SD_DAT3_in, SD_DAT3_out, SD_DAT3_oe;
wire SD_DAT2_in, SD_DAT2_out, SD_DAT2_oe;
wire SD_DAT1_in, SD_DAT1_out, SD_DAT1_oe;
wire SD_DAT0_in, SD_DAT0_out, SD_DAT0_oe;
wire SD_CMD_in, SD_CMD_out, SD_CMD_oe ;
wire SD_SCK_in, SD_SCK_out, SD_SCK_oe ;
assign SD_DAT3 = (SD_DAT3_oe) ? SD_DAT3_out : 1'bz;
assign SD_DAT2 = (SD_DAT2_oe) ? SD_DAT2_out : 1'bz;
assign SD_DAT1 = (SD_DAT1_oe) ? SD_DAT1_out : 1'bz;
assign SD_DAT0 = (SD_DAT0_oe) ? SD_DAT0_out : 1'bz;
assign SD_CMD = (SD_CMD_oe) ? SD_CMD_out : 1'bz;
assign SD_SCK = (SD_SCK_oe) ? SD_SCK_out : 1'bz;
assign SD_DAT3_in = SD_DAT3;
assign SD_DAT2_in = SD_DAT2;
assign SD_DAT1_in = SD_DAT1;
assign SD_DAT0_in = SD_DAT0;
assign SD_CMD_in = SD_CMD ;
assign SD_SCK_in = SD_SCK ;
由于都是双向口,在顶层就加上所有对应引脚的方向控制
wire [39:0] SD_command;
wire SD_command_ready;
wire SD_command_busy;
wire [7:0] SD_response;
wire SD_response_valid;
wire SD_response_busy;
wire [7:0] SD_dataout;
wire SD_dataout_ready;
wire [7:0] SD_datain;
wire SD_datain_valid;
SD_transmitter SD_transmitter(
.clk (clk),
.rst (rst),
// To SD ports
.DAT3_in (SD_DAT3_in),
.DAT3_out (SD_DAT3_out),
.DAT3_oe (SD_DAT3_oe),
.DAT2_in (SD_DAT2_in),
.DAT2_out (SD_DAT2_out),
.DAT2_oe (SD_DAT2_oe),
.DAT1_in (SD_DAT1_in),
.DAT1_out (SD_DAT1_out),
.DAT1_oe (SD_DAT1_oe),
.DAT0_in (SD_DAT0_in),
.DAT0_out (SD_DAT0_out),
.DAT0_oe (SD_DAT0_oe),
.CMD_in (SD_CMD_in),
.CMD_out (SD_CMD_out),
.CMD_oe (SD_CMD_oe),
.SCK_in (SD_SCK_in),
.SCK_out (SD_SCK_out),
.SCK_oe (SD_SCK_oe),
.SD_CD (SD_CD),
.SD_RESET (SD_RESET),
// Control input
.command (SD_command),
.command_ready (SD_command_ready),
.command_busy (SD_command_busy),
.response (SD_response),
.response_valid (SD_response_valid),
.response_busy (SD_response_busy),
.dataout (SD_dataout),
.dataout_ready (SD_dataout_ready),
.datain (SD_datain),
.datain_valid (SD_datain_valid)
);
SD接口模块
// Data IO with UART
wire [7:0] uart_din;
wire uart_din_valid;
wire [7:0] uart_dout;
wire uart_dout_ready;
UART_transmitter UART_transmitter(
.clk (clk),
.rst (rst),
// UART port
.TXD (TXD),
.RXD (RXD),
.CTS (CTS),
.RTS (RTS),
// Control port
.dout (uart_dout),
.dout_ready (uart_dout_ready),
.din (uart_din),
.din_valid (uart_din_valid)
);
串口通信模块
UART_SD_converter UART_SD_converter(
.clk (clk),
.rst (rst),
// Interface with SD
.SD_command (SD_command ),
.SD_command_ready (SD_command_ready ),
.SD_command_busy (SD_command_busy ),
.SD_response (SD_response ),
.SD_response_valid (SD_response_valid),
.SD_response_busy (SD_response_busy ),
.SD_dataout (SD_dataout ),
.SD_dataout_ready (SD_dataout_ready ),
.SD_datain (SD_datain ),
.SD_datain_valid (SD_datain_valid ),
// Interface with UART
.UART_dout (uart_dout), // 8-bit character out from FPGA
.UART_dout_ready (uart_dout_ready), // data ready to send from FPGA
.UART_din (uart_din), // 8-bit character in to FPGA
.UART_din_valid (uart_din_valid) // data valid in to FPGA
);
endmodule
SD转串口转换模块,三个模块连成一条线
改进串口通信模块
串口通信模块UART_transmitter.v,和前面博客中使用的串口通信模块不同,这次不在串口通信模块将输入UTF-8字符直接翻译成十六进制数,而是输出UTF-8字符,这样包括回车在内的所有符号都可以输出。
改进代码如下:
module UART_transmitter(
input clk,
input rst,
// UART port
output reg TXD,
input RXD,
output reg CTS,
input RTS,
// Control port
input [7:0] dout, // 8-bit character out from FPGA
input dout_ready, // data ready to send from FPGA
output reg [7:0] din, // 8-bit character in to FPGA
output reg din_valid // data valid in to FPGA
);
UART接口在之前介绍过,RXD和RTS分别是接收信号和接收认可,TXD和CTS是发送信号和发送许可
下面是自定义的接口
- dout是需要从FPGA通过串口发送出去的UTF-8字符
- dout_ready为高电平时表示准备好输出,如果持续多个时钟就是多个字符输出
- din是FPGA通过串口收到的UTF-8字符
- din_valid为高电平表示收到了有效字符
// FIFO definition
reg [7:0] fifo_data_in;
wire [7:0] fifo_data_out;
reg fifo_rd_en;
reg fifo_rd_en_d1;
reg fifo_rd_en_d2;
reg fifo_wr_en;
wire fifo_empty;
wire fifo_full;
// Even though it's possible to have loopback and output data in the same time, the possibility is very low
always @(posedge clk or posedge rst) begin
if(rst) begin
fifo_data_in <= 8'h00;
fifo_wr_en <= 1'b0;
end
else if(dout_ready) begin // Get the data from outside
fifo_data_in <= dout;
fifo_wr_en <= 1'b1;
end
else if(din_valid) begin // Looping back the input data to output
fifo_data_in <= din;
fifo_wr_en <= 1'b1;
end
else begin
fifo_data_in <= 8'h00;
fifo_wr_en <= 1'b0;
end
end
syn_fifo #
(
// FIFO constants
.DATA_WIDTH(8),
.ADDR_WIDTH(6)
) fifo
(
.clk (clk), // Clock input
.rst (rst), // Active high reset
.data_in (fifo_data_in), // Data input
.rd_en (fifo_rd_en), // Read enable
.wr_en (fifo_wr_en), // Write Enable
.data_out (fifo_data_out), // Data Output
.empty (fifo_empty), // FIFO empty
.full (fifo_full) // FIFO full
);
首先需要一个FIFO缓存,将FPGA收到的UTF-8字符存下来,因为串口相对较慢,但自定义的接口有可能一次性送几个字符,这时会来不及传输。这里我们定义一个深度为2^6=64的FIFO,足以处理这个任务
需要注意的是输入FIFO的逻辑,它不只是存下要从FPGA输出的串口数据,还把键盘通过串口输入到FPGA的UTF-8字符回送回去,这样键盘输入的数据可以直接在窗口中看到。有比较低的可能性,键盘的输入和内部逻辑输出同时发生,这时会丢失数据,但可能性很小
// resource definition
reg [15:0] tx_count;
reg [9:0] tx_shift;
reg tx_start;
reg [9:0] CTS_delay;
reg [7:0] RXD_delay;
reg [15:0] rx_count;
reg [3:0] rx_bit_count;
reg rx_start;
always @(posedge clk) begin
fifo_rd_en_d1 <= fifo_rd_en;
fifo_rd_en_d2 <= fifo_rd_en_d1;
end
always @(posedge clk or posedge rst) begin
if(rst) begin
tx_count <= 16'd0;
TXD <= 1'b1;
tx_shift <= 10'h3FF;
CTS <= 1'b1;
CTS_delay <= 10'h3FF;
fifo_rd_en <= 1'b0;
tx_start <= 1'b0;
end
// When FIFO is not empty, and last sending completed, read the next data, and send through UART
else if(~tx_start && ~fifo_empty) begin
fifo_rd_en <= 1'b1;
tx_start <= 1'b1;
end
如果FIFO还有数据剩余,并且上一个UTF-8字符已经发送完毕,则将tx_start发送开始标志拉高,从FIFO读出一个数
从FIFO读一个数需要两个时钟周期,因此我们将fifo_rd_en延迟两个时钟再进行下一步
// FIFO ready complete, get the data, transfer and buffer it into register
else if(fifo_rd_en_d2) begin
fifo_rd_en <= 1'b0;
tx_shift <= {1'b0,fifo_data_out[0],fifo_data_out[1],fifo_data_out[2],fifo_data_out[3],
fifo_data_out[4],fifo_data_out[5],fifo_data_out[6],fifo_data_out[7],1'b1};
CTS_delay <= 10'h000;
end
将FIFO中读出的UTF-8八位字符反向,并加上头尾成为10位的串口传输格式,同时给CTS_delay赋值,让它可以持续十个时钟周期保持低电平
// Shift out the received data
else begin
fifo_rd_en <= 1'b0;
if(tx_count < 16'd867) begin
tx_count <= tx_count + 16'd1;
end
else begin
tx_count <= 16'd0;
end
if(tx_count == 16'd0) begin
TXD <= tx_shift[9];
tx_shift <= {tx_shift[8:0], 1'b1};
CTS <= CTS_delay[9];
CTS_delay <= {CTS_delay[8:0], 1'b1};
tx_start <= ~CTS_delay[9];
end
end
end
将时钟降频868倍,从100MHz减到115200Hz波特率,然后把转换好的串口格式顺序放出
到这里串口输出的逻辑结束
// Input from uart
always @(posedge clk or posedge rst) begin
if(rst) begin
RXD_delay <= 8'h00;
rx_count <= 16'd0;
rx_bit_count <= 4'd0;
din_valid <= 1'b0;
rx_start <= 1'b0;
din <= 8'h00;
end
else if(~RTS) begin
if(rx_count < 16'd867) begin
rx_count <= rx_count + 16'd1;
end
else begin
rx_count <= 16'd0;
end
if( (rx_count == 16'd0) && (~RXD) && (~rx_start) ) begin
RXD_delay <= 8'h00;
rx_bit_count <= 4'd0;
rx_start <= 1'b1;
end
else if( (rx_count == 16'd0) && rx_start && (rx_bit_count != 4'd8)) begin
rx_bit_count <= rx_bit_count + 4'd1;
RXD_delay <= {RXD_delay[6:0], RXD};
end
else if( (rx_count == 16'd0) && rx_start) begin
rx_start <= 1'b0;
rx_bit_count <= 4'd0;
din_valid <= 1'b1;
din <= {RXD_delay[0],RXD_delay[1],RXD_delay[2],RXD_delay[3],
RXD_delay[4],RXD_delay[5],RXD_delay[6],RXD_delay[7]};
end
else begin
din_valid <= 1'b0;
end
end
end
endmodule
串口接收逻辑要简单很多,当侦测到RTS为低时,就按照115200波特率顺序存下,并在结束时反序成UTF-8符号输出
FIFO的代码syn_fifo.v如下所示,之前出现过,不多介绍了
module syn_fifo #
(
// FIFO constants
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 8
)
(
input clk , // Clock input
input rst , // Active high reset
input [DATA_WIDTH-1:0] data_in , // Data input
input rd_en , // Read enable
input wr_en , // Write Enable
output reg [DATA_WIDTH-1:0] data_out , // Data Output
output empty , // FIFO empty
output full // FIFO full
);
// RAM definition
parameter RAM_DEPTH = (1 << ADDR_WIDTH);
reg [DATA_WIDTH-1:0] data_ram[RAM_DEPTH-1:0];
// Pointers and counters
reg [ADDR_WIDTH-1:0] wr_pointer;
reg [ADDR_WIDTH-1:0] rd_pointer;
reg [ADDR_WIDTH :0] status_cnt;
assign full = (status_cnt == (RAM_DEPTH-1));
assign empty = (status_cnt == 0);
// WRITE_POINTER
always @(posedge clk or posedge rst) begin
if(rst) begin
wr_pointer <= 0;
end else if(wr_en) begin
wr_pointer <= wr_pointer + 1;
end
end
// READ_POINTER
always @(posedge clk or posedge rst) begin
if(rst) begin
rd_pointer <= 0;
end
else if(rd_en) begin
rd_pointer <= rd_pointer + 1;
end
end
// READ DATA
always @(posedge clk or posedge rst) begin
if(rst) begin
data_out <= 0;
end
else if(rd_en) begin
data_out <= data_ram[rd_pointer];
end
end
// WRITE DATA
always @(posedge clk) begin
if(wr_en) begin
data_ram[wr_pointer] <= data_in;
end
end
// STATUS COUNTER
always @(posedge clk or posedge rst) begin
if(rst) begin
status_cnt <= 0;
// Read but no write.
end
else if(rd_en && !wr_en && (status_cnt != 0)) begin
status_cnt <= status_cnt - 1;
// Write but no read.
end
else if(wr_en && !rd_en && (status_cnt != RAM_DEPTH)) begin
status_cnt <= status_cnt + 1;
end
end
endmodule
SD接口通信模块
SD接口通信模块代码SD_transmitter.v如下所示
module SD_transmitter(
input clk,
input rst,
// To SD ports
input DAT3_in,
output reg DAT3_out,
output reg DAT3_oe,
input DAT2_in,
output reg DAT2_out,
output reg DAT2_oe,
input DAT1_in,
output reg DAT1_out,
output reg DAT1_oe,
input DAT0_in,
output reg DAT0_out,
output reg DAT0_oe,
input CMD_in,
output reg CMD_out,
output reg CMD_oe,
input SCK_in,
output reg SCK_out,
output reg SCK_oe,
input SD_CD,
output reg SD_RESET,
// Control input
input [39:0] command,
input command_ready,
output reg command_busy,
output reg [7:0] response,
output reg response_valid,
output reg response_busy,
input [7:0] dataout,
input dataout_ready,
output reg [7:0] datain,
output reg datain_valid
);
SD接口中,每个双向接口对应这个模块的in,out和oe三个接口,具体含义前面介绍了。
下面是自定义接口:
- 40位command输入,ready表示command准备好了,busy表示接收到command正在输出;
- response是SD接口传回的回复,valid表示有有效回复,可以取走数据,busy表示正在接收数据;
- dataout是从FPGA到SD卡的数据,ready表示数据准备好了;
- datain是从SD卡到FPGA的数据,valid表示有有效输入数据,可以取走数据
// SCK generator, from 100MHz to 200kHz
reg [15:0] SCK_counter;
reg SCK_out_d;
wire SCK_out_posedge;
wire SCK_out_negedge;
always @(posedge clk or posedge rst) begin
if(rst) begin
SCK_counter <= 16'd0;
SCK_out <= 1'b0;
SCK_oe <= 1'b0;
end
else if(SCK_counter == 16'd249) begin
SCK_counter <= 16'd0;
SCK_out <= ~SCK_out;
SCK_oe <= 1'b1;
end
else begin
SCK_counter <= SCK_counter + 16'd1;
SCK_oe <= 1'b1;
end
end
// Detect rising edge and falling edge of SCK_out
always @(posedge clk) begin
SCK_out_d <= SCK_out;
end
assign SCK_out_posedge = ({SCK_out,SCK_out_d}==2'b10) ? 1'b1 : 1'b0;
assign SCK_out_negedge = ({SCK_out,SCK_out_d}==2'b01) ? 1'b1 : 1'b0;
将100MHz时钟降频到200kHz,作为SD卡在待机模式和卡片识别模式时候使用的时钟;并且设置SCK上升沿和下降沿标志为数据传输提供方便
// CRC generator
reg [6:0] CRC;
// State machine
parameter [3:0] INACTIVE = 4'd0;
parameter [3:0] IDLE = 4'd1;
parameter [3:0] READY = 4'd2;
parameter [3:0] RESPONSE_IN = 4'd3;
parameter [3:0] COMMAND_OUT = 4'd4;
parameter [3:0] ENDING = 4'd5;
(* dont_touch = "true" *)reg [3:0] state;
reg [3:0] next_state;
always @(posedge clk or posedge rst) begin
if(rst) begin
state <= INACTIVE;
end
else begin
state <= next_state;
end
end
SD接口通信模块由状态机控制,分成六种状态
reg [7:0] receive_count;
reg [47:0] receive_buf;
reg [7:0] send_count;
reg [39:0] send_buf;
always @(posedge clk) begin
case(state)
INACTIVE: begin
DAT3_out <= 1'b1;
DAT3_oe <= 1'b0;
DAT2_out <= 1'b1;
DAT2_oe <= 1'b0;
DAT1_out <= 1'b1;
DAT1_oe <= 1'b0;
DAT0_out <= 1'b1;
DAT0_oe <= 1'b0;
CMD_out <= 1'b1;
CMD_oe <= 1'b0;
command_busy <= 1'b0;
response <= 48'h0;
response_valid <= 1'b0;
response_busy <= 1'b0;
datain <= 8'h0;
datain_valid <= 1'b0;
receive_count <= 8'd0;
receive_buf <= 48'h0;
send_count <= 8'd0;
send_buf <= 40'h0;
CRC <= 7'h0;
if(SD_CD) begin // no card detected, power off
SD_RESET <= 1'b1;
next_state <= INACTIVE;
end
else begin // card detected, power on
SD_RESET <= 1'b0;
next_state <= IDLE;
end
end
IDLE: begin
next_state <= READY;
end
READY: begin
if(~CMD_in && SCK_out_posedge) begin // detect the first 0, start receiving
next_state <= RESPONSE_IN;
CMD_oe <= 1'b0;
receive_buf <= {receive_buf[46:0], CMD_in};
response_busy <= 1'b1;
receive_count <= 8'd1;
end
else if(command_ready) begin // command ready, start sending
next_state <= COMMAND_OUT;
CMD_oe <= 1'b1;
CMD_out <= 1'b1;
send_buf <= command;
command_busy <= 1'b1;
send_count <= 8'd0;
end
end
- reset进入INACTIVE状态,将所有相关寄存器复位
- reset解除后进入IDLE状态,随后进入READY状态
- READY状态等待有效回复,如果在SCK上升沿时CMD为低,也就是有有效输入时,进入RESPONSE_IN状态,并且接收第一个数据位,拉高response_busy;如果有command准备完成,则进入COMMAND_OUT状态,送出command第一位数据,拉高command_busy
RESPONSE_IN: begin
if(SCK_out_posedge && receive_count<8'd48) begin
receive_count <= receive_count + 8'd1;
receive_buf <= {receive_buf[46:0], CMD_in};
end
else if(SCK_out_posedge && receive_count==8'd48)begin
receive_count <= 8'd0;
next_state <= ENDING;
end
if(SCK_out_posedge && (receive_count[2:0] == 3'b000)) begin
response <= receive_buf[7:0];
response_valid <= 1'b1;
end
else begin
response_valid <= 1'b0;
end
end
在每个SCK上升沿接收一个数据位,总共48位,每8位输出一个response,因为有时收到的不是48的整数倍,这时要及时输出。如果收到48位后还有数据进入,则进入ENDING后,如果在SCK上升沿CMD还是低电平,则有充足的时间返回到这个状态接着接收数据。
不过这么做有风险,因为CMD2的回复种类R2有136位,虽然其他绝大多数种类的回复都是48位,在下一篇中,将初始化流程自动化后,就可以固定回复长度,不用等待手动输入串口命令,做一个通用化的回复接收逻辑了。
COMMAND_OUT: begin
if(SCK_out_negedge && send_count<8'd48) begin
send_count <= send_count + 8'd1;
if(send_count == 8'd40) begin
{CMD_out, send_buf} <= {CRC,1'b1,33'h0};
end
else begin
{CMD_out, send_buf} <= {send_buf, 1'b0};
end
end
else if(SCK_out_negedge && send_count==8'd48) begin
{CMD_out, send_buf} <= {send_buf, 1'b0};
end
else if(SCK_out_posedge && send_count==8'd48) begin
next_state <= ENDING;
end
if(SCK_out_negedge) begin
CRC <= {CRC[5:3], send_buf[39]^CRC[6]^CRC[2],CRC[1:0],send_buf[39]^CRC[6]};
end
end
输出40位command的同时,计算CRC循环冗余校验位,然后将最后八位输出,输出完后进入ENDING状态
ENDING: begin
CMD_out <= 1'b1;
CMD_oe <= 1'b0;
response_busy <= 1'b0;
command_busy <= 1'b0;
CRC <= 7'h0;
next_state <= IDLE;
end
endcase
end
endmodule
ENDING状态复位一些寄存器并回到IDLE状态
SD和串口转接模块
这部分转接模块的代码UART_SD_converter.v如下所示。它可以识别所有和十六进制有关的UTF-8字符,字母包括大小写两种情况,另外特别留意了回车的UTF-8符号,也就是0x0D,只有在接收到回车符号,并且在这之前已经收到完整的10个字符时,才会将存好的40位command发送给SD通信模块,否则就清空所有buffer,防止送出任何不正确的指令
module UART_SD_converter(
input clk,
input rst,
// Interface with SD
output reg [39:0] SD_command,
output reg SD_command_ready,
input SD_command_busy,
input [7:0] SD_response,
input SD_response_valid,
input SD_response_busy,
output reg [7:0] SD_dataout,
output reg SD_dataout_ready,
input [7:0] SD_datain,
input SD_datain_valid,
// Interface with UART
output reg [7:0] UART_dout, // 8-bit character out from FPGA
output reg UART_dout_ready, // data ready to send from FPGA
input [7:0] UART_din, // 8-bit character in to FPGA
input UART_din_valid // data valid in to FPGA
);
前面已经介绍过自定义的两种接口,一种是SD的,另一种是串口的
// Command control
reg [47:0] UART_cmd_buf;
reg [7:0] din_count;
reg dout_count;
reg [7:0] UART_dout_d;
reg UART_dout_ready_d;
reg SD_response_valid_d;
always @(posedge clk) begin
SD_response_valid_d <= SD_response_valid;
end
always @(posedge clk) begin
if(rst) begin
UART_cmd_buf <= 48'h0;
UART_dout_ready <= 1'b0;
UART_dout <= 8'h00;
UART_dout_d <= 8'h00;
UART_dout_ready_d <= 1'b0;
din_count <= 8'd0;
dout_count <= 1'b0;
SD_command_ready <= 1'b0;
SD_command <= 40'h0;
end
// Got data from UART, save to 40-bit buffer
else if(UART_din_valid && UART_din != 8'h0D /*din_count < 8'd10*/) begin
UART_cmd_buf[47:4] <= UART_cmd_buf[43:0];
// Convert UTF-8 to hex command
// Can recognize both cases
case(UART_din)
8'h30: begin UART_cmd_buf[3:0] <= 4'h0; end
8'h31: begin UART_cmd_buf[3:0] <= 4'h1; end
8'h32: begin UART_cmd_buf[3:0] <= 4'h2; end
8'h33: begin UART_cmd_buf[3:0] <= 4'h3; end
8'h34: begin UART_cmd_buf[3:0] <= 4'h4; end
8'h35: begin UART_cmd_buf[3:0] <= 4'h5; end
8'h36: begin UART_cmd_buf[3:0] <= 4'h6; end
8'h37: begin UART_cmd_buf[3:0] <= 4'h7; end
8'h38: begin UART_cmd_buf[3:0] <= 4'h8; end
8'h39: begin UART_cmd_buf[3:0] <= 4'h9; end
8'h41: begin UART_cmd_buf[3:0] <= 4'hA; end
8'h61: begin UART_cmd_buf[3:0] <= 4'hA; end
8'h42: begin UART_cmd_buf[3:0] <= 4'hB; end
8'h62: begin UART_cmd_buf[3:0] <= 4'hB; end
8'h43: begin UART_cmd_buf[3:0] <= 4'hC; end
8'h63: begin UART_cmd_buf[3:0] <= 4'hC; end
8'h44: begin UART_cmd_buf[3:0] <= 4'hD; end
8'h64: begin UART_cmd_buf[3:0] <= 4'hD; end
8'h45: begin UART_cmd_buf[3:0] <= 4'hE; end
8'h65: begin UART_cmd_buf[3:0] <= 4'hE; end
8'h46: begin UART_cmd_buf[3:0] <= 4'hF; end
8'h66: begin UART_cmd_buf[3:0] <= 4'hF; end
default: begin UART_cmd_buf[3:0] <= 4'h0; end
endcase
din_count <= din_count + 8'd1;
end
在没有收到回车符号0x0D之前,将所有收到的UTF-8字符进行识别,如果是和十六进制有关,也就是数字0-9,以及字母a-f,包括大小写,就放入缓存,并将缓存计数器加一
// Done buffering data from UART, and the length of command is correct, sent to SD and clear data input counter
else if(UART_din_valid && UART_din == 8'h0D && din_count == 8'd10) begin
din_count <= 8'd0;
SD_command_ready <= 1'b1;
SD_command <= UART_cmd_buf[39:0];
end
收到回车符号0x0D时,如果之前缓存下来的字符数为10,也就是完整的40位命令,则将缓存的完整命令输出,把command_ready拉高
// Received command doesn't have correct length, maybe just typo or returning without anything, clear buffer
else if(UART_din_valid && UART_din == 8'h0D && din_count != 8'd10) begin
din_count <= 8'd0;
end
万一收到回车符号时候缓存的计数器不是10,表示中间出了问题,有可能手动输入有误,不是完整的10个字符,或者只是按到了回车,那就把缓存计数器清空,重新计数
// Got response from SD, send MSB first, and then LSB
else if(SD_response_valid_d) begin
case(SD_response[7:4])
4'h0: begin UART_dout <= 8'h30; end
4'h1: begin UART_dout <= 8'h31; end
4'h2: begin UART_dout <= 8'h32; end
4'h3: begin UART_dout <= 8'h33; end
4'h4: begin UART_dout <= 8'h34; end
4'h5: begin UART_dout <= 8'h35; end
4'h6: begin UART_dout <= 8'h36; end
4'h7: begin UART_dout <= 8'h37; end
4'h8: begin UART_dout <= 8'h38; end
4'h9: begin UART_dout <= 8'h39; end
4'ha: begin UART_dout <= 8'h41; end
4'hb: begin UART_dout <= 8'h42; end
4'hc: begin UART_dout <= 8'h43; end
4'hd: begin UART_dout <= 8'h44; end
4'he: begin UART_dout <= 8'h45; end
4'hf: begin UART_dout <= 8'h46; end
default: begin UART_dout <= 8'h30; end
endcase
case(SD_response[3:0])
4'h0: begin UART_dout_d <= 8'h30; end
4'h1: begin UART_dout_d <= 8'h31; end
4'h2: begin UART_dout_d <= 8'h32; end
4'h3: begin UART_dout_d <= 8'h33; end
4'h4: begin UART_dout_d <= 8'h34; end
4'h5: begin UART_dout_d <= 8'h35; end
4'h6: begin UART_dout_d <= 8'h36; end
4'h7: begin UART_dout_d <= 8'h37; end
4'h8: begin UART_dout_d <= 8'h38; end
4'h9: begin UART_dout_d <= 8'h39; end
4'ha: begin UART_dout_d <= 8'h41; end
4'hb: begin UART_dout_d <= 8'h42; end
4'hc: begin UART_dout_d <= 8'h43; end
4'hd: begin UART_dout_d <= 8'h44; end
4'he: begin UART_dout_d <= 8'h45; end
4'hf: begin UART_dout_d <= 8'h46; end
default: begin UART_dout_d <= 8'h30; end
endcase
UART_dout_ready <= 1'b1;
UART_dout_ready_d <= 1'b1;
end
SD接口的回复数据是8位的,但这里只能一次性转换4位,因此把MSB的4位先翻译输出,而把LSB四位先缓存下来,到下一个时钟输出
else begin
UART_dout <= UART_dout_d;
UART_dout_ready <= UART_dout_ready_d;
UART_dout_ready_d <= 1'b0;
SD_command_ready <= 1'b0;
end
end
endmodule
把缓存的回复数据输出,并将缓存清空,当平时不输出也无输入时自然全刷成0
模拟仿真
和前几篇博客一样,ModelSim仿真模拟需要一个Testbench和一个仿真脚本。由于找不到合适的SD卡仿真模块,我们只能自己写一个模拟输入逻辑,并目测其是否和文档中匹配
testbench
Testbench代码tb_MicroSD.v源代码如下:
`timescale 1ns/1ns
module tb_MicroSD;
reg clock;
reg reset;
wire led;
// inout ports control
wire SD_RESET;
reg SD_CD;
wire SD_DAT3;
wire SD_DAT3_out;
reg SD_DAT3_in;
reg SD_DAT3_oe;
wire SD_DAT2;
wire SD_DAT2_out;
reg SD_DAT2_in;
reg SD_DAT2_oe;
wire SD_DAT1;
wire SD_DAT1_out;
reg SD_DAT1_in;
reg SD_DAT1_oe;
wire SD_DAT0;
wire SD_DAT0_out;
reg SD_DAT0_in;
reg SD_DAT0_oe;
wire SD_CMD;
wire SD_CMD_out;
reg SD_CMD_in;
reg SD_CMD_oe;
wire SD_SCK;
assign SD_DAT3 = (SD_DAT3_oe) ? 1'bz : SD_DAT3_in;
assign SD_DAT2 = (SD_DAT2_oe) ? 1'bz : SD_DAT2_in;
assign SD_DAT1 = (SD_DAT1_oe) ? 1'bz : SD_DAT1_in;
assign SD_DAT0 = (SD_DAT0_oe) ? 1'bz : SD_DAT0_in;
assign SD_CMD = (SD_CMD_oe) ? 1'bz : SD_CMD_in;
assign SD_DAT3_out = SD_DAT3;
assign SD_DAT2_out = SD_DAT2;
assign SD_DAT1_out = SD_DAT1;
assign SD_DAT0_out = SD_DAT0;
assign SD_CMD_out = SD_CMD;
wire TXD;
reg RXD;
reg RTS;
wire CTS;
reg [9:0] RXD_buf;
reg [9:0] TXD_buf;
reg [47:0] SD_buf;
正常的资源定义
initial begin
clock = 1'b0;
reset = 1'b0;
RXD_buf = 10'h0;
TXD_buf = 10'h0;
SD_CD = 1'b1;
SD_DAT3_oe = 1'b1;
SD_DAT2_oe = 1'b1;
SD_DAT1_oe = 1'b1;
SD_DAT0_oe = 1'b1;
SD_CMD_oe = 1'b1;
SD_DAT3_in = 1'b1;
SD_DAT2_in = 1'b1;
SD_DAT1_in = 1'b1;
SD_DAT0_in = 1'b1;
SD_CMD_in = 1'b1;
// Reset for 1us
#100
reset = 1'b1;
#1000
reset = 1'b0;
// Card detected and start
#1000
SD_CD = 1'b0;
// Send CMD0 through uart
#1 uart_write_5byte(40'h4000000000);
#100000 uart_write_5byte(40'h4800000155);
#100000 uart_write_5byte(40'h4B00000000);
#300000
#1 SD_send(48'h0B00000320BD);
end
在复位以后,先给顶层模块的串口接口输入三个40位指令,一个是CMD0,一个是CMD8,最后一个是CMD11,每个CMD之间等待100us,再300us过后给顶层模块的SD接口输入48位回复。这里用来给顶层模块输送数据是由task完成的,这是之前没有介绍过的仿真方式,通过定义类似于软件程序中的函数,把冗长的步骤简化
// Generate 100MHz clock signal
always #5 clock <= ~clock;
MicroSD MicroSD(
.clk(clock),
.rst(reset),
.led(led),
// SD port
.SD_RESET (SD_RESET),
.SD_CD (SD_CD ),
.SD_DAT3 (SD_DAT3 ),
.SD_DAT2 (SD_DAT2 ),
.SD_DAT1 (SD_DAT1 ),
.SD_DAT0 (SD_DAT0 ),
.SD_CMD (SD_CMD ),
.SD_SCK (SD_SCK ),
// UART port
.TXD(TXD),
.RXD(RXD),
.CTS(CTS),
.RTS(RTS)
);
生成100MHz时钟并连接顶层模块
task SD_send;
input [47:0] data;
begin
$display("Send %12h back to host", data);
SD_buf = data;
SD_CMD_oe = 1'b0;
repeat(48) begin
@(negedge SD_SCK) {SD_CMD_in, SD_buf} = {SD_buf, 1'b0};
end
@(negedge SD_SCK)
SD_CMD_oe = 1'b1;
end
endtask
此处定义task,如上所示,task的用法是先定义task的名称SD_send,用input来定义task的输入参数,这样每次调用这个task时就不需要重复写下方那段了,可以让代码显得简洁易读
task uart_write_4bit;
input [4:0] data;
begin
$display("Send %g through UART", data);
case(data)
5'h00: begin RXD_buf <= 10'b0000011001; end
5'h01: begin RXD_buf <= 10'b0100011001; end
5'h02: begin RXD_buf <= 10'b0010011001; end
5'h03: begin RXD_buf <= 10'b0110011001; end
5'h04: begin RXD_buf <= 10'b0001011001; end
5'h05: begin RXD_buf <= 10'b0101011001; end
5'h06: begin RXD_buf <= 10'b0011011001; end
5'h07: begin RXD_buf <= 10'b0111011001; end
5'h08: begin RXD_buf <= 10'b0000111001; end
5'h09: begin RXD_buf <= 10'b0100111001; end
5'h0A: begin RXD_buf <= 10'b0100000101; end
5'h0B: begin RXD_buf <= 10'b0010000101; end
5'h0C: begin RXD_buf <= 10'b0110000101; end
5'h0D: begin RXD_buf <= 10'b0001000101; end
5'h0E: begin RXD_buf <= 10'b0101000101; end
5'h0F: begin RXD_buf <= 10'b0011000101; end
5'h10: begin RXD_buf <= 10'b0101100001; end
endcase
RTS = 1'b0;
repeat(10) begin
repeat(867) @(posedge clock);
{RXD, RXD_buf} = {RXD_buf, 1'b1};
end
end
endtask
task uart_write_5byte;
input [39:0] data;
begin
#1 uart_write_4bit({1'b0,data[39:36]});
#1 uart_write_4bit({1'b0,data[35:32]});
#1 uart_write_4bit({1'b0,data[31:28]});
#1 uart_write_4bit({1'b0,data[27:24]});
#1 uart_write_4bit({1'b0,data[23:20]});
#1 uart_write_4bit({1'b0,data[19:16]});
#1 uart_write_4bit({1'b0,data[15:12]});
#1 uart_write_4bit({1'b0,data[11: 8]});
#1 uart_write_4bit({1'b0,data[ 7: 4]});
#1 uart_write_4bit({1'b0,data[ 3: 0]});
#1 uart_write_4bit(5'h10);
end
endtask
endmodule
再定义和串口传输有关的task,仿真用的testbench就完成了
仿真脚本
新建脚本文件sim.do,源代码如下:
vlib work
vlog ../src/MicroSD.v ../src/SD_transmitter.v ../src/UART_transmitter.v ../src/UART_SD_converter.v ../src/syn_fifo.v ./tb_MicroSD.v
vsim work.tb_MicroSD -voptargs=+acc +notimingchecks
log -depth 7 /tb_MicroSD/*
#do wave.do
run 5ms
由于串口和SD接口的初始化过程用的时钟都比较慢,因此仿真5毫秒以免看不完全程
调用前面全部的代码,打开ModelSim后转到脚本在的路径,使用命令do sim.do即可开始仿真。
仿真时可以添加想要的信号到waveform窗口中观察,然后可以保存为wave.do,这样下次可以通过调用它来加入一样的信号,节省一个一个加入的时间,这时你可以把sim.do中被#注释掉的那行去注释
仿真结果
调用仿真脚本后,加入想看的相应信号,得到波形如下:
从中可以看出SD接口和串口接口的仿真波形,以及通过SD_UART转接逻辑显示的,经过处理后的并行数据,可以看到有三个指令输出,以及一个SD回复
编译烧写
新建一个叫MicroSD的project,配置为开发板NEXYS4。添加前面介绍的全部5个相关代码文件
下一步加入约束constraint文件microphone.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC):
## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project
## Clock signal
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk]
create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]
##Switches
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]
## LEDs
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports led]
##Micro SD Connector
set_property -dict {PACKAGE_PIN E2 IOSTANDARD LVCMOS33} [get_ports SD_RESET]
set_property -dict {PACKAGE_PIN A1 IOSTANDARD LVCMOS33} [get_ports SD_CD]
set_property -dict {PACKAGE_PIN B1 IOSTANDARD LVCMOS33} [get_ports SD_SCK]
set_property -dict {PACKAGE_PIN C1 IOSTANDARD LVCMOS33} [get_ports SD_CMD]
set_property -dict {PACKAGE_PIN C2 IOSTANDARD LVCMOS33} [get_ports SD_DAT0]
set_property -dict {PACKAGE_PIN E1 IOSTANDARD LVCMOS33} [get_ports SD_DAT1]
set_property -dict {PACKAGE_PIN F1 IOSTANDARD LVCMOS33} [get_ports SD_DAT2]
set_property -dict {PACKAGE_PIN D2 IOSTANDARD LVCMOS33} [get_ports SD_DAT3]
##USB-RS232 Interface
set_property -dict {PACKAGE_PIN C4 IOSTANDARD LVCMOS33} [get_ports RXD]
set_property -dict {PACKAGE_PIN D4 IOSTANDARD LVCMOS33} [get_ports TXD]
set_property -dict {PACKAGE_PIN D3 IOSTANDARD LVCMOS33} [get_ports CTS]
set_property -dict {PACKAGE_PIN E5 IOSTANDARD LVCMOS33} [get_ports RTS]
到此我们可以点击Generate Bitstream让Vivado开始自动编译综合,最终生成可烧写文件MicroSD.bit。和以前不同的是,这次我们不设置chipscope,因为串口模块在之前已经测试过,性能还是比较稳定的,而新的SD接口逻辑和I2C接口有点相似,可以尝试着直接编译。
不过对于初学者还是建议加上一个chipscope观察波形,特别是几个模块中的状态机控制,以免卡在中间某一个步骤时候,无法找到出错的原因
生成好bitstream之后,和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序。
实测结果
烧写完成后,将从右到左第一个开关打开,结束复位,在SD插槽插入微型SD卡,可以看到第一个LED灯亮了,这表示识别到了SD卡的存在
打开Putty,按照前面博客中介绍的方式配置好串口,这里具体用哪个串口COM是可以在设备管理器device manager里看到的,如果找不到,你可以拔一下开发板的USB,看一下哪个消失了,我这里是COM4。:
需要注意的是,我们需要额外勾选这个选项,由于Windows中的回车是\r\n,也就是LF+CR,但putty传给串口的却只有\n也就是CR,因此在putty回传所有的信号时候,要在CR前面加上LF,这样才能看到真正的换行
打开putty后,开始手动输入指令,具体情况如下所示:
可以看出具体步骤,键盘输入4000000000后回车就完成了CMD0传输,如果有回复就会紧接着输出,具体分析如下:
- 键盘输入0x4000000000,CMD0从待机模式进入卡片识别模式
- 键盘输入0x4800000155,CMD8,设置电压3.3V,并加入识别字符0x55,可以看到回复0x0800000155E1,08是指令序号,01是电压配置回复,确认为3.3V,返回同样的识别字符0x55,并加上CRC+1’b1 = 0xE1
- 键盘输入0x7700000000,CMD55,进入用户自定义指令集,可以看到回复0x370000012083,0x00000120意味着第8位和第5位是1,显示卡片准备好接收数据,并且下一个输入的CMD序列号将作为ACMD处理
- 键盘输入0x6940ff8000,ACMD41,看到回复0x3F00FF8000FF,这里[39]为0,表示还在忙碌状态,还未完成
- 重复步骤3和4,这次收到的回复是0x3FC0FF8000FF,这次配置完成了
- 键盘输入0x4200000000,CMD2,请求SD卡序列号,其中包含设备制造商,SD卡参数等和卡片相关的数据,后面的回复就是这些信息集合,具体内容可以到SD卡制造商的手册中查到
- 键盘输入0x4300000000,CMD3,请求SD卡相对地址,其返回的数据指示着读取的相对地址,到这一步就完成了初始化,可以进行实际的数据传输了
总结
这篇教程第一次只写了接口技术的介绍,不得不说SD卡真的是个大坑。文档长而难读,细节一大堆。
下一篇将SD卡接口的初始化过程变成自动化,然后写入一定的随机数进行测试