FPGA学习之SPI驱动的FLASH存储芯片(W25Q64)

FLASH存储芯片(W25Q64)

背景

最近一直都在出差,断断续续抽了一周的时间才调通了FLASH存储芯片。在SPI通讯代码没问题的情况下,整体FLASH芯片驱动的代码编写以及调试难度属于中等,其中主要困难点在于W25Q64芯片的资料手册理解。我即使是到现在也仍旧还有一个疑问尚未解决,还希望大神解答。

代码功能介绍

功能实现flash扇区擦除、flash单byte读、flash单byte写:
1、FLASH擦除–完成FLASH指定地址扇区擦除
2、FLASH写入–完成FLASH指定地址的数据写入
3、FLASH读出–完成FLASH指定地址的数据读取

同时,为了方便观察代码调试,我们使用了VIO(Virtual INPUT OUTPUT)IP核以及ILA IP核参与调试。

软硬件配置

FLASH芯片:W25Q64
FPGA芯片:XC7A35T
编程工具:Vivado 2018.3
调试工具:串口调试助手软件

具体代码

flash_operation_top模块

/
// 
// 			flash操作顶层
//			功能实现flash扇区擦除、flash单byte读、flash单byte写
//			FLASH擦除--读状态寄存器、发送擦除指令+24位地址、读状态寄存器确认完成
//			FLASH编程--读状态寄存器、发送写使能指令、发送编程指令+24位地址数据+需写入数据、读状态寄存器
//			FLASH读出--读状态寄存器、发送读指令+24位地址数据	


module flash_operation_top(
	input						clk,
	input						rst_n,
	input						flash_start,
	input	[7:0]				flash_order,																		//flash需完成动作指令
	input	[7:0]				need_write_data,																	//需要保存的数据
	input	[23:0]				flash_address,																		//数据地址
	input						miso,																	
	output						sclk,																	
	output						cs,																	
	output						mosi,																	
	output	reg		[7:0]		read_flash_data,																	
	output	reg					full_order_done																		//flash操作完毕
    );
	
//
// 
// 					 		操作指令集
//					
//
	
	parameter 	WRITE_ORDER				=	8'h02,
				READ_ORDER				=	8'h03,
				BLOCK_ERASE_ORDER		=	8'hD8,															//目前代码并未匹配块擦除指令
				SECTOR_ERASE_ORDER		=	8'h20,															
				WRITE_ENABLE_ORDER		= 	8'h06,
				READ_REGISTER_ORDER		=	8'h05;

//
// 
// 					 		检测SPI发送与接收完成
//
//
	
	wire 	spi_tx_done;
	wire	spi_rx_done;
	wire 	spi_tx_done_pos;
	wire 	spi_rx_done_pos;
	reg		spi_tx_done_0;
	reg		spi_tx_done_1;
	reg		spi_rx_done_0;
	reg		spi_rx_done_1;
	
	assign spi_rx_done_pos = spi_rx_done_0 && !spi_rx_done_1;
	assign spi_tx_done_pos = spi_tx_done_0 && !spi_tx_done_1;
	
	always @(posedge clk, negedge rst_n) begin
		if(!rst_n) begin
			spi_tx_done_0 <= 'd0;
			spi_tx_done_1 <= 'd0;
			spi_rx_done_0 <= 'd0;
			spi_rx_done_1 <= 'd0;
		end else begin
			spi_tx_done_0 <= spi_tx_done;
			spi_tx_done_1 <= spi_tx_done_0;
			spi_rx_done_0 <= spi_rx_done;
			spi_rx_done_1 <= spi_rx_done_0;
		end
	end

//
// 
// 					 		启动FLASH操作
//
//	
	
	reg START;
	
	always @(posedge clk, negedge rst_n) begin
		if(!rst_n) begin
			START <= 1'd0;
		end else begin
			if(flash_start) begin
				START <= 1'd1;
			end else begin
				if(full_order_done) begin
					START <= 1'd0;
				end
			end
		end
	end
	
	
	
