FPGA基础设计:Verilog行为级建模(过程赋值)

阅读《IEEE Standard for Verilog 2005》时,做一些整理和记录。

使用逻辑门和连续赋值对电路建模,是相对详细的描述硬件的方法。使用过程块可以从更高层次的角度描述一个系统,称作行为级建模(behavirol modeling)。

1. 过程赋值

阻塞赋值和非阻塞赋值的区别都很熟悉了。这里记录两个特性。

1.1 特性1

绝大多数情况下,非阻塞赋值都是一个时间点处最后执行的赋值语句。看下面的示例代码:

module test
(
    input clk,
    output reg a, b
);

always @ (posedge clk) begin
    a = 0;
    b = 1;
    a <= b;
    b <= a;
end

endmodule

非阻塞赋值可以认为包括两步:
(1)求值和调度(evaluate and schedule),先得到非阻塞赋值等式右侧的值,并将这次赋值安排在当前时间点的结束时刻。
(2)当前时间点结束时,更新左侧的值。因此这段代码的结果是 a = 1,b = 0。

1.2 特性2

如果过程块内,有针对同一个变量的多个非阻塞赋值,那么这些非阻塞赋值会按顺序执行(但我认为不能简单地说过程块内是“顺序执行的”,容易造成误导,应该说具有一定的“顺序性”特点)。

看下面的示例代码:

module test
(
    input clk,
    output reg a, b
);

always @ (posedge clk) begin
    a <= 0;
    a <= 1;
end

endmodule 

always块内有两条对于变量a的赋值语句,但由于顺序性特点,a的赋值结果应该是1。利用这个特性,会经常见到下面这种代码写法:

always @ (posedge clk) begin
    a <= 0;
    if (flag == 1)
        a <= 1;
end

只有当flag=1时,a才为1。

2. 过程连续赋值

这种赋值方式允许在过程块中连续地驱动网络或变量。但这种建模方法不可综合,因此这里只简单记录一下两种过程连续赋值方式的作用。

assign和deassign:assign连续赋值会优先占用一个变量,让其它对这个变量进行赋值的过程块无效。deassign连续赋值会解除占用关系。

看下面的示例代码:

`timescale 1ns / 1ps

module sim();

reg clk = 0, rst_n = 0, d = 1;
reg q;

//test i1
//(
//    .clk (clk),
//    .rst_n(rst),
//    .q(q), 
//    .d(d)
//);

always @ (rst_n)
    if (!rst_n) assign q = 0;
    else deassign q;

always @ (posedge clk) 
    q <= d;

always #5 clk <= ~clk;

initial begin
    #50 rst_n = 1;
end

endmodule

当rst_n=0时,asssign连续赋值占用了q,q的值恒为0;当rst_n=1时,deassign解除了占用,q的值由其它过程赋值决定,在clk上升沿随d变化而变化。

force和release:功能和assign、deassign相同,只是赋值对象可以是变量也可以是网络。force过程赋值的对象为网络时,会使其它所有对该网络的驱动无效。

3. case语句

case语句的default分支不是必须的,只要设计者清楚设计意图即可。记录一下case两个比较少见但有时候特别有用的用法。

3.1 do-not-cares

包括两种:
  •  casez表示不关心高阻状态(z);
  •  casex表示不关心高祖状态(z)和未知状态(x)。

在不关心的bit位上使用“?”表示要更加方便。casex和casez完全是可以综合的,例如下面的代码可以实现优先编码器:

module test
(
    input clk, 
    input [3:0] d,
    output reg [15:0] q
);

always @ (posedge clk) 
    casez (d) 
        4'b1??? : q <= 1;
        4'b01?? : q <= 2;
        4'b001? : q <= 3;
        4'b0001 : q <= 4; 
    endcase

endmodule 

如果想使用casex和casez,还是要从“设计上”能否综合的角度考虑一下,并且做好综合后的仿真。

比如上面的代码,使用case语句+16条分支可以实现同样的效果,这个设计完全是可以综合实现的。使用casez更多还是起到简化代码设计的作用。

3.2 常数case

case语句中可以使用常数表达式,这个常数会和每个分支中的表达式进行比较。如下面的代码:

module test
(
    input clk, 
    input [3:0] d,
    output reg [15:0] q
);

always @ (posedge clk) 
    casez (1) 
        d[0] : q <= 1;
        d[1] : q <= 2;
        d[2] : q <= 3;
        d[3] : q <= 4; 
    endcase

endmodule 

可以实现,根据d中的哪个bit为1,执行相应的代码。如果d中多个bit同时为1,此时多条分支同时满足,会执行顺序最前面的一条。总之还是要清楚设计意图,做好仿真工作。

4. 循环语句

forever 和 repeat 是完全不可综合的,只用于仿真文件的设计。

循环语句 for 和 while 并不是完全不能综合的,但因为Verilog是对硬件进行建模,for和while的使用肯定不像在软件编程语言中使用一样灵活。还是上面的老话,要从“设计上”能否综合的角度进行考虑。

如果循环语句的使用出现问题,综合工具会给出提示,如Vivado的提示信息如下:

[Synth 8-3380] loop condition does not converge after 2000 iterations 

用好循环,可以简化代码设计。一个寄存器链的示例如下(使用for也能达到同样的效果):

module test
(
    input clk, rst_n,
    input [15:0] d,
    output reg [15:0] q
);

integer i;
(* keep = "true" *)reg [15:0] mem [7:0];
always @ (posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        i = 0;
        while(i < 8) begin
            mem[i] <= 0;
            i = i + 1'b1;
        end
    end
    else begin
        i = 0;
        mem[0] <= d;
        while(i < 7) begin
            mem[i+1] <= mem[i];
            i = i + 1'b1;
        end
    end
    q <= mem[7];
end

endmodule 

相应的RTL原理图如下 :


按软件编程思维考虑,循环语句是一条一条执行的一个过程。而从上面的设计结果来说,显然while循环中的所有语句是“同时”执行的,代码只是将很多具有重复性特点的赋值语句改用 while/for 的形式来编写。

5. 过程块

过程块(procedure)包括四种:initial结构、always结构、任务(task)、函数(function)。这里只记录两个不太熟悉的特性。

5.1 零延迟无限循环

always块在仿真文件中,都要与一些时序控制配合使用。如果always块中没有任何推动仿真时间的控制,仿真会卡在一个时间点。比如经常用如下语句创建时钟信号:

always #10 clk = ~clk;

如果写成了如下的形式:

always clk = ~clk;

相当于形成了一个零延迟的无限循环,仿真时间会卡在0s无法前进。如果运行这个代码,轻则程序卡死,重则系统奔溃只能重启。

5.2 initial用于初始化

initial块是可以综合的,只不过不能添加时序控制语句,因此作用有限,一般用于变量的初始化。如下面代码:

reg [15:0] mem [7:0];
integer i;
initial begin
    for (i = 0; i < 8; i=i+1)
        mem[i] = i;
end

6. 过程块时序控制

此特性主要用于仿真文件中,部分在硬件设计中也会涉及。Verilog有两种明确的时序控制类型:延时控制和事件表达式。仿真时间正是靠过程块中的延时控制、事件控制以及wait语句来推动的。

6.1 延时控制

用于控制语句的执行时间,比如描述激励的波形。延迟值可以是表达式,比如“#d rega = regb;”,这条赋值语句会在延迟d个时间单位后执行。
(1)如果d的计算结果是高阻(z)或未知(x),则当作0处理;
(2)如果d的计算结果为负数,也会将其视作无符号数来看待,如下面的代码:

parameter [7:0] delay = -50;

initial begin
    rst_n = 0;
    #(-delay) rst_n = 1;
    #delay rst_n = 0;
end

rst_n先延迟50个时钟后变为1;由于8bit -50的二进制补码当作无符号数看时值为206,因此在延时206个时钟后,rst_n值又变为0。

6.2 事件表达式

直到某些仿真事件发生时,语句才会只执行。网络或变量的值发生变化,称作隐式事件(implict event);设计者设置一些命名事件,可能会由其它过程块触发,称作显式事件(explicit event)。

值的变化、或变化的方向(上升沿posedge或下降沿negedge)都是隐式事件。虽然在硬件设计中经常和always配合使用(比如 always @ (posedge clk) ),但在仿真文件中有更多灵活的使用方法。看下面的代码示例:

// example1:clk上升沿,语句执行
reg [7:0] delay = 0;
initial begin
    forever @(posedge clk) delay = delay + 1'b1;
end

// example2:clk的值发生变化,语句执行
reg [7:0] delay = 0;
always begin
    @(clk) delay = delay + 1'b1;
end

如果 posedge 和 negedge 检测的对象是一个表达式或多位宽的数据,则只会检测LSB上的边沿变化。如下:

// example3
reg [2:0] cnt = 0;
always @ (posedge clk)
    cnt <= cnt + 1'b1;

reg [7:0] delay = 0;
always begin
    @(posedge cnt) delay = delay + 1'b1;
end

检测3bit变量cnt的上升沿,相当于检测cnt[0]的边沿事件。

事件(event) 是除了变量和网络外Verilog中的另一种数据类型,如果一个标识符被申明为事件类型,则称作“命名事件”,需要显示地触发。虽然事件是一种数据类型,但它本身又没有任何“数据”。如下面的示例:

event trig;        // 命名事件申明
reg [2:0] cnt = 0;
always @ (posedge clk) begin
    cnt <= cnt + 1'b1;
    if (cnt == 7) -> trig;    // 事件显示触发 
end

reg [7:0] delay = 0;
always begin
    @(trig) delay = delay + 1'b1;   // 事件捕获
end

使用命名事件可以有效的实现多个过程块之间的通信和同步。

如果过程块语句的执行同时对多个事件敏感,可以使用事件的逻辑或特性。在事件敏感列表中使用 “or” 或 “,”(这两个符号含义等价),如“always @ (posedge clka or posedge clkb, trig)”。

还有一个特性称作隐式事件表达式,符号为“@*”,会把过程时序控制语句中所有读取的变量和网络添加到事件表达式中

6.3 wait语句

上面的事件控制方法都是边沿敏感型的。还可以使用wait语句控制过程块的时序,直到某项条件为true时才执行相应语句,这种方法称作电平敏感型。

如果wait中的条件为false,则过程块会一直阻塞,直到条件变为真时,才会执行后面的语句。比如下面的代码:

reg [7:0] cnta = 0, cntb = 0;
initial begin
    wait(en) #10 cnta <= 60;    
    #10 cntb <= 70;
end

对于begin…end(顺序块) 而言,wait会阻止顺序块的执行,直到en为1时,cnta和cntb的两条赋值语句才会执行。如果使用fork…join(并行块),则上述代码中的wait只会对cnta的赋值语句有效,此时最好也为wait语句加上块声明(begin…end或fork…join)。

6.4 赋值间(Intra-assignment)时序控制

赋值间延迟和事件控制是另一种时序控制方法,如

 a = #5 b;

与“ #5 a = b; ”不同,赋值语句右边的表达式会马上求值,延迟和事件只是控制这个值赋值给赋值语句左边的时间。比如上面的代码等效于:

begin
 	temp = b;
	#5 a = temp;
end	

利用赋值间时序控制的特性,可以巧妙地完成一些行为建模。比如下面的代码可以避免赋值语句间的“竞争”,达到数据交换的效果:

fork       // 并行块,存在竞争
    #5 a = b;
    #5 b = a;
join

fork       // 数据交换
    a = #5 b;
    b = #5 a;
join

赋值间延迟之前会先求等式右边的值,延迟后才会把这个值赋到左边,因此上面代码相当于交换了a和b的值。很多工具在实现Verilog的赋值间时序控制这个特性时,都会使用临时存储来存放右边表达式的值。

也可以用事件控制:

a = @(posedge clk) b;

//等效于
begin
	temp = b;
	@(posedge clk) a = temp;
end

赋值间时序控制还有一个特点是可以用repeat来控制延迟或事件执行的次数,如:

a = repeat(3) @(posedge clk) b;    

//等效于
begin
	temp = b;
	@(posedge clk);
	@(posedge clk);
	@(posedge clk); a = temp;
end

要注意如果采用变量的形式 “ repeat (num) ”:

  •   若num是无符号数:当num为负数时,相当于二进制补码对应的无符号数。比如num = -1,repeat(num) 相当于 repeat(7) 。

  •   若num是带符号数:当num为0或负数时,这条语句将永远不会被执行。

7. 块(block)

块(block)是一些赋值语句的组合,包括:

  •   顺序块begin-end:块中语句按照给定的顺序执行,因此块中的延迟、事件控制相当于起到了隔断的作用。顺序块的开始时间是第一条语句开始执行的时刻,结束时间是最后一条语句执行完的时刻。

  •   并行块fork-join:块中语句同时执行,即所有语句的开始时间相同。并行块的结束时间是所有语句都执行完的时刻。

7.1 嵌套块

通常要使用多个块的嵌套实现更复杂的控制逻辑,因此最好要理解各个块的开始时间和结束时间。下面给出几个例子:

// Example1
begin
	fork 
		@Aevent;
		@Bevent;
	join
	areg = breg;
end

由于fork-join的并行性,A和B两个事件可以以任意的顺序出现,fork-join块结束后执行赋值语句 areg = breg。

// Example2
begin
	begin
		@Aevent;
		@Bevent;
	end
	areg = breg;
end

如果换成begin-end,事件的触发必须按照给定的顺序。如果B事件先出现,再出现A,那么内部嵌套的begin-end还要再等待B事件的发生。

// Example3
fork 
	@Aevent;
	begin #ta wa = 0; #ta wa = 1; end
	
	@Bevent;
	begin #tb wb = 1; #tb wb = 0; end
join

fork-join中的两个顺序块的执行分别受到两个事件的控制。由于fork-join的并行性,两个begin-end的触发和执行同样也是并行的。

7.2 命名块

每个块都可以在begin和fork后面为其附加名字,称为命名块。其它语句可以通过这个名字来引用命名块,最常见的是“命名块+disable”的用法。

disable语句可以终止命名块的运行,一般用于处理异常情况,比如下面的代码:

begin : block_name
	...
	if (a == 0)
		disable block_name;
	...
end

当满足a == 0时,begin-end块会终止运行。disable会终止整个命名块的运行,包括命名块中的其它所有块和已调用的任务。利用这个特性可以实现两个功能:
  •   中止一个循环语句(相当于C语言中的break)
  •   跳过循环中的某些状态(相当于C语言中的continue)

虽然Verilog没有直接提供类似于C语言中break和continue的关键词,但可以使用“命名块+disable”来实现此特性。看下面的示例代码:

reg [7:0] cnt = 0;
always @ (posedge clk) cnt <= cnt + 1'b1;

reg [7:0] data;
integer i; 
initial begin : break
    for (i = 0; i < 100; i = i + 1) begin : continue
        @(posedge clk)
        if (cnt == 5)
            disable break;
        data <= cnt;
    end
end 

for循环中,当满足一定条件时," disable break; "会终止initial之后的begin-end块的执行,整个循环也就终止了。

如果改成" disable continue; ",当满足条件时,会终止for之后的begin-end块的执行,这样只会终止当前的循环状态,而不会影响循环的下一次迭代。

版权声明:本文为CSDN博主「FPGADesigner」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://bestfpga.blog.csdn.net/article/details/102784167

推荐阅读