FPGA之SD卡读写操作

(呕心沥血,写了接近三个小时。我觉得如果耐心看的话,真的会有所帮助哦哈哈哈。看在博主这么kindhearted的份上,点个赞吧!!!!)

学到后面发现例程文件越来越多,代码越来越恶心了。SD卡,I2C_EEPROM,SPI_Flash这种芯片的各种时序看得头晕,数据手册冗长啰嗦,搞不好还是100多页的英文版。。。不是人看的。。所以我觉得更加要把自己的心得写出来,让更多爱学习的人提高自己的效率。虽说看完博客不会给你特别大的帮助,但起码会给你入个门,有个感性的了解。

OK,讲了那么多,下面开始我们今天的小任务:解剖FPGA黑金开发板AX301SD卡的例程:

在研究代码之前,应该要事先对SD卡的操作指令和时序了解:

可以去看看这篇博客:https://blog.csdn.net/c602273091/article/details/41593955 

(我浏览了下这个作者的主页,真的是太大佬了,任重而道远啊,要学的东西还很多,希望我将来也可以成为这样的大佬。)

请牢记刚刚那篇博客所讲的东西,然后我开始结合代码讲解。PS:代码要剖开讲,完整的代码就是我剖开的顺序。

首先分析sd_card_cmd.v这个文件

sd_card_cmd.v

module sd_card_cmd(
	input                       sys_clk,
	input                       rst,
	input[15:0]                 spi_clk_div,                  //SPI module clock division parameter
	input                       cmd_req,                      //SD card command request
	output                      cmd_req_ack,                  //SD card command request response
	output reg                  cmd_req_error,                //SD card command request error
	input[47:0]                 cmd,                          //SD card command
	input[7:0]                  cmd_r1,                       //SD card expect response
	input[15:0]                 cmd_data_len,                 //SD card command read data length
	input                       block_read_req,               //SD card sector data read request
	output reg                  block_read_valid,             //SD card sector data read data valid
	output reg[7:0]             block_read_data,              //SD card sector data read data
	output                      block_read_req_ack,           //SD card sector data read response
	input                       block_write_req,              //SD card sector data write request
	input[7:0]                  block_write_data,             //SD card sector data write data next clock is valid
	output                      block_write_data_rd,          //SD card sector data write data
	output                      block_write_req_ack,          //SD card sector data write response
	output                      nCS_ctrl,                     //SPI module chip select control
	output reg[15:0]            clk_div,
	output reg                  spi_wr_req,                   //SPI module data sending request
	input                       spi_wr_ack,                   //SPI module data request response
	output[7:0]                 spi_data_in,                  //SPI module send data
	input[7:0]                  spi_data_out                  //SPI module data returned
);
parameter S_IDLE         = 0;
parameter S_WAIT         = 1;
parameter S_INIT         = 2;
parameter S_CMD_PRE      = 3;
parameter S_CMD          = 4;
parameter S_CMD_DATA     = 5;
parameter S_READ_WAIT    = 6;
parameter S_READ         = 7;
parameter S_READ_ACK     = 8;
parameter S_WRITE_TOKEN  = 9;
parameter S_WRITE_DATA_0 = 10;
parameter S_WRITE_DATA_1 = 11;
parameter S_WRITE_CRC    = 12;
parameter S_WRITE_SUC    = 13;
parameter S_WRITE_BUSY   = 14;
parameter S_WRITE_ACK    = 15;
parameter S_ERR          = 16;
parameter S_END          = 17;

reg[4:0]                      state;
reg                           CS_reg;
reg[15:0]                     byte_cnt;
reg[7:0]                      send_data;
wire[7:0]                     data_recv;
reg[9:0]                      wr_data_cnt;

assign cmd_req_ack = (state == S_END);
assign block_read_req_ack = (state == S_READ_ACK);
assign block_write_req_ack= (state == S_WRITE_ACK);
assign block_write_data_rd = (state == S_WRITE_DATA_0);
assign spi_data_in = send_data;
assign data_recv = spi_data_out;
assign nCS_ctrl = CS_reg;

