写在前面


SPI协议系列文章:
FPGA实现的SPI协议(一)——SPI驱动
FPGA实现的SPI协议(二)——基于SPI接口的FLASH芯片M25P16的使用
在上篇文章,简要介绍了SPI协议,编写了SPI协议的FPGA驱动,但是在验证环节,仅仅验证了发送时序,而没有与从机进行通信验证,未免测试不够周全。本文通过对FLASH芯片M25P16的仿真模型进行一系列测试,从而验证SPI驱动的代码的正确性,同时对M25P16进行一个了解。

1、M25P16芯片


1.1、概述


M25P16是一款带有先进写保护机制和高速SPI总线访问的2MB串行Flash存储器,该存储器主要特点:

  • 2M字节的存储空间,分32个扇区,每个扇区256页,每页256字节;
  • 写入1页数据所需时间为0.64ms(典型值);能单块擦除和整块擦除:
  • 2.7~3.6 V单电源供电电压;
  • SPI总线和50 MHz数据传输时钟频率;
  • 每扇区擦写次数保证10万次、数据保存期限至少20年。


该款器件特别适用于一体化打印机、PC主板、机顶盒、CD唱机和DVD视盘机、数字电视、数码相机、图形卡和平面显示器等各种应用的代码和数据存储需求。


1.2、引脚


其引脚描述如下:


  • DQ1:数据输出,相当于SPI总线的主机输入、从机输出MISO
  • DQ0:数据输入,相当于SPI总线的主机输出、从机输入MOSI
  • C:时钟信号,相当于SPI总线的SCLK
  • S#:片选信号,相当于SPI总线的片选信号CS
  • HOLD:在选中期间期间输出高阻态,实际上比较像SDRAM的“掩码”
  • W#:写保护,低电平有效,在写保护有效时无法写入数据
  • VCC:电源信号
  • VSS :电源地


1.3、SPI模式


M25P16根据SPI时钟信号的高低电平自适应的支持SPI通讯模式的模式0和模式3:



1.4、存储架构


M25P16一共2MB字节的存储空间,分32个扇区(SECTOR),每个扇区256页(PAGE),每页256字节(BYTE)。每个字节的的存储地址由扇区地址(8bit)+页地址(8bit)+字节地址(8bit)构成,地址表如下:



1.5、指令表

M25P16支持页写入,全擦除,扇区擦除,读取数据等一系列指令,具体指令表如下:



1.6、其他


需要注意的是M25P16支持的频率如下,所以在我们的仿真实验中选择12.5M这个频率。



此外,页写入, 全擦除,扇区擦除等指令在发出后,仍需要一定的时间才能真正执行完,各个指令所需的时间如下:



最后需要注意的一点是,在两个指令之间需要间隔一定的时间(比如发送全擦除指令前需要发送写使能指令,在这两个指令之间就需要间隔一定的时间),具体时间如下:



2、指令测试


在这一章针对集中常用的指令进行代码编写及仿真测试。


2.1、页写(PAGE PROGRAM)
2.1.1、时序


页写(Page Program)操作,简称 PP,操作指令为 8’b0000_0010(02h)。页写指令是根据写入数据将存储单元中的“1” 置为“0”,实现数据的写入。在写入页写指令之前,需要先写入写使能(WREN)指令,将芯片设置为写使能锁存(WEL)状态;随后要拉低片选信号,写入页写指令、扇区地址、页 地址、字节地址,紧跟地址写入要存储在 Flash 的字节数据,在指令、地址以及数据写入过程中,片选信号始终保持低电平,待指令、地址、数据被芯片锁存后,将片选信号 高;片选信号拉高后,等待一个完整的页写周期(tPP),才能完成 Flash 芯片的页写操作。
Flash 芯片中一页最多可以存储 256 字节数据,这也表示页写操作一次最多向 Flash 芯片写入 256 字节数据。如字节首地址为 8’0000_1111,字节首地址地址到末地址之间的存储单元个数为 241 个,即本页最多可写入 241 字节数据,若写入数据为 200 个字节,数据可以被正确写入; 若写入数据为 256 个字节,前 241 个字节的数据可以正确写入 Flash 芯片,而超出的 15 个字节就以本页的首地址 8’b0000_0000 为数据写入首地址顺序写入,覆盖本页原有的前 15 个字节的数据。


页写时序如下:



2.1.2、Verilog代码


