FPGA基础入门【18】开发板MicroSD接口控制--初始化

之前挖了个MicroSD的坑,当时以为是个小坑,做了才发现是个天坑,光是介绍就要写一整篇,因此打算把它分成两部分,第一部分讲SD卡的初始化流程,第二部分做具体的数据传送

其实在介绍开发板的几种烧写方式时,有略微提到了怎么用MicroSD卡来烧写,但没有深入介绍是怎么使用这种小巧轻便,而且容量不小,还比较通用的存储方式,对于嵌入式攻城狮们挺有用的

MicroSD介绍

在介绍UART串口控制的时候有提到过板载的PIC24芯片,它除了控制串口和USB,还控制MicroSD。在NEXYS 4开发板文档中看到它和FPGA的连接如下(省去了PIC24的部分)。引脚定义下面会讲,只是这个SD_RESET复位信号接了一个PNP三极管,是低电平连通的,用来保证为SD卡的电流供应,因为FPGA的输出电流,也就是驱动能力很低:
connection
开始找和SD卡接口相关的时序才发现这是个大坑,内容多且杂乱,连英文文档都不容易读,希望通过这一篇和实际开发板有关的教程,帮助理解SD的驱动流程

和SD卡有关的官方定义是由SD association做的,链接如下:
SD association simplified specification
我们主要读的是Physical Layer Simplified Specification简化物理层文档

结构和引脚

SD卡的形状和内部结构如下,这里放的是大的卡,和微型卡差不多。可以看出卡中包含一个接口控制器、多个控制寄存器以及存储核心
shape
architecture
各个引脚的定义如下:
pin
SD卡根据初始化时候的寄存器配置,有SD和SPI两种模式。SPI模式只需要修改一些SPI借口逻辑就可以,很多人用这种方式避免额外的工作,这里我们选择做出完整的SD接口

  • DAT3是数据线第3位,也当做卡识别位,以及SPI中的片选位
  • CMD是命令/回复位,在SPI中是数据输入位
  • VSS1、VSS2和VDD在两种模式中都是电源和接地位
  • CLK在两种模式中作为时钟位
  • DAT0是数据线第0位,在SPI中是数据输出位
  • DAT1和DAT2分别是数据线第1位和第2位,在SPI中不使用

在卡中各个控制寄存器定义如下,暂时不介绍具体细节,下面的指令中需要的时候再找它的具体定义
register

  • CID,ID寄存器
  • RCA,相关地址寄存器
  • DSR,驱动状态寄存器,表示SD卡的输出状态
  • CSD,指定数据寄存器,包含SD卡的操作状态
  • SCR,配置寄存器
  • OCR,操作状态寄存器
  • SSR,SD状态寄存器
  • CSR,卡片状态寄存器

时钟时序

关于时钟频率,找到最基础的UHS-I SD卡时钟频率表,我们使用最基础的25MHz。SD卡的时钟是可以实时改变的,如果需要中断一个指令,甚至可以把时钟停一段时间。需要注意的是,这个只是在数据传输模式的时钟频率,在待机模式和卡片识别模式中,要保持时钟在100kHz到400kHz之间,为了保险我们选择中间的200kHz:
clock speed
SD卡有三种模式如下,每种模式都包含自己的状态,无卡时处于Inactive待机模式,侦测到卡后进入card identification卡片识别模式,识别并配置SD卡,之后进入数据传输模式。这篇教程先处理待机模式和卡片识别模式,进入数据传输模式之后的事情到下一篇再解决:
card state
跟其他的接口比较相似的是,SD卡的输入与输出都是上升沿有效,因此沿用之前的设计,时钟下降沿输出,上升沿读入。下面两张图是SD接口的时序,但是对于CMD引脚是一样的
read timing
write timing
在SD模式下,和SD之间的通信由CMD和DAT引脚完成,初始化完全由CMD引脚完成,大致时序如上所示,方块里所有数据都是时钟上升沿有效,因此时钟被省略了

数据结构

在初始化流程中不需要回复或者数据的命令传送格式如下:
operation

命令结构如下,头2位01,加上6位命令,32位内容,7位CRC循环校验位,最后1位终结
command
command_format
CRC7循环校验位的计算和举例如下,从一个指令的最高位01开始,一直输入到第40位,生成7位的CRC校验,加在指令或者回复的后端:
CRC generate
CRC example
在Verilog代码中,每当一位数准备好时,可以用如下语句生成:

