基于FPGA的QSPI底层驱动代码实现

QSPI简介

相信各位优秀的工程师们对SPI协议已经是非常了解了,SPI全名为串行外围设备接口(Serial Peripheral Interface),是一种高速全双工的同步通信总线,广泛应用于设备间的通讯传输。
而本文所要讲的QSPI,为SPI接口的扩展,Q代表quad即4倍传输的意思,也称为四线制SPI,因此该接口的传输速率将远远快于标准的SPI,其广泛应用于SPI Flash存储介质。下面本文将通过一个Flash芯片的Datasheet,来详细的描述该如何利用FPGA实现QSPI的通信。

写时序

QSPI写时序图
由时序图可以看到,图中总共有6个信号,从上至下分别为CE(片选信号)、CLK(时钟信号)、SIO0–SIO34根数据线。其中与SPI接口相似的是片选和时钟信号不变,在读写数据时片选信号均为低电平,在采样或发送数据时均在时钟的上升沿或者下降沿。唯一的区别在数据线由原来的MOSI、MISO变成了4根数据线,那么我们该如何将这四根数据线应用起来呢?由图中可知,SIO0会发送命令、地址以及数据,而SIO1–SIO3则只发送命令和数据,再进一步观察可以看到,写命令为0x38,它由8个CLK发送完成,地址信号总共24bit,由4跟数据线在6个CLK内同时完成发送,且每根数据线所发送的起始比特位都不同,最后便是发送数据,同理也由4根数据线同时进行发送,1个CLK发送4bit数据,发送的大小可由用户自己设定。总结一下,QSPI通信写的流程可以概括为先发送一个字节的命令字(这个命令字对于不同的芯片是不一样的),再是发送3个字节的地址(同理),最后才是发送数据。因此在FPGA的设计上就有思路了,最简单的方法就是采用状态机来描述这一过程,具体代码将在下面展示。

读时序

QSPI读时序图
由图可知,读时序的操作流程与写时序大同小异,只是命令字由0x38变成了0xEB,其余操作流程均与写时序相同,因此不再进行详细阐述。
但需要注意的是,由SPI扩展为QSPI,它已经不是全双工通信了,而是变成了半双工。SIO0–SIO3 4根线将变成三态门,也就是FPGA中的inout接口,需要满足特定的条件才能输入或者输出数据。
下面将给出QSPI通信的底层驱动代码,在实际工程应用中,还需要结合芯片的数据手册来编写应用层的程序,再结合底层的逻辑来实现特定的功能,例如利用QSPI或者SPI接口对某个Flash芯片进行读写。

QSPI实现的Verilog代码

module QSPI_DRIVE #(
		parameter DIV = 3
)(
	input wire clk,
	input wire rst,
	//--------应用层传输进该模块的命令、地址、数据等--------//
	input wire [3:0] i_cmd_mode,
    input wire [7:0] i_flash_cmd,
    input wire [23:0] i_addr,
    input wire [7:0] i_data,
    input wire [15:0] i_data_num,
    input wire i_wr,
 	output reg [7:0] o_data,
 	//---------QSPI 接口---------//
	output reg qspi_cs,
	output reg qspi_csk,
	inout   reg qspi_sio0,
	inout   reg qspi_sio1,
	inout   reg qspi_sio2,
	inout   reg qspi_sio3
);

reg [7:0] div_cnt;
reg [7:0] cmd_cnt;
reg [7:0] addr_cnt;
reg [7:0] data_cnt;
reg [15:0] num_cnt;
reg [3:0] cmd_mode_lock;
reg [7:0] flash_cmd_lock;
reg [23:0] addr_lock;
reg [7:0] r_data_temp;
reg  qspi_sckd0;
wire qspi_sck_p,qspi_sck_n;

//---------------FSM---------------//
reg [7:0] state,n_state;
localparam  IDLE  = 8'h00,
			START = 8'h01,
			CMD   = 8'h02,
			ADDR  = 8'h04,
			DATA  = 8'h08,
			STOP  = 8'h10;
always@(posedge clk)begin
	if(rst)
		state <= IDLE;
	else
		state <= n_state;
end