//
// 
// 					 		指令发送状态机
//					
//
	
	parameter	IDLE 			= 	4'd0,																		//该状态下读取写保护位(S1)以及忙位(S0)
				WRITE_ENABLE	=	4'd1,																		//进行写使能
				ERASE_STATE		=	4'd2,																		//进行擦除操作--需先写使能
				WRITE_STATE		=	4'd3,																		//进行数据写入--需先写使能、擦除再写使能
				READ_STATE		=	4'd4;																		//读指令无需写使能
											
	reg [3:0] 	current_state;									
	reg 		WEL_FLAG;																						//写保护位
	reg 		BUSY_FLAG;																						//忙标志位
	reg 		read_state_done;																				//读取状态寄存器完毕标志位
	reg			flash_en;																						//FLASH使能
	reg			done_tx_order;																					//已完成flash指令以及地址发送
	reg			done_tx_flash;																					//仅完成flash指令发送
	reg 		operation_done;																					//子指令完成
	reg			ready_to_write;																					//已完成擦除,并再次进入写使能,可开始写数据
										
	always @(posedge clk, negedge rst_n) begin									
		if(!rst_n) begin									
			current_state <= IDLE;									
		end else begin									
			case(current_state)									
				IDLE		: begin									
					if(BUSY_FLAG && read_state_done) begin									
						current_state <= IDLE;																	//当处于忙时,只能执行读状态器指令
					end else begin
						if(read_state_done) begin
							if(flash_order == WRITE_ENABLE_ORDER) begin
								current_state <= WRITE_ENABLE;
							end else begin	
								if(WEL_FLAG) begin
									if(flash_order == READ_ORDER) begin
										current_state <= READ_STATE;
									end else begin
										if(flash_order == WRITE_ORDER || flash_order == SECTOR_ERASE_ORDER) begin
											if(ready_to_write) begin
												current_state <= WRITE_STATE;
											end else begin
												if(erase_done_flag && flash_order == WRITE_ORDER) begin		
													current_state <= WRITE_ENABLE;													
												end else begin
													if(flash_order == SECTOR_ERASE_ORDER && !done_enable) begin
														current_state <= WRITE_ENABLE;
													end else begin
														current_state <= ERASE_STATE;
													end
												end
											end	
										end
									end
								end else begin
									if(flash_order == READ_ORDER) begin
										current_state <= READ_STATE;
									end else begin
										current_state <= WRITE_ENABLE;
									end
								end
							end	
						end
					end
				end
				
				WRITE_ENABLE: begin																			//当写使能完成,需返回IDLE状态进行读状态寄存器
					if(done_tx_order) begin								
						current_state <= IDLE;								
					end								
				end								
												
				ERASE_STATE	: begin																			//当完成擦除,仍需返回IDLE状态进行读状态寄存器
					if(operation_done) begin								
						current_state <= IDLE;								
					end								
				end								
												
				WRITE_STATE	: begin								
					if(operation_done) begin								
						current_state <= IDLE;								
					end								
				end								
												
				READ_STATE: begin								
					if(operation_done) begin								
						current_state <= IDLE;								
					end								
				end								
												
				default: current_state <= IDLE;								
			endcase								
		end								
	end								
													
	wire 	[7:0] 	spi_rx_data;								
	reg 			start_read_flag;																		//可以开始读数据标志
	reg		[23:0]	TEM_flash_address;																		//方便地址发送
	reg		[2:0]	address_send_cnter;																		//发送地址计数器
	reg		[7:0]	spi_flash_order;																		//待发送flash命令
	reg				erase_done_flag;																		//擦除完成标志位
	reg				done_enable;													
	always @(posedge clk, negedge rst_n) begin													
		if(!rst_n) begin													
			WEL_FLAG 				<= 1'd0;																//掉电后、执行完指令
			BUSY_FLAG 				<= 1'd0;																
			read_state_done 		<= 1'd0;								
			start_read_flag 		<= 1'd0;								
			flash_en 				<= 1'b0;								
			TEM_flash_address 		<= 24'd0;								
			address_send_cnter 		<= 3'd0;								
			done_tx_order 			<= 'b0;								
			done_tx_flash			<= 'b0;								
			spi_flash_order			<= 'd0;								
			operation_done			<= 'd0;								
			erase_done_flag			<= 'd0;								
			full_order_done			<= 'd0;																	//外部需操作指令完成
			ready_to_write			<= 'd0;								
			done_enable				<= 1'b0;																//完成写使能指令
		end else begin								
			case(current_state)								
				IDLE		: begin								
					done_tx_flash 	<= 1'b0;								
					operation_done 	<= 'd0;								
					spi_flash_order <= 'd0;								
					full_order_done	<= 'd0;									
					spi_flash_order <= READ_REGISTER_ORDER;								
					if(START) begin								
						flash_en <= 1'b1;								
						if(spi_rx_done_pos) begin													
							start_read_flag <= 1'd1;														//可以开始读取flash返回数据
						end else begin								
							if(receive_1byte_flag) begin
								BUSY_FLAG 		<= flash_return_data[0];
								WEL_FLAG  		<= flash_return_data[1];
								start_read_flag <= 1'd0;
								flash_en 		<= 1'b0;
								read_state_done <= 1'b1;
							end else begin
								read_state_done <= 1'b0;
							end
						end
					end else begin
						flash_en <= 1'b0;
					end
				end
				
				WRITE_ENABLE: begin
					flash_en 			<= 1'b1;
					operation_done 		<= 1'b0;
					done_tx_order 		<= 1'b0;
					spi_flash_order 	<= WRITE_ENABLE_ORDER;
					read_state_done 	<= 1'b0;
					if(spi_rx_done_pos) begin
						flash_en 		<= 1'b0;
						done_tx_order 	<= 1'b1;
						done_enable		<= 1'b1;															//已经完成写使能
						if(erase_done_flag) begin															//已经完成过擦除、可进行写操作
							ready_to_write <= 'd1;								
						end 								
					end
				end
				
				ERASE_STATE	: begin
					done_tx_order 	<= 1'b0;
					read_state_done <= 1'b0;
					if(spi_rx_done_pos) begin
						full_order_done <= 'd0;
						done_tx_flash 	<= 1'b1;															//当指令发送完成后,即发送地址
						erase_done_flag <= 'd0;																//已完成擦除标志位(当FLASH已完成擦除,write指令即可开始执行)
						if(address_send_cnter <= 2) begin
							spi_flash_order 	<= TEM_flash_address[23:16];								//还需发送24位地址
							address_send_cnter 	<= address_send_cnter + 1'b1;
							TEM_flash_address 	<= {TEM_flash_address[15:0],TEM_flash_address[23:16]};
						end else begin
							address_send_cnter 	<= 'b0;
							done_tx_order 		<= 'b1;
							operation_done 		<= 'd1;
							flash_en 			<= 1'b0;
							done_tx_flash 		<= 1'b0;
							done_enable			<= 1'b0;
							if(flash_order 	== SECTOR_ERASE_ORDER) begin
								full_order_done	<= 'd1;	
								erase_done_flag <= 'd0;														//此处的erase_done_flag仅用于页编程指令的后续状态跳转
							end else begin
								full_order_done	<= 'd0;	
								erase_done_flag <= 'd1;
							end
						end
					end	else begin
						if(!done_tx_flash) begin
							TEM_flash_address 	<= flash_address;
							full_order_done		<= 'd0;	
							if(operation_done != 'd1) begin													//由于状态跳转之前还会再次打一拍,故需对其进行处理
								flash_en <= 1'b1;
								spi_flash_order <= SECTOR_ERASE_ORDER;
							end	
						end
					end 
				end
				
				WRITE_STATE	: begin
					done_tx_order 	<= 'b0;
					erase_done_flag <= 'd0;
					read_state_done <= 1'b0;
					ready_to_write  <= 'd0;
					done_enable		<= 1'b0;
					if(spi_rx_done_pos) begin
						full_order_done	<= 'd0;	
						done_tx_flash <= 1'b1;
						if(address_send_cnter <= 'd2) begin
							spi_flash_order 	<= TEM_flash_address[23:16];								//还需发送24位地址
							address_send_cnter 	<= address_send_cnter + 1'b1;
							TEM_flash_address 	<= {TEM_flash_address[15:0],TEM_flash_address[23:16]};
						end else begin
							if(address_send_cnter == 'd3) begin												//由于编程指令还需发送需要写的数据故多计数一次
								spi_flash_order 	<= need_write_data;										//后续可在此处进行修改使得模块能连续写
								address_send_cnter 	<= address_send_cnter + 1'b1;
							end else begin
								flash_en 			<= 1'b0;
								operation_done 		<= 'd1;
								done_tx_order 		<= 'b1;
								address_send_cnter 	<= 1'b0;
								done_tx_flash 		<= 1'b0;
								if(flash_order 	== WRITE_ORDER) begin
									full_order_done	<= 'd1;	
								end else begin
									full_order_done	<= 'd0;	
								end
							end
						end
					end else begin
						if(!done_tx_flash) begin
							TEM_flash_address 	<= flash_address;
							full_order_done		<= 'd0;	
							if(full_order_done != 'd1) begin
								flash_en 		<= 1'b1;
								spi_flash_order <= WRITE_ORDER;
							end
						end
					end
				end
				
				READ_STATE: begin
					flash_en 		<= 1'b1;
					done_tx_order	<= 'b0;
					read_state_done <= 1'b0;
					if(spi_rx_done_pos) begin									
						done_tx_flash <= 1'b1;
						if(address_send_cnter <= 'd2) begin
							spi_flash_order 	<= TEM_flash_address[23:16];								//还需发送24位地址
							address_send_cnter 	<= address_send_cnter + 1'b1;
							TEM_flash_address 	<= {TEM_flash_address[15:0],TEM_flash_address[23:16]};
						end else begin
							spi_flash_order <= 8'b0;							
							start_read_flag <= 'd1;
						end
					end else begin
						if(!done_tx_flash) begin
							TEM_flash_address 	<= flash_address;
							full_order_done		<= 'd0;	
							spi_flash_order 	<= READ_ORDER;
						end else begin
							if(receive_1byte_flag) begin													//后续可在此处进行修改使得模块能连续读
								flash_en 			<= 1'b0;
								operation_done 		<= 'd1;
								done_tx_order 		<= 'b1;
								address_send_cnter 	<= 'b0;
								start_read_flag 	<= 'd0;
								done_tx_flash 		<= 1'b0;
								if(flash_order 	== READ_ORDER) begin
									full_order_done	<= 'd1;	
								end else begin
									full_order_done	<= 'd0;	
								end
							end	
						end
					end
				end	
			endcase
		end
	end

//
// 
// 					 		读FLASH返回数据
//		IDLE状态中读取状态寄存器的数据、读状态中读取flash存储数据
//					
//	
	
	reg					receive_1byte_flag;																//接收到flash返回的1字节数据
	reg		[7:0] 		flash_return_data;																//命令发送完毕,等待flash返回数据
	always @(posedge clk, negedge rst_n) begin
		if(!rst_n) begin
			flash_return_data 	<= 'd0;
			receive_1byte_flag 	<= 'd0;
		end else begin
			if(start_read_flag) begin							
				if(spi_rx_done_pos) begin			
					flash_return_data 	<= spi_rx_data;
					receive_1byte_flag 	<= 'd1;
				end
			end else begin
				receive_1byte_flag 	<= 'd0;
			end
		end
	end
//
// 
// 					 		SPI通讯顶层
//						发送、接收FLASH数据
//					
//	
	
	spi_master flash_spi_inst(
		.clk			(clk),
		.rst_n			(rst_n),
		.tx_data		(spi_flash_order),
		.spi_en			(flash_en),
		.miso			(miso),
		.mosi			(mosi),
		.cs				(cs),
		.sclk			(sclk),
		.tx_done		(spi_tx_done),
		.rx_done		(spi_rx_done),
		.TEM_rx_data	(spi_rx_data)
    );
	
//
// 
// 					 		FLASH读取指令取出数据
//					
//		
	
	always @(posedge clk) begin
		if(flash_order == READ_ORDER && full_order_done) begin
			read_flash_data <= flash_return_data;
		end
	end

//
// 
// 					 		调试用
//					
//	
	
/* 	ila_0 ila_inst(
		.clk(clk),
		.probe0(current_state),
		.probe1({receive_1byte_flag,WEL_FLAG, BUSY_FLAG, start_read_flag, spi_rx_done_pos, operation_done, erase_done_flag}),
		.probe2({full_order_done, sclk, miso, mosi, flash_en, cs, spi_rx_done, read_state_done}),
		.probe3(read_flash_data),
		.probe4(spi_flash_order),
		.probe5(spi_rx_data)
); */
	
endmodule

代码总体难度不高,其实主要是芯片操作时序的理解。

状态寄存器(共8位–S7-S0)–指令为05H:
(S0)BUSY位(忙位)–当FLASH处于页编程、擦除、写状态寄存器等写指令时,此位为1表示FLASH不再处理除读状态寄存器指令之外的指令。
(S1)WEL位(写保护位)–在执行页编程、擦除指令之前写保护位都必须为1(进入写保护状态条件–此时WEL为0:1、FLASH上电;2、完成页编程、擦除指令之后)

写使能(06H)–在进行擦除、页编程指令之前都必须先进行写使能
写使能指令可以使写保护位置1

页编程(02H)–在进行页编程之前必须先进行擦除,由于W25Q64芯片数据只能由1变成0,故在写入数据之前必须先擦除,使得写入地址数据变为FF,即可开始正常写入数据。

FLASH写入流程(页编程02H)–读状态寄存器(05H)、写使能(06H)、扇区擦除(20H)+ 24位地址、写使能(06H)、读状态寄存器(05H)、页编程指令(02H)+ 24位地址 + 8位待写入数据

FLASH读取流程(读指令03H)–读状态寄存器(05H)、读指令(03H)+ 24位地址

FLASH擦除流程(扇区擦除20H)–读状态寄存器(05H)、扇区擦除(20H)+ 24位地址

注意:
1、若想连续写入数据,则在页编程指令后持续发送数据(页编程指令中最多一次送入256字节,即一页),片选持续选通。
2、若想连续读数据、则在读指令后片选持续选通。

Flash_top模块

`timescale 1ns / 1ps
//
// 		FLASH读写实验
// 		功能点1:接收串口下发数据并将其写入FLASH指定地址
// 		功能点2:读取指定FLASH地址的数据并将其通过串口发送
//      功能点3:对FLASH进行扇区擦除操作
//


module Flash_top(
	input			clk,
	input			rst_n,
	input			UART_RX,
	input			miso,
	output			cs,
	output			sclk,
	output			mosi,
	output			UART_TX
    );

//
// 
// 					 		操作指令集
//					
//
	
	parameter 	WRITE_ORDER				=	8'h02,
				READ_ORDER				=	8'h03,
				SECTOR_ERASE_ORDER		=	8'h20;

	parameter	default_address			=	24'h0000FF;
	
//
// 		
// 							读取串口下发数据
// 		    			
//	
	
	wire [7:0]Uart_TxData;										//读取FLASH数据并发送
	wire [7:0]Uart_RxData;              						//串口接收到的数据
	wire rx_done;												//串口接收完成标志位
	wire tx_done;												//串口发送完成标志位
	
	uart_top uart_top_inst(
        .clk			(clk),
        .rst_n			(rst_n),
        .rx_data		(UART_RX),
		.need_tx_data	(Uart_TxData),
		.tx_en			(tx_en),
        .tx_data		(UART_TX),
        .tx_done		(tx_done),
		.rx_done		(rx_done),
		.rx_men			(Uart_RxData)
    );

//
// 		
// 							FLASH驱动顶层
// 		    			
//	
	
	wire	[7:0]	read_flash_data;
	wire			full_order_done;
	
	flash_operation_top flash_drive_inst(
		.clk				(clk),
		.rst_n				(rst_n),
		.flash_start		(rx_done),
		.flash_order		(vio_flash_order),					//FLASH需完成动作指令
		.need_write_data	(Uart_RxData),						//FLASH需写入的数据
		.flash_address		(vio_flash_address),				//FLASH操作数据地址
		.miso				(miso),
		.sclk				(sclk),
		.cs					(cs),
		.mosi				(mosi),
		.read_flash_data	(Uart_TxData),
		.full_order_done	(full_order_done)					//flash操作完毕
    );

//
// 		
// 							串口发出数据使能控制
// 		    			
//	

	reg 			tx_en 			= 'd0;
	
	always @(posedge clk) begin
		if(full_order_done && vio_flash_order == READ_ORDER) begin
			tx_en			<= 'd1;
		end else begin
			tx_en			<= 'd0;
		end
	end
	
//
// 		
// 							FLASH在线调试模块--VIO核
// 		    			
//
	
	wire [7:0] 	vio_flash_order;
	wire [7:0] 	vio_flash_write;
	wire [23:0]	vio_flash_address;
	
	vio_0 vio_inst(
		.clk(clk),
		.probe_in0(Uart_TxData),								//8
		.probe_out0(vio_flash_order),							//8
		.probe_out1(vio_flash_write),							//8
		.probe_out2(vio_flash_address)							//24
);
	
endmodule

该模块主要是将FLASH读取数据通过串口上传至上位机方便查看,同时VIO核的使用可以使得调试更加方便。

SPI以及UART串口通讯代码均沿用前两篇博客中的模块,有兴趣的可以查阅一下

功能仿真

注意:其中aa0f0b为操作FLASH地址、aa为待写入数据;同时,在读指令发送完成后,一直持续发送0(实际上只要你片选持续选通,无论发送什么都对FLASH读无影响)。

写指令时序图

在这里插入图片描述

擦除指令时序图

在这里插入图片描述

读指令时序图

在这里插入图片描述

板级调试

读操作

在这里插入图片描述
从VIO指定为03H读指令,读取地址为00_0FFF,可以看出读取当前地址数据为FF,然后在串口助手里面可以看见发出的数据也为FF;

写操作

在这里插入图片描述
我们对FLASH的00_0FFF地址位写入串口发送的AA数据,页编程指令为02H。后续通过读取该地址可知写入操作成功,当前地址数据为AA。
在这里插入图片描述

擦除操作

在这里插入图片描述
我们输入扇区擦除指令20H对当前扇区进行擦除操作,如下图读回数据FF可知擦除成功。
在这里插入图片描述

仍存在疑问的地方

根据查阅到的博客以及官方手册,在FLASH重新上电后,其WEL位(写保护位)应置0,但实际情况是FLASH上电后即为1。同时,在执行完擦除、页编程指令后,该位也应该置0,但实际情况是依旧为1。这个情况导致我花费了大量的时间去调试,而且目前为止仍未找到原因。
最终的情况是,虽然在代码对WEL作了判断,但其实是无效的,因为该位始终为1;但是BUSY位是正常的,处于忙状态时会置1。同时,由于WEL位始终为1,但实际是并未使能的,故我在执行擦除、页编程指令之前都会先进行写使能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值