FPGA设计思想与验证方法学系列学习笔记002

还记得之前软件的同事说过的一句话。怎么凸显自己的工作量,就是自己给自己写BUG。

Q1:状态机怎么写?

看过夏宇闻老师书的都知道,verilog的FSM有moore和mealy,然后有一段,二段,三段式。记得我还是学生的时候,看到这里的时候,感觉很烧脑。毕竟这与数字电路设计息息相关。

今天我想把问题简单化。只谈mealy型三段式写法。

借用前辈们总结的一句话说:三段式描述方法虽然代码结构复杂了一些,但是换来的优势是使 FSM 做到了同步寄存器输出,消除了组合逻辑输出的不稳定与毛刺的隐患,而且更利于时序路径分组,一般来说在 FPGA/CPLD 等可编程逻辑器件上的综合与布局布线效果更佳。

 (闪耀着哲学的光辉......)

//3-paragraph method to describe FSM
//Describe sequential state transition in the 1st sequential always block
//State transition conditions in the 2nd combinational always block
//Describe the FSM out in the 3rd sequential always block
//Verilog Training -- How to write FSM better

module state3 ( rst_n,
				clk,
                i1,
				i2,
                o1,
				o2,
                err
               );
         
input          rst_n,clk;
input          i1,i2;
output         o1,o2,err;
reg            o1,o2,err;


reg    [2:0]   NS,CS;

//one hot with zero idle
parameter [2:0]      IDLE   = 3'b000;
parameter [2:0]      S1     = 3'b001;
parameter [2:0]      S2     = 3'b010;
parameter [2:0]      ERROR  = 3'b100;

//1st always block, sequential state transition
always @(posedge clk or negedge rst_n)
      if (!rst_n)            
         CS <= IDLE;        
      else                  
         CS <=NS;           

//2nd always block, combinational condition judgment
always @ (rst_n or CS or i1 or i2)
          begin
               NS = 3'bx;
               case (CS)
                    IDLE:     begin
									if (~i1)           NS = IDLE;
									if (i1 && i2)      NS = S1;
									if (i1 && ~i2)     NS = ERROR;
                              end
                    S1:       begin
									if (~i2)           NS = S1;
									if (i2 && i1)      NS = S2;
									if (i2 && (~i1))   NS = ERROR;
                              end
                    S2:       begin
									if (i2)            NS = S2;
									if (~i2 && i1)     NS = IDLE;
									if (~i2 && (~i1))  NS = ERROR;
                              end
                    ERROR:    begin
									if (i1)            NS = ERROR;
									if (~i1)           NS = IDLE;
                              end
               endcase
         end


//3rd always block, the sequential FSM output
always @ (posedge clk or negedge rst_n)
 if (!rst_n)
      {o1,o2,err} <= 3'b000;
 else
    begin
       {o1,o2,err} <=  3'b000;
       case (NS)
           IDLE:  {o1,o2,err}<=3'b000;
           S1:    {o1,o2,err}<=3'b100;
           S2:    {o1,o2,err}<=3'b010;
           ERROR: {o1,o2,err}<=3'b111;
       endcase
    end

endmodule

同样这段代码也很好理解:

module FSM(
			clk,
			clr,
			out,
			start,
			step2,
			step3
		);
input				clk;		
input				clr;		
input				start;	
input				step2;	
input				step3;

output[2:0]			out;

reg[2:0]			out;
reg[1:0]			state,next_state;



/*状态编码,采用格雷(Gray)编码方式*/
parameter						state0 = 2'b00;
parameter						state1 = 2'b01; 
parameter						state2 = 2'b11;
parameter						state3 = 2'b10; 

/*该进程定义起始状态*/
always @(posedge clk or posedge clr) 
begin 
	if (clr) 
		state <= state0; 
	else 
		state <= next_state; 
end

