宋桓公

【verilog】循环与计数

0
阅读(3421)

在verilog的世界里,循环与计数有着“暧昧的”关系。

 

不知道,读者有没有这种感觉——明明我想循环10次,结果怎么才9(却有11)次??

明明我想延时10个时钟,为什么只有9(却有11)??

 

这种不确定感觉,也曾经一度困扰这笔者,今天我们就让“循环”和“计数”做到心中

有数!

 

循环,编程中再常见不过了,verilog中如何实现循环?For?NO!For是不推荐使用的。

在verilog中推荐用状态机实现循环,做个实验,循环发送7个脉冲:

	reg [3:0]i;
	reg [3:0]num1;
	always @(posedge CLK or negedge RSTn)
		if(!RSTn)
		begin
			i <= 4'd0;
			num1 <= 4'd0;
			plus <= 1'b0;
		end
		else
			case(i)
				0://循环判断
				begin
					if(num1 == 7)begin num1 <= 4'd0;  i <= 4'd3; end //跳出循环
					else i <= i + 1'b1;     //进入循环
				end
				1://循环体
				begin
					//具体操作内容
					plus <= 1'b1;
					i <= i + 1'b1; 
				end
				2:
				begin
					plus <= 1'b0;
					num1 <= num1 + 1'b1;//循环次数加1
					i <= 4'd0; 
				end
				3://循环出口
				begin
					i <= i;
				end
				
			endcase


整个循环分为3个主体部分:

1、循环判断:判断——是继续进入循环体,还是跳出循环。

2、循环体:循环时,需要完成的任务。

3、循环出口:跳出循环的去处。

过程分析:

  首先,num1是用来计算循环的次数,所以初始值为0,表示一开始的时候,一次也

没有循环。

  其次,循环判断置于循环的最上方,进入循环体后,执行每次循环需完成的任务,任务

完成后,累加num1,跳回循环判断状态。直到num1等于7,才跳出循环。

  我们发现,num1从头到尾,始终能反映循环的真实次数,num1等于几,就表示当前已经

循环了都少次,绝不含糊(这个就叫做循环不变式!)

   那么当我们判断num1 == 7是,表明程序已经循环了7次(而不是模棱两可的,是第7次,还是已经7次了?)也就是说已经发了7个脉冲!此时跳出循环。

   如此一来,循环次数,心中有数,num1 就是我们的指示标,仿真如下:

image

如果,感觉还不过瘾,我们看看,循环如何嵌套,我继续做实验,每个脉冲下再发3个脉冲。

思路基本是一样的,我先把仿真图贴出来如下,笔者想想如何用嵌套循环实现~~

image

实现程序如下:

 

	reg [3:0]i;
	reg [3:0]num1;
	reg [3:0]num2;
	always @(posedge CLK or negedge RSTn)
		if(!RSTn)
		begin
			i <= 4'd0;
			num1 <= 4'd0;
			num2 <= 4'd0;
			plus <= 1'b0;
			pp <= 1'b0;
		end
		else
			case(i)
				0://W判断
				begin
					if(num1 == 7)begin num1 <= 4'd0;  i <= 4'd6; end //跳出循环
					else i <= i + 1'b1;     //进入循环
				end
				1://W循环体
				begin
					plus <= 1'b1;
					i <= i + 1'b1;		
				end
						2://N循环判断
						begin
							if(num2 == 3)begin num2 <= 4'd0; i <= 4'd5; end //跳出循环
							else i <= i + 1'b1; //进入循环
						end
						3://N循环体入口
						begin
							pp <= 1'b1;
							i <= i + 1'b1; 
						end
						4://N循环体
						begin
							pp <= 1'b0;
							i <= 4'd2; 
							
							num2 <= num2 + 1'b1;//循环次数加1
						end
				5://W循环体,N入出口
				begin
					plus <= 1'b0;
					i <= 4'd0;
					
					num1 <= num1 + 1'b1;//循环次数加1
				end
				6://W出口
				begin
					i <= i;
				end
				
			endcase


W表示外层循环,N表示内层循环,为了可读性更强,我故意将内层循环缩进了~~

 //------------------------------------------------------------------------------------------------------

好了循环,说完了,我们再来看看计数!

在驱动一些芯片的时候,我们往往会去写一些驱动时序。FPGA的时钟往往是比较快的,我们往往通过

计数来实现精准的延时。

举个例子,FPGA跑在100M时钟下(周期是10ns),一个芯片的读信号需要持续拉高40ns有效,

那么FPGA就需要将读信号置高4个周期。此时我们就要借助计数的力量了。

我来模拟一下这个程序:(计数,试验1)