Verilog代码分为3个模块:SPI驱动模块spi_drive、SPI页写控制模块spi_page_program_ctrl和页写顶层模块spi_page_program。
SPI驱动模块spi_drive:提供SPI模式0的读写驱动,具体可参见: FPGA实现的SPI协议(一)——SPI驱动
SPI页写控制模块spi_page_program_ctrl:该模块使用一段式状态机编写,功能就是调用SPI驱动模块,发送页写指令,然后发送扇区地址+页地址+字节地址,接着给SPI总线上发送一定量的数据(可设置)。
页写顶层模块spi_page_program:例化前面两个子模块。


SPI页写控制模块spi_page_program_ctrl代码如下:

//SPI页写控制模块
`timescale 1ns/1ns		//时间单位/精度
module spi_page_program_ctrl
#(
	parameter 	SECTOR_ADDR = 8'b0000_0000, 		//扇区地址
	parameter	PAGE_ADDR   = 8'b0000_0000,			//页地址
	parameter	BYTE_ADDR   = 8'b0000_0000			//字节地址
)
(
    input               sys_clk		, 				// 全局时钟50MHz
    input               sys_rst_n	, 				// 复位信号,低电平有效
		
    input  		        send_done	, 				// 主机发送一个字节完毕标志位
    output  reg         spi_start	,				// 发送传输开始信号,一个高电平
    output  reg        	spi_end		,				// 发送传输结束信号,一个高电平
    output  reg  [7:0]  data_send    				// 要发送的数据         
);
//指令定义
localparam 	WR_EN 		 = 8'b0000_0110, 			//写使能指令	
			PAGE_PROGRAM = 8'b0000_0010;			//页写指令
localparam	DATA_MAX 	 = 8'd10;					//最大数据写入个数
				
//reg define		
reg	[7:0]	flow_cnt;								//状态跳转计数器
reg	[7:0]	cnt_wait;								//上电等待计数器
reg	[7:0]	data_cnt;								//数据写入个数计数器
 
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		data_send <= 8'd0;
		spi_start <= 1'b0;	
		spi_end <= 1'b0;	
		flow_cnt <= 1'd0;
		cnt_wait <= 8'd0;
		data_cnt <= 8'd0;
	end
	else begin
		spi_start <= 1'b0;							//便于生成脉冲信号
		spi_end <= 1'b0;                            //便于生成脉冲信号
		case(flow_cnt)
			'd0:begin
				if(cnt_wait == 100)begin			//上电后等待稳定
					cnt_wait <= 8'd0;
					flow_cnt <= flow_cnt + 1'd1;
				end
				else begin
					cnt_wait <= cnt_wait + 1'd1;
					flow_cnt <= flow_cnt;								
				end
			end
			'd1:begin									
				data_send <= WR_EN;					//写使能指令
				spi_start <= 1'b1;					//拉高spi开始通讯信号
				flow_cnt <= flow_cnt + 1'd1;
			end
			'd2:begin
				if(send_done)begin					//主机一个字节数据被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					spi_end <= 1'b1;				//结束第1次SPI通信
				end
				else
					flow_cnt <= flow_cnt;
			end	
			'd3:begin
				if(cnt_wait == 10)begin				//等待200ns,两次命令的间隔时间
					cnt_wait <= 8'd0;				//等待计数器清零
					flow_cnt <= flow_cnt + 1'd1;
				end
				else begin
					cnt_wait <= cnt_wait + 1'd1;
					flow_cnt <= flow_cnt;								
				end
			end
			'd4:begin									
				data_send <= PAGE_PROGRAM;          //页写指令
				spi_start <= 1'b1;					//拉高spi开始通讯信号
				flow_cnt <= flow_cnt + 1'd1;
			end				
			'd5:begin								//发送扇区地址
				if(send_done)begin					//指令被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= SECTOR_ADDR;		//数据为扇区地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end	
			'd6:begin								//发送页地址
				if(send_done)begin					//发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= PAGE_ADDR;			//数据为页地址地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end	
			'd7:begin								//发送字节地址
				if(send_done)begin					//指令被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= BYTE_ADDR;			//数据为字节地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end	
			'd8:begin								//停留在这个状态
				if(send_done)begin					//指令被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= 8'd0;				//发送数据从0开始
				end	
				else 
					flow_cnt <= flow_cnt;										
			end				
			'd9:begin										//写入数据
				if(send_done)begin							//主机一个字节数据被发送完成
					if(data_cnt == DATA_MAX - 1'b1)begin	//数据全部写入
						flow_cnt <= flow_cnt + 1'd1;
						spi_end <= 1'b1;					//结束第1次SPI通信
						data_cnt <= 8'd0;
						data_send <= 8'd0;
					end
					else begin
						flow_cnt <= flow_cnt;
						data_cnt <= data_cnt + 8'd1;		//计数器累加1	
						// data_send <= data_send + 8'd2;	//数据累加2	
						data_send <= data_send + 8'd4;		//数据累加4	
					end
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;
					data_cnt <= data_cnt;
				end
			end					
			'd10:begin										//停留在这个状态
					flow_cnt <= flow_cnt;
			end				
			default:;
		endcase
	end
end
 
endmodule

页写顶层模块spi_page_program代码如下:

`timescale 1ns/1ns		//时间单位/精度
//页写
module spi_page_program(
// 系统接口
	input	sys_clk		,		//全局时钟50MHz
	input	sys_rst_n	,   	//复位信号,低电平有效
// SPI物理接口							
	input	spi_miso	,   	//SPI串行输入,用来接收从机的数据
	output	spi_sclk	,   	//SPI时钟
	output	spi_cs    	,   	//SPI片选信号,低电平有效
	output	spi_mosi	    	//SPI输出,用来给从机发送数据   
);
parameter 	SECTOR_ADDR = 8'b0000_0000; 		//扇区地址
parameter	PAGE_ADDR   = 8'b0000_0000;			//页地址
parameter	BYTE_ADDR   = 8'b0000_0000;			//字节地址
wire			spi_start	;	//发送传输开始信号,一个高电平
wire			spi_end		;   //发送传输结束信号,一个高电平
wire	[7:0]  	data_send   ;   //要发送的数据
wire	[7:0]  	data_rec   	;   //接收到的数据
wire         	send_done	;   //主机发送一个字节完毕标志
wire         	rec_done	;   //主机接收一个字节完毕标志
//------------<例化模块>----------------------------------------------------------------
//页写模块
spi_page_program_ctrl
#(
	.SECTOR_ADDR 	(SECTOR_ADDR),
	.PAGE_ADDR   	(PAGE_ADDR  ),
    .BYTE_ADDR   	(BYTE_ADDR  )
)	
spi_sector_erase_ctrl_inst
(
    .sys_clk		(sys_clk	), 			
    .sys_rst_n		(sys_rst_n	), 			
	.send_done		(send_done	), 			
    .spi_start		(spi_start	),			
    .spi_end		(spi_end	),			
    .data_send    	(data_send	)						
);
//SPI驱动
spi_drive	spi_drive_inst(
	.sys_clk		(sys_clk	), 			
	.sys_rst_n		(sys_rst_n	), 			
		
	.spi_start		(spi_start	), 			
	.spi_end		(spi_end	), 			
	.data_send		(data_send	), 			
	.data_rec  		(data_rec	), 			
	.send_done		(send_done	), 			
	.rec_done		(rec_done	), 			
				
	.spi_miso		(spi_miso	), 			
	.spi_sclk		(spi_sclk	), 			
	.spi_cs    		(spi_cs		), 			
	.spi_mosi		(spi_mosi	)			
);
endmodule


