Outline

    • FPGA简介
    • Verilog是什么
    • 数字电路的通用结构
          • FPGA数字逻辑设计与传统数字逻辑设计的区别
          • 数字电路一般模型
    • 怎样使用Verilog实现电路
    • 应用例子:Verilog实现LED闪烁
          • 电路原理
          • 代码实现
    • 总结

FPGA简介

本科时期我们在数字电路课堂上做过很多关于数字电路的实验,各种74系列芯片,通过一把一把的杜邦线连接起来,如果有一根线连错了,电路就不能正常工作,检查电路连接的时候真的是让人头皮发麻。使用分立元件设计数字电路不但复杂容易出错,而且电路的速度慢、可靠性不高。

FPGA的出现就使得数字电路的设计与实现变得更为简单,它把常用的组合逻辑电路和时序逻辑电路等资源大量地堆在芯片内部,逻辑资源之间通过可编程通断的连线连接在一起,这样就可以通过控制逻辑资源之间的连线来构成相应的电路。用户在设计好数字电路后输入到EDA软件中,软件自动编译、综合、布局布线,形成网表文件,最终下载到FPGA芯片配置相应连线的通断,就形成了实现特定功能的电路。

那么FPGA具体是通过什么原理实现可编程的组合逻辑电路和时序逻辑电路的呢?不同的FPGA有不同的结构,但是它们的基本原理都是一致的,我们知道存储器是可以实现组合逻辑电路的,存储器的地址端口可以作为逻辑变量,而存储器的数据输出就是逻辑函数Y,通过向存储器写入不同的数据就能实现不同的逻辑函数,这样就实现了组合逻辑的可编程。FPGA内部一般是通过小块的RAM以及数据选择器来实现可编程的组合逻辑电路,而通过D触发器实现时序逻辑电路(其他类型的触发器如JK触发器、T触发器等都可由D触发器和组合逻辑电路结合起来实现)。

Verilog是什么

接下来的问题就是怎样把自己设计的数字电路通过FPGA实现,这里就要引入硬件描述语言(HDL, Hardware Description Language),数字电路可以使用文本语言来描述,这种文本语言就是硬件描述语言,Verilog HDL就是一种硬件描述语言。从上述的分析中可以得出,编写Verilog代码的过程并不是编程,而是把已经设计好的数字电路转换成文本语言的形式,当然有很多有经验的高手,可以跳过设计数字电路的步骤,能做到“心中有电路”。很多刚刚接触FPGA的初学者,一上来就抱着一本Verilog语法书硬啃,容易误入歧途,正确的学习方法应该是先学好数字电路的基础以及关于FPGA的一些基本概念,在这个基础上再学习Verilog语法。

数字电路的通用结构

FPGA数字逻辑设计与传统数字逻辑设计的区别

很多人不会用Verilog实现自己想要的电路功能,并不是因为不懂Verilog语法,而是不会设计数字电路,“心中无电路”。确实,在本科我们都学过数字电路,接触过一些数字电路的设计方法。但是由于EDA软件的存在,FPGA数字逻辑设计与传统的数字逻辑设计是有一定的区别的,很多步骤可以自动化实现,但是也有一些地方需要特别注意。所以说FPGA数字逻辑设计与传统的数字逻辑设计所关注的重点是不同的。For example,传统数字逻辑设计中设计组合逻辑需要需要费很多精力,要手动化简逻辑函数、必须要使用某种固定组合逻辑功能的芯片来实现电路等等,这些不方便的地方在FPGA数字逻辑设计中是不存在的。FPGA数字逻辑设计中,开发者更多地是关注寄存器传输级(RTL, Register Transfer Level)的电路设计,而且电路可以灵活自由配置,不需要使用功能被固定死的器件,拐弯抹角地实现想要的电路功能。

数字电路一般模型

在数字电路课堂上,我们学过同步状态机的一般模型,如图1.这个模型其实就是数字电路的通用模型,图1中包含状态存储器、下一状态逻辑和状态译码器三个模块,当时钟上升沿到达时,状态存储器(实际上就是触发器或寄存器)就将下一状态逻辑的输出存储到寄存器内部,之前下一状态就变成了现态,而从图中可以看出。下一状态又是由现态和输入决定的(输入在有些情况下不存在),也就是已知现态(和输入)就决定了下一状态是什么。这样,状态存储器可以在时钟的控制下,输出一系列状态编码波形,状态编码通过状态译码就得到了输出。