/*该进程实现状态的转换*/
always @(state or start or step2 or step3) 
begin 
	case (state)
		state0: begin
			if (start) 
				next_state <=state1;
			else 
				next_state <=state0;
		end

		state1: begin
			next_state <= state2;
		end

		state2: begin
			if (step2) 
				next_state <=state3;
			else 
				next_state <=state0;
		end

		state3: begin
			if (step3) 
				next_state <=state0;
			else 
				next_state <=state3;
		end

		default: next_state <=state0; /*default语句*/
	endcase
end

/*该进程定义组合逻辑(FSM的输出)*/
always @(state) 
begin
	case(state)

		state0: out=3'b001;
		state1: out=3'b010;
		state2: out=3'b100;
		state3: out=3'b111;
		default:out=3'b001; 
		/*default语句,避免锁存器的产生*/

	endcase
end

endmodule

状态转移图如下:

 

实现下面这个2状态、1输入、1输出的摩尔型状态机(异步复位、复位状态为B)

 

module top_module(
    input 	clk		,					//输入时钟
    input 	reset	,    				//高电平有效的复位信号
    input 	in		,					//输入信号
    output 	out							//输出信号
);
//------------<状态机参数定义>------------------------------------------
//使用独热码进行状态定义
parameter 	A=2'b01, 
			B=2'b10; 
 
//------------<reg定义>-------------------------------------------------			
reg	[1:0]	cur_state	,				//定义现态寄存器
			next_state	;				//定义次态寄存器
 
//三段式状态机第一段:同步时序描述状态转移
always @(posedge clk) begin    
	if(reset)
		cur_state <= B;					//现态的默认状态
	else 
		cur_state <= next_state;		//将次态赋值给现态
end
 
//三段式状态机第二段:组合逻辑判断状态转移条件,描述状态转移规律以及输出
always @(*) begin
	if(reset)
		next_state = B;					//次态的默认状态
	else
		case(cur_state)					//基于现态的状态转移
				A:	
					if(!in)	
						next_state = B;	//转移条件发生
					else
						next_state = A;	//转移条件未发生
				B:	
					if(!in)
						next_state = A;	//转移条件发生
					else
						next_state = B;	//转移条件未发生				
				default:;
		endcase
end
 
//三段式状态机第三段:时序逻辑描述输出
always @(posedge clk) begin    
    if(reset)
		out <= 1'b1;					//复位默认输出
	else 
		case(next_state)				//基于次态比基于现态输出提前一个时钟周期
			A:	out <= 1'b0;
			B:	out <= 1'b1;
			default:out <= 1'b1;
		endcase
end
 
endmodule

Q2:同步FIFO怎么写?

了解设计的原理,这样用起来也就踏实。不需要完全明白,但是会用是最基本的要求。

同步FIFO的代码实现有2种方式,参考文献:

同步FIFO的两种Verilog设计方法(计数器法、高位扩展法)_同步fifo verilog-CSDN博客

 方法一:计数器法

//~构建一个计数器,该计数器(fifo_cnt)用于指示当前 FIFO 中数据的个数:
//~复位时,该计数器为0,FIFO中的数据个数为0
//~当读写使能信号均有效时,说明又读又写,计数器不变,FIFO中的数据个数无变化
//~当写使能有效且 full=0,则 fifo_cnt +1;表示写操作且 FIFO 未满时候,FIFO 中的数据个数增加了 1 
//~当读使能有效且 empty=0,则 fifo_cnt -1;表示读操作且 FIFO 未空时候,FIFO 中的数据个数减少了 1 
//~fifo_cnt =0 的时候,表示 FIFO 空,需要设置 empty=1;fifo_cnt = fifo的深度 的时候,表示 FIFO 现在已经满,需要设置 full=1
//~————————————————
//~版权声明:本文为CSDN博主「孤独的单刀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
//~原文链接:https://blog.csdn.net/wuzhikaidetb/article/details/121136040