always@(*)begin
	if(rst)begin
		n_state = IDLE;
	end else begin
		case(state)
			IDLE : begin
				if(i_cmd_mode[3])
					 n_state = START;
				else
					 n_state = IDLE;
		   	end
		   	START : begin
				 n_state = CMD;
			end
			CMD : begin
			 if(cmd_cnt == 8'd15)
				 if(cmd_mode_lock[1])begin
					  n_state = ADDR;
			 	 end else if(cmd_mode_lock[0])begin
					  n_state = DATA;		
				 end else begin
					  n_state = STOP;
				 end
			 else
			 	n_state = CMD;
			end
			ADDR : begin
				if(addr_cnt == 8'd12)
					if(cmd_mode_lock[0])begin
						n_state = DATA;
					end else begin
						n_state =STOP;
					end
				else
					n_state = ADDR;
			end
			DATA : begin
			 if(data_cnt == 8'd4)
				 if(cmd_mode_lock[2] && (num_cnt == 16'b0))begin
					  n_state = STOP;
			 	 end else if(!cmd_mode_lock[2])begin
					  n_state = STOP;		
				 end else begin
					  n_state = DATA;
				 end
			 else
			 	n_state = DATA;
			end	
			STOP : begin
				n_state = IDLE;
			end
			default : begin
				n_state = IDLE;
			end
		endcase
	end
end

//----------锁数据-----------//
always@(posedge clk)begin
	if(rst)begin
		cmd_mode_lock <= 4'b0;
		flash_cmd_lock <= 8'b0;
		addr_lock <= 24'b0;
	end else if(i_cmd_mode[3] && (state == IDLE))begin
		cmd_mode_lock <= i_cmd_mode;
		flash_cmd_lock <= i_flash_cmd;
		addr_lock <= i_addr;		
	end else begin
		cmd_mode_lock <= cmd_mode_lock ;
		flash_cmd_lock <= flash_cmd_lock ;
		addr_lock <= addr_lock ;
	end
end

//-----------各个功能计数器计数---------//
always@(posedge clk)begin//时钟分频,DIV为分频系数
	if(rst)
		div_cnt <= 8'h00;
	else if(div_cnt == DIV)
		div_cnt <= 8'h00;
	else if((state == CMD) || (state == ADDR) || (state == DATA ))
		div_cnt <= div_cnt + 1'b1;
	else
		div_cnt <= 8'h00;
end

always@(posedge clk)begin//命令字计数
	if(rst)
		cmd_cnt <= 8'h00;
	else if((state == CMD) && (div_cnt == DIV))
		cmd_cnt <= cmd_cnt + 1'b1;
	else if(state == CMD)
		cmd_cnt <= cmd_cnt;
	else
		cmd_cnt <= 8'h00;
end

always@(posedge clk)begin//地址计数
	if(rst)
		addr_cnt <= 8'h00;
	else if((state == ADDR) && (div_cnt == DIV))
		addr_cnt  <= addr_cnt + 1'b1;
	else if(state == ADDR)
		addr_cnt <= addr_cnt ;
	else
		addr_cnt <= 8'h00;
end

always@(posedge clk)begin//数据计数,在sck上升沿和下降沿均会加1
	if(rst)
		data_cnt <= 8'h00;
	else if((state == DATA) && cmd_mode_lock[1] &&  (data_cnt == 8'd4))
		data_cnt <= 8'h00;
	else if((state == DATA) && (qspi_sck_p || qspi_sck_n))
		data_cnt <= data_cnt + 1'b1;
	else if(state == DATA)
		data_cnt <=data_cnt;
	else
		data_cnt <= 8'h00; 
end

always@(posedge clk)begin//传输的数据长度计数,传输完成后num为0
	if(rst)
		num_cnt <= 16'h00;
	else if((state == IDLE) && i_cmd_mode[3])
		num_cnt <= i_data_num;
	else if((cmd_mode_lock[3] && (div_cnt == DIV) &&  (data_cnt == 8'd3))
		num_cnt <= num_cnt - 1'b1;
	else 
		num_cnt <=num_cnt ;
end

//-------------QSPI数据采样及发送--------------//
always@(posedge clk)begin//产生片选信号
	if(rst)
		qspi_cs <=1'b1;
	else if(state == START)
		qspi_cs <=1'b0;
	else if(state == STOP)
		qspi_cs <=1'b1;
	else
		qspi_cs <=qspi_cs ;		
end

always@(posedge clk)begin//产生qspi采样时钟
	if(rst)
		qspi_sck <=1'b0;
	else if((state == CMD) || (state == ADDR) || (state == DATA) && (div_cnt == DIV))
		qspi_sck <=!qspi_sck ;
	else if((state == CMD) || (state == ADDR) || (state == DATA))
		qspi_sck <=qspi_sck ;
	else
		qspi_sck <=1'b0;		
end

always@(posedge clk)begin
	if(rst)
		qspi_sckd0 <= 1'b1;
	else
		qspi_sckd0  <= qspi_sck;
end
assign qspi_sck_n = (qspi_sckd0 && (!qspi_sck)) ? 1'b1 : 1'b0;//取sck下降沿
assign qspi_sck_p = ((!qspi_sckd0) && qspi_sck) ? 1'b1 : 1'b0;//取sck上升沿

always@(posedge clk)begin//sio0数据线传输命令字、地址以及数据
	if(rst)
		qspi_sio0_temp <=1'b0;
	else if((state == START) || (state == ADDR) || (state == DATA) && (div_cnt == DIV))
		qspi_sio0_temp <=i_flash_cmd[7];
	else if(qspi_sck_n)begin
		if(state == CMD)
			qspi_sio0_temp <=flash_cmd_lock[7 - (cmd_cnt>>1)];
		else if(state == ADDR)
			qspi_sio0_temp <= addr_lock[20 - (addr_cnt<<1)];
		else if(state == DATA)
			qspi_sio0_temp <= i_data[4 - (data_cnt<<1)]; 
		else
			qspi_sio0_temp <= qspi_sio0_temp ;
	end else
		qspi_sio0_temp <= qspi_sio0_temp ;
end

always@(posedge clk)begin//sio1数据线传输地址以及数据
	if(rst)
		qspi_sio1_temp <=1'b0;
	else if(qspi_sck_n)begin
	    if(state == ADDR)
			qspi_sio1_temp <= addr_lock[21 - (addr_cnt<<1)];
		else if(state == DATA)
			qspi_sio1_temp <= i_data[5 - (data_cnt<<1)]; 
		else
			qspi_sio1_temp <= qspi_sio1_temp ;
	end else
		qspi_sio1_temp <= qspi_sio1_temp ;
end

always@(posedge clk)begin//sio2数据线传输地址以及数据
	if(rst)
		qspi_sio2_temp <=1'b0;
	else if(qspi_sck_n)begin
	    if(state == ADDR)
			qspi_sio2_temp <= addr_lock[22 - (addr_cnt<<1)];
		else if(state == DATA)
			qspi_sio2_temp <= i_data[6 - (data_cnt<<1)]; 
		else
			qspi_sio2_temp <= qspi_sio2_temp ;
	end else
		qspi_sio2_temp <= qspi_sio2_temp ;
end

always@(posedge clk)begin//sio3数据线传输地址以及数据
	if(rst)
		qspi_sio3_temp <=1'b0;
	else if(qspi_sck_n)begin
	    if(state == ADDR)
			qspi_sio3_temp <= addr_lock[23 - (addr_cnt<<1)];
		else if(state == DATA)
			qspi_sio3_temp <= i_data[7 - (data_cnt<<1)]; 
		else
			qspi_sio3_temp <= qspi_sio3_temp ;
	end else
		qspi_sio3_temp <= qspi_sio3_temp ;
end

reg qspi_sio0_temp;//由于是三态门,需要定义中间变量
reg qspi_sio1_temp;
reg qspi_sio2_temp;
reg qspi_sio3_temp;
//在各状态下赋相对应的值,在写数据的时候i_wr信号为高,读时为低
assign qspi_sio0 = (state == CMD || state == ADDR) ? qspi_sio0_temp : (i_wr) ? qspi_sio0_temp : 1'bz;
assign qspi_sio1 = (state == ADDR) ? qspi_sio1_temp: (i_wr) ? qspi_sio1_temp: 1'bz;
assign qspi_sio2 = (state == ADDR) ? qspi_sio2_temp: (i_wr) ? qspi_sio2_temp: 1'bz;
assign qspi_sio3 = (state == ADDR) ? qspi_sio3_temp: (i_wr) ? qspi_sio3_temp: 1'bz;

always@(posedge clk)begin//QSPI发送数据,将数据线上的数据移位至r_data_temp寄存器
	if(rst)begin
		r_data_temp <= 8'b0;
	end else if(qspi_sck_p && (state == DATA))begin
			r_data_temp[7 - (data_cnt-1)<<1] <= qspi_sio3 ;
			r_data_temp[6 - (data_cnt-1)<<1] <= qspi_sio2 ;
			r_data_temp[5 - (data_cnt-1)<<1] <= qspi_sio1 ;
			r_data_temp[4 - (data_cnt-1)<<1] <= qspi_sio0 ;
	end else begin
		r_data_temp <= r_data_temp;
	end
end

always@(posedge clk)begin//将移位寄存器中的数据输出
	if(rst)
		o_data <= 8'b0;
	else if(data_cnt == 8'd4)
		o_data <= r_data_temp;
	else
		o_data  <= o_data;
end

endmodule

仿真波形图

请添加图片描述
请添加图片描述
波形图中命令字0xEB代表读操作,0x38代表写操作,0x06代表写使能命令,本次所写入的数据为0x01、0x12、0x23依次按照顺序至0xF0,由图中可以看出写入的数据与读出的数据是一致的,表明QSPI通讯功能正常。经过实测,本文的QSPI速率可达到75MHz。(FPGA时钟频率为150MHz)。

总结

综上,其实可以看出底层的QSPI代码与SPI代码的编写思路都是相似的,主要的区别就是写命令字、写地址以及4根数据线的数据采集。最关键的其实还是要结合实际的应用来进行编写,才能实现特定的功能!

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
FPGA的QSPI代码可以用于访问外部的QSPI闪存器件。下面是一个示例代码,用于在FPGA实现读取和写入QSPI闪存的功能: ```verilog module QSPI_Controller ( input wire clk, input wire rstn, input wire [1:0] spi_cs, input wire spi_sclk, inout wire spi_io[3:0], output wire spi_io_ready, input wire spi_io_valid, output wire [7:0] spi_io_data ); // 内部寄存器定义 reg [7:0] read_data_reg; reg [7:0] write_data_reg; reg [31:0] address_reg; reg [2:0] state; // 状态机定义 localparam IDLE = 3'b000; localparam READ = 3'b001; localparam WRITE = 3'b010; // 初始化状态机 always @(posedge clk or negedge rstn) begin if (!rstn) begin state <= IDLE; end else begin case (state) IDLE: if (spi_io_valid) begin if (spi_io[3:2] == 2'b01) begin state <= READ; // 如果接收到读命令,则转入读状态 end else if (spi_io[3:2] == 2'b10) begin state <= WRITE; // 如果接收到写命令,则转入写状态 end end READ: state <= IDLE; // 读操作完成后返回空闲状态 WRITE: state <= IDLE; // 写操作完成后返回空闲状态 endcase end end // 读写操作 always @(posedge clk or negedge rstn) begin if (!rstn) begin read_data_reg <= 8'h00; write_data_reg <= 8'h00; address_reg <= 32'h00000000; end else begin case (state) IDLE: if (spi_io_valid) begin address_reg <= spi_io_data[31:0]; end READ: read_data_reg <= spi_io_data; WRITE: write_data_reg <= spi_io_data; endcase end end // QSPI闪存模块接口 QSPI_Flash_Controller qspi_flash ( .clk(clk), .rstn(rstn), .spi_cs(spi_cs), .spi_sclk(spi_sclk), .spi_io(spi_io), .spi_io_ready(spi_io_ready), .spi_io_valid(spi_io_valid), .spi_io_data(spi_io_data), .read_data(read_data_reg), .write_data(write_data_reg), .address(address_reg) ); endmodule ``` 以上代码是一个简单的QSPI控制器,其中包含状态机和读写操作。通过QSPI_Flash_Controller模块与实际的QSPI闪存器件进行通信。请注意,这只是一个示例代码,具体实现可能会根据实际的FPGA和QSPI闪存器件进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值