本文从整体上带你完成Verilog HDL语言的三种不同描述方式,让你从宏观上有所把握。
最核心的原则:一切设计实际需求而定,需要存储变量就用reg
,需要有符号数就用integer/real/reg signed
……
端口的设计,区别主要在于输出端口是默认的wire
还是自定义的reg
,本篇将以1位四选一数据选择器为例进行说明。
这两种描述的时候,使用默认的wire
即可。
这两种描述方式,本质上都是直接使用逻辑门
门级描述与数据流描述,就好比结绳记事和使用符号记事的区别,用一连串的符号标志,代替了绳子,减少了许多麻烦。
以下是四选一数据选择器的端口声明,关注output out
语句
module choose_4to1(
input d0,d1,d2,d3,
input add1,add0,
output out // 注意输出端口的设定
);
endmodule
你需要记住Verilog描述形式
需要记住,门级描述的输出和数据流描述的连续赋值语句的左值,必须是线网类型,所以必须使用默认的输出端口
行为级描述,输出端口类型应该使用output reg OUT
,使用reg
类型。
因为过程赋值语句的左值必须是寄存器类型
ANSI C风格的描述如下
module choose_4to1(
input d0,d1,d2,d3,
input add1,add0,
output reg out // 注意输出端口的设定
);
endmodule
你也可以将输出端口初始化output reg out = 0
另外一种端口风格,但是不推荐
module choose_4to1(d0,d1,d2,d3,add1,add0,out);
input d0,d1,d2,d3;
input add1,add0;
// 以下两条语句才能将out声明为reg类型的输出端口
output out;
reg out;
endmodule
就像盖房子那样,同样是楼房,使用不同的材料,建造的方式不同,速度也不同。
下面我对这几种描述进行一个近似比喻:
门级原语:and
、or
……
门级描述与门级原语为基本单元
连续赋值语句:assign
数据流描述以连续赋值语句为基本单元
结构化过程语句:initial
和always
行为级描述以结构化过程语句为基本单元
独立的语句指的是
module Example (
input a,b,
output reg OUT = 0 //【这里是关键点!】
);
<其他内容>
endmodule
wire a = 1;
reg b = 0;
门级描述,使用门级原语对硬件设计进行描述,它直接反应了逻辑门直接的关系,更加接近底层,接近硬件。
数据流描述,描述了输出数据与输入数据之间的逻辑关系,通过逻辑表达式来建立输入输出数据的联系。
逻辑表达式可以理解为对硬件设计功能的数学表达形式。
行为级描述,直接描述硬件设计所能实现的功能,相当于:设计者告诉软件需要实现怎样的功能,由软件自动生成其门机描述。当然,没有那么智能。
此处,我将会以1位四选一数据选择器的设计为例
其行为描述是:
00
,则输出d0
01
,则输出d1
10
,则输出d2
11
,则输出d3
x
设计块如下:
if语句版本的设计块
module mux_4to1 (
input d0,d1,d2,d3,
input s1,s0,
output reg out = 0
);
always @(*)
begin
if ({s1,s0} == 2'b_00)
out = d0;
else if ({s1,s0} == 2'b_01)
out = d1;
else if ({s1,s0} == 2'b_10)
out = d2;
else if ({s1,s0} == 2'b_11)
out = d3;
else
out = 1'bx;
end
endmodule
case语句版本的设计块
module mux_4to1 (
input d0,d1,d2,d3,
input s1,s0,
output reg out = 0
);
always @(*)
begin
case({s1,s0})
2'b00: out = d0; // 也可写成【2'd0】
2'b01: out = d1; // 【2'd1】
2'b10: out = d2; // 甚至于你可以直接写【2】
2'b11: out = d3; // 【3】
default: $display("错误!\n"); // 千万别忘记这个
endcase
end
endmodule
激励块如下:
module test4;
reg d0 = 0,d1 = 1,d2 = 0,d3 = 1;
reg s1,s0;
wire out;
mux_4to1 MT0 (d0,d1,d2,d3,s1,s0,out);
initial
$monitor("s1 = %b, s0 = %b, out = %b\n",s1,s0,out);
initial
begin
#1 s1 <= 0; s0 <= 0;
#1 s1 <= 0; s0 <= 1;
#1 s1 <= 1; s0 <= 0;
#1 s1 <= 1; s0 <= 1;
end
endmodule
输出结果为:
事实上,行为级描述,不仅仅可以适用于1位位宽,更可以直接设置为32位位宽,这是其他描述方式做不到的,他们需要将1位的模块组合成32位的。
out = (~s1 & ~s0 & d0) | (~s1 & s0 & d1) | (s1 & ~s0 & d2) | (s1 & s0 & d3)
逻辑表达式,表示了输出与输入直接的逻辑关系,可以直接使用数据流描述。
事实上,只有你写得出逻辑表达式,就能使用数据流描述,但是,对于复杂问题往往很难将其逻辑表达式写清楚,并且当今时代有很多集成的模块,完全可以直接调用他们,而没有必要再自己设计,这一点我在后面再进行阐述。
设计块:
逻辑表达式版本的设计块
module mux_4to1(
input d0,d1,d2,d3,
input s1,s0,
output out
);
assign out = (~s1 & ~s0 & d0) |
(~s1 & s0 & d1) |
(s1 & ~s0 & d2) |
(s1 & s0 & d3);
endmodule
条件操作符版本的设计块,这个其实已经和行为级描述类似了。
module mux_4to1 (
input d0,d1,d2,d3,
input s1,s0,
output out
);
assign out = s1? (s0? d3:d2):(s0? d1:d0);
endmodule
激励块与仿真结果和行为级一样,不再赘述。
此处选用基本的逻辑门作为器件。
相比之下,门级描述显得非常复杂,这里不再赘述,请读者自行查阅资料。
当今时代也很少有人再使用门级描述。
当今时代人们会使用数据流描述和行为级描述,对于某些必要的部分使用门级描述,但是这种情况非常少。
通常我们使用的是RTL级描述,也就是数据流和行为级描述的混合描述方式。
我们来观察两条线对比以下
结果显而易见,行为级描述更加简单,提高了效率,但是,由于行为级描述目前没有足够智能,有些事情不能完成,因此我们依然需要数据流描述,但是门级描述几乎已经不需要了。
首先,采用分治思想,将激励块和设计块分开看,激励块的输出显示结果,是由激励信号的类型决定的,在符合端口对接规则的前提下,需要对激励信号的数据类型加以修饰,以达到验证输出结果的目的。
目前我们的激励块是这样是:
reg d0 = 0,d1 = 1,d2 = 0,d3 = 1;
reg s1,s0;
wire out;
如果,我们需要输入的是有符号数,则可以改为reg signed d0;
或者integer d0;
或者real d0;
,请记住,输入端口的reg类型,代表的是一组寄存器类型,而不单单是reg。
如果我们需要输出的结果显示为十进制的负数,则需要设置为wire signed out;
,代表其是有符号数。
这也充分体现了开篇所说的:一切设计由需求决定。
科技黑箱就是其他设计者已经开发好的功能,你可以直接拿来使用,以提高开发效率。它也可以是C++中的STL库,Python的库等等。
同时,我想你也已经感受到三种描述方式在开发效率方面的差别,多多使用RTL级描述,会大大提高设计者的开发效率。
简而言之,就是把别人做好的东西直接拿来用,帮助你快速完成你设计的东西。