这些参数的设定比较多,大家一定要有个初步的印象。我自己学习的时候,特地用一张白纸记下一些寄存器的状态,例如spi_wr_ack什么时候为1,只有这样在if条件判断的时候才会理解得更加清楚。而且大家要记住,只要是input的寄存器,就一定是另一个文件的output,一一对应好,把条件理清楚,再开始看接下来的模块化代码。

always@(posedge sys_clk or posedge rst)
begin
	if(rst == 1'b1)
	begin
		CS_reg <= 1'b1;
		spi_wr_req <= 1'b0;
		byte_cnt <= 16'd0;
		clk_div <= 16'd0;
		send_data <= 8'hff;
		state <= S_IDLE;
		cmd_req_error <= 1'b0;
		wr_data_cnt <= 10'd0;
	end
	else
		case(state)
			S_IDLE:
			begin
				state <= S_INIT;
				clk_div <= spi_clk_div;
				CS_reg <= 1'b1;
			end
			S_INIT:
			begin
				//send 11 bytes on power(at least 74 SPI clocks)
				if(spi_wr_ack == 1'b1)
				begin
					if(byte_cnt >= 16'd10)
					begin
						byte_cnt <= 16'd0;
						spi_wr_req <= 1'b0;
						state <= S_WAIT;
					end
					begin
						byte_cnt <= byte_cnt + 16'd1;
					end
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hff;
				end
			end

这段代码对应的原理是:SD卡的初始化,需要先给予至少74个CLK后。那么为什么要74个CLK呢?因为在上电初期,电压的上升过程据SD卡组织的计算约合64个CLK周期才能到达SD卡的正常工作电压他们管这个叫做Supply ramp up time,其后的10个CLK是为了与SD卡同步,之后开始CMD0的操作,严格按照此项操作,一定没有问题。因此,在S_INIT状态下,要发送10个字节,才进入等待状态。我一开始也很懵逼,为什么是10,然后看到初始化这个状态应该就是等待的那个原理,转念一想10*8=80(一个字节8位2333),刚好大于74个CLK。

S_WAIT:
			begin
				cmd_req_error <= 1'b0;
				wr_data_cnt <= 10'd0;
				//wait for  instruction
				if(cmd_req == 1'b1)
					state <= S_CMD_PRE;
				else if(block_read_req == 1'b1)
					state <= S_READ_WAIT;
				else if(block_write_req == 1'b1)
					state <= S_WRITE_TOKEN;
				clk_div <= spi_clk_div;
			end

然后就是等待状态了。假想一下SD卡是一个工人,这个时候它有三个任务,发送命令,读操作,写操作,这个时候就是在等待有没有人让它去做那样的事情。

S_CMD_PRE:
			begin
				//before sending a command, send an byte 'ff',provide some clocks
				if(spi_wr_ack == 1'b1)
				begin
					state <= S_CMD;
					spi_wr_req <= 1'b0;
					byte_cnt <= 16'd0;
				end
				else
				begin
					spi_wr_req <= 1'b1;
					CS_reg <= 1'b1;
					send_data <= 8'hff;
				end
			end
S_CMD:
			begin
				if(spi_wr_ack == 1'b1)
				begin
					if((byte_cnt == 16'hffff) || (data_recv != cmd_r1 && data_recv[7] == 1'b0))
					begin
						state <= S_ERR;
						spi_wr_req <= 1'b0;
						byte_cnt <= 16'd0;
					end
					else if(data_recv == cmd_r1)
					begin
						spi_wr_req <= 1'b0;
						if(cmd_data_len != 16'd0)
						begin
							state <= S_CMD_DATA;
							byte_cnt <= 16'd0;
						end
						else
						begin
							state <= S_END;
							byte_cnt <= 16'd0;
						end
					end
					else
						byte_cnt <=  byte_cnt + 16'd1;
				end
				else
				begin
					spi_wr_req <= 1'b1;
					CS_reg <= 1'b0;
					if(byte_cnt == 16'd0)
						send_data <= (cmd[47:40] | 8'h40);
					else if(byte_cnt == 16'd1)
						send_data <= cmd[39:32];
					else if(byte_cnt == 16'd2)
						send_data <= cmd[31:24];
					else if(byte_cnt == 16'd3)
						send_data <= cmd[23:16];
					else if(byte_cnt == 16'd4)
						send_data <= cmd[15:8];
					else if(byte_cnt == 16'd5)
						send_data <= cmd[7:0];
					else
						send_data <= 8'hff;
				end
			end

我把命令的两段代码都放在一起写。我一开始也混淆了,为什么这个spi_wr_ack老是出现,把我完全弄糊涂了。后来一想,发送命令也是write操作啊,,只不过我是在写 命 令!而已啊。所以,这段代码的理解是:当写命令应答完成,就进入下一个状态S_CMD,写命令清0,字节计数器清零,否则(应答没完成),写请求,CS保持,然后发送校验码0xff。

对应的原理,那篇博客也有讲到:写入一个CMD命令时,首先将CS拉低,在SD卡的DIN引脚输入命令及相应的参数和校验码。

接着就是S_CMD指令:如果应答完成,需要校验。发送一个写命令和地址参数,后跟校验码,不采用校验码,发送0xff即可,SD卡会以R1回应。这里就是校验到底与R1返回的数据对不对。如果不对,则进入错误,如果正确,当命令的数据长度不为0时,进入S_CMD_DATA状态,否则结束。否则计数器开始计时等待响应。如果应答没有完成,这才是S_CMD真正要做的事情,把CS拉低,然后开始发送命令。然后,我相信你会对这段代码感到懵逼,没关系,我来给你解释下:每一个命令的长度都是固定的6个字节,前1个字节的值=命令号+0x40;中间4个字节为参数,不同的命令参数格式都不相同,但参数最多为4个字节;最后1个字节是CRC校验码和1位固定结束位‘1’。所以你会看到cmd的命令在sd_card_sec_read_write.v中,每一个cmd都是由6个字节组成,如:8'd0,8'h00,8'h00,8'h00,8'h00,8'h95。

然后进入了一个叫S_CMD_DATA的状态。

S_CMD_DATA:
			begin
				if(spi_wr_ack == 1'b1)
				begin
					if(byte_cnt == cmd_data_len - 16'd1)
					begin
						state <= S_END;
						spi_wr_req <= 1'b0;
						byte_cnt <= 16'd0;
					end
					else
					begin
						byte_cnt <= byte_cnt + 16'd1;
					end
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hff;
				end
			end

这个又是什么东西。真的恶心。后来顺着思路,我去找cmd_data_len到底是个什么东西,发现在sd_card_sec_read_write.v中,只有CMD8有4个字节的长度,其他均为0!这其中一定有什么骚规则,没错,后来百度了资料,果然:原来CMD8会返回5个字节的数据,首先接收到第一个字节格式为R1的数据,这个数据只要判断是否为0X01即可,如果为0X01,表示SD卡响应了CMD8命令,如果不为0X01(一般会是0X09或0X05,不用关心),则表示SD卡不支持CMD8命令。在接收到0X01之后,随后需要接收4字节数据,其中31-28位为command version,即命令的类型,此处为CMD8;然后27-12位是保留的数据位,通常为0;然后11-8位是SD卡支持的电压范围,此处得到的应该是‘0001’;最后一个字节是我们在CMD8的命令中发送给SD卡的数据,SD卡又原模原样的返回来了,在命令中我们发送的是0XAA,此处得到的也应该是0XAA。

命令就此讲完了,然后就开始了读和写的等待状态:

S_READ_WAIT:
			begin
				if(spi_wr_ack == 1'b1 && data_recv == 8'hfe)
				begin
					spi_wr_req <= 1'b0;
					state <= S_READ;
					byte_cnt <= 16'd0;
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hff;
				end
			end
			S_READ:
			begin
				if(spi_wr_ack == 1'b1)
				begin
					if(byte_cnt == 16'd513)
					begin
						state <= S_READ_ACK;
						spi_wr_req <= 1'b0;
						byte_cnt <= 16'd0;
					end
					else
					begin
						byte_cnt <= byte_cnt + 16'd1;
					end
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hff;
				end
			end

首先是读:SD卡读一个块:  发送一个读命令,后面的参数是读取的块的首地址和0xff,SD会以R1回应,如果地址正确,回应应该为0。接着是SD卡发送数据,以令牌0xfe为起始,后面是要读取的数据,最后是2个字节的校验码

首先,在读的等待状态时,要等待写应答,为什么呢?我自己的理解是,读也需要先写地址,才能够读到地址所对应的数据。然后就是0xfe,如上所说。如果写没有应答,则发送写请求。

当进入读状体时,这个时候就要读正式数据512Bytes了,读完时,,则进入了S_READ_ACK的读应答状态。否则,字节计数器+1,等待读完。如果没有写应答信号,则发送写请求。

然后进入写状态:

S_WRITE_TOKEN:
				if(spi_wr_ack == 1'b1)
				begin
					state <= S_WRITE_DATA_0;
					spi_wr_req <= 1'b0;
					byte_cnt <= 16'd0;  
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hfe;
				end
			S_WRITE_DATA_0:
			begin
				state <= S_WRITE_DATA_1;
				wr_data_cnt <= wr_data_cnt + 10'd1;
			end
			S_WRITE_DATA_1:
			begin
				if(spi_wr_ack == 1'b1 && wr_data_cnt == 10'd512)
				begin
					state <= S_WRITE_CRC;
					spi_wr_req <= 1'b0;
				end
				else if(spi_wr_ack == 1'b1)
				begin
					state <= S_WRITE_DATA_0;
					spi_wr_req <= 1'b0;
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= block_write_data;
				end
			end
			S_WRITE_CRC:
			begin
				if(spi_wr_ack == 1'b1)
				begin
					if(byte_cnt == 16'd1)
					begin
						state <= S_WRITE_SUC;
						spi_wr_req <= 1'b0;
						byte_cnt <= 16'd0;
					end
					else
					begin
						byte_cnt <= byte_cnt + 16'd1;
					end
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hff;
				end
			end
			S_WRITE_SUC :
			begin
				if(spi_wr_ack == 1'b1)
				begin
					if(data_recv[4:0] == 5'b00101)
					begin
						state <= S_WRITE_BUSY;
						spi_wr_req <= 1'b0;
					end
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hff;
				end
			end
			S_WRITE_BUSY :
			begin
				if(spi_wr_ack == 1'b1)
				begin
					if(data_recv == 8'hff)
					begin
						state <= S_WRITE_ACK;
						spi_wr_req <= 1'b0;
					end
				end
				else
				begin
					spi_wr_req <= 1'b1;
					send_data <= 8'hff;
				end
			end

写的状态要求如下:

在S_WRITE_TOKEN状态下,发送0xfe,发送写请求(对,应该是从else if开始理解),当写应答时,进入S_WRITE_DATA_0状态,此时计数器加1,进入S_WRITE_DATA_1状态,对SD卡进行写操作。这段代码我怎么感觉有点多余,后面还没应答的时候一直返回S_WRITE_DATA_0,其实就是计数计到513,把开始的0xfe也算上,到达513之后,开始进入CRC检验。然后就是上面的第三条,读到00101才说明写成功。然后进入第四条。当接收到0xff时则说明写操作完成,发送一个写成功的应答信号,否则继续写。。。

然后就是错误警告和应答状态又回到空闲状态:

	S_ERR:
			begin
				state <= S_END;
				cmd_req_error <= 1'b1;
			end
			S_READ_ACK,S_WRITE_ACK,S_END:
			begin
				state <= S_WAIT;
			end
			default:
				state <= S_IDLE;

最后还有两段代码,就是对读操作的补充,因为读操作其实就是读SD卡的内容,当读有效时,把spi通信接收到的data_recv发送到主机。

吐血。。。把sd_card_cmd的思路讲清楚了,细节上的理解需要你们自己去写代码,然后才能够透彻地理解清楚。

接下来我们来看看另一个文件:sd_card_sec_read_write.v

sd_card_sec_read_write.v

这里无非就是一些命令,我们来看看。

鉴于篇幅太长,输入输出以及寄存器的代码还有RESET的代码我就不发上来了。直接进入状态机:

case(state)
			S_IDLE:
			begin
				state <= S_CMD0;
				sd_init_done <= 1'b0;
				spi_clk_div <= SPI_LOW_SPEED_DIV[15:0];
			end

空闲状态下,发送CMD0,

S_CMD0:
			begin
				if(cmd_req_ack & ~cmd_req_error)
				begin
					state <= S_CMD8;
					cmd_req <= 1'b0;
				end
				else
				begin
					cmd_req <= 1'b1;
					cmd_data_len <= 16'd0;
					cmd_r1 <= 8'h01;
					cmd <= {8'd0,8'h00,8'h00,8'h00,8'h00,8'h95};
				end

大家不要意外,8'd0,8'h00,8'h00,8'h00,8'h00,8'h95是CMD0的序列,CMD0时,发送命令请求,且没有返回长度(这在前面的CMD8时详细讲过,r1的返回值应该是0x01。没有错误时,进入CMD8.

后面的CMDx大同小异,我就忽略带过,接着随着CMD8进入CMD55,CMD41,CMD16。具体为什么请看上面的图片,初始化时的步骤。这些步骤就是为了确定SD卡的型号。发送指令看它返回什么。

接着

S_WAIT_READ_WRITE:
			begin
				if(sd_sec_write ==  1'b1)
				begin
					state <= S_CMD24;
					sec_addr <= sd_sec_write_addr;
				end
				else if(sd_sec_read == 1'b1)
				begin
					state <= S_CMD17;
					sec_addr <= sd_sec_read_addr;
				end

				spi_clk_div <= 16'd0;
			end

然后就是写和读的等待命令。当写时,发送地址,同时进入CMD24,读时,发送地址,进入CMD17

S_CMD24:
			begin
				if(cmd_req_ack & ~cmd_req_error)
				begin
					state <= S_WRITE;
					cmd_req <= 1'b0;
				end
				else
				begin
					cmd_req <= 1'b1;
					cmd_data_len <= 16'd0;
					cmd_r1 <= 8'h00;
					cmd <= {8'd24,sec_addr,8'hff};

当进入CMD24时,发送cmd:命令加地址加0xff,然后进入S_WRITE,真正开始写了。

S_WRITE:
			begin
				if(block_write_req_ack == 1'b1)
				begin
					block_write_req <= 1'b0;
					state <= S_WRITE_END;
				end
				else
					block_write_req <= 1'b1;
			end

这里只是给了给寄存器赋了1,让它执行命令,具体的写前面已经讲过了。

然后就是读了,一样,CMD17和CMD24有异曲同工之处,然后同样读也是发送一个读指令。

S_CMD17:
			begin
				if(cmd_req_ack & ~cmd_req_error)
				begin
					state <= S_READ;
					cmd_req <= 1'b0;
				end
				else
				begin
					cmd_req <= 1'b1;
					cmd_data_len <= 16'd0;
					cmd_r1 <= 8'h00;
					cmd <= {8'd17,sec_addr,8'hff};
				end
			end
			S_READ:
			begin
				if(block_read_req_ack)
				begin
					state <= S_READ_END;
					block_read_req <= 1'b0;
				end
				else
				begin
					block_read_req <= 1'b1;
				end
			end

然后就结束了,进入等待读写状态。

S_WRITE_END:
			begin
				state <= S_WAIT_READ_WRITE;
			end
			S_READ_END:
			begin
				state <= S_WAIT_READ_WRITE;
			end
			default:
				state <= S_IDLE;

天呐。终于讲完了。博主本来也是半懂的状态开始写,写着写着思路越来越清晰,现在自己也完全理清楚了。希望能帮到大家。也期待自己的博文一篇篇变多,然后成为大佬哈哈哈哈。

猜你喜欢

转载自blog.csdn.net/weixin_41892263/article/details/83039174