可见,尽管做出了关键路径的选取,组合逻辑电路还是非常容易产生毛刺。下面我们使用上面的说法,加入寄存器后观察结果。 reg out; wire temp; assign temp = a&b&c|d&e&b; always @ (posedge clk) out = temp;
从RTL结构图中可以直观看到加入D触发器(即寄存器)输出:
依旧采用改动后的仿真文件,所得出的仿真波形如下:
可见,上面的毛刺完全被消除了,而且不留一点痕迹。
由于使用触发器免不了使用时钟,详细的使用放到后面的时序电路学习一节中。
小结:从本节中,实现了简单的逻辑组合,并透过阅读修改仿真文件,了解毛刺的产生;通过观察RTL结构图,了解到代码风格与生成的模型之间的联系,并通过使用D触发器消除毛刺。
反思:如果外部情况比较恶劣,延时级别大于ns级,或者输入信号的跳变在时钟跳变时期发生,会产生什么效果,这需要在后面的学习中逐步体会。
3.使用组合逻辑:加法器的设计 一个设计,其实现方案多样,而且不同的实现方案,电路结构是不一样的,这将导致速度或使用资源的差异。所以,在学习过程中,需要对不同的方法进行比较,了解各种编码风格实现的差别,对将来的系统设计有极大的帮助。下面我们以加法器的设计为引导,延伸本节的讨论。 首先,我们要知道一个加法器模型应该有的端口,他们分别为:加数,被加数,上级进位,和,进位这几个端口。注意verilog的编码规范,模块端口有高位至低位,从输出到输入进行描述:module adder(cout,sum,clk,rst,a,b,cin); (1)实现方法一
assign { cout, sum } = a + b + cin;
这样看来,加法器的实现非常简单,从结构上看,它直接调用了一个加法器(如图):
但是对于“+”号的综合,FPGA内部是采用查找表所实现的,下面是由上述语句生成的二级查找表结构:(查找表的级数是延时的主要因素,降低查找表级数是代码优化的首要任务)
其中LUT3_E8表示3输入查找表,E8指查找表内部真值表的结果值。
由于使用查找表实现,容易出现毛刺,我们回顾下上一节所用到的方法,我们再使用一次:
再次出现了D触发器。(仿真波形就不看了,大家可以自己试试) (2)串行加法器 wire [1:0] temp; assign { temp[0],sum[0] } = a[0] + b[0];
assign { temp[1],sum[1] } = a[1] + b[1] + temp[0]; assign cout = temp[1];
虽然咋一看,与第一种方法相当类似,你确定是一样吗?我们看看生成的
,如下图:
我们可以看出,这样的代码,只使用了一级的LUT,这样设计有助于降低延时,而且并没有增多LUT的使用量,反而少用一个LUT。
可见资源利用率是第二种略为节省些。 (3)查找法 reg [1:0] sum; reg cout; always @ (posedge clk or negedge rst) begin if(!rst) {cout,sum}=3'bzzz; else case( {a,b} ) 4'b00_00:{cout,sum}=3'b000; 4'b00_10:{cout,sum}=3'b010; 4'b01_00:{cout,sum}=3'b001; 4'b01_10:{cout,sum}=3'b011; 4'b10_00:{cout,sum}=3'b010; 4'b10_10:{cout,sum}=3'b100; 4'b11_00:{cout,sum}=3'b011; 4'b11_10:{cout,sum}=3'b101;
4'b00_01:{cout,sum}=3'b001;
4'b00_11:{cout,sum}=3'b011; 4'b01_01:{cout,sum}=3'b010; 4'b01_11:{cout,sum}=3'b100; 4'b10_01:{cout,sum}=3'b011; 4'b10_11:{cout,sum}=3'b101; 4'b11_01:{cout,sum}=3'b100; 4'b11_11:{cout,sum}=3'b110;
default:{cout,sum}=3'bzzz; endcase end
我们看生成的RTL图如下:
可见编译器知道我们的用意,使用了ROM作为查找的存储,加数为取址信号。 通过观察.ngc文件可知,这样的描述仍然使用了一级的3个LUT,输出使用寄存器同步输出。 (4)卡诺图法
我们写出他的真值表,通过卡诺图进行简化,得出各输出的逻辑表达式: assign sum[0] = (~a[0]&b[0])|(a[0]&~b[0]); assign sum[1] =
(~a[1]&~a[0]&b[1])|(~a[1]&a[0]&~b[1]&b[0])|(a[1]&a[0]&b[1]&b[0])|(~a[1]&b[1]&~b[0])|(a[1]&~b[1]&~b[0])|(a[1]&~a[0]&~b[1]); assign cout = (a[0]&b[1]&b[0])|(a[1]&a[0]&b[0])|(a[1]&b[1]);
这种方法是用门电路数目较多,延时参次不齐,而且使用不方便,但速度相对会快些。 要注意,有时候适当的加上括号,除了有助于阅读,还可以减少逻辑门的级数,下面再举个例子:
assign out = (da + db) + (dc + dd);和assign out = da + db + dc + dd;
da + db + dc + dd生成的模块内部图
(da + db) + (dc + dd)所生成的模块内部图
尽管使用的加法器个数一样,但级数变少了,这样的编码风格,提高了模块运行的速度,但由于FPGA内部使用LUT实现,所以最终对提高速度影响不大,但对于ASIC(专用集成芯片)的模块设计,这样的考虑是很有必要的。 (5)超前加法器 assign sum[0] = a[0] ^ b[0] ^ cin; assign sum[1] = a[1] ^ b[1] ^ ( (a[0] & b[0]) | (a[0] ^ b[0]) & cin );
assign cout = ( a[1] & b[1] ) | ( (a[1]^b[1]) & (a[0]&b[0]) ) | ( (a[0]^b[0]) & (a[1]^b[1]) & cin );
根据数电书上的解释,人为地观察进位与输入的关系,由输入直接导出进位是这种方法的核心思想。但我们仍然可以看出,当位数增加的时候,电路的设计变得非常复杂,下面我们通过调用模块来简化多位加法器的设计。 (6)调用模块 module one_bit_adder(cout,sum,cin,ina,inb); input ina,inb,cin; output cout,sum; assign {cout,sum} = ina + inb + cin; endmodule //two bit adder (by using one bit adder) //This is a top module module use_adder(cout,sum,c,x,y); input [1:0] x,y; input c; output [1:0] sum; output cout; wire c_temp; one_bit_adder u1(.cout(c_temp),.sum(sum[0]),.cin(c),.ina(x[0]),.inb(y[0])); one_bit_adder u2(.cout(cout),.sum(sum[1]),.cin(c_temp),.ina(x[1]),.inb(y[1])); endmodule 这里先设计一位计数器(当然,计数器的设计可以使用上面的任何一个例子),然后通过对模块端口的连线,而模块的调用有以下两种方法: 1模块端口的对齐:即按照模块原型声明的端口顺序 ○
2模块端口对应连接:使用 .端口名(线名)○,.端口名(线名)??这样的方式进行模块调用,当某一端口需要悬空,必须加上“,”但可以不填写任何东西。 (7)流水线 input[3:0] a,b; input clk,cin; output[3:0]sum; output cout;
reg[3:0] tempa,tempb; reg tempci; reg cout; reg firstco; reg[1:0] firstsum; reg[2:0] firsta,firstb; reg[3:0] sum;
always@(posedge clk) begin
tempa=a; tempb=b; tempci=cin; end
always@(posedge clk) begin {firstco,firstsum}=tempa[1:0]+tempb[1:0]+tempci; firsta=tempa[3:2]; firstb=tempb[3:2];