2.1.3、Testbench及仿真结果


Testbench比较简单直接例化SPI页写模块和仿真模型m25p16即可,需要注意的是SPI的的页写操作需要一定的时间(前面已经提到过—5ms)。
仿真结果如下:



从地址24’h0开始,一次写入数据0x00,0x02,0x04···0x12一共10个数据,可以看到在MOSI上,依次出现了上述10个数据,说明符合SPI协议规范。
命令窗口打印内容如下(单位:ps):



在约12us处开始进行页写操作,5ms后页写操作完成,同样符合芯片参数。


2.1.4、上板验证


同读数据操作一同验证,详见2.2.4章节。


2.2、读数据(READ DATA BYTES)
2.2.1、时序


读数据操作,操作指令为 8’b0000_0011(03h),要执行数据读指令,首先拉低片选信号选中 Flash 芯片,随后写入数据读(READ)指 令,紧跟指令写入 3 字节的数据读取首地址,指令和地址会在串行时钟上升沿被芯片锁存。随后存储地址对应存储单元中的数据在串行时钟下降沿通过串行数据总线输出。 数据读取首地址可以为芯片中的任何一个有效地址,使用数据读(READ)指令可以对芯 片内数据连续读取,当首地址数据读取完成,会自动对首地址的下一个地址进行数据读取。若最高位地址内数据读取完成,会自动跳转到芯片首地址继续进行数据读取,只有再次拉高片选信号,才能停止数据读操作,否者会对芯片执行无线循环读操作。具体时序如下:



2.2.2、Verilog代码


Verilog代码分为3个模块:SPI驱动模块spi_drive、SPI读数据控制模块spi_read_ctrl和例化前面两个子模块的读数据顶层模块spi_read。
SPI驱动模块spi_drive:提供SPI模式0的读写驱动,具体可参见: FPGA实现的SPI协议(一)——SPI驱动
SPI读数据控制模块spi_read_ctrl:该模块使用一段式状态机编写,功能就是调用SPI驱动模块,发送读数据指令,然后发送扇区地址+页地址+字节地址,接着从SPI总线上接收一定量的数据(可设置)。
读数据顶层模块spi_read:例化前面两个子模块。


SPI读数据控制模块spi_read_ctrl代码如下:

//FLASH读数据控制模块:合适的调用SPI驱动模块
module spi_read_ctrl
#(
	parameter	BYTE_MAX 	= 8'd10			,		//一共读取多少个BYTE的数据
				SECTOR_ADDR = 8'b0000_0000	,		//扇区地址
				PAGE_ADDR   = 8'b0000_0000	,		//页地址
				BYTE_ADDR   = 8'b0000_0000			//字节地址
)
(
    input               sys_clk		, 				// 全局时钟50MHz
    input               sys_rst_n	, 				// 复位信号,低电平有效
	
    input		[7:0]	data_rec  	, 				// 接收到的数据
    input				rec_done	, 				// 主机接收一个字节完毕标志位	
    input  		        send_done	, 				// 主机发送一个字节完毕标志位
    output  reg         spi_start	,				// 发送传输开始信号,一个高电平
    output  reg        	spi_end		,				// 发送传输结束信号,一个高电平
    output  reg  [7:0]  data_send    				// 要发送的数据         
);	
	
//指令定义	
localparam	READ	 	= 8'h03; 					//读数据指令
			
//reg define		
reg	[7:0]	flow_cnt;								//状态跳转计数器
reg	[7:0]	data_cnt;								//数据接收计数器
reg	[7:0]	cnt_wait;								//上电等待计数器
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin	                            //复位状态
		data_send <= 8'd0;
		spi_start <= 1'b0;	
		spi_end <= 1'b0;	
		flow_cnt <= 1'd0;
		cnt_wait <= 8'd0;
		data_cnt <= 8'd0;
	end
	else begin
		spi_start <= 1'b0;							//便于生成脉冲信号
		spi_end <= 1'b0;                            //便于生成脉冲信号
		case(flow_cnt)
			'd0:begin
				if(cnt_wait == 100)begin			//上电后等待稳定
					cnt_wait <= 8'd0;
					flow_cnt <= flow_cnt + 1'd1;
				end
				else begin
					cnt_wait <= cnt_wait + 1'd1;
					flow_cnt <= flow_cnt;								
				end
			end
			'd1:begin								//发送读数据指令	
				data_send <= READ;					//读数据指令
				spi_start <= 1'b1;					//拉高spi开始通讯信号
				flow_cnt <= flow_cnt + 1'd1;
			end	
			'd2:begin								//发送扇区地址
				if(send_done)begin					//指令被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= SECTOR_ADDR;		//数据为扇区地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end	
			'd3:begin								//发送页地址
				if(send_done)begin					//发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= PAGE_ADDR;			//数据为页地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end	
			'd4:begin								//发送字节地址
				if(send_done)begin					//指令被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= BYTE_ADDR;			//数据为字节地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end				
			'd5:begin
				if(send_done)begin					//字节地址被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= 8'd0;				//清空发送数据
				end
				else
					flow_cnt <= flow_cnt;
			end	
			'd6:begin
				if(rec_done)						//这个发送最后一个字节的接收完成标志
					flow_cnt <= flow_cnt + 1'd1;				
				else
					flow_cnt <= flow_cnt;
			end				
			'd7:begin								//读取数据阶段
				if(rec_done)begin					//接收到了一个BYTE数据
					if(data_cnt == BYTE_MAX - 1'd1)begin	//接收到了指定长度个数据
						data_cnt <= 8'd0;			//计数器清零
						spi_end <= 1'b1;			//结束SPI传输
						flow_cnt <= flow_cnt + 1'd1;
					end
					else begin						//没有接收到指定长度的数据则继续接收
						data_cnt <= data_cnt + 1'd1;
						flow_cnt <= flow_cnt;								
					end				
				end
				else begin							//一个BYTE数据接收未完成
						data_cnt <= data_cnt;
						flow_cnt <= flow_cnt;								
				end				
			end
			'd8:begin								//停留在这个状态
					flow_cnt <= flow_cnt;
			end				
			default:;
		endcase
	end
end
 
endmodule

读数据顶层模块spi_read代码如下:

//FLASH读取数据顶层模块
module spi_read
#(
	parameter	BYTE_MAX 	= 8'd10			,		//一共读取多少个BYTE的数据
				SECTOR_ADDR = 8'b0000_0000	,		//扇区地址
				PAGE_ADDR   = 8'b0000_0000	,		//页地址
				BYTE_ADDR   = 8'b0000_0000			//字节地址
)
(
// 系统接口
	input	sys_clk		,			//全局时钟50MHz
	input	sys_rst_n	,   		//复位信号,低电平有效
// SPI物理接口								
	input	spi_miso	,   		//SPI串行输入,用来接收从机的数据
	output	spi_sclk	,   		//SPI时钟
	output	spi_cs    	,   		//SPI片选信号,低电平有效
	output	spi_mosi	    		//SPI输出,用来给从机发送数据   
);	
	
