第三章 VERILOG
4 Verilog HDL基本语句
4.1 顺序块语句
语句块块提供将两条或更多条语句组合成语法结构上相当于一条语句的机制。这里主要讲Verilog HDL的顺序语句块(begin . . . end):语句块中的语句按给定次序顺序执行。顺序语句块中的语句按顺序方式执行。每条语句中的时延值与其前面的语句执行的模拟时间相关。一旦顺序语句块执行结束,跟随顺序语句块过程的下一条语句继续执行。顺序语句块的语法如下:
begin [ :block_id{declarations} ]
procedural_statement ( s )
end
例如:
/ / 产生波形:
begin
#2 Stream = 1;
#5 Stream = 0;
#3 Stream = 1;
#4 Stream = 0;
#2 Stream = 1;
#5 Stream = 0;
End
假定顺序语句块在第10个时间单位开始执行。两个时间单位后第1条语句执行,即第12个时间单位。此执行完成后,下1条语句在第17个时间单位执行(延迟5个时间单位)。然后下1条语句在第20个时间单位执行,以此类推。该顺序语句块执行过程中产生的波形如图:
图 33
4.2 连续赋值语句
数据流的描述是采用连续赋值语句(assign)语句来实现的。语法如下:
assign net_type = 表达式;
连续赋值语句用于组合逻辑的建模。等式左边是wire类型的变量。等式右边可以是常量、由运算符如逻辑运算符、算术运算符参与的表达。如下几个实例:
wire [3:0] Z, Preset, Clear; //线网说明
assign Z = Preset & Clear; //连续赋值语句
wire Cout, C i n ;
wire [3:0] Sum, A, B;
. . .
assign {Cout, Sum} = A + B + Cin;
assign Mux = (S = = 3)? D : 'bz;
注意如下几个方面:
1、连续赋值语句的执行是:只要右边表达式任一个变量有变化,表达式立即被计算,计算的结果立即赋给左边信号。
2、连续赋值语句之间是并行语句,因此与位置顺序无关。
4.3 条件语句
4.3.1 条件语句 if
语句的语法如下:
if(condition_1)
procedural_statement_1
{else if(condition_2)
procedural_statement_2}
{else
procedural_statement_3}
如果对condition_1 条件满足,不管condition_2是否满足,都执行procedural_statement_1,procedural_statement_2和procedural_statement_3都不执行。
如果condition_1不满足而condition_2满足,则执行procedural_statement_2,procedural_statement_1和procedural_statement_3都不执行。
如果condition_1不满足并且condition_2也不满足时,执行procedural_statement_3,procedural_statement_1和procedural_statement_2都不执行。
以下是一个例子。
if(Sum < 60) begin
Grade = C;
Total_C = Total _c + 1;
end else if(Sum < 75) begin
Grade = B;
Total_B = Total_B + 1;
end else begin
Grade = A;
Total_A = Total_A + 1;
end
注意条件表达式必须总是被括起来,如果使用if - if - else 格式,那么可能会有二义性,如下例所示:
if(C l k)
if(R e s e t)
Q = 0;
else
Q = D;
问题是最后一个else 属于哪一个if? 它是属于第一个if 的条件(Clk)还是属于第二个if的条件 (Reset)? 这在Verilog HDL 中已通过将else 与最近的没有else 的if 相关联来解决。在这个例子中, else 与内层if 语句相关联。
以下是另一些if 语句的例子。
if(Sum < 100)
Sum = Sum + 10;
if(Nickel_In)
Deposit = 5;
elseif (Dime_In)
Deposit = 10;
else if(Quarter_In)
Deposit = 25;
else
Deposit = ERROR;
书写建议:
1、条件表达式需用括号括起来。
2、若为if - if 语句,请使用块语句 begin --- end :
if(C l k) begin
if(R e s e t)
Q = 0;
else
Q = D;
end
以上两点建议是为了使代码更加清晰,防止出错。
4.3.2 条件语句 case
case 语句是一个多路条件分支形式,其语法如下:
case(case_expr)
case_item_expr{ ,case_item_expr} :procedural_statement
. . . . . .
[default:procedural_statement]
endcase
case语句首先对条件表达式case_expr 求值,然后依次对各分支项求值并进行比较,第一个与条件表达式值相匹配的分支中的语句被执行。可以在1 个分支中定义多个分支项;这些值不需要互斥。缺省分支覆盖所有没有被分支表达式覆盖的其他分支。
例:
case (HEX)
4'b0001 : LED = 7'b1111001; // 1
4'b0010 : LED = 7'b0100100; // 2
4'b0011 : LED = 7'b0110000; // 3
4'b0100 : LED = 7'b0011001; // 4
4'b0101 : LED = 7'b0010010; // 5
4'b0110 : LED = 7'b0000010; // 6
4'b0111 : LED = 7'b1111000; // 7
4'b1000 : LED = 7'b0000000; // 8
4'b1001 : LED = 7'b0010000; // 9
4'b1010 : LED = 7'b0001000; // A
4'b1011 : LED = 7'b0000011; // B
4'b1100 : LED = 7'b1000110; // C
4'b1101 : LED = 7'b0100001; // D
4'b1110 : LED = 7'b0000110; // E
4'b1111 : LED = 7'b0001110; // F
default :LED = 7'b1000000; // 0
endcase
书写建议: case 的缺省项必须写,防止产生锁存器。
4.4 过程语句
Verilog HDL中提供两种过程赋值语句initial和always语句,用这两种语句来实现行为的建模。这两种语句之间的执行是并行的,即语句的执行与位置顺序无关。这两种语句通常与语句块(begin ....end)相结合,则语句块中的执行是按顺序执行的。
4.4.1 initial 语句
initial语句只执行一次,即在设计被开始模拟执行时开始(0时刻)。通常只用在对设计进行仿真的测试文件中,用于对一些信号进行初始化和产生特定的信号波形。语法如下:(大家只要先有个概念就可以)
initial [timing_control]
procedural_statement
procedural_statement 是下列语句之一:
procedural_assignment (blocking or non-blocking ) //阻塞或非阻塞性过程赋值语句
procedural_continuous_assignment
conditional_statement
case_statement
loop_statement
wait_statement
disable_statement event_trigger task_enable (user or system)
事例如上产生一个信号波形:
initial begin
#2 Stream = 1;
#5 Stream = 0;
#3 Stream = 1;
#4 Stream = 0;
#2 Stream = 1;
#5 Stream = 0;
end
再次强调,initial语句只用于仿真测试,一般不会用到设计代码中。
4.4.2 always 语句
需要看对应的视频,请点击视频编号::001100000068
1、本节介绍时序逻辑代码(即always语句),分位同步复位的时序逻辑和异步复位的时序逻辑,本教学统一采用异步时钟逻辑。
2、这是ALTERA和VIVADO文档
always语句与initial语句相反,是被重复执行,执行机制是通过对一个称为敏感变量表的事件驱动来实现的,下面会具体讲到。
always 语句的基本格式是
always @(敏感事件)begin
程序语句
end
always是“一直、总是”的意思,@后面跟着事件。整个always的意思是:当敏感事件的条件满足时,就执行一次“程序语句”。每敏感事件每满足一次,就执行“程序语句”一次。
1 2 3 4 5 6 |
always @(a or b or d)begin if(sel==0) c = a + b; else c = a + d; end |
这段程序的意思是,当信号a或者信号b或者信号d发生变化时,就执行一次下面语句。在执行该段语句时,首先判断信号sel是否为0,如果为0,则执行第783行代码;如果sel不为0,则执行第785行代码。需要强调的是,a、b、c任意一个发生变化一次,782行至785行也只执行一次,不会执行第二次的。
注意到的是,sel这个信号变化时,是不会执行第782行到785行代码的。通常这不符合设计者的想法。例如,一般想法是当sel为0时,c的结果是a+b;当sel不为0时,c的结果是a+d。但如果sel由0变1之后,c的结果a+b。总之,这不是一个规范的设计思维。
一般设计者的想法是:当信号a或者信号b或者信号d或者信号sel执行变化时,就执行782行至785行。这样就确保,当sel为0时,c的结果一定为a+b;当sel不为0时,c的结果一定是a+d。所以要在敏感列表中加上sel,如下面代码。
1 2 3 4 5 6 |
always @(a or b or d or sel)begin if(sel==0) c = a + b; else c = a + d; end |
当敏感信号非常多时,很容易就会把敏感信号遗漏,为避免这种情况,可以用“*”来代替。这个“*”是指“程序语句”中所有的条件信号,也就是a、b、d、sel(不包括c)发生变化。明德扬也推荐这种写法。
1 2 3 4 5 6 |
always @(*)begin if(sel==0) c = a + b; else c = a + d; end |
这种条件信号变化,结果立即变化的always,我们称之为“组合逻辑”。
1 2 3 4 5 6 |
always @(posedge clk)begin if(sel==0) c <= a + b; else c <= a + d; end |
这段代码敏感列表是“posedge clk”,其中posedge表示上升沿。也就是说,当clk由0变成1的瞬间,执行一次程序代码:第806至809行。其他时候,c的值保持不变。要特别强调的是,如果clk没有由0变成1,那么即使a、b、d、sel发生变化,c的值也是不变的。
1 2 3 4 5 6 |
always @(negedge clk)begin if(sel==0) c <= a + b; else c <= a + d; end |
这段代码的敏感列表是“negedge clk”,其中negedg表示下降沿。也就是说,当clk由1变成0的瞬间,执行一次程序代码:第806至809行。其他时候,c的值保持不变。要特别强调的是,如果clk没有由1变成0,那么即使a、b、d、sel发生变化,c的值也是不变的。
1 2 3 4 5 6 7 8 9 |
always @(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin c <= 0; end else if(sel==0) c <= a + b; else c <= a + d; end |
这段代码的敏感列表是“posedge clk or negedge rst_n”,也就是说,当clk由0变成1的瞬间,或者rst_n由1变化0的瞬间,执行一次程序代码:第814至820行。其他时候,c的值保持不变。
我们把这种信号边沿触发,即信号上升沿或者下降沿才变化的always,称之为“时序逻辑”。而那个信号clk是时钟。注意,识别是不是时钟,不是看名称,而是看这个信号放在哪里。如放在敏感列表并且是边沿触发的,才是时钟。而信号rst_n是复位信号,也不是看名字,而是它放在敏感列表里,并且也是边沿触发,更关键的是“程序语句”首先判断了rst_n的值,也就是优先级最高,一般都是用于复位使用的。
注意以下几点:
1、对组合逻辑的always 语句,敏感变量必须写全,或者用“*”代替。
2、对组合逻辑器件的赋值采用阻塞赋值“=;时序逻辑器件的赋值语句采用非阻塞赋值“<=”;具体原因请看“阻塞赋值和非阻塞赋值”一节内容。
4.5 阻塞赋值和非阻塞赋值
需要看对应的视频,请点击视频编号:001100000072
1、本节介绍在always语句块中,verilog语言支持的两种类型的赋值:阻塞赋值(=)和非阻塞赋值(<=),前者表示顺序执行,后者表示并行执行。
2、这是ALTERA和VIVADO文档
在always语句块中,verilog语言支持两种类型的赋值:阻塞赋值和非阻塞赋值。阻塞赋值使用“=”语句;非阻塞赋值使用“<=”语句。
阻塞赋值:在一个begin end的多行赋值语句,是先执行当前行的赋值语句,再执行下一行的赋值语句。
非阻塞赋值:在一个begin end的多行赋值语句,在同一时间同时赋值。
1 2 3 4 5 6 7 8 9 |
begin c = a; d = c + a; end
begin c <= a; d <= c + a; end |
例如上面两个例子中,1到4行这一段是阻塞赋值,程序会先执行825行,得到结果后再执行826行。829至832行这一段是非阻塞赋值,830行和831行的赋值语句是同时执行的。
假设当前c的值为0,d的值为0,a的新值为1。
阻塞赋值的执行过程和结果:程序会先执行第825行,此时c的值将更新为1,然后再执行826行,此时c+a也就是相当于1+1=2,也就是d的值为2。
非阻塞赋值的执行过程和结果:程序会同时执行第830行和831行。特别注意是,在执行831行的时候,830行是还没有执行的,也就是意味着c的值还没变化,即此时c的值为0。同时执行的结果是,c的值为1,d的值为1。
明德扬的规范要求,组合逻辑使用阻塞赋值“=”,时序逻辑使用非阻塞赋值“<=”。读者可以把这个规则记下来,绝对不会出错。
制定这个规范的原因,不是语法上的原因,而是为了正确描述硬件的原因。
5 D触发器
需要看对应的视频,请点击视频编号:001100000069
1、本节主要介绍了在FPGA中使用最简单的触发器——D触发器,介绍了它的结构、波形,以及如何看FPGA波形。
2、这是ALTERA和VIVADO文档
5.1 D触发器
数字电路介绍了多种触发器:JK触发器、D触发器、RS触发器、T触发器等。在FPGA中,我们使用的是最简单的触发器---D触发器。
5.1.1 D触发器结构
图 34
图 34是D触发器的结构图,读者可以把它看成一个芯片。该芯片拥有4个管脚,其中3个是输入管脚:时钟clk、复位rst_n、信号d;1个是输出管脚:q。
该芯片的功能是这样的:当给管脚rst_n给低电平,也就是赋值为0时,输出管脚q就处于低电平状态。如果管脚rst_n为高电平,然后再看管脚clk,在clk由0变1即上升沿的时候,将现在d的值赋给q,d是低电平,q也是低电平,d是高电平,q也是高电平。
5.1.2 D触发器波形
图 35
图 35是D触发器的功能波形图。该波形图反映了D触发器各个信号的变化情况,从左到右表示时间的走势。时钟信号有规律地高低变化。
波形图从左开始往右看。
Ø 最开始时,rst_n等于1,d=0,q等于1。
Ø 然后rst_n由1变0,此时输出信号q立即变成0。对应的功能是:当给管脚rst_n给低电平,也就是赋值为0时,输出管脚q就处于低电平状态。
Ø 在rst_n为0期间,即使有时钟,即使信号d变化了,q仍然保持为低电平。
Ø 在rst_n由0变成1,撤消复位后,q没有立刻变化。
Ø 在第4个时钟上升沿时,此时rst_n等于1,而d等于1,所以q变成了1。
Ø 第5个时钟上升沿,仍然是同样情况,rst_n=1,d=1,所以之后q=1。
Ø 在第6个时钟上升沿,rst_n=1,d=0,所以之后q=0。
Ø 第7~10个时钟沿也是按同样方式判断。对应的功能是:如果管脚rst_n为高电平,然后再看管脚clk,在clk由0变1即上升沿的时候,将现在d的值赋给q,d是低电平,q也是低电平,d是高电平,q也是高电平。
5.1.3 D触发器代码
我们看下面这段时序逻辑的代码。
1 2 3 4 5 6 7 8 |
always @(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin q <= 0; end else begin q <= d; end end |
我们从语法上,分析该段代码的功能。该段代码总是在“时钟clk上升沿或者复位rst_n下降沿”的时候执行一次。怎样执行呢?
1. 如果复位rst_n=0,则q的值为0;
2. 如果复位rst_n=1,则将d的值赋给q(注意,前提条件是时钟上升沿的时候)。
上面的功能,与这个功能是相同的:当给管脚rst_n给低电平,也就是赋值为0时,输出管脚q就处于低电平状态。如果管脚rst_n为高电平,然后再看管脚clk,在clk由0变1即上升沿的时候,将现在d的值赋给q,d是低电平,q也是低电平,d是高电平,q也是高电平。
没错,上面代码功能,与D触发器的功能是一样的。这份代码,其实就是在描述一个D触发器,这就是D触发器的代码。
前面已经讲过,在FPGA设计中,你可以用原理图的形式来设计,也可以用硬件描述语言。当用原理图来设计时,几个D触发器还可以忍受,但如果是几千几万个D触发器呢,那必定是头晕眼花了。用硬件描述语言verilog,则没有这问题。
5.1.4 怎么看FPGA波形
我们再来讨论一下波形。先请读者思考,在第4个时钟上升沿的时刻,在此时此刻看到的信号q的值是多少?是0还是1?还是说看到q的上升沿?
图 36
我们的verilog代码对应的是硬件,我们应该从硬件来分析这个问题。我们想一下代码的因果关系。是先有时钟上升沿,这个是因。然后将d的值赋给q,这个是结果。这个因果是有先后关系的,对于硬件来说,这个“先后”无论是多么地快,也是占有一定时间的,所以q的变化会稍后于clk的上升沿。例如下图就是硬件的实际变化情况。
图 37
图中就很容易看出,第4个时钟上升沿时刻,看到的q值为0,也就是变化前的值。上面的波形虽然更将近于实际,但这样画图实在是没累了,而且也没有完成必要。我们只需掌握这种看波形规则:时钟上升沿看信号,是看到变化之前的值。
图 38
所以第4个时钟上升沿时,看到q值为0;在第6个时钟上升沿时,看到q值为1;在第7个时钟上升沿时,看到q值为0;在第8个时钟上升沿时,看到q值为1;在第10个时钟上升沿时,看到q值为0。
注意一下,复位信号是在系统开始时刻或者出现异常时才使用,一般上电后就不会再次复位了,可以认为复位是特殊的情况。
我们考虑正常使用的情况。我们无论是从功能上,还是波形上,都可以看到信号q只在时钟上升沿才变化,它绝对不会在中间变化。在一般的数字系统中,大部分信号之间的传递,都是在同一个时钟传递的,即大部分都是同步电路。跨时钟的电路占比非常小,属于特殊的异步电路。在本教材中,所有的案例、练习,如果没有提前说明,默认都是同步电路。
既然是同步电路,那么输入信号d也是另一个D触发器产生的,并且是同一个时钟clk产生的,如下图。
这就意味着信号d也只会在时钟上升沿变化,理想波形中,它不会在时钟的中间变化的,所以修正下波形图,下图就是理想的、正确的波形图。
图 39
在rst_n由1变0时,q立刻变成0。
在第2个时钟上升沿,看到rst_n为0。按代码功能,q仍然为0。
在第3个时钟上升沿,看到rst_n为0。按代码功能,q仍然为0。
在第4个时钟上升沿,看到rst_n为1,d值为1,q值为0。按代码功能,q变成1。
在第5个时钟上升沿,看到rst_n为1,d值为1,q值为1。按代码功能,q变成1。
在第6个时钟上升沿,看到rst_n为1,d值为0,q值为1。按代码功能,q变成0。
在第7个时钟上升沿,看到rst_n为1,d值为1,q值为0。按代码功能,q变成1。
在第8个时钟上升沿,看到rst_n为1,d值为0,q值为1。按代码功能,q变成0。
在第9个时钟上升沿,看到rst_n为1,d值为0,q值为0。按代码功能,q变成0。
在第10个时钟上升沿,看到rst_n为1,d值为1,q值为0。按代码功能,q变成1。
5.2 时序逻辑实现的加法器
我们分析下面这段代码
1 2 3 4 5 6 7 8 |
always @(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin q <= 0; end else begin q <= a + d; end end |
我们仍然从语法上,分析该段代码的功能。该段代码总是在“时钟clk上升沿或者复位rst_n下降沿”的时候执行一次。怎样执行呢?
1. 如果复位rst_n=0,则q的值为0;
2. 如果复位rst_n=1,则将(a+d)的结果赋给q(注意,前提条件是时钟上升沿的时候)。
假设用信号c表示a+d的结果,则第2点可改为:如果复位rst_n=1,则将c的值赋给q(注意,前提条件是时钟上升沿的时候)。这很明显就是一个D触发器,输入信号为d,输出为q,时钟为clk,复位为rst_n。
图 40
c是怎么来的?c是a+d的结果,自然是一个加法器,所以可以画出上面代码所对应的电路结构图,可以看出在D触发器的基础上增加了一个加法器。
图 41
很容易分析出上面电路的功能:信号a和信号b相加得到c,c连到D触发器的输入端。当clk出现上升沿时,将c的值传给q。这与代码功能是一致的。
下面是代码和硬件所对应的波形图。
图 42
先看信号c的波形,c的产生只有与a和b有关,与rst_n和clk无关。c是a+d的结果,按照二进制加法:0+0=0,0+1=1,1+1=0。就可以画出c的波形。
在第1个时钟期间,a=0,b=0,所以c=0+0=0;
在第2个时钟期间,a=1,b=0,所以c=1+0=1;
在第3个时钟期间,a=1,b=1,所以c=1+1=0;
在第4个时钟期间,a=0,b=1,所以c=0+1=1;
在第5到第6个时钟期间,a=0,b=0,所以c=0+0=0;
在第7个时钟期间,a=1,b=1,所以c=1+1=0;
在第8个时钟期间,a=0,b=1,所以c=0+1=1;
在第9个时钟期间,a=0,b=0,所以c=0+0=0;
在第10个时钟期间,a=0,b=1,所以c=0+1=1。
再看信号q的波形。q是D触发器的输出,它只在rst_n的下降沿或者clk的上升沿才变化,其他时候不变化,也就是a、b、c有变化时,q都不会立刻变。
图 43
在rst_n由1变0时,q立刻变成0。
在第2个时钟上升沿,看到rst_n为0。按代码功能,q仍然为0。
在第3个时钟上升沿,看到rst_n为0。按代码功能,q仍然为0。
在第4个时钟上升沿,看到rst_n为1,c值为0,q值为0。按代码功能,q变成0;
在第5个时钟上升沿,看到rst_n为1,c值为1,q值为0。按代码功能,q变成1;
在第6个时钟上升沿,看到rst_n为1,c值为0,q值为1。按代码功能,q变成0;
在第7个时钟上升沿,看到rst_n为1,c值为0,q值为0。按代码功能,q变成0;
在第8个时钟上升沿,看到rst_n为1,c值为0,q值为0。按代码功能,q变成0;
在第9个时钟上升沿,看到rst_n为1,c值为1,q值为0。按代码功能,q变成1;
在第10个时钟上升沿,看到rst_n为1,c值为0,q值为1。按代码功能,q变成0;
在第11个时钟上升沿,看到rst_n为1,c值为1,q值为0。按代码功能,q变成1。
读者有没有发现,在讨论时序逻辑的加法器时,我们是分开讨论加法器的输出c和D触发器的输出q,这就像两块独立的电路。那我们Verilog代码,是否也可以分开来写呢?当然是可以的。
我们可以先将下面的硬件电路用Verilog描述出来。
图 44
该电路对应的电路,可以写成:
1 2 3 |
always @(*)begin c = a + d; end |
也可以写成
1 |
assign c = a + d; |
上面的两段代码,都是描述同一硬件电路,并且是加法器。
其次,我们再用Verilog描述一下触发器。
图 45
其代码的写法如下:
1 2 3 4 5 6 7 8 |
always @(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin q <= 0; end else begin q <= c; end end |
最后,我们可以看到,两段代码都有信号c,说明这两个是相连的,因此硬件连接起来,变成下面电路。
图 46
由此可见,下面两段代码所对应的硬件电路是一模一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
always @(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin q <= 0; end else begin q <= c; end end
always @(*)begin c = a + d; end |
1 2 3 4 5 6 7 8 |
always @(posedge clk or negedge rst_n)begin if(rst_n==1'b0)begin q <= 0; end else begin q <= a + d; end end |
这两种代码哪种比较好?答案是一样好。因为它们的硬件是相同的,这就是评估verilog代码好不好的最基本标准,不是看代码行数,而是看硬件。
6 时钟的重要性
需要看对应的视频,请点击视频编号:001100000070
1、本节主要介绍了数字电路中非常重要的概念——时钟,以及时钟频率和时钟周期的换算方法。
2、这是ALTERA和VIVADO视频
时钟信号是每隔固定时间上下变化的信号。本次上升沿和上次上升沿占用的时间,就是时钟周期,它的倒数就是时钟频率。高电平占整个时钟周期的时间,称之为占空比。
FPGA中的时钟,占空比一般是50%,即高电平时间和低电平时间一样。其实占空比在FPGA内部没有太大的意义,因为我们使用的是时钟上升沿来触发,更加关心的是时钟频率。
如果时钟的上升沿每秒出现一次,说明时钟的时钟周期为1秒,时钟频率为1Hz。如果时钟的上升沿每1毫秒出现一次,说明时钟的时钟周期为1毫秒,时钟频率为1000Hz,或写成1kHz。
现在普通的FPGA器件,所支持的时钟频率范围一般不超过150M,高端器件可能不超过700M(经验值,实际能跑多愉,取决于器件、设计的电路有相当大的关系)。反对应的时钟周期在纳秒级范围。所以本教材里,所有案例的时钟频率一般都是几十到一百多M。
下面列出本教材常用到的时钟频率以及所对应的时钟周期,方便读者进行换算。
时钟频率 |
时钟周期 |
100KHz |
10_000ns |
1MHz |
1_000ns |
8MHz |
125ns |
50MHz |
20ns |
100MHz |
10ns |
125MHz |
8ns |
150MHz |
6.667ns |
200MHz |
5ns |
时钟是FPGA最重要的信号,所有的其他信号在时钟的上升沿统一变化,这就像军队里的令旗,所有军队在看到令旗到来的时刻,执行已经设定好的命令。
时钟这块令旗影响着整体电路的稳定。首先,时钟要非常地稳定跳动。就如军队令旗,时快时慢,就会让人无所适从,容易出错。而如果令旗非常稳定,每个人都知道令旗什么时候过来,在令旗到来前能不能完成任务,不能就通知改正(修改代码),避免系统出错。
其次,一个高效的军队,令旗越少越好,不同部队看不同的令旗,那么部队协作就容易出现问题,整个军队必定做不到高效,或者容易出错。同样道理,FPGA系统的时钟,必定是越少越好,最好就是一个时钟。这也是要求读者,不要把信号放在时序逻辑敏感列表的原因。
7 模块
7.1 模块结构
需要看对应的视频,请点击视频编号:001100000051
1、本节主要介绍模块结构,模块(module)是verilog的基本描述单位,是用于描述某个设计功能或结构及与其他模块通信的外部端口,有5个主要部分:端口定义、参数定义(可选)、I/O说明、内部信号声明、功能定义。 2、2、这是ALTERA和VIVADO文档
模块(module)是Verilog 的基本描述单位,用于描述某个设计的功能或结构及与其他模块通信的外部端口。
模块在概念上可等同一个器件就如我们调用通用器件(与门、三态门等)或通用宏单元(计数器、ALU、CPU)等,因此,一个模块可在另一个模块中调用。一个电路设计可由多个模块组合而成,因此一个模块的设计只是一个系统设计中的某个层次设计,模块设计可采用多种建模方式。
每个模块实现特定的功能,模块可进行层次的嵌套,因此可以将大型的数字电路设计分割成大小不一的小模块来实现特定的功能,最后通过由顶层模块调用子模块来实现整体功能,这就是Top-Down的设计思想,
Verilog 的基本设计单元是“模块”。采用模块化的设计使系统看起来更有条理也便于仿真和测试,那么整个项目的设计思想就是模块套模块,自顶向下依次展开。
本书主要以verilog硬件描述语言为主,模块是Verilog的基本描述单位,用于描述每个设计的功能和结构,以及其他模块通信的外部接口。
模块有五个主要部分:端口定义、参数定义(可选)、 I/O说明、内部信号声明、功能定义。且模块总是以关键词module开始,以关键词endmodule结尾。它的一般语法结构如下所示:
下面我们来详细分析一下这段代码:
7.2 模块的端口定义
第1至5行声明了模块的名字和输入输出口。其格式如下:
module 模块名(端口 1,端口 2,端口 3,……);
其中模块是以module开始,以endmodule结束。模块名是模块唯一的标识符,我们建议模块名尽量用能够描述其功能的名字来命名,并且模块名和文件名相同。
模块的端口表示的是模块的输入和输出口名,也就是它与别的模块联系端口的标识。
7.3 参数定义
第7行参数定义是将常量用符号代替,以增加代码可读性和可修改性,是一个可选择的语句,用不到可以省略,参数定义一般格式如下:
parameter DATA_W = x;
7.4 I/O 说明
第9至12行是I/O说明,模块的端口可以是输入端口、输出端口或双向端口。其说明格式如下。
输入口: input [信号位宽-1 : 0] 端口名 1;
input [信号位宽-1 : 0] 端口名 2;
……;
输出口: output [信号位宽-1 : 0] 端口名 1;
output [信号位宽-1 : 0] 端口名 2;
……;
双向口:inout [信号位宽-1 : 0] 端口名 1;
inout [信号位宽-1 : 0] 端口名 2;
……;
7.5 信号类型
需要看对应的视频,请点击视频编号:001100000052
1、本节主要介绍,Verilog HDL的信号类型,主要包括两种数据类型:线网类型(net type)和寄存器类型(reg type),在进行工程设计中也只会使用到这两个类型的信号;信号位宽,定义信号类型的同时,必须定义好信号的位宽,取决于该信号要表示的最大值,例如a信号的最大值为1000,那么信号a的位宽必须大于或等于10位;线网类型wire,用于对结构化器件之间的物理连接的建模,代表的是物理连接线,不存储其逻辑值,通常用assign进行赋值;寄存器类型reg,通常用于对存储单元的描述,如D型触发器、ROM等。必须注意的是:reg类型的变量不一定是存储单元,如在always语句中进行描述的必须是用reg类型的变量;wire和reg的区别,本书总结出一套解决方法:在本模块中使用always设计的信号都定义为reg型,其他都定义为wire型。
2、这是ALTERA和VIVADO文档
第16至18行定义了信号的类型。在模块内用到的和与端口有关的 wire 和 reg 类型变量的声明。如:
reg [width-1 : 0] R 变量 1, R 变量 2 ……;
wire [width-1 : 0] W 变量 1,W 变量 2……;
如果不写,默认是wire型,并且信号位宽为1。
7.6 功能描述
第21至31行是功能描述部分。模块中最重要的部分是逻辑功能定义部分。有三种方法可在模块中产生逻辑。
1. 用“assign”声明语句,如描述一个两输入与门:assign a = b & c。
2. 用“always”块。即前面介绍的时序逻辑和组合逻辑。
3. 模块例化。
7.6.1 例化
对一个数字系统的设计,我们采用的是自顶向下的设计方式。可把系统划分成几个功能模块,每个功能模块再划分成下一层的子模块。每个模块的设计对应一个module ,一个module 设计成一个verilog HDL 程序文件。因此,对一个系统的顶层模块,我们采用结构化的设计,即顶层模块分别调用了各个功能模块。
一个模块能够在另外一个模块中被引用,这样就建立了描述的层次。模块实例化语句形式如下:
module_name instance_name(port_associations) ;
信号端口可以通过位置或名称关联;但是关联方式不能够混合使用。端口关联形式如下:
port_expr / /通过位置。
.PortName (port_expr) / /通过名称。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module and (C,A,B); input A,B; output C; //省略 endmodule
module and_2(xxxxx) ... //实例化时采用位置关联,T3对应输出端口C,A对应A,B对应B。 and A1 (T3, A, B ); //实例化时采用名字关联,.C是and 器件的端口,其与信号T3相连 and A2( .C(T3), .A(A), .B(B)); |
建议:在例化的端口映射中请采用名字关联,这样,当被调用的模块管脚改变时不易出错。
在我们的实例化中,可能有些管脚没用到,可在映射中采用空白处理,如:
1 2 3 4 5 6 |
DFF d1 ( .Q(QS), .Qbar ( ), .Data (D ) , .Preset ( ), // 该管脚悬空 .Clock (CK) ); //名称对应方式。 |
对输入管脚悬空的,则该管脚输入为高阻 Z,输出管脚被悬空的,该输出管脚废弃不用。
下面以一个实例(一个频率计数器系统)说明如何用HDL进行系统设计。
在该系统中,我们划分成如下三个部分:2输入与门模块,LED显示模块,4位计数器模块。系统的层次描述如下:
图 47
顶层模块CNT_BCD,文件名CNT_BCD.v,该模块调用了低层模块 AND2、CNT_4b和 HEX2LED 。系统的电路结构图如下:
图 48
顶层模块CNT_BCD对应的设计文件 CNT_BCD.v 内容为:
1 2 3 4 5 6 |
module CNT_BCD (BCD_A,BCD_B,BCD_C,BCD_D,CLK,GATE,RESET) ; // ------------ Port declarations --------- // input CLK; input GATE; input RESET; output [3:0] BCD_A; output [3:0] BCD_B; output [3:0] BCD_C; output [3:0] BCD_D; wire CLK; wire GATE; wire RESET; wire [3:0] BCD_A; wire [3:0] BCD_B; wire [3:0] BCD_C; wire [3:0] BCD_D; // ----------- Signal declarations -------- // wire NET104; wire NET116; wire NET124; wire NET132; wire NET80; wire NET92; // -------- Component instantiations -------// CNT_4bU0( .CLK(CLK), .ENABLE(GATE), .FULL(NET80), .Q(BCD_A), .RESET(RESET) ); CNT_4b U1( .CLK(CLK), .ENABLE(NET116), .FULL(NET92), .Q(BCD_B), .RESET(RESET) ); CNT_4bU2( .CLK(CLK), .ENABLE(NET124), .FULL(NET104), .Q(BCD_C), .RESET(RESET) ); CNT_4b U3( .CLK(CLK), .ENABLE(NET132), .Q(BCD_D), .RESET(RESET) ); AND2 U4( .A0(NET80), .A1(GATE), .Y(NET116) ); AND2 U5( .A0(NET92), .A1(NET116), .Y(NET124) ); AND2 U6( .A0(NET104), .A1(NET124), .Y(NET132) ); endmodule
|
注意:这里的AND2是为了举例说明,在实际设计中,对门级不要重新设计成一个模块,同时对涉及保留字的(不管大小写)相类似的标识符最好不用。
7.7 模块案例
下面先介绍几个简单的Verilog HDL程序。
例[1] 加法器
1 2 3 4 5 6 |
module addr (a, b, cin, count, sum); input [2:0] a; input [2:0] b; input cin; output count; output [2:0] sum; assign {count,sum} = a +b + cin; endmodule |
该例描述一个3位加法器,从例子可看出整个模块是以module 开始,endmodule 结束。
例[2] 比较器
1 2 3 4 5 6 |
module compare (equal,a,b); input [1:0] a,b; // declare the input signal ; output equare ; // declare the output signal; assign equare = (a == b) ? 1:0 ; endmodule |
该例描述一个比较器,从上可看到,/* .... */ 和 // ... 表示注释部分。注释只是为了方便设计者读懂代码,对编译并不起作用。
例[3] 三态驱动器
1 2 3 4 5 6 |
module mytri (din, d_en, d_out); input din; input d_en; output d_out; // -- Enter your statements here -- // assign d_out = d_en ? din :'bz; endmodule module trist (din, d_en, d_out); input din; input d_en; output d_out; // -- statements here -- // mytri u_mytri(din,d_en,d_out); endmodule
|
该例描述了一个三态驱动器。其中三态驱动门在模块 mytri 中描述,而在模块trist 中调用了模块mytri 。模块mytri 对trist 而言相当于一个已存在的器件,在trist 模块中对该器件进行实例化,实例化名 u_mytri 。
8 可综合设计
需要看对应的视频,请点击视频编号:001000000049
1、本节主要介绍使用综合器对Verilog代码进行解释并将代码转化成实际电路来表示,最终产生实际电路(网表),即综合;为了避免在编写好代码、综合成电路、烧写到FPGA后才发现问题,此时再去定位问题就会非常的地困难,所以,在综合前,设计师可以通过仿真软件对代码进行仿真测试,检测出BUG并将其解决,最后再将程序烧写进FPGA,即仿真;在Veriglog语言中,有些语法结构只是以仿真测试为目的,是不能与实际硬件电路对应起来的,也称之为不可综合语法,
2、本节整理了不可综合或者不推荐使用的代码。
3、ALTERA和VIVADO文档
Verilog硬件描述语言有很完整的语法结构和系统,类似高级语言,这些语法结构的应用给我们的设计描述带来很多方便。但是,Verilog是描述硬件电路的,它是建立在硬件电路的基础上的。有些语法结构只是为了仿真测试目的,是不能与实际硬件电路对应起来的,也就是说我们在把一个语言描述的程序映射成实际硬件电路中的结构时是不能实现的。
综合就是把你写的rtl代码转换成对应的实际电路。
比如你写代码assign a=b&c;
EDA综合工具就会去元件库里拿一个二输入与门出来,然后输入端分别接上b和c,输出端接上a
假如你写了很多这样的语句
assign a=b&c;
assign c=e|f;
assign e=x^y;
……
综合工具就会像搭积木一样的把你这些“逻辑”电路用一些“门”电路来搭起来。当然,工具会对必要的地方做一些优化,比如你写一个电路assing a=b&~b,这样工具就吧a恒接为0了,而不会去给你找一个与门来搭这个电路。
所以,“综合”要做的事情有:编译rtl代码,从库里选择用到的门器件,把这些器件按照“逻辑”搭建成“门”电路。
不可综合,是指找不到对应的“门”器件来实现相应的代码。比如#100之类的延时功能,简单的门器件是无法实现延时100个单元的。还有打印语句等,也是门器件无法实现的。
我们在设计的时候,要确保所写的代码是可以综合的,这一个是依赖于设计者的能力,知道什么是可综合的代码,什么是不可综合的代码。对于初学者来说,最好是先记住规则,遵守规则,先按规则来设计电路。在这一过程中,逐渐理解,这是一个最好的学习路径。
下面表格,列出了不可综合或者不推荐使用的代码。
表不可综合或不推荐使用的代码
代码 |
要求 |
initial |
严禁在设计中使用,只能在测试文件中使用。 |
task/function |
不推荐在设计中使用,在测试文件中可用。 |
for |
在设计中、测试文件中均可以使用。但在设计中多数会将其用错,所以建议在初期设计时不使用,熟练后按规范使用 |
while/repeat/forever |
严禁在设计中使用,只能在测试文件中使用 |
integer |
不推荐在设计中使用 |
三态门 |
内部模块不能有三态接口,三态门只有顶层文件才使用。三态门目的是为了节省管脚,FPGA内部完全没有必要使用。关于三态门的介绍,请看后续三态门章节内容 |
casex/casez |
设计代码内部不能有X态和Z态,因此casez、casex设计时不使用。 |
force/wait/fork |
严禁在设计中使用,只能在测试文件中使用 |
#n |
严禁在设计中使用,只能在测试文件中使用 |
下表是明德扬推荐使用的设计。
表明德扬规范推荐使用的代码及其说明
代码 |
备注 |
reg/wire |
设计中所有的信号类型定义,只有reg和wire两种 |
parameter |
设计代码中所有的位宽、长度、状态机命名等,建议都用参数表示,阅读方便并且修改容易。 |
assign/always |
程序块主要部分,明德扬对always使用有严格规范。 组合逻辑格式为: always@(*) begin 代码语句; end 或者用assign
时序逻辑格式为: always@(posedge clk or negedge rstn) begin if(rstn==1’b0)begin 代码语句; end else begin 代码语句; end end 时序逻辑中,敏感列表一定是clk的上升沿和复位的下降沿、最开始必须判断复位。详细见看本章组合逻辑和时序逻辑一节。 |
if else和case |
always里面的语句,使用if else和case两种方法用来作选择判断,可以完成全部设计。 |
算术运算符 (+,-,×,/,%) |
可以直接综合出相对应的电路。但除法和求余运算的电路面积一般比较大,不建议直接使用除法和求余。 |
赋值运算符(=,<=) |
时序逻辑用“<=”,组合逻辑用“=”;其他情况不存在。 |
关系运算符(==,!=,>,<,>=,<=,) |
详细见本章组合逻辑一节 |
逻辑运算符(&&,||,!) |
|
位运算符(~,|,^,&) |
|
移位运算符(<<,>>) |
|
拼接运算符({ }) |
8.1 组合逻辑及其Verilog设计
表门级逻辑及其verilog设计
类型 |
情况 |
功能 |
verilog代码 |
电路示意图 |
备注 |
连线
|
1位相连 |
将两根线连接 |
reg A,B; always@(*)begin B=A; end |
|
|
多位相连 |
将两端的线一对一连接 |
reg[3:0] A,B; always@(*)begin B=A; end |
|
当A和B是n位时,实际就有n根线。但为看图方便,我们通常只画出一根线表示 |
|
移位 (右移:>> 左移:<<) |
A向右或向左移动后,赋值给B |
reg[3:0] A; reg[2:0] B; always@(*)begin B=A>>2; end |
|
移位操作,实际上是选哪线相连。 |
|
拼接 (符号:{}) |
将大括号内的内,按位置一对一连接 |
reg[3:0] A; reg[2:0] B; always@(*)begin B={A[2],A[3],A[0]}; end |
|
拼接,实际上是选哪线相连。 |
|
反 相 器
|
1位反相器 (符号:~) |
将值取反 |
reg A,B; always@(*)begin B=~A; end |
|
|
多位反相器(符号:~) |
将值取反 |
reg[1:0] A,B; always@(*)begin B=~A; end |
|
如果A和B都是n位,实际电路就是有n个反相器。画电路图时可画一个来简化。 |
|
与门 |
1位逻辑与(符号:&&) |
A和B都为1,C为1;否则C为0。 |
reg A,B; always@(*)begin C=A&&B; end |
|
注意:FPGA支持多输入的与门,例如四输入与门,输入可为ABCD,输出为E,当ABCD同时为1时,E为1 |
多位逻辑与(符号:&&) |
A或B都不为0时,C为1,否则为0。 |
reg[2:0] A,B,C; always@(*)begin C=A&&B; end |
|
多位信号之间的逻辑与,很容易引起歧义,设计最好不要用多位数的逻辑与。如果要实现上面功能,建议代码改为如下: always@(*)begin C=(A!=0)&&(B!=0); end |
|
按位与 (符号:&) 常用1 |
A和B对应的比特分别相与。 |
reg[2:0] A,B,C; always@(*)begin C=A&B; end |
|
|
|
按位与 (符号:&) 常用2 |
A的各位之间相与。 |
reg[2:0] A; always@(*)begin C=&A; end |
|
|
|
或门 |
1位逻辑或 (符号:||) |
A和B其中1个为1,C为1;否则C为0。 |
reg A,B; always@(*)begin C=A||B; end |
|
注意:FPGA支持多输入的与门,例如四输入与门,输入可为ABCD,输出为E,当ABCD同时为1时,E为1 |
多位逻辑或 (符号:||) |
A和B其中1个非0,C为1;否则C为0。 |
reg[2:0] A,B,C; always@(*)begin C=A||B; end
|
|
多位信号之间的逻辑或,很容易引起歧义。最好不要用多位的逻辑或。如果要实现相同功能,建议改为如下: always@(*)begin C=(A!=0)||(B!=0); end |
|
按位或 (符号:|) 常用1 |
A和B对应的比特相或。 |
reg[2:0] A,B,C; always@(*)begin C=A|B; end
|
|
|
|
按位或 (符号:||) 常用2 |
A的各位之间相或 |
reg[2:0] A; always@(*)begin C=|A; end
|
|
|
表选择器和比较器及其verilog设计
分类 |
情况 |
功能 |
代码 |
电路示意图 |
备注 |
选择器
|
常见 形式1 |
通过S,选择输入给输出C。 |
always@(*)begin case(S) 2’b00 : C=D0; 2’b01 : C=D1; 2’b10 : C=D2; default : C= D3; endcase end |
|
|
常见 形式2 |
通过S,选择输入给输出C。 |
always@(*)begin C = D[S]; end |
|
此时代码亦非常常用,其本质也是选择器。 |
|
用if else |
通过判断s来选择。 |
always@(*)begin if(S==0) C=D0; else if(S==2’b01) C=D1; else C=D2; end |
|
请注意此处用到了相等比较器和选择器。请掌握这种if else代码的画法。 |
|
比较器 |
相等 |
相等则为1 |
always@(*)begin if(A==B) C=1; else C=0; end |
|
|
大于 |
大于则为1 |
always@(*)begin if(A>B) C=1; else C=0; end |
|
|
|
大于 等于 |
大于等于则为1 |
always@(*)begin if(A>=B) C=1; else C=0; end |
|
|
|
小于 |
小于则为1 |
always@(*)begin if(A C=1; else C=0; end |
|
|
|
小于 等于 |
小于等于则为1 |
always@(*)begin if(A<=B) C=1; else C=0; end |
|
|
表运算逻辑及其verilog设计
分类 |
功能 |
代码 |
电路示意图 |
备注 |
加法器 |
两数相加 |
always@(*)begin C =A+B; end |
|
本质上,运算逻辑都是由与门、或门等门逻辑搭建起来的电路,例如1位的加法就是S= A ^B;Cout= A&&B。 |
减法器 |
两数相减 |
always@(*)begin C =A-B; end |
|
|
乘法器 |
两数相乘 |
always@(*)begin C =A*B; end |
|
在二进制运算中,乘法运算实质上就是加法运算,例如1111*111 = (1111) + (11110) +(111100)。所以乘法器会比加法器消耗的资源多。 |
除法器 |
两数相除 |
always@(*)begin C =A/B; end |
|
二进制运算中,除法和求余涉及到加法、减法和移位等运算,所以除法和求余电路资源都非常大,在设计时要尽力避免除法和求余。如果一定要用到除法,尽量让除数为2的n次方,如2,4,8,16等。因为a/2实质就是a向右移1位;a/4实质就是a向右移2位。移位运算是不消耗资源的。 |
求余器 |
两数求余 |
always@(*)begin C =A%B; end |
|
8.2 时序逻辑verilog设计方法
时序逻辑的代码一般有两种,同步复位的时序逻辑和异步复位的时序逻辑。同步复位的时序逻辑,即复位不是立即有效,而在时钟上升沿时复位才有效。代码结构如下:
always@(posedge clk) begin
if(rstn==1’b0)
代码语句;
else begin
代码语句;
end
end
异步复位的时序逻辑,复位立即有效,与时钟无关。代码结构如下:
always@(posedge clk or negedge rstn) begin
if(rstn==1’b0)
代码语句;
else begin
代码语句;
end
end
对于时序逻辑verilog设计明德扬提出以下建议:
为了教学的方便,明德扬的代码统一采用异步时钟逻辑,建议同学们都采用此结构,这样设计时只需考虑是用时序逻辑还是组合逻辑结构来写代码即可;实际工作,请遵从公司规范。
在明德扬提供的gVim版本软件中打开代码后,输入“Zuhe”命令(回车后)可得到组合逻辑的代码结构;输入“Shixu”命令(回车后)可得到时序逻辑的代码结构;
时序逻辑没有复位信号是不规范的代码,建议不要这样使用。