FPGA 기반의 QSPI 기본 드라이버 코드 구현

QSPI 소개

뛰어난 엔지니어들은 모두 이미 SPI 프로토콜에 대해 매우 잘 알고 있다고 생각합니다. SPI의 전체 이름은 Serial Peripheral Interface입니다. 장치 간 통신 전송에 널리 사용되는 고속, 전이중 동기 통신 버스입니다. .
이번 글에서 다루게 될 QSPI는 SPI 인터페이스의 확장으로, Q는 쿼드(quad)의 약자로 전송 속도가 4배라는 의미로 4와이어 SPI라고도 불린다.따라서 이 인터페이스의 전송 속도는 다음과 같다. 표준 SPI보다 훨씬 빠르며 SPI 플래시 저장 매체에 널리 사용됩니다. 다음 기사에서는 플래시 칩 데이터시트를 사용하여 FPGA를 사용하여 QSPI 통신을 구현하는 방법을 자세히 설명합니다.

쓰기 타이밍

QSPI 쓰기 타이밍 다이어그램
타이밍 다이어그램에서 볼 수 있듯이 다이어그램에는 위에서 아래로 CE(칩 선택 신호), CLK(클럭 신호), SIO0~SIO34 데이터 라인 등 총 6개의 신호가 있습니다. SPI 인터페이스와 유사하게 칩 선택 및 클록 신호는 변경되지 않습니다.데이터를 읽고 쓸 때 칩 선택 신호는 모두 로우 레벨이고 데이터를 샘플링하거나 보낼 때 클록의 상승 에지 또는 하강 에지에 있습니다. 유일한 차이점은 데이터 라인이 원래 MOSI 및 MISO에서 4개의 데이터 라인으로 변경되었다는 것인데, 이 4개의 데이터 라인을 어떻게 적용합니까? 그림에서 볼 수 있듯이 SIO0은 명령, 주소 및 데이터를 보내는 반면 SIO1~SIO3은 명령과 데이터만 보냅니다. 자세히 관찰하면 쓰기 명령은 0x38이고 8개의 CLK에서 전송되며 주소 신호 합계는 다음과 같습니다. 24비트. 6개의 CLK 내에서 4개의 데이터 라인에 의해 동시에 전송이 완료되며, 각 데이터 라인이 보내는 시작 비트가 서로 다르며, 최종적으로 데이터가 전송된다. 마찬가지로 4개의 데이터 라인이 동시에 전송되고, 1 CLK 4비트 데이터를 전송합니다. 전송되는 크기는 사용자가 설정할 수 있습니다. 요약하자면, QSPI 통신 쓰기 프로세스는 먼저 1바이트 명령 단어를 전송하고(이 명령 단어는 칩마다 다름) 3바이트 주소를 전송하고(동일 원리) 마지막으로 데이터를 전송하는 것으로 요약할 수 있습니다. 따라서 FPGA 설계에는 아이디어가 있으며 가장 간단한 방법은 상태 머신을 사용하여 이 프로세스를 설명하는 것입니다. 구체적인 코드는 아래에 나와 있습니다.

읽기 타이밍

QSPI 읽기 타이밍 다이어그램
그림에서 알 수 있듯이, 읽기 타이밍의 동작 흐름은 명령 워드가 0x38에서 0xEB로 변경된 것을 제외하면 쓰기 타이밍과 유사하며, 나머지 동작 흐름은 쓰기 타이밍과 동일하며, 그래서 자세히 설명하지 않겠습니다.
그러나 SPI가 QSPI로 확장되면 더 이상 전이중 통신이 아니라 반이중 통신이 된다는 점에 유의해야 합니다. 4개의 라인 SIO0~SIO3은 FPGA의 입력 인터페이스인 3상태 게이트가 되며, 데이터를 입력하거나 출력하려면 특정 조건을 충족해야 합니다.
다음은 QSPI 통신을 위한 기본 드라이버 코드를 제공합니다. 실제 엔지니어링 애플리케이션에서는 칩의 데이터 매뉴얼과 함께 애플리케이션 계층 프로그램을 작성한 다음 이를 기본 로직과 결합하여 다음과 같은 특정 기능을 구현해야 합니다. 플래시 칩에 대한 QSPI 또는 SPI 인터페이스를 읽고 씁니다.

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은 쓰기 활성화 명령을 나타내며, 이번에 쓰여진 데이터는 0xF0부터 순차적으로 0x01, 0x12, 0x23 을 보면 알 수 있다. 작성된 데이터가 읽은 데이터와 일치한다는 그림은 QSPI 통신 기능이 정상임을 나타냅니다. 실제 테스트 후 이 기사의 QSPI 속도는 75MHz에 도달할 수 있습니다. (FPGA 클록 주파수는 150MHz입니다.)

요약하다

정리하자면, 기본 QSPI 코드와 SPI 코드는 비슷한 쓰기 아이디어를 가지고 있음을 알 수 있으며, 주요 차이점은 명령어 쓰기, 주소 쓰기, 4개 데이터 라인의 데이터 수집입니다. 가장 중요한 것은 특정 기능을 구현하기 위해 실제 애플리케이션과 연계하여 작성하는 것입니다!

추천

출처blog.csdn.net/m0_51575600/article/details/132693943