wire			spi_start	;		//发送传输开始信号,一个高电平
wire			spi_end		;   	//发送传输结束信号,一个高电平
wire	[7:0]  	data_send   ;   	//要发送的数据
wire	[7:0]  	data_rec   	;   	//接收到的数据
wire         	send_done	;   	//主机发送一个字节完毕标志
wire         	rec_done	;   	//主机接收一个字节完毕标志
 
//------------<例化模块>----------------------------------------------------------------
//读数据控制模块
spi_read_ctrl
#(
	.BYTE_MAX		(BYTE_MAX		),
	.SECTOR_ADDR	(SECTOR_ADDR	),
	.PAGE_ADDR		(PAGE_ADDR		),
	.BYTE_ADDR		(BYTE_ADDR		)
)	
spi_read_ctrl_inst(
    .sys_clk		(sys_clk	), 			
    .sys_rst_n		(sys_rst_n	), 			
 
	.send_done		(send_done	), 			
    .spi_start		(spi_start	),			
    .spi_end		(spi_end	),			
    .data_send    	(data_send	),			
    .data_rec    	(data_rec	),			
    .rec_done    	(rec_done	)			
);
//SPI驱动
spi_drive	spi_drive_inst(
	.sys_clk		(sys_clk	), 			
	.sys_rst_n		(sys_rst_n	), 			
		
	.spi_start		(spi_start	), 			
	.spi_end		(spi_end	), 			
	.data_send		(data_send	), 			
	.data_rec  		(data_rec	), 			
	.send_done		(send_done	), 			
	.rec_done		(rec_done	), 			
				
	.spi_miso		(spi_miso	), 			
	.spi_sclk		(spi_sclk	), 			
	.spi_cs    		(spi_cs		), 			
	.spi_mosi		(spi_mosi	)			
);
 
endmodule


2.2.3、Testbench及仿真结果


Testbench比较简单直接例化读数据模块和仿真模型m25p16即可,同时让命令窗口打印读取到的数据。

