这章节偷了懒,大多数都是文字识别复制的书本原话,加粗和红色字体标注了些自己觉得的重点。
这章节对规范Verilog写法很有帮助,看很多例子总结出一点自己的经验。
因为大多数 FPGA 内部的触发器数目相当多,又加上独热码状态机 (one hot state machine)的译码逻辑最为简单,所以在设计采用 FPGA 实现的状态机时,往往采用独热码状态机(即每个状态只有一个寄存器置位的状态机)。
建议采用 case,casex 或 casez 语句来建立状态机的模型,因为这些语句表达清晰明了,可以方便地从当前状态分支转向下一个状态并设置输出。不要忘记写上 case语句的最后一个分支default,并将状态变量设为‘bx,这就等于告知综合器:case 语句已经指定了所有的状态。这样综合器就可以制除不需要的译码电路,使生成的电路简洁,并与设计要求一致。
如果将默认状态设置为某一确定的状态(例如:设置 default: state = state1),这样做有一个问题需要注意。因为尽管综合器产生的逻辑和设置 default: state= ‘bx时相同,但是状态机的 Verilog HDL模型综合前和综合后的仿真结果会不一致。因为启动仿真器时,状态机所有的输入都不确定,因此立即进入 default 状态。这样的设置便会将状态变量设为 state1,但是实际硬件电路的状态机在通电之后,进人的状态是不确定的,很可能不是 state1 的状态,因此还是设置 default:state='bx 与实际情况更一致。但在有多余状态的情况下,可以通过综合指令将默认状态设置为某一确定的有效状态,因为这样做能使状态机在偶然进入多余状态后,仍能在下一时钟跳变时返回正常工作状态,否则会引起死锁。
状态机应该有一个异步或同步复位端,以便在通电时将硬件电路复位到有效状态,也可以在操作中將硬件电路复位(大多数 FPGA 结构都允许使用异步复位端)。
目前大多数综合器往往不支特在一个always块中由由多个事件触发的状态机(即隐含状态机,implicit state machine),为了能综合出有效的电路,用 Verlog HDL 描述的状态机应明确地由唯一时钟触发。如果设计要求必须有不同时钟触发的状态机,可以采用以下办法:编号另一个模块,在那个模块中使用另外一个时钟;然后用实例引用的方法在另外一个模块中把它们连接起来。为了使设计比较简单,调试比较容易,应该尽量使这两个状态机的时钟有一定的关系。例如甲模块的时钟是乙模块时钟同步计数器的输出。
把异步触发的电路转换为同步时钟触发的电路并不因难,目前大多数综合器不能综合,但可采用 Verilog HIDL描述的异步状态机转换为电路网表。异步状态机是没有确定时钟的状态机,它的状态转移不是由唯一的时钟跳变沿所触发。
之所以不能用异步状态机来综合的原因是因为异步状态机不容易规范触发的瞬间,因此不容易判别触发脉冲是正常的触发还是冒险竞争产生的毛刺。而同步状态机的时钟是由同一时钟产生的,通过特殊设计的全局时钟网络将唯一的时钟连接到每个触发器的时钟端,使得时钟沿到达每个触发器时钟端的时刻几乎完全相同。因此,只要知道组合逻辑的延迟,例如小于一个时钟周期或若千个时钟周期,就可以安排适当的时序让组合逻辑块有最快的稳定和正确的输出,并可靠地存在寄存器中,作为另一级电路的输入。若存在异步的触发时钟,则逻辑电路就很难用统一的方法来解决由于逻辑元件延迟产生的冒险和竞争现象出现的毛刺所造成的混乱。
千万不要使用综合工具来设计异步状态机。因为目前大多数综合工具在对异步状态机进行逻辑优化时会胡乱地简化逻辑,使综合后的异步状态机不能正常工作。如果一定要设计异步状态机,建议采用电路图输入的方法(或用实例引用的写法把几个引用的实例用异步时钟连接起来),而不要直接用 Verilog RTL 级别的描述方法通过综合来产生。
在Verilog HDL 中,状态必须明确赋值。通常使用参数(parameters)或宏定义(define)语句加上赋值语句来实现。
使用参数 (parameters)语句赋状态值见下列程序:
parameter state1 = 2 'h1, state2 = 2 'h2;
...
current_ state = state2; // 把current state设置成 2'h2
...
使用宏定义(define)语句赋状态值见下列程序:
'define state1 2'h1
'define state2 2'h2
...
current_state = 'state2; //把 current state 设置成 2'h2
通常,综合的一般原则为:
(1)综合之前一定要进行仿真,这是因为仿真会暴解逻辑错误,所以建议这样做。如果不做仿真,没有发现的逻辑错误会进入综合器,使综合的结果产生同样的逻辑错误。
(2)每一次布局布线之后都要进行仿真,在器件编程或流片之前要做最后的仿真。
(3)用 Verilog HDL 描述的异步状态机是不能综合的,因此应该避免用综合器来设计;如果一定要设计异步状态机,则可用电路图输人的方法来设计。
(4)如果要为电平敏感的锁存器建模,使用连续赋值语句是最简单的方法。
1. always块
(1)每个always块只能有一个事件控制"@(event-expression)”,而且要紧跟在always关键字的后面。
(2)always 块可以表示时序逻辑或者组合逻辑,也可以用 always 块既表示电平敏感的透明锁存器又同时表示组合逻辑,但是不推荐使用这种描述方法,因为这容易产生错误和多余的电平敏感的透明锁存器。
(3)带有posedge 或 nesedse 关键字的事件表达式表示沿触发的时序逻辑,没有 postedgee或 negedge 关键字的表示组合逻辑或电平敏感的锁存器,或者两种都表示。在表示时序和组合逻辑的事件控制表达式中,如有多个沿和多个电平,其间必须用关键字“or”连接。
(4)每个表示时序 always 块只能由一个时钟跳变沿触发,置位或复位最好也由该时钟跳变沿触发。
(5)每个在always 块中赋值的信号都必须定义成 reg 型或整型。整型变量默认为 32位,使用 Verilog 操作符可对其进行二进制求补的算术运算。综合器还支持整型变量的范围说明,这样就允许产生不是32位的整型量。句法结构为:integer[ < msb >:< lsb >] < identifier >。
(6)always块中应该避免组合反馈回路。每次执行 always 块时,在生成组合逻辑出always块中赋值的所有信号必须有明确的值;否则,需要设计者在设计中加入电平敏感的锁存器来保持赋值前的最后一个值。只有这样,综合器才能正常生成电路,如果不这样做,综合器会发出警告,提示设计中插入了锁在器。如果在设计中存在综合器认为不是电平敏感锁存器的组合回路,综合器会发出错误信息(例如设计中有异步状态机时)。
上达原则不太好理解,再解释一下。这也就是说,用always 块设计纯组合逻辑电路时,在生成组合逻辑的 always 块中参与赋值的所有信号都必须有明确的值,即在赋值表达式右端参与赋值的信号都必须在always @(敏感电平列表)中列出。如果在赋值表达式右端引用了敏感电平列表中没有列出的信号,那么在综合时,将会为该没有列出的信号隐含地产生一个透明锁存器。这是因为该信号的变化不会立刻引起所赋值的变化,而必须等到敏感电平列表中某一个信号变化时,它的作用才显现出来,也就是相当于存在着一个透明锁存器,即把该信号的变化暂存起来,待敏感电平列表中某一个信号变化时再起作用,纯组合逻辑电路不可能做到这一点。这样,综合后所得的电路已经不是纯组合逻辑电路了,这时综合器会发出警告,提示设计中插入了锁存器。见下例。
input a,b,c;
reg e,d;
always @(a or b or c)
begin
e = d & a & b;
/* 因为d没有在敏感电平列表中,所以d变化时,e不能立刻变化,要等到a或b或c变化时才体现出来。
这就是说,实际上相当于存在一个电平敏感的透明锁存器在起作用,把d信号的变化锁存其中 */
d = e | c;
end
2. 赋值
(1)对一个奇存器型(reg) 和整型(integer)变量给定位的赋值,只允许在一个always块内进行,如在另一 always 块中也对其赋值,这是非法的。
(2)把某一信号值赋为’bx,综合器就把它解释成无关状态,因而综合器为其生成的硬件电路最简洁。
【例1】8位带进位端的加法器的设计实例(利用简单的算法描述)
module adder 8(cout, sum,a, b, cin) ;
output cout;
output [7:0] sum;
input cin;
input[7: 0] a,b;
assign (cout, sum) =a+b+ cin;
enmodule
【例2】指令译码电路的设计实例(利用电平敏感的always块来设计组合逻辑)·
// 操作码的宏定义
`define plus 3'd0
`define minus 3'd1
`define band 3'd2
`define bor 3'd4
`define unegate 3'd4
module alu (out, opcode a,b):
output [7,0] out;
input [2:0] opcode;
input [7:0] a,b;
reg [7:0] out;
always @ (opcode or a or b)
// 用电平敏感的 always 块描述组合逻辑
begin
case(opcode)
// 算术运算
`plus: out = a + b;
`minus: out = a - b;
// 位运算
`band: out = a & b,
`bor: out = a | b;
// 单目运算
'unegate: out = ~a;
default: out = 8'hx:
endcase
end
endmodule
【例3】 利用task 和电平敏感的 always 块设计经比较后重组信号的组合逻辑
module sort4(ra,rb,re,rd,a,b,c,d);
parameter t=3;
output [t:0] ra, rb, re, rd,
input [t;0] a, b, c, d;
reg [t:0] ra, rb, re, rd;
always @(a or b or c or d)
// 用电平级感的 always 块描述组合逻辑
begin: local // 此处besin-end块必须有一模块名,因为块中定义了局部变量
reg [t:0] va, vb, ve, vd;
{va,vb, ve,vd} = {a,b,c,d};
sort2 (va, ve);
sort2 (vb, vd);
sort2 (va, vb);
sort2 (ve, vd);
sort2 (vb, vc);
{ra,rb,re,rd} = {va, vb, ve, vd};
end
task sort2;
inout [t:0] x, y:
reg [t:0] tmp,
if(x> y)
begin
tmp = x;
x = y;
y = tmp;
end
endtask
endmodule
【例4】 比较器的设计实例(利用赋值语句设计组合逻辑)
module compare(equal, a, b);
parameter size=1;
output equal;
input [size-1,0] a, b;
assign equal = (a == b) ? 1 : 0;
endmodule
【例5】3-8译码器设计实例(利用赋值语句设计组合逻辑)
module decoder(out,in) ;
output [7:0] out;
input [2:0] in;
assign out = 1'b1 << in;
/**** 把最低位的1左移in(根据从 in口输入的值)位,并赋予out ****/
endmodule
【例6】 8-3 编码器的设计实例
编码器设计方案之一:
module encoder1 (none on,out, in);
output none_on;
output [2,0] out;
input [7,0] in;
reg [2,0] out;
reg none_on;
always @(in)
begin: local // 此处begin-end块必须有一模块名,因为快中定义了局部变量
integer i;
out = 0;
none_on = 1;
/* 返回输入信号品的8位中,为1的最高位数*/
for(i = 0; i < 8; i = i + 1)
begin
if( in[i] )
begin
out = i;
none_on =0;
end
end
end
endmodule
编码器设计方案之二:
module encoder2 ( none_on, out?, outl, out0, h, g, f, e, d, c, b, a);
input h, g, f, e, d, c, b, a;
output none_on, out2, out1, out0;
wire [3:0] outvec;
assign outvec= h? 4'b0111 : g? 4'b0110 : f? 4'b0101:
e ? 4'60100 : d? 4'b0011 :c? 4'b0010 : b? 4'b0001:
a? 4'60000 : 4'b1000;
assign none _on = outvec[3];
assign out2 = outvec[2];
assign out1 = outvec[1];
assign out0 = outvec[0];
endmodule
编码器设计方案之三:
module encoder3 (none, on, out2, outl, out0, h, g,f, e, d, c, b, a);
input h, g, f, e, d, c, b, a;
output out2, out1, out0;
output none_on;
reg [3,0] outvec;
assign (none_on,out2,out1,out0} = outvec;
always @( a or b or c or d or e or for g or h)
begin
if(h) outvec = 4'b0111;
else if(g) outvec = 4'b0110;
else if(f) outvec = 4'b0101;
else if(e) outvec = 4'b0100;
else if(d) outvec = 4'b0011;
else if(c) outvec = 4'b0010;
else if(b) outvec = 4'b0001;
else if (a) outvec = 4'b0000;
else outvec = 4'b1000;
end
endmodule
【例7】多路器的设计实例
使用连续赋值、case 语句或 if-else 语句可以生成多路器电路。如果条件语句(case 或 if-else)中分支条件是互斥的话,综合器能自动地生成并行的多路器。
多路器设计方案之一:
module muxl (out, a, b, sel);
output out.
input a, b, sel;
assign out = sel? a : b;
endmodule
多路器设计方案之二:
module mux2( out, a, b, sel);
output out;
input a, b, sel;
reg out;
// 用电平触发的 always 块来设计多路器的组合逻辑
always @( a or b or sel)
begin
/*检查输人信号 sel 的值,如为1,输出 out 为a,如为 0,输出 out 为 b.*/
case( sel)
1'b1: out = a;
1'b0: out = b;
default: out = 'bx;
endcase
end
endmodule
多路器设计方案之三:
module mux3( out, a, b, sel);
output out;
input a, b, sel;
reg out;
always @(a or b or sel)
begin
if( sel)
out = a;
else
out = b;
end
endmodule
【例8】 奇偶校验位生成器设计实例
module parity( even_numbits,odd_numbits,input_bus);
output even_ numbits, odd_numbits;
input [7,0] input bus;
assign odd_numbits = -input_bus;
assign even_numbits = ~odd_numbits;
endmodule
【例9】 三态输出驱动器设计实例(用连续赋值语句建立三态门模型)
三态输出驱动器设计方案之一:
module trist1( out, in, enable);
output out:
input in, enable:
assign out = enable? in; 'bz;
endmodule
三态输出驱动器设计方案之二:
module trist2( out, in, enable );
output out;
input in, enable;
// bufif1是一个Verilog门级原语(primitive)
bufif1 mybuf1 (out, in, enable) ;
endmodule
【例10】三态双向驱动器设计实例
module bidir(tri inout, out, in, en, b) ;
inout tri_inout;
output out;
input in, en, b;
assign tri_inout = en ? In : 'bz;
assign out = tr_inout ^ b;
endmodule
【例11】 触发器设计实例
module dif( q, data, clk);
output q;
input data, clk;
reg q;
always @( posedge clk)
begin
q <= data;
end
endmodule
【例12】电平敏感型锁存器设计实例之一
module latch1( q, data, elk);
output q;
input data, clk;
assign q = clk? data : q;
endmodule
【例13】带置位和复位端的电平敏感型锁存器设计实例之二
module latch2( q, data, clk, set, reset);
output q;
input data, clk, set, reset;
assign q= reset? 0 : (set? 1 : (clk? data : q) );
endmodule
【例14】电平敏感型锁存器设计实例之三
module latch3( q, data, clk);
output q;
input data, clk;
reg q;
always @(clk or data)
begin
if(clk)
d = data;
end
endmodule
注意:有的综合器会产生一个警告信息,告诉产生了一个电平敏感型锁存器。因为设计的就是一个电平敏感型锁存器,就不用管这个警告信息。
【例15】移位寄存器设计实例
module shifter( din, clk, cIr, dout);
input din, cik, clr;
output [7:0] dout;
reg [7:0] dout;
always @(posedge clk)
begin
if(cir) // 清零
dout <= 8'b0;
else
begin
dout <= dout<<1; // 左移一位
dout[0] <= din; // 把输人信号放人寄存器的最低位
end
end
endmodule
【例16】8位计数器设计实例之一
module counter1( out, cout, data, load, cin, clk);
output [7;0] out;
output cout;
input [7:0] data;
input load, cin, clk;
reg [7:0] out,
always @(posedge clk)
begin
if( load )
out <= data;
else
out <= out + cin;
end
assign cout= (& out) & cin;
// 只有当 out[7:0]的所有各位都为1,并且进位 cin 也为1时才能产生进位 cout
endmodule
【例17】8 位计数器设计实例之二
module counter2( out, cout, data, load, cin, clk);
output [7:0] out;
output cout;
input [7:0] data;
input load, cin, clk;
reg [7:0] out;
reg cout;
reg [7:0] preout;
// 创建 8位寄存器
always @(posedge clk)
begin
out <= preout;
end
/****计算计数器和进位的下一个状态。注意:为提高性能不希望加我影响进位****/
always @( out or data or load or cin)
begin
{cout, preout) = out + cin;
if (load)
preout = data;
end
endmodule
该小结举例了 典型的 可综合的 组合逻辑 和 时序逻辑 电路,对于规范Verilog写法很有参考意义。
因为此前没有系统学习Verilog语言,在实际写代码实现时很多不太明白的地方,比如在模块中输入输出和模块实例化的输入输出的变量该定义成reg还是wire类型,assign赋值和always块等等,很不清楚。看了这节的十几个例子,自己总结如下(不一定正确……)。
对于模块module m ( input a, output b);
input a 默认是wire,不可以定义为reg;
output b 默认是wire,可以定义为reg。
对于模块实例化 m exmaple_m(.a(exmaple_a), .b(example_b));
example_a可以是wire和reg;
exmaple_b可以时wire不可以是reg。
对于组合逻辑模块modul(…);
输入和输出都是wire,直接使用assign赋值;(【例1】)
输入是wire输出是reg,用电平敏感的always块描述。(【例2】)
对于时序逻辑模块module(clk, …);
输入和输出都是wire,可以使用assign out = clk ? …表达;(【例12、13】)
输入是wire输出是reg,always@(posedge clk)和非阻塞赋值<=描述。(【例11】)
异步置位与复位是与时钟无关的。当异步置位与复位到来时它们立即分别置触发器的输出为1或0,不需要等到时钟沿到来才置位或复位。**把它们列入 always 块的事件控制括号内就能触发 always 块的执行。**因此,当它们到来时就能立即执行一次指定的操作。所以当触发条件(如时钟的跳变沿)反复出现时,就可以反复执行指定的操作。
状态机的异步置位与复位是用always 块和事件控制实现的。如下为事件控制的语法:
(1)事件控制语法:
@(<沿关键词 时钟信号
or 沿关键词 复位信号
or 沿关键词 置位信号>)
沿关键词包括 posedge(用于高电平有效的 set 、reset 或上升沿触发的时钟)和negedge(用于低电平有效的 set、 reset 或下降沿触发的时钟),信号可以按任意顺序列出。
(2)事件控制实例:
① 异步、高电平有效的置位(时钟的上升沿):@ (posedge clk or posedge set)。
② 异步、低电平有效的复位(时钟的上开沿):@ (posedge clk or negedge reset)。
③ 异步、低电平有效的置位和高电平有效的复位(时钟的上升沿):@( posedge clk or negedge set or posedge reset )。
④ 带异步、高电平有效的置位与复位的 always 块样板:
always @(posedge elk or posedge set or posedge reset)
begin
if(reset)
begin
/* 置输出为0*/
end
else
if (set)
begin
/* 置输出为 1*/
end
else
begin
/* 与时钟同步的逻掛*/
end
end
⑤ 带异步高电平有效的置/复位滑的D触发器实例:
module (q, qb, d, clk, set, reset);
input d, clk, set, reset;
output q, qb;
// 声明g和gb为reg 类型,因为它需要在always 块内赋值
reg q, qb:
always @( posedge clk or posedge set or posedge reset )
begin
if (reset)
begin
q<=0;
qb<=1;
end
else
if (set)
begin
q<= 1;
qb<=0;
end
else
begin
q<=d;
qb<=~d;
end
end
endmodule
同步置位与复位是指只有在时钟的有效跳变沿时刻置位或复位,信号才能使触发器置位或复位(即,使触发器的输出分别转变为逻辑1或 0)。因此不要把set和reset 信号名列入always 块的事件控制表达式,因为当它们有变化时不应触发always块的执行。 相反,always 块的执行应只由时钟有效跳变沿触发,是否置位或复位应在always 块中首先检查 set 和 reset信号的电平。所以,set 或reset 的电平维持时间必须大于时钟沿的间隔时间,否则set 和 reset不能每次都能有效地完成置位和复位的工作。为此在编写测试模块时和设计与其配合的电路时要注意这个问题。
(1)事件控制语法:
@(<沿关键词 时钟信号>)
其中,沿关键词是指posedge(正沿触发的时钟)或 negedge(负沿触发的时钟)。
(2)事件控制实例:
① 正沿触发:@(posedge clk)
② 负沿触发:@(negedge clk)
③ 同步的具有高电平有效的置位与复位端的 always 块样板:
always @(posedge clk)
begin
if (reset)
begin
/*置输出为0*/
end
else
if (set)
begin
/* 置输出为 1*/
end
else
begin
/*与时钟同步的逻辑*/
end
end
④ 同步的具有高电平有效的置位/复位端的D触发器:
module dff2( q, qb, d, clk, set, reset) ;
input d, clk, set, reset;
output q, qb;
reg q, qb;
always @(posedge clk)
begin
if (reset)
begin
q<=0;
qb<=1;
end
else
if (set)
begin
q<=1;
qb<=0;
end
else
begin
q<=d;
qb<=~d;
end
end
endmodule