//计数器法实现同步FIFO
module	sync_fifo_cnt
#(
	parameter   DATA_WIDTH = 'd8  ,							//FIFO位宽
    parameter   DATA_DEPTH = 'd16 							//FIFO深度
)
(
	input									clk		,		//系统时钟
	input									rst_n	,       //低电平有效的复位信号
	input	[DATA_WIDTH-1:0]				data_in	,       //写入的数据
	input									rd_en	,       //读使能信号,高电平有效
	input									wr_en	,       //写使能信号,高电平有效
															
	output	reg	[DATA_WIDTH-1:0]			data_out,	    //输出的数据
	output									empty	,	    //空标志,高电平表示当前FIFO已被写满
	output									full	,       //满标志,高电平表示当前FIFO已被读空
	output	reg	[$clog2(DATA_DEPTH) : 0]	fifo_cnt		//$clog2是以2为底取对数	
);

//reg define
reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];	//用二维数组实现RAM	
reg [$clog2(DATA_DEPTH) - 1 : 0]	wr_addr;				//写地址
reg [$clog2(DATA_DEPTH) - 1 : 0]	rd_addr;				//读地址

//读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		rd_addr <= 0;
	else if (!empty && rd_en)begin							//读使能有效且非空
		rd_addr <= rd_addr + 1'd1;
		data_out <= fifo_buffer[rd_addr];
	end
end
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		wr_addr <= 0;
	else if (!full && wr_en)begin							//写使能有效且非满
		wr_addr <= wr_addr + 1'd1;
		fifo_buffer[wr_addr]<=data_in;
	end
end
//更新计数器
always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		fifo_cnt <= 0;
	else begin
		case({wr_en,rd_en})									//拼接读写使能信号进行判断
			2'b00:fifo_cnt <= fifo_cnt;						//不读不写
			2'b01:	                               			//仅仅读
				if(fifo_cnt != 0)				   			//fifo没有被读空
					fifo_cnt <= fifo_cnt - 1'b1;   			//fifo个数-1
			2'b10:                                 			//仅仅写
				if(fifo_cnt != DATA_DEPTH)         			//fifo没有被写满
					fifo_cnt <= fifo_cnt + 1'b1;   			//fifo个数+1
			2'b11:fifo_cnt <= fifo_cnt;	           			//读写同时
			default:;                              	
		endcase
	end
end
//依据计数器状态更新指示信号
//依据不同阈值还可以设计半空、半满 、几乎空、几乎满
assign full  = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0;		//空信号
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0;				//满信号

endmodule

方法二:高位扩展法

//~举例在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3'b000-3'b111这8个地址。
//~若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4'b0000-4'b1111。
//~假设不看最高位的话,后面3位的表示区间仍然是3'b000-3'b111,也就意味着最高位可以拿来作为指示位。
//~
//~当最高位不同,且其他位相同,则表示读指针或者写指针多跑了一圈,而显然不会让读指针多跑一圈(多跑一圈读啥?)
//~所以可能出现的情况只能是写指针多跑了一圈,与就意味着FIFO被写满了
//~当最高位相同,且其他位相同,则表示读指针追到了写指针或者写指针追到了读指针
//~显然不会让写指针追读指针(这种情况只能是写指针超过读指针一圈),所以可能出现的情况只能是读指针追到了写指针,也就意味着FIFO被读空了
//~————————————————
//~版权声明:本文为CSDN博主「孤独的单刀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
//~原文链接:https://blog.csdn.net/wuzhikaidetb/article/details/121136040


module	sync_fifo_ptr
#(
	parameter   DATA_WIDTH = 'd8  ,								//FIFO位宽
    parameter   DATA_DEPTH = 'd16 								//FIFO深度
)
(
	input										clk		,		//系统时钟
	input										rst_n	,       //低电平有效的复位信号
	input	[DATA_WIDTH-1:0]					data_in	,       //写入的数据
	input										rd_en	,       //读使能信号,高电平有效
	input										wr_en	,       //写使能信号,高电平有效
						                                        
	output	reg	[DATA_WIDTH-1:0]				data_out,	    //输出的数据
	output										empty	,	    //空标志,高电平表示当前FIFO已被写满
	output										full		    //满标志,高电平表示当前FIFO已被读空
);                                                              