//------------------------------------------------
//--SPI驱动仿真--读数据仿真
//------------------------------------------------
`timescale 1ns/1ns		//时间单位/精度
 
//------------<模块及端口声明>----------------------------------------
module tb_spi_read();
 
reg		sys_clk		;
reg		sys_rst_n	;
					
wire	spi_miso	;
wire	spi_sclk	;
wire	spi_cs    	;
wire	spi_mosi	;
 
parameter	BYTE_MAX 	= 8'd10			,		//一共读取多少个BYTE的数据
			SECTOR_ADDR = 8'b0000_0000	,		//扇区地址
			PAGE_ADDR   = 8'b0000_0000	,		//页地址
			BYTE_ADDR   = 8'b0000_0000	;		//字节地址
//------------<例化被测试模块>----------------------------------------
//读数据模块
spi_read	
#(
	.BYTE_MAX		(BYTE_MAX		),
	.SECTOR_ADDR	(SECTOR_ADDR	),
	.PAGE_ADDR		(PAGE_ADDR		),
	.BYTE_ADDR		(BYTE_ADDR		)
)
spi_read_inst(
	.sys_clk	(sys_clk	),
	.sys_rst_n	(sys_rst_n	),
 
	.spi_miso	(spi_miso	),
	.spi_sclk	(spi_sclk	),
	.spi_cs    	(spi_cs		),
	.spi_mosi	(spi_mosi	)
);
//m25p16仿真模型
m25p16  memory (
    .c          (spi_sclk	), 
    .data_in    (spi_mosi   ), 
    .s          (spi_cs   	), 
    .w          (1'b1		), 
    .hold       (1'b1   	), 
    .data_out   (spi_miso   )
);	
 
//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	#20								//20个时钟周期后
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态	
end
//打印数据
always@(*)begin
	if(spi_read_inst.rec_done && spi_read_inst.spi_read_ctrl_inst.flow_cnt == 'd7)
		$display("READ	:%h",spi_read_inst.data_rec);		//打印读取的数据
end
 
//重定义初始化数值
defparam memory.mem_access.initfile = "initM25P16_test.txt";	//其中的每页数据是从00累加到FF	
 
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns
 
endmodule


2.3.3、Testbench及仿真结果


Testbench比较简单直接例化扇区擦除模块和仿真模型m25p16即可。需要注意的是m25p16的扇区擦除需要等待的时间较长(3s),为了尽快完成仿真,我把这个等待参数改成了1s。

//------------------------------------------------
//--SPI驱动仿真--读数据仿真
//------------------------------------------------
`timescale 1ns/1ns		//时间单位/精度
 
//------------<模块及端口声明>----------------------------------------
module tb_spi_read();
 
reg		sys_clk		;
reg		sys_rst_n	;
					
wire	spi_miso	;
wire	spi_sclk	;
wire	spi_cs    	;
wire	spi_mosi	;
 
parameter	BYTE_MAX 	= 8'd10			,		//一共读取多少个BYTE的数据
			SECTOR_ADDR = 8'b0000_0000	,		//扇区地址
			PAGE_ADDR   = 8'b0000_0000	,		//页地址
			BYTE_ADDR   = 8'b0000_0000	;		//字节地址