乍一看这种结构实现的功能没什么大用,或者应用领域非常之窄,但实际上恰恰相反,任何的数字逻辑电路功能都可以由这种结构实现,这种结构具有完备性

图1. 摩尔状态机模型

这个模型的价值就在于无论实现什么功能的电路、无论有多复杂的电路,最终都可以转化为这种形式(之后我会举几个简单例子说明这种结构的用途),也就是说如果能用Verilog描述这种电路,就可以使用Verilog实现任意的电路功能。实际上,这种电路模型可以非常方便地用硬件描述语言来实现.

怎样使用Verilog实现电路

在图2中,我使用电路的形式重新画出了数字电路的一般模型,这里就可以看出怎样使用Verilog描述这一模型。如图2所示,电路中包含两个组合逻辑电路——下一状态逻辑和状态译码器;以及一个时序逻辑电路——D触发器,这里的D触发器是n位的D触发器,也就是一次能够存储n位二进制数据,这在分立元件组成的数字电路中需要使用n个D触发器来实现,但是在使用Verilog的FPGA数字逻辑设计中,从1位到n位只是改一个数字而已。

图2. 摩尔状态机电路实现

接下来我将使用Verilog完成一个具体实例——16进制计数器,并体现出如何套用图2中的数字电路的一般结构。首先,要实现计数器,就要有具备存储功能的器件,这当然就是图2中的寄存器来实现;要实现16进制的计数器,需要至少4位二进制数字才能表示16个状态,因此要使用4位D触发器作为状态寄存器。首先声明一下顶层模块的端口以及内部寄存器:
module counter(clk, rst, counter);
//外部端口声明
	input clk, rst;
	output reg [3:0] counter;		//counter代表寄存器的现态
//内部端口声明
	reg [3:0] counter_n;			//counter_n代表寄存器的下一状态

下面是状态寄存器的实现代码:

//状态寄存器的实现
always @(posedge clk, negedge rst) begin
	if(!rst)
		counter <= 4'd0;		//如果复位端口为低电平则时钟上升沿到达后现态变为0
	else
		counter <= counter_n;		//如果复位端口为高电平则时钟上升沿到达后现态等于下一状态
end

之后我们会看到无论多么复杂的电路,状态寄存器的代码几乎是一模一样的,上述代码就像模板一样,只有寄存器的位数和变量名称有变化,另外如果是高电平复位,那么应该去掉rst前面的!非号,其他的内容不应该有任何的变化这里要明确非常重要的一点:硬件描述语言的写法是非常非常固定的。 在我习惯使用的Notepad++编辑器中,我用FingerText插件将这一段代码做成了代码段,只需要输入slc再敲Tab键就可以自动输入实现状态寄存器的这段代码。

状态寄存器编写完成后,就要考虑下一状态逻辑应该实现怎样的逻辑功能,也就是下一状态等于谁的问题,组合逻辑电路很大程度上决定了整个电路的功能。我们要设计的是加法计数器,计数器的输出应该是从0到15,然后再回到0,因此下一状态逻辑要分两种情况:一是当现态小于15时,此时次态(下一状态)等于现态加1;二是当现态等于15时,此时次态(下一状态)等于0。

