今天继续学习Verilog的语法,参考菜鸟教程runoob.com。
assign 关键词
assign关键词用于连续赋值语句,连续赋值语句是Verilog数据流建模的基本语句,用于对wire型变量赋值。
格式为:
assign LHS_target = RHS_expression ;
LHS_target 必须是一个标量或者线型向量,而不能是寄存器类型。
RHS_expression 的类型没有要求,可以是标量或线型或存器向量,也可以是函数调用。
只要 RHS_expression 表达式的操作数有事件发生(值的变化)时,RHS_expression 就会立刻重新计算,同时赋值给 LHS_target。
就像是我们在观察一根线的输出一样。
也可以在wire型变量定义时直接定义连续赋值方式:
wire A, B ;
wire Cout = A & B ;
case:全加器
在数字逻辑中,我们可以推导出全加器的表达式为:
So = Ai ⊕ Bi ⊕ Ci ;
Co = AiBi + Ci(Ai+Bi)
可以使用如下的连续赋值语句实现全加器,这样,So和Co这两个线型就会实时输出结果:
module full_adder1(
input Ai, Bi, Ci,
output So, Co);
assign So = Ai ^ Bi ^ Ci ;
assign Co = (Ai & Bi) | (Ci & (Ai | Bi));
endmodule
或者使用 拼接符号{,} 以及加法运算,将Co和So连接起来,以便接收进位。
module full_adder1(
input Ai, Bi, Ci
output So, Co);
assign {Co, So} = Ai + Bi + Ci ;
endmodule
时延
连续赋值延时语句中的延时,用于控制任意操作数发生变化到语句左端赋予新值之间的时间延时。
时延一般是不可综合的。
寄存器的时延也是可以控制的,这部分在时序控制里加以说明。
连续赋值时延一般可分为普通赋值时延、隐式时延、声明时延。
//普通时延,A&B计算结果延时10个时间单位赋值给Z
wire Z, A, B ;
assign #10 Z = A & B ;
//隐式时延,声明一个wire型变量时对其进行包含一定时延的连续赋值。
wire A, B;
wire #10 Z = A & B;
//声明时延,声明一个wire型变量是指定一个时延。因此对该变量所有的连续赋值都会被推迟到指定的时间。除非门级建模中,一般不推荐使用此类方法建模。
wire A, B;
wire #10 Z ;
assign Z =A & B
在上述例子中,A 或 B 任意一个变量发生变化,那么在 Z 得到新的值之前,会有 10 个时间单位的时延。如果在这 10 个时间单位内,即在 Z 获取新的值之前,A 或 B 任意一个值又发生了变化,那么计算 Z 的新值时会取 A 或 B 当前的新值。所以称之为惯性时延,即信号脉冲宽度小于时延时,对输出没有影响。
因此仿真时,时延一定要合理设置,防止某些信号不能进行有效的延迟。
过程结构
过程结构语句有两种,initial语句与always语句,它们是行为级建模的两种基本语句。一个模块中可以包含多个initial语句和always语句,但两种语句不能嵌套使用。这些语句在模块间并行执行,与其在模块的前后顺序没有关系。但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。
initial语句
initial语句从0时刻开始执行,只执行一次,多个initial块之间是相互独立的。如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。initial 理论上来讲是不可综合的,多用于初始化、信号检测等,所以在编写 Verilog 代码时,应该避免使用 initial 语句进行复杂的初始化操作,因为这可能会导致综合结果不可预测。
always语句
always语句是重复执行的,相当于while(1),例如我们可以使用always语句产生仿真时钟:
// 100MHZ 时钟源
`timescale 1ns/1ns
module test ;
parameter CLK_FREQ = 100 ; //100MHz
parameter CLK_CYCLE = 1e9 / (CLK_FREQ * 1e6) ; //switch to ns
reg clk ;
initial clk = 1'b0 ; //clk is initialized to "0"
always # (CLK_CYCLE/2) clk = ~clk ; //generating a real clock by reversing
always begin
#10;
if ($time >= 1000) begin
$finish ;
end
end
endmodule
过程赋值
过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型。
这些变量在被赋值后,其值将保持不变,直到重新被赋予新值。
连续性赋值总是处于激活状态,任何操作数的改变都会影响表达式的结果;过程赋值只有在语句执行的时候,才会起作用。这是连续性赋值与过程性赋值的区别。
Verilog 过程赋值包括 2 种语句:阻塞赋值与非阻塞赋值。
阻塞赋值
阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。阻塞赋值语句使用等号 = 作为赋值符。当然,这个顺序执行是基于同一变量而言的,是确保后面使用此变量的值为最新值,不同变量之间该并行还是会并行。
非阻塞赋值
阻塞赋值属于并行执行,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。
非阻塞赋值语句使用小于等于号 <= 作为赋值符。
ex: 以下面的例子说明:
`timescale 1ns/1ns
module test ;
reg [3:0] ai, bi ;
reg [3:0] ai2, bi2 ;
reg [3:0] value_blk ;
reg [3:0] value_non ;
reg [3:0] value_non2 ;
initial begin
ai = 4'd1 ; //(1)
bi = 4'd2 ; //(2)
// 虽然(1)是阻塞执行,但是bi不利用ai产生的结果,(1)(2)并行
ai2 = 4'd7 ; //(3)
bi2 = 4'd8 ; //(4)
#20 ; //(5)
// 这里存在一个常规时延,20ns后执行后面的语句
//non-block-assigment with block-assignment
ai = 4'd3 ; //(6) 后面使用的ai都必须为最新结果
bi = 4'd4 ; //(7) 后面使用的bi都必须为最新结果
value_blk = ai + bi ; //(8)
// value_blk 使用阻塞赋值,他需要获得ai和bi的最新结果,无法与(6)(7)并行执行
value_non <= ai + bi ; //(9)
// value_non 使用非阻塞赋值,但是ai、bi都是阻塞赋值,value_non = 7
//non-block-assigment itself
ai2 <= 4'd5 ; //(10)
bi2 <= 4'd6 ; //(11)
value_non2 <= ai2 + bi2 ; //(12)
// (9)~(12)都是并发执行
// value_non2使用非阻塞赋值,ai2\bi2也使用非阻塞赋值,并行执行
// value_non2使用了ai2和bi2的之前值,即value_non2=7+8=f
end
//stop the simulation
always begin
#10 ;
if ($time >= 1000) $finish ;
end
endmodule
实际 Verilog 代码设计时,切记不要在一个过程结构中混合使用阻塞赋值与非阻塞赋值。两种赋值方式混用时,时序不容易控制,很容易得到意外的结果。更多时候,在设计电路时,always 时序逻辑块中多用非阻塞赋值,always 组合逻辑块中多用阻塞赋值;在仿真电路时,initial 块中一般多用阻塞赋值。
在C语言中,我们常常使用如下的模式来交换两个数:
void swap(int &a, int &b){
int c = a;
a = b;
b = c;
}
// Verilog阻塞执行写法,在时钟上升沿交换ab值
always @(posedge clk) begin
temp = a ;
a = b ;
b = temp ;
end
这三条语句类似于是阻塞执行的,在硬件实现环境下,除了采用阻塞执行的写法,可以直接这么做:
在时钟上升沿,交换ab值,简单直观。
always @(posedge clk) begin
a <= b ;
end
always @(posedge clk) begin
b <= a;
end
这么做存在竞争冒险!是错误的,最后都会a == b
always @(posedge clk) begin
a = b ;
end
always @(posedge clk) begin
b = a;
end
时序控制
Verilog 提供了 2 大类时序控制方法:时延控制和事件控制。事件控制主要分为边沿触发事件控制与电平敏感事件控制。
时延控制
基于时延的时序控制出现在表达式中,指定了语句从开始执行到执行完毕的时间间隔,时延可以是数字、标识符或者表达式。
常规时延
reg value_test ;
reg value_general ;
#10 value_general = value_test ;
等同于
#10 ;
value_ single = value_test ;
内嵌时延
reg value_test ;
reg value_embed ;
value_embed = #10 value_test ;
两者的区别是,内嵌时延是在执行时刻读值,延迟后赋值,常规时延是在延迟后读值并赋值。
边沿触发事件控制
在 Verilog 中,事件是指某一个 reg 或 wire 型变量发生了值的变化。
一般事件控制
事件控制用符号@表示,语句执行条件是信号的值发生特定的变化,posedge即上升沿,negedge即下降沿,未指明则默认为双边沿变化。
//信号clk只要发生变化,就执行q<=d,双边沿D触发器模型
always @(clk) q <= d ;
//在信号clk上升沿时刻,执行q<=d,正边沿D触发器模型
always @(posedge clk) q <= d ;
//在信号clk下降沿时刻,执行q<=d,负边沿D触发器模型
always @(negedge clk) q <= d ;
//立刻计算d的值,并在clk上升沿时刻赋值给q,不推荐这种写法
q = @(posedge clk) d ; // 类似内嵌时延
命名事件控制
用户可以声明 event(事件)类型的变量,并触发该变量来识别该事件是否发生。命名事件用关键字 event 来声明,触发信号用 -> 表示。
event start_receiving ;
always @( posedge clk_samp) begin
-> start_receiving ; //采样时钟上升沿作为时间触发时刻
end
always @(start_receiving) begin
data_buf = {data_if[0], data_if[1]} ; //触发时刻,对多维数据整合
end
敏感列表
当多个信号或事件中任意一个发生变化都能够触发语句的执行时,Verilog 中使用"或"表达式来描述这种情况,用关键字 or 连接多个事件或信号。这些事件或信号组成的列表称为"敏感列表"。当然,or 也可以用逗号 , 来代替。例如:
//带有低有效复位端的D触发器模型
always @(posedge clk or negedge rstn) begin
//always @(posedge clk , negedge rstn) begin
//也可以使用逗号陈列多个事件触发
if(! rstn)begin
q <= 1'b ;
end
else begin
q <= d ;
end
end
当组合逻辑需要对语句块中所有输入变量的变化都敏感时,可以用通配符*代替:
always @(*) begin
//always @(a, b, c, d, e, f, g, h, i, j, k, l, m) begin
//两种写法等价
assign s = a? b+c : d ? e+f : g ? h+i : j ? k+l : m ;
end
电平敏感事件控制
前述所讨论的都基于“变化”,Verilog还支持使用电平作为敏感信号来控制时序,使用wait关键字来表示这种电平敏感情况。
initial begin
wait (start_enable) ; //等待 start 信号
forever begin
//start信号使能后,在clk_samp上升沿,对数据进行整合
@(posedge clk_samp) ;
data_buf = {data_if[0], data_if[1]} ;
end
end
语句块
主要包括两种类型:顺序块和并行块。
顺序块
顺序块使用关键字begin和end来表示,其中的语句是一条条执行的(非阻塞赋值除外)每条语句的时延总是与前面语句执行的时间相关。
并行块
并行块有关键字 fork 和 join 来表示。
并行块中的语句是并行执行的,即便是阻塞形式的赋值。
并行块中每条语句的时延都是与块语句开始执行的时间相关。
`timescale 1ns/1ns
module test ;
reg [3:0] ai_sequen, bi_sequen ;
reg [3:0] ai_paral, bi_paral ;
reg [3:0] ai_nonblk, bi_nonblk ;
//============================================================//
//(1)Sequence block
initial begin
#5 ai_sequen = 4'd5 ; //at 5ns
#5 bi_sequen = 4'd8 ; //at 10ns
end
//(2)fork block
initial fork
#5 ai_paral = 4'd5 ; //at 5ns
#5 bi_paral = 4'd8 ; //at 5ns
join
//(3)non-block block
initial fork
#5 ai_nonblk <= 4'd5 ; //at 5ns
#5 bi_nonblk <= 4'd8 ; //at 5ns
join
endmodule
嵌套块
顺序块和并行块还可以嵌套使用。
`timescale 1ns/1ns
module test ;
reg [3:0] ai_sequen2, bi_sequen2 ;
reg [3:0] ai_paral2, bi_paral2 ;
initial begin
ai_sequen2 = 4'd5 ; //at 0ns
fork
#10 ai_paral2 = 4'd5 ; //at 10ns
#15 bi_paral2 = 4'd8 ; //at 15ns
join
#20 bi_sequen2 = 4'd8 ; //at 35ns
end
endmodule
命名块
我们可以给语句结构命名,并通过层次名引用的方法访问。
`timescale 1ns/1ns
module test;
initial begin: runoob //命名模块名字为runoob,分号不能少
integer i ; //此变量可以通过test.runoob.i 被其他模块使用
i = 0 ;
forever begin
#10 i = i + 10 ;
end
end
reg stop_flag ;
initial stop_flag = 1'b0 ;
always begin : detect_stop
if ( test.runoob.i == 100) begin //i累加10次,即100ns时停止仿真
$display("Now you can stop the simulation!!!");
stop_flag = 1'b1 ;
end
#10 ;
end
endmodule
命名的块可以通过disable关键字禁用,可以终止命名块的执行,用来从循环中退出及错误处理等,下面的代码中,disable退出了runoob_d2的循环。但是需要注意的是如果disable在always块或forever块中使用,只能退出当前回合,相当于“continue”。
`timescale 1ns/1ns
module test;
initial begin: runoob_d //命名模块名字为runoob_d
integer i_d ;
i_d = 0 ;
while(i_d<=100) begin: runoob_d2
# 10 ;
if (i_d >= 50) begin //累加5次停止累加
disable runoob_d3.clk_gen ;//stop 外部block: clk_gen
disable runoob_d2 ; //stop 当前block: runoob_d2
end
i_d = i_d + 10 ;
end
end
reg clk ;
initial begin: runoob_d3
while (1) begin: clk_gen //时钟产生模块
clk=1 ; #10 ;
clk=0 ; #10 ;
end
end
endmodule
条件语句if
if的用法与C语言不能说大差不差,只能说基本一样。但是Verilog没有大括号来进行语句块的分隔,建议使用begin-end进行包裹,降低歧义。
if(en) begin
if(sel == 2'b1) begin
sout = p1s ;
end
else begin
sout = p0 ;
end
end
多路分支语句case
Verilog里的case语句,类似于C中的switch。
case(case_expr)
condition1 : true_statement1 ;
condition2 : true_statement2 ;
……
default : default_statement ;
endcase
谁在前且条件为真先执行。
case 语句中的 x 或 z 的比较逻辑是不可综合的,所以一般不建议在 case 语句中使用 x 或 z 作为比较值。
循环语句
while (condition) begin
…
end
for(initial_assignment; condition ; step_assignment) begin
…
end
i = i + 1 不能像 C 语言那样写成 i++ 的形式,i = i -1 也不能写成 i -- 的形式
repeat (loop_times) begin
…
end
执行固定次数的循环。loop_times可以是常量也可以是变量,但是只读一次变量,变量在循环过程中改变,不会影响循环次数。
forever begin
…
end
=while(1)
assign和deassign
assign(过程赋值操作)与 deassign (取消过程赋值操作)表示第一类过程连续赋值语句。赋值对象只能是寄存器或寄存器组,而不能是 wire 型变量。
赋值过程中对寄存器连续赋值,寄存器中的值被保留直到被重新赋值。
force和release
force (强制赋值操作)与 release(取消强制赋值)表示第二类过程连续赋值语句。
使用方法和效果,和 assign 与 deassign 类似,但赋值对象可以是 reg 型变量,也可以是 wire 型变量。
因为是无条件强制赋值,一般多用于交互式调试过程,不要在设计模块中使用。
当 force 作用在寄存器上时,寄存器当前值被覆盖;release 时该寄存器值将继续保留强制赋值时的值。之后,该寄存器的值可以被原有的过程赋值语句改变。
当 force 作用在线网上时,线网值也会被强制赋值。但是,一旦 release 该线网型变量,其值马上变为原有的驱动值。