//------------<例化被测试模块>----------------------------------------
//读数据模块
spi_read	
#(
	.BYTE_MAX		(BYTE_MAX		),
	.SECTOR_ADDR	(SECTOR_ADDR	),
	.PAGE_ADDR		(PAGE_ADDR		),
	.BYTE_ADDR		(BYTE_ADDR		)
)
spi_read_inst(
	.sys_clk	(sys_clk	),
	.sys_rst_n	(sys_rst_n	),
 
	.spi_miso	(spi_miso	),
	.spi_sclk	(spi_sclk	),
	.spi_cs    	(spi_cs		),
	.spi_mosi	(spi_mosi	)
);
//m25p16仿真模型
m25p16  memory (
    .c          (spi_sclk	), 
    .data_in    (spi_mosi   ), 
    .s          (spi_cs   	), 
    .w          (1'b1		), 
    .hold       (1'b1   	), 
    .data_out   (spi_miso   )
);	
 
//------------<设置初始测试条件>----------------------------------------
initial begin
	sys_clk = 1'b0;					//初始时钟为0
	sys_rst_n <= 1'b0;				//初始复位
	#20								//20个时钟周期后
	sys_rst_n <= 1'b1;				//拉高复位,系统进入工作状态	
end
//打印数据
always@(*)begin
	if(spi_read_inst.rec_done && spi_read_inst.spi_read_ctrl_inst.flow_cnt == 'd7)
		$display("READ	:%h",spi_read_inst.data_rec);		//打印读取的数据
end
 
//重定义初始化数值
defparam memory.mem_access.initfile = "initM25P16_test.txt";	//其中的每页数据是从00累加到FF	
 
//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;		//系统时钟周期20ns
 
endmodule


仿真结果如下:



命令窗口打印内容如下(单位:ps):约5us处开始进行扇区擦除操作,1s后扇区擦除操作完成。与预期结果一致。



2.3.4、上板验证

首先使用扇区擦除模块24‘b0000_0000_0000_0000_0000_1000,实际上就是擦除扇区0,和后面的页地址和字节地址没有关系。在2.2节做页写操作的验证时,我们给地址扇区0的页0的地址0x00~0x0a分别写入了数据0x00、0x02、···、0x12,我们只要再使用读数据模块对这10个地址读取一遍,根据读出的内容就可以判断扇区擦除操作是否成功。
使用signal tap对读数据操作抓取的波形如下:可以看到连续读取的数据均为0XFF,说明扇区擦除操作成功。



2.4、全擦除(Bulk Erase)
2.4.1、时序


全擦除(Bulk Erase)操作,简称 BE,操作指令为 8’b1100_0111(C7h),全擦除指令是将 Flash 芯片中的所有存储单元设 置为全 1,在 Flash 芯片写入全擦出指令之前,需要先写入写使能(WREN)指令;随后要拉低片选信号,写入全擦除指令,在指令写入过程中,片选信号始终保持低电平,待指令被芯片锁存后,将片选信号拉高;全擦除指令被锁存并执行后,需要等待一个完整的全擦除周期(tBE),才能完成 Flash 芯片的全擦除操作。时序图如下:



2.4.2、Verilog代码


Verilog代码分为3个模块:SPI驱动模块spi_drive、SPI全擦除控制模块spi_bulk_erase_ctrl和全擦除顶层模块spi_bulk_erase。
SPI驱动模块spi_drive:提供SPI模式0的读写驱动,具体可参见: FPGA实现的SPI协议(一)——SPI驱动
SPI全擦除控制模块spi_bulk_erase_ctrl:该模块使用一段式状态机编写,功能就是调用SPI驱动模块,发送全擦除指令
SPI全擦除顶层模块spi_bulk_erase:例化前面两个子模块
SPI全擦除控制模块spi_bulk_erase_ctrl代码如下:

//SPI扇区擦除控制模块
`timescale 1ns/1ns		//时间单位/精度
module spi_sector_erase_ctrl
#(
	parameter 	SECTOR_ADDR = 8'b0000_0000, 		//扇区地址
	parameter	PAGE_ADDR   = 8'b0000_0000,			//页地址
	parameter	BYTE_ADDR   = 8'b0000_0000			//字节地址
)
(
    input               sys_clk		, 				// 全局时钟50MHz
    input               sys_rst_n	, 				// 复位信号,低电平有效
		
    input  		        send_done	, 				// 主机发送一个字节完毕标志位
    output  reg         spi_start	,				// 发送传输开始信号,一个高电平
    output  reg        	spi_end		,				// 发送传输结束信号,一个高电平
    output  reg  [7:0]  data_send    				// 要发送的数据         
);
//指令定义
localparam 	WR_EN 		 = 8'b0000_0110, 			//写使能指令	
			SECTOR_ERASE = 8'b1101_1000;			//扇区擦除指令
				
//reg define		
reg	[7:0]	flow_cnt;								//状态跳转计数器
reg	[7:0]	cnt_wait;								//上电等待计数器
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		data_send <= 8'd0;
		spi_start <= 1'b0;	
		spi_end <= 1'b0;	
		flow_cnt <= 1'd0;
		cnt_wait <= 8'd0;
	end
	else begin
		spi_start <= 1'b0;							//便于生成脉冲信号
		spi_end <= 1'b0;                            //便于生成脉冲信号
		case(flow_cnt)
			'd0:begin
				if(cnt_wait == 100)begin			//上电后等待稳定
					cnt_wait <= 8'd0;
					flow_cnt <= flow_cnt + 1'd1;
				end
				else begin
					cnt_wait <= cnt_wait + 1'd1;
					flow_cnt <= flow_cnt;								
				end
			end
			'd1:begin									
				data_send <= WR_EN;					//数据为写使能指令
				spi_start <= 1'b1;					//拉高spi开始通讯信号
				flow_cnt <= flow_cnt + 1'd1;
			end
			'd2:begin
				if(send_done)begin					//主机一个字节数据被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					spi_end <= 1'b1;				//结束第1次SPI通信
				end
				else
					flow_cnt <= flow_cnt;
			end	
			'd3:begin
				if(cnt_wait == 10)begin				//等待200ns,两次命令的间隔时间
					cnt_wait <= 8'd0;				//等待计数器清零
					flow_cnt <= flow_cnt + 1'd1;
				end
				else begin
					cnt_wait <= cnt_wait + 1'd1;
					flow_cnt <= flow_cnt;								
				end
			end
			'd4:begin									
				data_send <= SECTOR_ERASE;          //扇区擦除指令
				spi_start <= 1'b1;					//拉高spi开始通讯信号
				flow_cnt <= flow_cnt + 1'd1;
			end				
			'd5:begin								//发送扇区地址
				if(send_done)begin					//指令被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= SECTOR_ADDR;		//数据为扇区地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end	
			'd6:begin								//发送页地址
				if(send_done)begin					//发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= PAGE_ADDR;			//数据为页地址地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end	
			'd7:begin								//发送字节地址
				if(send_done)begin					//指令被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					data_send <= BYTE_ADDR;			//数据为字节地址
				end
				else begin
					flow_cnt <= flow_cnt;
					data_send <= data_send;							
				end
			end				
			'd8:begin
				if(send_done)begin					//主机一个字节数据被发送完成
					flow_cnt <= flow_cnt + 1'd1;
					spi_end <= 1'b1;				//结束第1次SPI通信
				end
				else
					flow_cnt <= flow_cnt;
			end					
			'd9:begin								//停留在这个状态
					flow_cnt <= flow_cnt;
			end				
			default:;
		endcase
	end
end
endmodule

SPI全擦除顶层模块spi_bulk_erase代码如下:

`timescale 1ns/1ns		//时间单位/精度
//扇区擦除
module spi_sector_erase(
// 系统接口
	input	sys_clk		,						//全局时钟50MHz
	input	sys_rst_n	,   					//复位信号,低电平有效
// SPI物理接口											
	input	spi_miso	,   					//SPI串行输入,用来接收从机的数据
	output	spi_sclk	,   					//SPI时钟
	output	spi_cs    	,   					//SPI片选信号,低电平有效
	output	spi_mosi	    					//SPI输出,用来给从机发送数据   
);
parameter 	SECTOR_ADDR = 8'b0000_0000; 		//扇区地址
parameter	PAGE_ADDR   = 8'b0000_0000;			//页地址
parameter	BYTE_ADDR   = 8'b0000_1000;			//字节地址
wire			spi_start	;					//发送传输开始信号,一个高电平
wire			spi_end		;   				//发送传输结束信号,一个高电平
wire	[7:0]  	data_send   ;   				//要发送的数据
wire	[7:0]  	data_rec   	;   				//接收到的数据
wire         	send_done	;   				//主机发送一个字节完毕标志
wire         	rec_done	;   				//主机接收一个字节完毕标志
//------------<例化模块>----------------------------------------------------------------
//扇区擦除模块
spi_sector_erase_ctrl
#(
	.SECTOR_ADDR 	(SECTOR_ADDR),
	.PAGE_ADDR   	(PAGE_ADDR  ),
    .BYTE_ADDR   	(BYTE_ADDR  )
)	
spi_sector_erase_ctrl_inst
(
    .sys_clk		(sys_clk	), 			
    .sys_rst_n		(sys_rst_n	), 			
	.send_done		(send_done	), 			
    .spi_start		(spi_start	),			
    .spi_end		(spi_end	),			
    .data_send    	(data_send	)						
);
//SPI驱动
spi_drive	spi_drive_inst(
	.sys_clk		(sys_clk	), 			
	.sys_rst_n		(sys_rst_n	), 			
		
	.spi_start		(spi_start	), 			
	.spi_end		(spi_end	), 			
	.data_send		(data_send	), 			
	.data_rec  		(data_rec	), 			
	.send_done		(send_done	), 			
	.rec_done		(rec_done	), 			
				
	.spi_miso		(spi_miso	), 			
	.spi_sclk		(spi_sclk	), 			
	.spi_cs    		(spi_cs		), 			
	.spi_mosi		(spi_mosi	)			
);
endmodule


仿真结果如下:



命令窗口打印内容如下(单位:ps):约3us处开始进行扇区擦除操作,1s后扇区擦除操作完成。与预期结果一致。



2.4.4、上板验证

首先使用扇区擦除模块24‘b0000_0000_0000_0000_0000_1000,实际上就是擦除扇区0,和后面的页地址和字节地址没有关系。在2.2节做页写操作的验证时,我们给地址扇区0的页0的地址0x00~0x0a分别写入了数据0x00、0x02、···、0x12,我们只要再使用读数据模块对这10个地址读取一遍,根据读出的内容就可以判断扇区擦除操作是否成功。

        使用signal tap对读数据操作抓取的波形如下:可以看到连续读取的数据均为0XFF,说明扇区擦除操作成功。



全擦除模块波形图:与仿真波形图一致。



接着调用读数据模块读取区域1的数据:读取的数据全部为0xFF,说明数据被擦除了。



接着调用读数据模块读取区域2的数据:读取的数据全部为0xFF,说明数据被擦除了。



以上就证明我们的全擦除模块是成功擦除了所有扇区。


3、其他


还有一些其他指令,如读ID,读写状态寄存器就不列出来了,参考上述模块应该很容易就改出来了
如果需要完整的工程文件请点这里:工程文件下载