CRC <= {CRC[5:3], data_in^CRC[6]^CRC[2],CRC[1:0],data_in^CRC[6]};

Response回复结构如下:
response
回复分为R1普通回复、R1b包含忙碌位的普通回复、R2 CID和CSD寄存器回复、R3 OCR回复、R6 RCA回复、R7 卡片接口状态回复:
R1
其中32位的card status卡片状态每一位对应如下:
CSD1
CSD2
R1b和R1结构相同,不过同时会在DAT信号线传输忙碌信号,host应该检查忙碌信号是否结束,再传输下一个指令
R2
R3
R6
R7
R7_voltage

指令集

SD卡的各个模式以及状态的切换是由指令Command完成的,上面有指令的大致架构,其具体架构如下。在物理层文档86页开始,有各个指令的细节,这里摘取初始化流程中需要的部分:

基础指令
CMD class0 part1
CMD class0 part2

用户指定指令
CMD class8
ACMD是用户指令,在执行普通指令CMD55后,SD卡就了解下一个接收到的指令是用户指令ACMD了:
ACMD part1
ACMD part2

初始化流程

初始化模式和数据传输模式之间的具体切换如下:

  1. 探测到卡片后先CMD0从待机模式引入卡片识别模式,在这个过程中如果将SPI模式中的CS引脚,也就是SD模式中的DAT3引脚拉低,则进入SPI模式,否则使用SD模式。这里我们使用SD模式
  2. 使用CMD8探测卡片是否可以接受目前的3.3V电压模式,FPGA的IO一般使用3.3V
  3. 如果SD卡可以接受3.3V,则发送用户指令ACMD41,看SD卡的初始化是否完成,如果没有完成就持续发送ACMD41,并按需求送出切换到1.8V的指令。由于我们使用FPGA,如果切换到1.8V会无法探测到高电平,从而带来一大堆问题,因此我们保持在3.3V,有可能会牺牲一定的速度
  4. 如果需要切换到1.8V,则送出CMD11指令,否则跳过这步
  5. 发送CMD2读取卡片识别寄存器,会返回和SD卡相关的信息
  6. 发送CMD3读取卡片的相对地址寄存器。这一步完成后,SD卡就正式进入了数据传输模式
    card identification mode

看起来比较眼花缭乱,在文档的第32页也对初始化步骤作出简化,这篇教程也以这个流程为主,和初始化相关的是CMD3及以前,后面的步骤到下一篇博客中完成:
initialize
ACMD和普通CMD的区别是要通过特殊指令CMD55才能输入,有点类似于键盘上的shift键。ACMD41的指令流程:
ACMD41
再梳理一次这个流程的简化版本:

  • CMD0卡片复位到IDLE
  • CMD8配置电压(我们用3.3V)
  • ACMD41配置初始化,持续送出直到收到初始化完成的回复,过程大约需要1秒
  • CMD11切换电压(如果需要的话)
  • CMD2读取CID寄存器
  • CMD3读取RCA相对地址寄存器

下面这张图更好的解释了为什么要这么配置:
mode selection

控制寄存器

在初始化过程中读取了CID和RCA两个寄存器,它们的结构分别如下
CID
而RCA就是16位的地址,默认值为0x0000

具体发送计划

从CMD指令的结构中可以看出,除了头两位是01,最后八位中,七位是循环校验码,最后一位是1,48位的指令中有38位是有效的。为了凑个整,把头两位固定的01也放进指令中,变成40位,而CRC循环冗余校验码交给代码自动生成。
z
这篇教程是详细介绍初始化的细节,因此不过多深入自动化,而是尽量手动完成,能看到更多信息