//reg define
//用二维数组实现RAM
reg [DATA_WIDTH - 1 : 0]			fifo_buffer[DATA_DEPTH - 1 : 0];	
reg [$clog2(DATA_DEPTH) : 0]		wr_ptr;						//写地址指针,位宽多一位	
reg [$clog2(DATA_DEPTH) : 0]		rd_ptr;						//读地址指针,位宽多一位	

//wire define
wire [$clog2(DATA_DEPTH) - 1 : 0]	wr_ptr_true;				//真实写地址指针
wire [$clog2(DATA_DEPTH) - 1 : 0]	rd_ptr_true;				//真实读地址指针
wire								wr_ptr_msb;					//写地址指针地址最高位
wire								rd_ptr_msb;					//读地址指针地址最高位

assign {wr_ptr_msb,wr_ptr_true} = wr_ptr;						//将最高位与其他位拼接
assign {rd_ptr_msb,rd_ptr_true} = rd_ptr;						//将最高位与其他位拼接

//读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
	if (rst_n == 1'b0)
		rd_ptr <= 'd0;
	else if (rd_en && !empty)begin								//读使能有效且非空
		data_out <= fifo_buffer[rd_ptr_true];
		rd_ptr <= rd_ptr + 1'd1;
	end
end
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
	if (!rst_n)
		wr_ptr <= 0;
	else if (!full && wr_en)begin								//写使能有效且非满
		wr_ptr <= wr_ptr + 1'd1;
		fifo_buffer[wr_ptr_true] <= data_in;
	end	
end

//更新指示信号
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign	empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
//当最高位不同但是其他位相等时,写指针超过读指针一圈,FIFO被写满
assign	full  = ( (wr_ptr_msb != rd_ptr_msb ) && ( wr_ptr_true == rd_ptr_true ) )? 1'b1 : 1'b0;

endmodule

Q3:异步FIFO怎么写?

权威文献:

<FPGA>异步FIFO的Verilg实现方法_fpga fifo verilog-CSDN博客

异步的意思是写数据用写时钟,读数据用读时钟。设计很大的一个工程,不可能只用一种时钟,不同速率的数据,用到的时钟频率也不一样。其中就隐藏了跨时钟域的问题。

怎么办?谁听谁的,男女爱人吵架,谁先道歉,恩爱如初?还是针尖对麦芒,老死不相往来?

聪明的爱人们,会选择各退一步。彼此双方相互接纳彼此。就当没有事情发生一样。这也就是异步FIFO的设计思路。

  • “写满”的判断:需要将读指针同步到写时钟域,再与写指针判断
  • “读空”的判断:需要将写指针同步到读时钟域,再与读指针判断

//~分别构造读、写时钟域下的读、写指针,指针位数需拓展一位。举例,设计的FIFO深度为16,16个地址需要4位二进制数表示,同时扩宽一位作为指示位,所以指针的位宽共需要5位。
//~分别将读、写指针从二进制码转换成格雷码
//~将格雷码形式的读指针同步到写时钟域;将格雷码形式的写指针同步到读时钟域
//~在写时钟域判断“写满”:格雷码形式的读写指针高2位相反,其余位相等
//~在读时钟域判断“读空”:格雷码形式的读写指针高2位相等,其余位也相等--即全部相
//~————————————————
//~版权声明:本文为CSDN博主「孤独的单刀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
//~原文链接:https://blog.csdn.net/wuzhikaidetb/article/details/121152844

//异步FIFO
module	async_fifo
#(
	parameter   DATA_WIDTH = 'd8  ,								//FIFO位宽
    parameter   DATA_DEPTH = 'd16 								//FIFO深度
)		
(		
//写数据		
	input							wr_clk		,				//写时钟
	input							wr_rst_n	,       		//低电平有效的写复位信号
	input							wr_en		,       		//写使能信号,高电平有效	
	input	[DATA_WIDTH-1:0]		data_in		,       		//写入的数据
//读数据			
	input							rd_clk		,				//读时钟
	input							rd_rst_n	,       		//低电平有效的读复位信号
	input							rd_en		,				//读使能信号,高电平有效						                                        
	output	reg	[DATA_WIDTH-1:0]	data_out	,				//输出的数据
//状态标志					
	output							empty		,				//空标志,高电平表示当前FIFO已被写满
	output							full		    			//满标志,高电平表示当前FIFO已被读空
);                                                              