reg [3:0]i;
always @(posedge CLK or negedge RSTn)
	if(!RSTn)
	begin
		i <= 4'd0;
		C0 <= 4'd0;
		read <= 1'b0;
	end
	else
		case(i)
			0:
			begin
				read <= 1'b1;
				if(C0 == 4)begin C0 <= 4'd0; i <= i + 1'b1; end
				else begin C0 <= C0 + 1'b1; end
			end
			1:
			begin
				read <= 1'b0;
				i <= i + 1;
			end
			2:
			begin
				read <= 1'b1;
				if(C0 == 4)begin C0 <= 4'd0; i <= i + 1'b1; end
				else begin C0 <= C0 + 1'b1; end
			end
			3:
			begin
			read <= 1'b0;
				i <= i;
			end
		endcase


image

我们的意图是延时4个周期,但是实际却延时了五个周期,原因是归零的时候,用了一个周期。

我修改程序如下:(计数,试验2)


reg [3:0]i;
always @(posedge CLK or negedge RSTn)
	if(!RSTn)
	begin
		i <= 4'd0;
		C0 <= 4'd0;
		read <= 1'b0;
	end
	else
		case(i)
			0://第一次拉高
			begin
				if(C0 == 4)begin C0 <= 4'd0; i <= i + 1'b1; read <= 1'b1;end
				else begin C0 <= C0 + 1'b1; read <= 1'b1;end
			end
			1:
			begin
				read <= 1'b0;
				i <= i + 1;
			end
			2://第二次拉高
			begin
				if(C0 == 4)begin C0 <= 4'd0; i <= i + 1'b1; read <= 1'b1;end
				else begin C0 <= C0 + 1'b1; read <= 1'b1;end
			end
			3:
			begin
			read <= 1'b0;
				i <= i;
			end
		endcase


image

此时,发现此时read是拉高了4个周期,与预期相符。但是你还得发现,此时两次拉高直接的间隔由一个

周期变成了两个周期。如果为此你感觉不爽,你可以继续改:(计数,试验3)

 

reg [3:0]i;
always @(posedge CLK or negedge RSTn)
	if(!RSTn)
	begin
		i <= 4'd0;
		C0 <= 4'd0;
		read <= 1'b0;
	end
	else
		case(i)
			0:
			begin
				read <= 1'b1;
				if(C0 == 3)begin C0 <= 4'd0; i <= i + 1'b1; end
				else begin C0 <= C0 + 1'b1; end
			end
			1:
			begin
				read <= 1'b0;
				i <= i + 1;
			end
			2:
			begin
				read <= 1'b1;
				if(C0 == 3)begin C0 <= 4'd0; i <= i + 1'b1; end
				else begin C0 <= C0 + 1'b1; end
			end
			3:
			begin
			read <= 1'b0;
				i <= i;
			end
		endcase


image

 

小结一下:以上面这种形式,计数时,每次等于4的时刻,应该是停止计数的时刻,也是复位计数值的

时刻,也同时可以是状态跳转的时刻。这个“时刻”是会消耗一个周期的。你可以将其利用起来,如

试验3,好处是节约了一个时钟,弊端是可读性,就没有试验2好了。

    我还是比较推荐试验2这种形式,因为他和之前讲的循环的思维是统一的(等于4是停止计数的时刻,

类似于,跳出循环的时刻)。

 

//---------------------------------------------------------------------------------------------------------------------

为了进一步,看清问题的本质,我们继续做两个试验:


reg [3:0]C0;
always @(posedge CLK or negedge RSTn)
	if(!RSTn)
	begin
		C0 <= 4'd0;
	end
	else
	begin
		if(C0 == 4)begin C0 <= 4'd0; end
		else begin C0 <= C0 + 1'b1; end
	end


image

这是一个无限的循环,第一次间隔的是4个时钟周期,之后的间隔都是5个周期。因为第一次置0这个事情是复位完成的,或者说,初始值本就是0,而之后的却是需要一个周期去完成置0,所以间隔变成了5。 


reg [3:0]C0;
always @(posedge CLK or negedge RSTn)
	if(!RSTn)
	begin
		C0 <= 4'd0;
	end
	else
	begin
		if(C0 == 4)begin C0 <= 4'd1; end
		else begin C0 <= C0 + 1'b1; end
	end


image

这样的话,直接利用C0 == 4这个周期,完成4到1的转变,从而之后的间隔也都变成了4.


总结:

1、循环注意,循环不变式,利用它,做到循环次数心中有数。

2、计数注意,停止计数的时刻(计数复位的时刻),是会消耗时钟的,你可以将其利用起来,充当计数周期,

也可以让其作为单纯的复位时刻。

3、注意循环和计数的相似性。


技术讨论欢迎加群~~电子技术协会   362584474