从上面的初始化流程得到如下具体步骤:

  1. CMD0,发送0x40_0000_0000,无回复
  2. CMD8,发送0x48_0000_0155,其中[11:8]=4’h1是VHS,根据下图的CMD8的VHS格式,我们选择0001,也就是3.3V在的区间;[7:0]=8’h55是选择的测试格式,预期回复中会有同样的信息表示这个接口正常工作。预期回复R7:0x08_0000_0155_E1
    CMD8_format
  3. CMD55,发送0x77_0000_0000,[37:32]=6’h37=6’d55,是指令索引号。预期回复R1:0x37_0000_0120_83,其中[39:8]是卡片状态寄存器,下图是CSR后半部分的定义,0x0120意味着第8位和第5位是1,显示卡片准备好接收数据,并且下一个输入的CMD序列号将作为ACMD处理
    CSD2
  4. 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位是否拉高
    ACMD41_detail
    OCR_def
    ACMD41_response
  5. CMD2,发送0x42_0000_0000,预期回复0x3F1B_534D_4743_64A2_A860_B29C_5F3C_04A8_1F,比较特殊的是回复中CMD索引部分都置1,当中的128位是如下所示的卡片信息,我用的是一张三星的SDXC卡,没有具体查过细节
    CID_def
  6. CMD3,发送0x43_0000_0000,预期回复0x03_XXXX_0520_{CRC},它的结构如下所示,0xXXXX是返回的RCA相对地址,0x0520是讲CSR寄存器中23、22、19和12:0结合起来的16位,可以查前面的定义,23、22、19都是错误指示位,[12:0]=0x520意味着卡片处于数据传输模式
    R6

逻辑设计

从顶层来看逻辑设计很简单,串口输入到FPGA的数据转换成40位的SD指令,经过一定转换后送到SD接口通信模块,SD通信模块发送到SD卡,如果有回复则通过相反方向传回串口模块输出

从细节看,分成三个部分,串口通信,SD通信,和SD转串口接口。
diagram design

串口通信代码之前的博客有写过,但缺点是要写两个字节才会在电脑端显示,有滞后,并且不显示回车,发送出去和收回来的数据缩成一团看不清;
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接口中基本上都是双向口;串口前面有介绍过
connection

// 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中被#注释掉的那行去注释

仿真结果

调用仿真脚本后,加入想看的相应信号,得到波形如下:
waveform
从中可以看出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卡的存在
result_led
打开Putty,按照前面博客中介绍的方式配置好串口,这里具体用哪个串口COM是可以在设备管理器device manager里看到的,如果找不到,你可以拔一下开发板的USB,看一下哪个消失了,我这里是COM4。:
putty 1
putty 2
需要注意的是,我们需要额外勾选这个选项,由于Windows中的回车是\r\n,也就是LF+CR,但putty传给串口的却只有\n也就是CR,因此在putty回传所有的信号时候,要在CR前面加上LF,这样才能看到真正的换行
enter_setting
打开putty后,开始手动输入指令,具体情况如下所示:
serial_command
可以看出具体步骤,键盘输入4000000000后回车就完成了CMD0传输,如果有回复就会紧接着输出,具体分析如下:

  1. 键盘输入0x4000000000,CMD0从待机模式进入卡片识别模式
  2. 键盘输入0x4800000155,CMD8,设置电压3.3V,并加入识别字符0x55,可以看到回复0x0800000155E1,08是指令序号,01是电压配置回复,确认为3.3V,返回同样的识别字符0x55,并加上CRC+1’b1 = 0xE1
  3. 键盘输入0x7700000000,CMD55,进入用户自定义指令集,可以看到回复0x370000012083,0x00000120意味着第8位和第5位是1,显示卡片准备好接收数据,并且下一个输入的CMD序列号将作为ACMD处理
  4. 键盘输入0x6940ff8000,ACMD41,看到回复0x3F00FF8000FF,这里[39]为0,表示还在忙碌状态,还未完成
  5. 重复步骤3和4,这次收到的回复是0x3FC0FF8000FF,这次配置完成了
  6. 键盘输入0x4200000000,CMD2,请求SD卡序列号,其中包含设备制造商,SD卡参数等和卡片相关的数据,后面的回复就是这些信息集合,具体内容可以到SD卡制造商的手册中查到
  7. 键盘输入0x4300000000,CMD3,请求SD卡相对地址,其返回的数据指示着读取的相对地址,到这一步就完成了初始化,可以进行实际的数据传输了

总结

这篇教程第一次只写了接口技术的介绍,不得不说SD卡真的是个大坑。文档长而难读,细节一大堆。

下一篇将SD卡接口的初始化过程变成自动化,然后写入一定的随机数进行测试

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值