//reg define
//用二维数组实现RAM
reg [DATA_WIDTH - 1 : 0]			fifo_buffer[DATA_DEPTH - 1 : 0];
	
reg [$clog2(DATA_DEPTH) : 0]		wr_ptr;						//写地址指针,二进制
reg [$clog2(DATA_DEPTH) : 0]		rd_ptr;						//读地址指针,二进制
reg	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d1;				//读指针格雷码在写时钟域下同步1拍
reg	[$clog2(DATA_DEPTH) : 0]		rd_ptr_g_d2;				//读指针格雷码在写时钟域下同步2拍
reg	[$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d1;				//写指针格雷码在读时钟域下同步1拍
reg	[$clog2(DATA_DEPTH) : 0]		wr_ptr_g_d2;				//写指针格雷码在读时钟域下同步2拍
	
//wire define
wire [$clog2(DATA_DEPTH) : 0]		wr_ptr_g;					//写地址指针,格雷码
wire [$clog2(DATA_DEPTH) : 0]		rd_ptr_g;					//读地址指针,格雷码
wire [$clog2(DATA_DEPTH) - 1 : 0]	wr_ptr_true;				//真实写地址指针,作为写ram的地址
wire [$clog2(DATA_DEPTH) - 1 : 0]	rd_ptr_true;				//真实读地址指针,作为读ram的地址

//地址指针从二进制转换成格雷码
assign 	wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);					
assign 	rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);

//读写RAM地址赋值
assign	wr_ptr_true = wr_ptr [$clog2(DATA_DEPTH) - 1 : 0];		//写RAM地址等于写指针的低DATA_DEPTH位(去除最高位)
assign	rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0];		//读RAM地址等于读指针的低DATA_DEPTH位(去除最高位)


//写操作,更新写地址
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)
		wr_ptr <= 0;
	else if (!full && wr_en)begin								//写使能有效且非满
		wr_ptr <= wr_ptr + 1'd1;
		fifo_buffer[wr_ptr_true] <= data_in;
	end	
end
//将读指针的格雷码同步到写时钟域,来判断是否写满
always @ (posedge wr_clk or negedge wr_rst_n) begin
	if (!wr_rst_n)begin
		rd_ptr_g_d1 <= 0;										//寄存1拍
		rd_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		rd_ptr_g_d1 <= rd_ptr_g;								//寄存1拍
		rd_ptr_g_d2 <= rd_ptr_g_d1;								//寄存2拍
	end	
end

//读操作,更新读地址
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)
		rd_ptr <= 'd0;
	else if (rd_en && !empty)begin								//读使能有效且非空
		data_out <= fifo_buffer[rd_ptr_true];
		rd_ptr <= rd_ptr + 1'd1;
	end
end
//将写指针的格雷码同步到读时钟域,来判断是否读空
always @ (posedge rd_clk or negedge rd_rst_n) begin
	if (!rd_rst_n)begin
		wr_ptr_g_d1 <= 0;										//寄存1拍
		wr_ptr_g_d2 <= 0;										//寄存2拍
	end				
	else begin												
		wr_ptr_g_d1 <= wr_ptr_g;								//寄存1拍
		wr_ptr_g_d2 <= wr_ptr_g_d1;								//寄存2拍		
	end	
end

//更新指示信号
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign	empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b10;

//当高位相反且其他位相等时,写指针超过读指针一圈,FIFO被写满
//同步后的读指针格雷码高两位取反,再拼接上余下位
assign	full  = ( wr_ptr_g == { ~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1])
				,rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;

endmodule

总结

文章难免会有些水平不足,不正确的地方请大家多多指正,共同进步。同时我也会不断地更新文章,争取让文章变得更加清晰明了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值