驱动SD卡是件容易让人抓狂的事情,驱动SD卡好比SDRAM执行页读写,SD卡虽然不及SDRAM的麻烦要求(时序参数),但是驱动过程却有猥琐操作。除此此外,描述语言只要稍微比较一下C语言,描述语言一定会泪流满面,因为嵌套循环,嵌套判断,或者嵌套函数等都是它的痛。.
史莱姆模块是多模块建模的通病,意指结构能力非常脆弱的模块,暴力的嵌套行为往往会击垮模块的美丽身躯,好让脆弱结构更加脆弱还有惨不忍睹,最终搞垮模块的表达能力。描述语言预想驾驭SD卡,关键的地方就是如何提升模块的结构能力。简单而言,描述语言如何不失自身的美丽,又用自身的方法,去实现嵌套循环或者嵌套函数等近似的内容呢?
低级建模I之际,论结构能力它确实有点勉强,所以SD卡的实验才姗姗来迟。如今病猫已经进化为老虎,而且进化之初的新生儿都会肌饿如心焚,理智也不健全。因为如此,低级建模II才会不停舔着嘴唇,然后渴望新生的第一祭品。遇见SD卡,它仿佛遇见美味的猎物,口水都下流到一塌糊涂。
诸位少年少女们,让我们一起欢呼活祭仪式的开始吧!
二十一世纪的今天,SD卡演化的速度简直迅雷不及掩耳,如今SD卡已经逐渐突破64GB大关。对此,SD卡也存在N多版本,如版本SDV1.×,版本SDV2,或者SDHCV2等,当然未来还会继续演化下去。所谓版本是指建造工艺还有协议,粗略而言,版本SDV1.×是指容量为2GB以下的SD卡,版本SDV2则指容量为2GB~4GB之间的SD卡,版本SDHCV2则是容量为4GB以上的SD卡。
话虽如此,不过实际情况还要根据各个厂商的心情而定,有些厂商的SD卡虽为4GB,但是版本却是SDV1.×,还有厂商的SD卡的虽为2GB,不过版本却是SDV2,情况尽是让人哭笑不得。此外,版本不会印刷在硬件的表面上,而且不同版本也有不同驱动方法。俗语有云,擒贼先擒卒——凡事从娃娃抓起,所以笔者遵循伟大的智慧,从版本SDV1.×开始动手。
图24.1SPI模式。
SD卡有SDIO还有SPI两种模式,后者简单又省事,所以SPI模式都是众多懒惰鬼的喜爱。SPI模式一般只用4只引脚,而且主机(FPGA)与从机(SD卡)之间的链接如图24.1所示,至于引脚的聂荣如表24.1所示:
表24.1SD卡SPI模式的引脚说明。
引脚
说明
SD_CLK
串行时钟,闲置为高
SD_NCS
片选,闲置为高,拉低有效
SD_DI
数据输入,也是主机输出
SD_DOUT
数据输出,也是主机输入
虽然DS1302也有SPI,但是数据线是双向IO,反之SD卡则是一对出入的数据线。话虽如此,它们两者都有乖乖遵守SPI的传输协议,即下降沿设置数据,上升沿锁存数据。
图24.2写一个字节(主机视角)。
图24.2是主机视角写一个字节的理想时序。主机会利用时钟的下降沿,由高至低发送一个字节的数据。
图24.3读一个字节(主机视角)。
图24.2是主机视角读一个字节的理想时序。从机会利用时钟的下降沿,由高至低发送一个字节的数据,主机则会利用时钟信号的上升沿,由高至低读取一个字节的数据。
图24.4同时读写一个字节(主机视角)。
对此,Verilog可以这样描述,结果如代码24.1所示:
1.case(i)2.3.0,1,2,3,4,5,6,7:4.begin5.rDI<=iData[7-i];6.7.if(C1==0)rSCLK<=1'b0;8.elseif(C1==isHalf)rSCLK<=1'b1;9.10.if(C1==isQuarter)D1[7-i]<=SD_DOUT;11.12.if(C1==isFull-1)beginC1<=10'd0;i<=i+1'b1;end13.elsebeginC1<=C1+1'b1;end14.end代码24.1
第3行表示,步骤0~7造就一个字节的读写。还有第5~10行的D1[7-i]表示,读写数据由高至低。
好奇的朋友一定会疑惑道,为何第10行的锁存行为不是时钟的半周期(上升沿),而是四分之一呢?原因很单纯,因为数据在这个时候最为有效。
图24.5写命令(主机视角)。
当然,SD卡不是给足两只骨头就会满足的哈士奇...为此,除了单纯的读写数据意外,SD卡还有所谓的写命令,而写命令则是读写字节的复合体。如图24.5所示,那是主机写命令的理想时序,主机先由高至低发送6个字节的命令。SD卡接受完毕以后,便会反馈一个字节的数据。期间,片选信号必须处于拉低状态。对此,Verilog可以这样表示,结果如代码24.2所示:
1.case(i)2.3.0:4.beginrCMD<=iAddr;i<=i+1'b1;end5.6.1,2,3,4,5,6:7.beginT<=rCMD[47:40];rCMD<=rCMD<<8;i<=FF_Write;Go<=i+1'b1;end8.9.7:10.begini<=FF_Read;Go<=i+1'b1;end11.12.8:13.if(C2==100)beginC2<=10'd0;i<=i+1'b1;end14.elseif(D1!=8'hff)beginC2<=10'd0;i<=i+1'b1;end15.elsebeginC2<=C2+1'b1;i<=FF_Read;Go<=i;end16.17....18.19.12,13,14,15,16,17,18,19:20.begin21.rDI<=T[19-i];22.if(C1==0)rSCLK<=1'b0;23.elseif(C1==isHalf)rSCLK<=1'b1;24.25.if(C1==isQuarter)D1[19-i]<=SD_DOUT;26.27.if(C1==isFull-1)beginC1<=10'd0;i<=i+1'b1;end28.elsebeginC1<=C1+1'b1;end29.end30.31.20:32.begini<=Go;end代码24.2
步骤12~20是读写一个字节的伪函数,步骤0准备6个字节的命令,步骤1~6由高至低发送命令,并且进入伪函数。步骤7进入伪函数,并且读取一个字节的反馈数据(注意FF_Write与FF_Read都指向步骤12)。反馈数据一般都是8’hff以外的结果,如果不是则重复读取反馈数据100次,如果SD卡反应正常,都会在这100次以内反馈8’hff以外的结果。
(一)CMD0,复位命令;
(二)CMD1,初始化命令;
(三)CMD24,写命令;
(四)CMD17,读命令。
CMD0用来复位SD卡,好让SD卡处于(IDLE)待机状态。CMD1用来初始化SD卡,好让SD卡处于(Transfer)传输状态。CMD24将512字节数据写入指定的地址,CMD17则将512字节数据从指定的地址读出来。
图24.6CMD0的理想时序图。
我们先假设isCall[1]执行写命令,isCall[0]则是执行读写字节。如代码24.3所示,步骤0用来准备CMD0命令。步骤1延迟1ms。步骤2执行10次无意义的读写,以示给予80个准备时钟。在此读者稍微注意一下第12行,每当完成一次读写C1便会递增一下,C1递增10次便表示读写执行10次。
步骤3拉低CS,并且步骤4发送命令。步骤4可能会吓坏一群小朋友,不过只要耐心解读,其它它并不可怕。首先执行第22行的写命令,如果反馈数据不为8’h01(第20行),消除isDo便递增C1,然后再返回第22行。如果反馈数据为8’h01(第21行)
,消除isDo与C1然后继续步骤。如果重复执行100次都失败,D2赋值CMD0的失败信息,消除C1并且i直接步骤8。
步骤5拉低CS,步骤6则给予8个结束时钟。步骤7为D2赋值CMD0的成功信息,步骤8~9拉高CS并且产生完成信号。
图24.7CMD1的理想时序图。
图24.7是CMD1的理想时序图,T0&T1之际拉低CS并且发送六个字节的命令CMD1{8’h41,32’d0,8’hff}。SD卡接受命令以后便反馈数据R1——8’h00。T2&T3之际拉高CS并且给予8个结束时钟。Verilog的描述结果如代码24.4所示:
1.case(i)2.3.0://Enablecs,prepareCmd14.beginrCS<=1'b0;D4<={8'h41,32'd0,8'hff};i<=i+1'b1;end5.6.1://Try100times,readyerrorcode.7.if(C1==10'd100)beginD2<=CMD1ERR;C1<=16'd0;i<=4'd5;end8.elseif(iDone&&iData!=8'h00)beginisCall[1]<=1'b0;C1<=C1+1'b1;end9.elseif(iDone&&iData==8'h00)beginisCall[1]<=1'b0;C1<=16'd0;i<=i+1'b1;end10.elseisCall[1]<=1'b1;11.12.2://Disablecs13.beginrCS<=1'b1;i<=i+1'b1;end14.15.3://Sendfreeclock16.if(iDone)beginisCall[0]<=1'b0;i<=i+1'b1;end17.elsebeginisCall[0]<=1'b1;D1<=8'hff;end18.19.4://Disablecs,readyOKcode.20.beginD2<=CMD1OK;i<=i+1'b1;end21.22.5://Disablecs,generatedonesignal23.beginrCS<=1'b1;isDone<=1'b1;i<=i+1'b1;end24.25.6:26.beginisDone<=1'b0;i<=4'd0;end代码24.4
如代码24.4所示,步骤0准备命令CMD1。步骤1重复发送CMD1命令100次,直至反馈数据R1为8’h00为止,否则反馈错误信息。步骤2拉高CS,步骤3则给予结束时钟。步骤4反馈成功信息,步骤5~6拉高CS之余也产生完成信号。
好奇的同学一定会觉得疑惑,命令CMD0与命令CMD1同样反馈数据R1,为何前者是8’h01,后者则是8’h00呢?事实上,R1的内容也反应SD卡的当前状态,SD卡有待机状态(IDLE)还有传输状态(Transfer)等两个常见状态。
图24.8版本V1.×的初始化流程图。
如图24.8所示,那是版本V1.x的初始化流程图。主机先发送CMD0,SD卡接收以后如果反馈R1为8’h01便继续流程,否则重复发送CMD0。主机接着发送CMD1,如果SD卡接收并且反馈R1为8’h00,该结果表示SD卡以从待机状态进入传输状态,余下CMD24还有CMD17才有效。
图24.9CMD24的理想时序图。
图24.9是CMD24的理想时序图。T0~1之际,主机拉低CS之余,主机也向SD卡发送写命令CMD24,其中Addr3~Addr0是写入地址。SD卡接收以后便以反馈数据8’h00表示接收成功。保险起见,主机在T2给足800个准备时钟,如果读者嫌准备时钟给太多,读者可以自行缩小至80。T3之际,主机发送8’hfe以示写512字节开始,T4~T7则是写512字节的过程。T8~T9分别写入两个CRC字节(CRC校验)。
完后,SD卡便会反馈8’h05以示写512字节成功,此刻(T10~11)主机读取并且检测。事后直至SD卡发送8’hff为止,SD卡都处于忙状态。换言之,如果主机在T12成功读取8’hff,结果表示SD卡已经忙完了。T13之际,主机再拉高CS。对此,Verilog可以这样描述,结果如代码24.5所示:
步骤0拉低CS之余,它也准备写命令CMD24,其中{iAddr,9’d0}表示地址有512偏移量,内容等价iAddr<<9。步骤1尝试写命令100次,直至反馈内容为8’h00,否则便准备错误信息。步骤2发送800个准备时钟,如果嫌多可以自行缩小。步骤3写入开始字节8’hfe。步骤4~5主要是从FIFO取得数据,步骤6则是将数据写入SD卡,步骤7用来控制循环的次数。步骤8~9分别写入两个CRC字节,内容随意。
步骤10读取反馈数据,步骤11则检测反馈数据是否为8’h05,是则继续,不是则准备错误信息,并且跳转步骤14(结束操作)。步骤12不断读取数据,直至读取内容为8’hff位置。步骤13准备成功信息。步骤14~15拉高CS之余也产生完成信号。在此,读者要稍微注意一下,步骤4~7组合起来,类似先执行后判断的do...while循环。
图24.10CMD17的理想时序图。
图24.10是CMD17的理想时序图。T0~T1之际,主机先拉低CS再发送命令CMD17,其中Addr3~Addr0是指定的读地址,事后SD卡便会反馈数据8’h00以示接收成功。T2之际,主机会不断读取数据,如果读取内容是8’hfe,结果表示SD卡已经准备发送512字节数据。T3~T6之际,主机不断从SD卡那里读取512个字节的数据。T7~T8之际,主机读取两个字节的CRC,然后在T9~10拉高CS之余也给足8个结束时钟。
换之,Verilog的描述结果如代码24.6所示:
步骤0拉低CS之余也准备命令CMD17,其中{iAddr,9’d0}为512的偏移量。步骤1重复100次写命令,直至SD卡反馈8’h00,否则准备错误信息,然后跳转步骤12(结束操作)。步骤2不断读取数据,直至读取内容为8’hfe为止。步骤3读取数据,步骤4~5则将数据写入FIFO,步骤6用来控制循环。步骤7~8读取两个字节CRC。步骤9拉高CS,步骤10则给足8个结束时钟。步骤11准备成功信息。步骤12拉高CS之余,也产生完成信号。
上述内容理解完毕以后,我们便可以开始建模了。
图24.11SD卡基础模块的建模图。
图24.11是SD卡基础模块的建模图,其中内容包括SD卡控制模块,SD卡功能模块,还有两个fifo储存模块。SD卡功能模块的沟通信号Call/Done有两位位宽,其中[1]为写命令,[0]为字节读写。SD卡控制模块的Call/Done位宽有四,表示它支持4个命令,其中[3]为CMD24,[2]为CMD17,[1]为CMD1,[0]为CMD0。两只FIFO储存模块充当写缓存(上)还有读缓存(下),它们被外界调用以外,它们也被SD卡控制模块调用。
图24.12SD卡功能模块的建模图。
图24.12是SD卡功能模块的建模图,右边是驱动SD卡的顶层信号,左边则是沟通用,还有命令,iData与oData等数据信号。Call/Done位宽有两,其中[1]为写命令,[0]为读写数据。
图24.13不同状态之间的传输速率。
话题继续之前,请允许笔者作足一些小补充。如图24.13所示,待机状态SD卡为低速状态,速率推荐为100Khz~500Khz之间。保险起见,笔者取为100Khz。反之,传输状态SD卡处于高速状态,速率推荐为2Mhz或者以上。笔者衡量各种因数以后,笔者决定选择10Mhz。丧心病狂的读者当然可以选择10Mhz以上的速率,如果硬件允许的话...据说,100Mhz也没有问题。
图24.14FIFO储存模块的建模图。
图24.14是大伙看烂的FIFO储存模块,具体内容让我们来看代码吧。
1.modulefifo_savemod2.(3.inputCLOCK,RESET,4.input[1:0]iEn,5.input[7:0]iData,6.output[7:0]oData,7.output[1:0]oTag8.);9.initialbegin10.for(C1=0;C1<1024;C1=C1+1'b1)11.beginRAM[C1]<=8'd0;end12.end13.14.reg[7:0]RAM[1023:0];15.reg[10:0]C1=11'd0,C2=11'd0;//N+116.reg[7:0]D1;17.18.always@(posedgeCLOCKornegedgeRESET)19.if(!RESET)20.begin21.C1<=11'd0;22.end23.elseif(iEn[1])24.begin25.RAM[C1[9:0]]<=iData;26.C1<=C1+1'b1;27.end28.29.always@(posedgeCLOCKornegedgeRESET)30.if(!RESET)31.begin32.C2<=11'd0;33.D1<=8'd0;34.end35.elseif(iEn[0])36.begin37.D1<=RAM[C2[9:0]];38.C2<=C2+1'b1;39.end40.41.assignoData=D1;42.assignoTag[1]=(C1[10]^C2[10]&C1[9:0]==C2[9:0]);//FullLeft43.assignoTag[0]=(C1==C2);//EmptyRight44.45.endmodule由于数据缓冲对象不是SDRAM,所以第41行的oData由D1驱动而不是RAM直接驱动。余下内容,读者自己看着办吧。
图24.15SD卡控制模块的建模图。
图24.15是SD卡控制模块的建模图,它好比一只刺猬,全身上下都长满箭头,让人看见也怕怕。右边是调用功能模块的信号群,上下则是调用储存模块的信号群。左边则是被外界调用的信号群,其中顶层信号SD_NCS是SD卡的片选信号。此外,Call/Done位宽有4,表示该模块支持4个命令,[3]为CMD24,[2]为CMD17,[1]为CMD1,[0]为CMD0。至于oTag则是用来反馈命令的执行状态。
所有寄存器的复位值为0,rCS除外。
该模块为SD卡基础模块,连线部署请参考图24.11。
图24.16实验二十四的建模图。
图24.16是实验二十四的建模图,右边是SD卡基础模块,右边则是调用该模块的核心程序。核心程序先初始化SD卡,期间也将反馈信息经由TXD发送出去。再者,它将512个字节写入SD卡,又从中读出,然后经由TXD发送出去。具体内容让我们来看代码吧:
53.case(i)54.55.0://cmd056.if(DoneU1)beginisCall[0]<=1'b0;i<=i+1'b1;end57.elsebeginisCall[0]<=1'b1;end58.59.1:60.beginT<={2'b11,TagU1,1'b0};i<=TXFUNC;Go<=i+1'b1;end61.62./********************/63.步骤0执行CMD0,然后步骤1反馈执行结果。
64.2://cmd165.if(DoneU1)beginisCall[1]<=1'b0;i<=i+1'b1;end66.elsebeginisCall[1]<=1'b1;end67.68.3:69.beginT<={2'b11,TagU1,1'b0};i<=TXFUNC;Go<=i+1'b1;end70.71./*********************/72.步骤2执行CMD1,然后步骤3反馈执行结果。
73.4://writedatatofifo74.beginisEn[1]<=1'b1;i<=i+1'b1;end75.76.5:77.beginisEn[1]<=1'b0;i<=i+1'b1;end78.79.6:80.if(C2==511)beginC2<=11'd0;i<=i+1'b1;end81.elsebeginD2<=D2+1'b1;C2<=C2+1'b1;i<=6'd4;end82.83./**************/84.步骤4~6将数据00~FF写入FIFO两遍。
85.7://cmd2486.if(DoneU1)beginisCall[3]<=1'b0;i<=i+1'b1;end87.elsebeginisCall[3]<=1'b1;D1<=23'd0;end88.89.8:90.beginT<={2'b11,TagU1,1'b0};i<=TXFUNC;Go<=i+1'b1;end91.92./***************/93.步骤7执行CMD24,写入地址为23’d0。步骤8反馈执行结果。
94.9://cmd1795.if(DoneU1)beginisCall[2]<=1'b0;i<=i+1'b1;end96.elsebeginisCall[2]<=1'b1;D1<=23'd0;end97.98.10:99.beginT<={2'b11,TagU1,1'b0};i<=TXFUNC;Go<=i+1'b1;end100.101./****************/102.步骤9执行CMD17,步骤10则反馈执行结果。
103.11://Readdatafromfifo104.beginisEn[0]<=1'b1;i<=i+1'b1;end105.106.12:107.beginisEn[0]<=1'b0;i<=i+1'b1;end108.109.13:110.beginT<={2'b11,DataU1,1'b0};i<=TXFUNC;Go<=i+1'b1;end111.112.14:113.if(C2==511)beginC2<=11'd0;i<=i+1'b1;end114.elsebeginC2<=C2+1'b1;i<=6'd11;end115.116.15:117.i<=i;118.119./****************/120.步骤11~14从FIFO哪里读出数据512次,然后再经由TXD发送出去。
121.16,17,18,19,20,21,22,23,24,25,26:122.if(C1==B115K2-1)beginC1<=11'd0;i<=i+1'b1;end123.elsebeginrTXD<=T[i-16];C1<=C1+1'b1;end124.125.27:126.i<=Go;127.128.endcase129.130.assignTXD=rTXD;131.132.endmodule步骤16~27是发送一帧数据的伪函数。综合完毕,插入版本V1.×的SD卡,例如笔者手上IProc制,容量为256MB的SD卡,然后下载程序。演示过程如下:
A2//CMD0执行成功
A4//CMD1执行成功
A6//CMD24执行成功
A8//CMD17执行成功
00~FF//读出数据0~255
00~FF//读出数据256~511
图24.17SD卡的内容。
为了验证SD卡是否成功写入00~FF两遍,笔者稍微瞧瞧SD卡的内容...如图24.17所示,地址0x00~0xF0(0~255)的内容是00~FF,地址0x0100~0x01F0(256~511)的内容也是00~FF。
细节一:完整的个体模块
虽然本实验的SD卡基础模块已经就绪,不过SD卡的前提条件必须是版本SDV1.×,还有健康的硬件。嘛,SD卡基础模块傻是傻了一点,不过它还可以继续扩展。