硬件描述语言(Hardware Description Language, HDL) 是一种能够用于描述数字电路的语言,可通过分层的模块表示复杂的数字系统,并可通过电子设计自动化(Electrionic Design Automation, EDA) 工具进行仿真,以得到具体的物理门级电路组合,即电路网表。
之后再将上述网表,在专用集成电路(Application Specific Integrated Circuit, ASIC) 或者 现场可编程门阵列(Field Programmable Gate Array, FPGA) 上将网表转化为具体的电路布局结构。
Verilog HDL 就是HDL的一种,用于数字电子系统设计,进行数字逻辑系统的仿真验证、时序分析、逻辑综合,是目前应用最广泛的一种硬件描述语言。
Verilog的特点在于:
● 与工艺无关性:在使用verilog功能逻辑设计时无须过多考虑门级及工艺实现的具体细节,通过EDA工具的帮助可自动实现具体的网表。其实就是写代码实现电路,所以大大提高了verilog的可重用性。
● 知识产权核(Intellectual Property, IP):将一些在数字电路中常用、功能比较复杂、已经预先设计好的可修改参数的模块,即封装,类似于C++中的各种函数。IP核有三种形式:软核(HDL形式的IP核,代码)、固核(在FPGA上实现的正确的电路结构编码文件)和 硬核(在ASIC上实现的正确的电路结构版图掩膜,以经过完全的布局布线的网表形式提供)
verilog可实现行为描述和结构描述,其中行为描述包括系统级模型(描述设计模块的外部特性)、算法级模型(实现算法运行)和寄存器级模型(Register Transfer Level, RTL)。
而结构描述就包括门级和开关级的模型,可描述逻辑门之间的连接。
比较重要而具体的功能:
● 顺序执行 or 并行执行:与电路网表一致,在verilog中既可以实现HDL的并行执行,也可实现与C相似的顺序执行结构。
● 时间延迟:可明确控制的启动时间延迟
● 事件触发:通过命名与设计事件设计激活or停止行为。
verilog建立的verilog模型就是模块,通过模块实现各种逻辑功能,并可通过EDA工具将verilog代码模块转化为门电路的连接,该过程为综合。
如下所示,一个名为OR的模块
module OR(input a, input b, output c); //分号千万别忘了
assign c = a || b;
endmodule
综合之后则为
注意模块module的结构写法,以及使用endmodule结尾
需要说明的是模块module实际上是创建了一个类型,例如与们、或门、非门、加法器等等,这些模块可以被实例化,即一个具体的有名字、输入信号确定的对象。
指的是该模块的输入/输出端口,即IO口。
既可以标注输入、输出以及输入输出口,注意信号位宽的写法一般是“高位:低位”,具体可见代码:
module OR(
input [2:0] a,
input [3:1] b, //不写明类型就默认为wire类型
output reg [2:0] c,
inout [5:0] d
);
assign d = a | b;
always@(*) begin
if(a)
c = 3'b1;
else
c = 3'b0;
end
endmodule
注意module输入必须为wire型,输出建议也用wire
例如上面的代码,输出c虽然是reg型但在后期综合的时候还是会把它综合成导线而不是触发器。
但always块赋值右侧又必须是reg型,所以推荐的写法如下
module block(
input a,
input b,
output c
);
reg c_r; //由于要通过always赋值故声明一个reg型变量
always@(*) begin
if(a)
c_r = 1'b1;
else
c_r = 1'b0;
end
assign c = c_r; //用一个assign为wire型输出c赋值
endmodule
实例化引用就是,在某个模块内部引用其他模块,并将该模块的IO与外部连起来。
引用方式如下:
module trist2(input a, input b, output c);
//......
block u_block1(a_i,b_i,c_o); //严格按照block模块定义端口的顺序
block u_block2(.c(c_o),.a(a_i),.b(b_i)); //指明谁连谁,注意写法是 ".模块端口(连接变量)"
endmodule
如上代码所示有两种实例化方式。
但是这里有一个问题:实例化时模块端口连接的变量,是wire型还是reg型呢?答案是输入可wire可reg,输出必须是wire
有个小技巧,既然是例化连接,其实还是导线的连接,所以将例化时的变量连接看成assign语句。
那么输入就相当于assign a = a_i;
,那显然模块定义的时候input a必须是wire型,而a_i任意
输出相当于assign c_o = c;
,那显然例化时连接的变量c_o必须是wire型,而模块定义时的output c任意(但从后期综合考虑建议为wire型)
指模块内部产生逻辑功能的语句,所有的逻辑一般均必须通过assign、always、initial以及实例化引用实现功能。
需要说明的是模块内的所有逻辑功能块均是并行实现的,即同时执行。所以执行顺序与代码的写作顺序无关。
module exercise(input in1, input in2,...,output out1, output out2,... );
//块1
initial
...
//块2
assign ...;
//块3
assign...;
//块4
always@(...)
...
endmodule //块1234同时执行
但是块内部则可能出现顺序执行的逻辑,这个后面会涉及到。
直接assign后面接方程式即可,表示持续地满足该方程式的关系。
assign a = b & c; //永远满足a为b与c的按位与的结果
assign #10 clk = ~clk; //延迟10个单位时间之后,clk按位翻转,且永远如此变换
注意上例子中的
#10
表示延迟10个单位时间再去执行。
不断运行的语句,同时可以为always添加时序控制语句来判断语句执行的条件。会永远持续地判断条件,一旦条件满足,则执行后面的语句。
always块@里面是敏感列表,表示always块触发的条件:时序逻辑包括posedge表示上升沿触发、negedge表示下降沿触发,
组合逻辑则使用(*)电平触发,即变量值发生变化时触发
同时可加入begin…end表示触发要执行的语句块,注意该语句块为顺序执行
具体的书写形式如下
always@(posedge clk or negedge rstn) //时序逻辑
begin
if(!rstn)
q <= 1'b0;
else if(flag) //若某个posedge clk处flag为低,则q会保持原值。即 q <= q;
q <= 1'b1;
end
always@(*) begin //组合逻辑
if(flag)
q = 1'b0;
else if(width) //若flag为低、width也为低,则q会锁存,被综合成锁存器Latch
q = 1'b1;
end
一定要注意verilog中的各always块均为并行执行,因此禁止多个always块对同一个变量赋值,可合并为一个always块解决此问题,例如:
always@(posedge clk) begin if(A) q <= 1'b0; ...... end always@(posedge clk) begin if(B) q <= 1'b1; ...... end //合并为一个always块/ always@(posedge clk) begin if(A) q <= 1'b0; else if(B) q <= 1'b1; ...... end
需要说明的是,always块中使用组合逻辑时,if语句的else、case语句的default等等要写全,否则会被综合成锁存器Latch。
而时序逻辑中写不全,触发器就不会成为Latch。
可在仿真开始时对各变量进行初始化,注意initial块只会执行一次,但initial内部的begin…end是顺序执行。
但是多个initial块则是并行运行的!!!见verilog中的initial语句
initial //注意结尾没有分号
begin
inputs = 3'b000;
#10 inputs = 3'b000;
#20 inputs = 3'b001;
#30 inputs = 3'b010;
end
在建立模块之后,可再建立一个专门用于测试的模块。在测试模块中引用之前建立的模块,并给定相应的输入,以观察所建立的模块的输出信号,以判断设计的结构合理与否。
所以测试模块不需要输入和输出,例子如下:
'include "mux.v"
module mux_test
reg ain,bin,select,out;
reg clock;
//参数初始化
initial
begin
ain = 0;
bin = 0;
select = 0;
clock = 0;
end
//设定时序
assign #100 clock = ~clock;
always@(posedge clock)
begin
#1 ain = {$random}%2;
#3 bin = {$random}%5;
end
assign #1000 select = !select;
//实例化mux
mux m(.out(out),.a(ain),.b(bin),.sl(select));
endmodule
之后可以用相关仿真软件对信号的波形作仿真,以观察信号是否符合要求。
即程序运行中不能改变的量。
由于verilog为数字系统描述语言,对于数字系统来说最基本的是端口电平的高低,即基本为数字量,所以常用的是整数。
常见形式为
● 仅数字
默认为32bit的十进制的数
● bit位宽 + ’ + 进制 + 数字
表示一定位宽下某个进制的值,例如3’b101、8’ha2等等。
注意位宽为二进制的bit,例如 8’ha2 表示 8bit的 十六进制a2。
其中进制表示法如下:
字母 | 进制 |
---|---|
b 或 B | 二 进制 |
d 或 D | 十 进制 |
h 或 H | 十六 进制 |
别忘记二进制 和 十六进制的相互转换
1位十六进制 = 4位二进制,例如 4’ha = 4’b1010, 4’hf = 4’b1111, 16’habc = 12’b1011_1100_1101,12’h1010 = 16’b0001_0000_0001_0000所以位数÷4就是十六进制的总位数,例如16‘ha2的十六进制位数为16/4 = 4,因此完整写法为16’h00a2。32’h10的十六进制位数为32/4 = 8,所以完整写法为32’h00000010
对于数字而言,除了完整地按照位宽正常地写出各位的值以外,还有其他写法和注意事项:
● 值相同即可,bit数可以不相同,例如4’b0010 == 2’b10 == 2,这一点与数组相区别。
● 可通过下划线分割位数 以提高可读性,例如12’b1011_1100_1101
● x 可用来表示不定值,z或?可用来定义高阻值,且均可以作为十六、八、二进制的一位,例如 4’hx == 4’bxxxx,4’b1x0z等等。
● bit数与数字的二进制位数不相等的,前补0,只有最左边的x或z才具有扩展性
这一点容易弄混,例如8’ha == 8’b0000_1010, 4’b1 == 4’b0001,16’h4x == 16’h004x, 8’bzx == 8’bzzzzzzzx, 8’hx == 8’hxx
● 十六进制数字部分不区分大小写, 不定值x 和 高阻值z 也不区分大小写
可以为某个参数定义某个常量数值,方便程序中使用,例如
parameter width = 1,
polarity = 2;
length = width * 10;
localparam phy = 1.396;
localparam pi = 3.14159;
begin
a = 2 * pi * r; //等价于2*3.14159*r
...
end
无论是localparam还是parameter,一定要注意定义的是常量!是常量!值不可以更改的!上面的代码中出现pi = pi +1;
是错误!想要更改的话要把类型定义成integer或是large等等的变量!
而parameter和localparam的区别是,parameter参数可通过其他模块重新配置,而localparam为私有参数常量不可被外部配置。
那么如何外部模块如何对该模块的parameter进行配置呢?一般在例化时指定
module block#( //这里不可出现localparam!
parameter width = 1,
parameter polarity = 2;
parameter length = width * 10;
)(
input a,
input b,
output c
);
//...
endmodule
module top;
//...
block u_block#(
.width (`WIDTH),
.polarity (`POLARITY),
.length (`LENGTH)
)(
.a (a_i),
.b (b_i),
.c (c_o)
);
endmodule
parameter其实很像即将要讲到的宏定义’define,二者均可以进行参量代换,但是区别在于:parameter只能表示数字且可以在其他模块进行修改
'define可以表示任意量甚至是表达式的一部分,例如'define DelayNand nand #5
就可以用于表示DelayNand n1(a,b,c)
,等价于nand #5 n1(a,b,c);
,但是’define一旦定义不可修改。
注意define使用结尾不加分号
即在程序运行过程中其值可以改变的量。
注意:禁止在initial, always块中进行常量、变量的定义
即导线,属于线网net型,用于描述模块内部各结构实体(例如门)之间的连接。因此wire型均为无符号数
常用assign 描述的组合逻辑信号,模块中的输入输出自动定义为wire,默认为高阻值z
wire型可以指多个位的变量,如下所示定义方式。
wire a;
wire [31:0] b;
wire [32:1] c,d; //位不好数的时候可以将最低位下标设为1
既然是导线的含义,通过如下例子解释wire的含义:
module compose(input a, input b, output c) //可以写成input wire a, input wire b, output wire c
wire mid;
assign mid = a && b,c = ~mid;
endmodule
模块结构如下图所示,确实是一根导线。
来自于register寄存器,可用来表示数据存储器、寄存器、触发器等等。
常用于always模块中代表触发器,并且always模块中 每一个被赋值的信号 都必须是reg型,并且reg型默认为不定值x
见如下定义方式
reg [31:0] r1,r2,r3; //定义了3个寄存器r1,r2,r3,每个寄存器有32bit
reg r4 [9:0]; //定义了10个寄存器,r4[0]...r4[9],每个寄存器有1bit
reg [31:0] r5 [9:0]; //定义了10个寄存器,r5[0]...r5[9],每个寄存器有32bit
always@(posedge clk)
begin
r5[0][0] <= 1;
r5[0][1] < =0;
end
需要说明的是reg型可以被赋予负值,但是reg型数据实际上表示的是无符号数。
无符号数可按照如下数字循环取到,以3bit为例:
reg signed [7:0] a;
有符号数中,
● 负数的表示方法为 符号位(1为负)+补码,例如-3在verilog中就表示为4’b1101,-5就表示为4’b1011。
● 正数的表示方法为 符号位(0为正)+原码。
计算时直接相加即可,例如
reg signed [3:0] a = 4'b1011; //-5
reg signed [3:0] b = 4'b0100; //4
reg signed [3:0] sum;
assign sum = a + b; // 4-5 = 4 + (-5) = 4'b1111,为-1
reg signed [3:0] a = 4'b1101; //-3
reg signed [3:0] b = 4'b0101; //5
reg signed [3:0] sum;
assign sum = a + b; // 5-3 = 5 + (-3) = 5'b10010,截掉最高位为1
在SystemVerilog中为了避免纠结变量类型是wire还是reg,引入了新的变量类型logic,系统会自动推断是wire还是reg。
但是logic类型变量并不允许多于一次的持续赋值or输出端口给同一变量赋值
详情可参考systemverilog中logic变量的使用
由于经常出现模块类型定义和子模块变量传递类型的矛盾,此处对reg和wire的使用做如下总结:
● always中 被赋值为reg,assign中 被赋值为wire
● 顶层模块:输入为wire,输出为 reg/wire
● 子模块实例:输出用wire连接,如下图所示:
其本质还是reg型,只不过是一种有符号的32位寄存器类型,其实直接看成整数变量即可。
例如常见的循环for(integer i = 0; i <= 10 ;i = i + 1)
注意verilog中变量没有小数的概念,可以有小数的运算,但是最终输入输出的值永远是整数。毕竟是数字电路只有高电平1和低电平0二者之分。
有很多运算符,可分成单目、双目和三目三种。
比较简单,注意如果有个操作数存在不确定值x,则算术运算结果也为x
%表示求余运算符,但是余数符号只与第一个操作数一致
例如 -14%3 = -2,但 14%-3 = 2
实际设计中+、-可随意使用,但*、/、%、**不建议使用,因为无法判断最后会综合成什么电路。
那么怎么作这些计算呢?使用各种封装的IP模块,用多周期的流水设计实现运算结果,综合可控
表示一个操作数或两个操作数相互按位一位一位地进行关系运算。
位数不同则自动高位补0
下面举例说明
wire [3:0] a;
wire [2:0] b;
assign a = 4'b1010;
assign b = 3'b011;
wire [3:0] c;
begin
c = ~a; // 按位取反,c为 4'b0101
c = a & b; // 按位与,c为 4'b0010
c = a | b; // 按位或,c为 4'b1011
c = a ^ b; // 按位异或,c为 4'b1001
c = a ^~ b; // 按位同或,c为 4'b0110
end
位运算符同样可以一个操作数的不同位一位一位的相互运算,即
wire d;
begin
d = & a; // 等价于a[0]&a[1]&a[2]&a[3],d为 1'b0
d = | a; // 等价于a[0]|a[1]|a[2]|a[3],d为 1'b1
end
注意逻辑运算符永远只能是单个bit进行运算,不能如~|&这种对应为相互运算。
wire a,b;
assign a = 1'b1;
assign b = 1'b0;
wire [3:0] c;
begin
c = !(a && b); //c为 4'b0001
c = !(a || b); //c为 4'b0000
end
但注意取反运算符~和!的区别,二者都可以对一个bit进行取反操作,但是多个bit只能用 ~。
常用!来进行低电平判断:
always@(negedge reset)
begin
if(!reset)
...
end
如果关系是模糊的,则返回不定值x
注意的变量值为x或z时a===b
一律返回false,只有a与b均为0或均为1时才会返回true。例如a==b
在a为x、b为1仍然为true,而a===b
就为false
左移位后补0,右移位删除对应位。
注意变量位数是确定的,左移位可能会消掉某些高位,见下例
reg [4:0] a;
reg [6:0] b;
initial
begin
a = 10;
#10 a = (a<<2); //a原为5'b01010,移位之后为5'b01000
b = 10;
#10 b = (b<<2); //b原为7'b0001010,移位之后为7'b0101000
end
将某些信号的某些bit拼接在一起形成新的值,
注意一些写法,例如{3{w}}={w,w,w}
,还可以加入常量,例如
wire [3:0] a,b;
assign a = 4'b1011;
assign b = 4'b0101;
wire [16:1] c;
assign c = {a,b[2:0],a[1],a[2],1'b1,1'b0,2{a[1],b[2]}};
//c为 16'b1011_101_1_0_1_0_11_11
很多语句都与C语言非常类似。
将多条语句组合在一起,非常类似于C中的大括号。
begin…end内部的语句是顺序执行的,并且可以为该顺序块命名,执行完最后一条语句跳出。
begin:give //命名不是必须
a = b;
c = d;
end
别忘了可以使用#10
进行延迟执行。
'timescale 1ns //定义时间单位
module seq
reg [2:0] r;
initial r = 4'b000;
begin
#5 r = 4'b001;
#10 r = 4'b010;
#15 r = 4'b011;
#20 r = 4'b100;
end
endmodule
变量r
产生的波形如下,其中delay表示持续的时间
也就是说 顺序块中的延迟时间为上一条语句执行结束之后等待的时间。
即并行块中的语句为并行执行的,即所有语句同时开始执行,这个在C中没有定义。
所以如果要产生与seq
模块相同的波形,使用并行块写法如下:
'timescale 1ns //定义时间单位
module para
reg [2:0] r;
initial r = 4'b000;
fork
#5 r = 4'b001;
#15 r = 4'b010;
#30 r = 4'b011;
#50 r = 4'b100;
join
endmodule
由于是并行的,所以一定要检查并行块是否存在矛盾的语句,即竞争冒险现象,例如重复赋值等等。
同时块语句相互之间可以嵌套,例如
begin
x = 1'b0;
fork
#5 y = 1'b1;
#10 z = {x,y};
join
end
可用于动态生成代码
例如
genvar i;
generate if(`DATA1 > `DATA2) //只有满足if条件才会生成下面的代码
for(i=0;i<`WIDTH;i=i+1) //以for循环的方式生成多行相同代码
always@(posedge clk) begin
//...
end
else
//...
endgenerate
可用disable使任意块语句停止运行,也可用于跳出循环,类似于break
但注意必须提前给块语句命名才能disable,例如要终止块语句block1则有disable block1;
例如跳出循环
begin:block1
while(i<16)
begin:block2
if( flag[i] )
begin
$display("True bit at %d", i);
disable block1; //跳出while,类似break
end
else
begin
$display("Wrong bit at %d",i);
i = i + 1;
disable block2; //跳出本次循环,类似continue
end
end
end
上面的语句意思是如果变量flag
的第i
位为1就disable block1
,即跳出循环,表达出C里面break
的效果。
如果flag
的第i
为为0,就先移动到下一位然后disable block2
,之后根据while语句的规则,继续循环判断下一位是否为0,因此此处的disable
表达出continue
的效果。
所以如果将disable block1
替换成i = i + 1; disable block2;
循环的意思就变成了检查flag
中所有的bit是否为1,此处就不再是break
的效果而是continue
。
非常熟悉的条件语句,常见写法如下:
if(!rst)
begin
instruction = 0;
index = 0;
end
else
begin
instruction = sa;
index = index + 1;
end
但是有一些需要注意的地方
● if后面的条件表达式结果,0,x,z均为假,只有1为真
● 在同一个块内,else只与最近的if 相对应
见下面的代码,请问下面的那个else
是与哪个if
相对应呢?
if(index>0)
for(i = 0;i<index;i++)
if(memory[i]>0)
memory[i] = 0;
else
$display("error");
答案是第二个if
,这是由于第二个if
并没有在顺序块内,所以else
可以与之匹配,但如果是下面这种情况就是else
与第一个if
相匹配了
if(index>0)
begin
for(i = 0;i<index;i++)
if(memory[i]>0)
memory[i] = 0;
end
else
$display("error");
● 多个if嵌套需要用 begin…end包括
例如下面的代码并不等价于if(a>b && c>d) sig<= 1'b1;
if(a>b)
if(c>d)
sig <= 1'b1;
上面代码中即使c>d
满足也会执行sig <= 1'b1;
这是因为上面的代码本质是
if(a>b) //a>b满足什么都不做
if(c>d) //c>d满足则赋值
sig <= 1'b1;
如果想等价于if(a>b && c>d) sig<= 1'b1;
就要加入begin…end块
if(a>b) begin
if(c>d) //等价于if(a>b && c>d)
sig <= 1'b1;
end
多分枝选择语句,常用于指令译码、状态机等。
通过计算case后面括号内部的表达式,找到与表达式相等的分支项,注意比较时位宽必须相等,x与z也必须严格在对应的bit上彼此相等,则执行该分支项后面的语句,执行完后跳出case结构
如果没有与表达式相等的分支项,则执行default
后面的语句,且default
只有一项。
例子如下,注意写法,冒号等写法:
case(rega)
2'b00: res = 4'b1110;
2'b01: res = 4'b1101;
2'b10: res = 4'b1011;
2'b11: res = 4'b0111;
2'bx,
2'bz: res = 4'bx;
default: begin
res = 4'bx;
$display("Input is wrong.");
end
endcase
注意HDL中的case与C++中的switch…case有着很大的区别,首先是写法:
switch(grade) { case 'A' : cout << "很棒!" << endl; break; case 'B' : case 'C' : cout << "做得好" << endl; break; case 'D' : cout << "您通过了" << endl; break; case 'F' : cout << "最好再试一下" << endl; break; default : cout << "无效的成绩" << endl; }
然后HDL中case执行完对应的语句之后直接跳出,而C++的switch则是执行完某个case之后会继续执行下一个case的内容,直到遇到break才会跳出switch。
HDL中的case与状态机的原理是对应的:状态机就是在某个状态下经过某个条件转换到下一个状态,所以case语句在找到分支之后就直接跳出,而不会再执行下一个分支。
这两种语句是为了处理比较过程中不必考虑的情况
casez不考虑高阻值z的比较过程,即 控制表达式或分支表达式 的某个bit出现z,该bit就不再参与比较,但x还是要进行严格比较的
例如:
casez(ir)
4'bzzz1: out = a;
4'bzz1z: out = b;
4'bz1zz: out = c;
default: out = d;
endcase
但存在一个问题:如果存在多个相符的分支,该如何判定执行哪个?例如上述代码中ir = 4'bzzzz
注意case不会出现这种情况,因为case要求对应的bit严格相等才行,x必须与x对应,z也必须与z对应
答案是最上面的分支语句被执行,若ir = 4'bzzzz
虽然与4'bzzz1
、4'bzz1z
、4'bz1zz
都匹配但out = a
,也就是说系统的匹配检查是从第一个语句开始依次检查的。结果可参考Verilog语法有关casez和casex的分析。
而casex则表示z和x都被视为不必关心的情况。
这两个语句与C中的循环语句相同,各子循环按顺序执行,但注意使用块语句begin…end。
while(temp)
begin
if(temp[0])
count = count + 1;
end
for(integer i = 0;i <= 10;i = i + 1)
temp[i] = 0;
再次复习一下for循环各表达式的执行顺序:
若定义:
for(表达式1;表达式2;表达式3) 循环语句;
即将某个值赋给某个量,包括两种方式:阻塞赋值和非阻塞赋值。
● 阻塞赋值 =
例如a = b;
表示在执行该语句的时候直接计算b的值,并 立刻改变 a的值。
这个与C语言是相同的,并且下面再用到a时,使用的是新值。
但是需要注意的是verilog的并行执行的特点有时会产生竞争与冒险
always@(posedge clk)
y1 = y2;
always@(posedge clk)
y2 = 0;
在上述代码中当clk上升沿的时候,y1 = y2;
和y2 = 0;
应是同时执行的,而系统内部实际的执行顺序是不确定的,不同的顺序会导致不同的结果。
注意阻塞赋值包括计算右侧值 + 赋予左侧变量 两个步骤,执行这两个步骤时绝不允许任何其他其他语句的干扰,阻塞的含义就在于此。
● 非阻塞赋值 <=
非阻塞赋值的符号与小于等于的符号是相同的。例如a<=b;
表示先计算b的值,直到块结束之后才改变a的值。
所以在块内的下面的语句用的a值是旧值。
关键在于赋值操作是何时实现的,从下面这个例子可以看出区别:
module BlockAndUnblock;
integer a,b,c;
reg clk;
initial
begin
clk = 0;
a = 1;
b = 2;
c = 3;
end
assign #10 clk = ~clk;
always@(posedge clk)
begin
b <= a; //阻塞赋值就为 b = a;
c <= b; //阻塞赋值就为 c = b;
end
endmodule
如果是非阻塞赋值,在一个clk上升沿到来时,b的值为a的值,c的值为b的旧值,综合的电路如下:
但如果是阻塞赋值(注释),在上升沿来了之后,b和c均为a的值,综合的电路如下:
也就是说在always模块中,如果是阻塞赋值,效果就可以概括为时钟到来时,赋值结果直接完成,表达的是组合逻辑(该时刻的输出仅仅与该时刻的输入有关)。
如果在always模块中采用非阻塞赋值,效果就是时钟到来时,每个赋值操作含有旧值(例如c<=b;
这一句),因此表达的是时序逻辑(该时刻的输出与原来电路的状态有关)
此处这么理解,还是上面模块BlockAndUnblock的例子,将a看作输入c看作输出,其余部分看作电路,b就为电路内部的量。
当clk上升沿到来之后,采取阻塞赋值c就是a的值(组合逻辑),而采取非阻塞赋值c的结果不是a的值,而是另一个电路内部的原来的值b(时序逻辑)。
因此得到结论:建立时序逻辑电路模型时,用非阻塞赋值;建立组合逻辑电路模型时,用阻塞赋值;时序和组合逻辑电路模型时,用非阻塞赋值。
一些常见的系统函数如下
以及最重要的 $clog2(),注意这是个向上取整函数
如果一件事情需要好几个阶段一步一步地完成,各个步骤之间没有反馈,而且这件事情需要重复做很多次,就可以使用pipeline。
pipeline的核心思想是若前一个事件的某个阶段已经完成,则立即进行下一个事件的该阶段工作
例如有一家咖啡吧有客人ABCDE依次排队买套餐。
一般为客人供给食物的策略是:员工甲放个盘子,员工乙装上薯条,员工丙放上豌豆,老板最后配上一杯饮料,完成对客人A的服务,然后再服务下一位客人B服务,顺序依次。
但是这样的策略有个问题:员工甲给了客人盘子之后,就一直闲着直到该客人套餐购买完全结束,才为下一个客人服务。其他员工同样存在相同的问题
而pipeline的策略是:在甲为客人A放完盘子之后,为客人B放盘子,再为客人C放盘子,直到客人E。其他员工也是这样的策略。
与核心思想类似,可将一个组合逻辑电路分为好几个级,这些级只有前馈没有反馈。
一般情况下是对于某个输入,该电路的所有级全部处理完输出结果之后,再更换新的输入再运行。
而使用pipeline的方法是:在每一级之后插入寄存器组暂存数据,所有寄存器同步触发
下面分析这样做的好处,如下图所示是否采用pipeline结构示意图:
在采用了pipeline结构时,考虑每个 组合逻辑级传输延迟 T p d T_{pd} Tpd 、 寄存器传输延迟 T c o T_{co} Tco 和 时钟周期 T c l k T_{clk} Tclk,时序图为:
可以看出,组合逻辑中并不是整体处理完输入a之后才处理输入b,而是每一级处理完a在下一个脉冲到来时直接处理b。
从图中可以看出,在输出 K ( J ( F ( a ) ) ) K(J(F(a))) K(J(F(a)))时消耗了很长时间,但是在此之后每来一个时钟就会输出一个值。
注意 T p d + T c o < T c l k T_{pd}+T_{co}
就此提出以下概念:
● 首次延迟: 从开始输入第一个值到该值输出 所消耗的时间。
● 吞吐延迟: 每个输出之间的时间间隔。
并给出以下表格(忽略触发器的建立时间):
项目 | 不使用pipeline | 使用pipeline |
---|---|---|
首次延迟 | 3 T p d 3 T_{pd} 3Tpd | 最大为 3 T c l k + T c o + T p d 3 T_{clk}+T_{co}+T_{pd} 3Tclk+Tco+Tpd |
吞吐延迟 | 3 T p d 3 T_{pd} 3Tpd | T c l k T_{clk} Tclk |
所以如果组合逻辑非常冗长,即 3 T p d > > T c l k 3 T_{pd}>>T_{clk} 3Tpd>>Tclk,则数据的输入时间间隔or输出时间间隔就很长,吞吐量就很小。
但如果采用了pipeline,数据的输入时间间隔or输出时间间隔被控制在 T c l k T_{clk} Tclk以内,即短时间内输入or输出多个数据,因此提高了吞吐量。
对于时序电路而言,触发器是必不可少的元件,有了触发器才能够实现按照时钟的时序控制。
实际上,每一拍(时钟有效沿)的控制都可看作是电路状态的转换,类似于离散时间状态方程:
x ( k + 1 ) = A x ( k ) + B u ( k ) x(k+1)=Ax(k)+Bu(k) x(k+1)=Ax(k)+Bu(k)
而如果无论怎么控制,数字电路的状态都是有限的,且均是基于时钟沿进行转换的,这就构成了有限状态机。
此处给出标准的状态机Verilog模块写法,状态转移图如下所示:
方框表示状态名称,箭头表示状态转移,箭头上的标注表示:转移条件/输出结果。
建议的Verilog语言如下:
module FSM(
input clk,
input reset,
input A,
output reg K1,
output reg K2
);
reg [3:0] state;
状态码编写///
parameter Idle = 4'b0001,
Start = 4'b0010,
Stop = 4'b0100,
Clear = 4'b1000;
仅状态转移逻辑///
always@(posedge clk)
begin
if(reset)
state <= Idle;
else
case(state)
Idle:if(A)
state <= Start;
else
state <= Idle;
Start:if(!A)
state <= Stop;
else
state <= Start;
Stop:if(A)
state <= Clear;
else
state <= Stop;
Clear:if(!A)
state <= Idle;
else
state <= Clear;
default:state <= 4'bxxxx;
endcase
end
仅输出逻辑///
always@(state or reset or A)
if(!reset)
K1 = 0;
else if(state == Clear && !A)
K1 = 1;
else
K1 = 0;
always@(state or reset or A)
if(!reset)
K2 = 0;
else if(state == Stop && A)
K2 = 1;
else
K2 = 0;
endmodule
下面总结写状态机的几个注意点:
● 使用parameter确定状态码, 状态码尽量采用独热码,即只有一位为1的编码形式
这里说明以下格雷码和独热码。
格雷码:相邻的代码中只有一bit不同,例如十进制1到5就为000,001,011,010,110,111
其优势在于相邻数字的变化均只对应一个bit变化,相比于普通的二进制码,可靠性高错误率低。比如二进制码从0111到1000变化过程中,可能出现类似1100的中间状态,进而可能引起电路不稳定,而格雷码就没有这个问题。
独热码:只有一bit为高的编码,例如00000,00001,00010,00100,01000,10000
优势也是显而易见,只需比较一bit即可得到结果,但需要消耗更多触发器,然而会节省很多组合逻辑。
对于状态机来说,如果用独热码只需比较对应的bit来确定在哪个状态即可,例如上面的代码中只需根据
state[0] == 1
即可判断当前状态是否为Idle。但是格雷码or二进制码就得每一位都要比较,消耗很多逻辑。
FPGA内部有丰富的Slice,每个Slice有几个FF,所以FPGA不缺触发器的,故使用独热码更好。
● 大型状态机,可将状态转移always逻辑和输出always逻辑分开写
● case语句必须加入default,其状态编码为’bx
● 有同步or异步复位端