//下一状态逻辑的实现
always @(*) begin				//敏感变量列表里填*,代表让编译器自动寻找敏感变量
	if(counter == 4'd15)
		counter_n = 4'd0;		//当现态为15时,下一状态为0
	else
		counter_n = counter + 4'd1;	//当现态不等于15时,下一状态等于现态加1
end

与状态寄存器相比,下一状态逻辑(组合逻辑电路)的写法就要灵活很多,大概有两种写法,一种是用always语句编写,使用always语句时,always内部的变量必须是reg类型的。always语句内部可以使用行为级建模,用ifelse来描述组合逻辑,也可以用case语句像真值表一样把所有输入和对应的输出罗列出来。另外一种写法是使用assign语句,assign语句用于数据流建模,就是使用逻辑函数或者运算符来定义组合逻辑电路。

到这里一个完整的计数器例子就已经完成了,这个例子中不需要状态译码,因为它只是简单的计数,并且在实际中,也可以将状态译码去掉,直接用下一状态逻辑实现需要的状态编码。下面是计数器的完整代码:

module counter(clk, rst, counter);
//外部端口声明
 	input clk, rst;
 	output reg [3:0] counter;  		//counter代表寄存器的现态
//内部端口声明
 	reg [3:0] counter_n;   			//counter_n代表寄存器的下一状态

//状态寄存器的实现
always @(posedge clk, negedge rst) begin
 	if(!rst)
 		counter <= 4'd0;  		//如果复位端口为低电平则时钟上升沿到达后现态变为0
 	else
  		counter <= counter_n;  		//如果复位端口为高电平则时钟上升沿到达后现态等于下一状态
end

//下一状态逻辑的实现
always @(*) begin   				 //敏感变量列表里填*,代表让编译器自动寻找敏感变量	
	if(counter == 4'd15)
		counter_n = 4'd0; 		 //当现态为15时,下一状态为0
	else
		counter_n = counter + 4'd1; 	//当现态不等于15时,下一状态等于现态加1
end
endmodule

应用例子:Verilog实现LED闪烁

电路原理

在这一部分我将使用Verilog实现硬件领域的Hello World——LED灯闪烁,这个例子会比之前的计数器更加深入。现在假设输入FPGA芯片的时钟为16Hz(实际上不会是这么低的频率,这里仅仅是为了举例),我们打算让LED小灯每隔1s切换一次亮灭。这里我们很自然就产生了一种想法,那就是使用计数器构成的分频器把16Hz的时钟变为1Hz的时钟,然后把1Hz的接到T触发器上,T触发器的输出就会每隔1s切换一次高低电平。如图32所示。


图3. 异步时序电路实现LED闪烁

图3中所示的电路从原理上讲是没问题的,而且用74系列的芯片也完全能够实现,但是对于FPGA来说,这个电路有一个大问题,那就是它是一个异步时序电路。当电路实际上是异步时序电路时,布局布线器仍然会默认电路是同步时序电路,并且按照同步时序电路来进行时序优化,这样一来,最终的电路就有很大概率会出现问题。实际上,实用的数字电路基本上都是同步时序电路,因为同步时序电路设计简单,易于分析,不容易出现bug,而且对于同步时序电路的时序分析已经很成熟。

那么怎样使用同步时序电路来实现LED闪烁呢?我们通过观察会发现,实际上分频得到的1Hz的时钟信号并没有必要产生,我们可以通过下一状态逻辑实现n分频使能。如图4所示。

图4. 同步时序电路实现LED闪烁

图4中红色方框所示的组合逻辑电路决定了对应寄存器的下一状态逻辑,与之前的计数器中的下一状态组合逻辑不同 ,它有两个输入,一个是寄存器的现态,另外一个是我们之前设计的16进制计数器的计数值。为了实现每个1s切换一次LED亮灭,红色方框中的组合逻辑应该实现这样的功能:当计数器的计数值等于15时,次态等于现态取反;当计数值不等于15是次态等于现态。这样,只有计数值达到15时寄存器的高低电平才会跳变。下面就来编写该电路对应的Verilog代码。
代码实现

先把之前写的16进制计数器原封不动的照搬过来:

module LED(clk, rst, led);
//外部端口声明
	input clk, rst;
	output reg led;			//LED灯现态
//内部端口声明
	reg [3:0] counter;		//计数器现态
	reg [3:0] counter_n;		//计数器次态
	reg led_n			//LED灯次态
	
//计数器的寄存器实现
	always @(posedge clk or negedge rst) begin
		if(!rst)
			counter <= 4'd0;
		else
			counter <= counter_n;
	end
	
//计数器的下一状态逻辑实现
	always @(*) begin
		if(counter == 4'd15)
			counter_n = 4'd0;
		else
			counter_n = counter + 4'd1;
	end

接下来是LED寄存器的实现,与计数器中的寄存器代码完全一样,只是变量名和位数不一样:

//LED寄存器
	always @(posedge clk  or negedge rst) begin
		if(!rst)
			led <= 1'b0;
		else
			led <= led_n;
	end

LED下一状态逻辑:

//LED寄存器下一状态逻辑
	always @(*) begin
		if(counter == 4'd15)
			led_n = ~led;		//如果计数器的计数值为15,则次态等于现态取反
		else
			led_n = led;		//如果计数器的计数值不等于15,则次态等于现态
	end
endmodule

总结

本文从底层电路的角度讲述了编写Verilog代码的思路,虽然学好FPGA并不只是编写好RTL代码,要完成更加复杂的功能应该善于使用IP核,但是不管怎么说,会编写RTL代码对理解底层电路的原理有很大的帮助,不然很难解决项目中遇到的问题。

更多推荐

到底怎样编写Verilog代码——FPGA入门(一)