三、8【Verilog HDL】RTL级建模——行为级建模

前言

参考书籍:《Verilog HDL 数字设计与综合》第二版,本文档为第7章的学习笔记。

对于行为级建模可以与数据流建模认为其是一个黑箱建模,不需要考虑内部结构,即从项目的功能考虑模块的行为,由点自上而下的建模,而像之前的开关级、门级建模更像自下而上建模。

行为级建模通常会和数据流建模一同使用,共称为RTL(寄存器传输级)。

数据流建模:

三、7【Verilog HDL】RTL建模——数据流建模_追逐者-桥的博客-CSDN博客在数字设计领域,RTL(Register Transfer Leveel,寄存器传输级)通常是指数据流级建模和行为级建模(下节会讲)的结合。.https://blog.csdn.net/ARM_qiao/article/details/124378595

学习目标

  • 掌握结构化过程语句always和initial再行为级建模中的作用及用法
  • 掌握阻塞赋值(=)和非阻塞(<=)过程性赋值语句的作用及用法
  • 学习行为级建模中延迟的时序控制机制,一般延迟、内嵌赋值延迟和零延迟
  • 学习行为级建模中事件的时序控制机制,一般事件控制、命名事件控制和事件OR(或)控制
  • 学习行为级建模中使用电平敏感的时序控制机制
  • 掌握条件语句if-else、分支语句case,casex,casez语句
  • 理解while、for、repet、forever等循环语句
  • 定义顺序块和并行块
  • 理解命名块和命名块的禁用

7.1 结构化过程语句

虽然行为级建模方式与C语言功能有些类似,但是Verilog再本质上是并发而非顺序的。

两个结构化过程语句:initial和always语句,两者执行过程都从仿真事件0开始执行,两种语句不可以嵌套使用。

7.1.1 initial语句

仿真从0时刻开始执行,整个仿真过程只执行一次,如果由多个initial时,则所有initial语句的仿真均从0时刻开始并发执行,且每个块的执行独立并行。如果块中包含多条语句,那么需要begin-end将这些语句组合称为一个语句块,如果只有1条则不需要,类似c语言中的{}。

module stimuls();
    reg a, b, c, d;
    initial 
        a = 1'b0;
    initial beign
        b = 1'b1;
        #10 d = 1'b0;
    end
    initial
        #20 c = 1'b1;
    initial begin
        #20 a = 1'b1;
        #10 d = 1'b1;
    end
endmodule

上面例子中,三条initial语句并发从0时刻执行。如果再某一条语句之前存在延迟#<delay>,那么对这条initial语句的仿真会停顿下来,在经过指定的延迟事件后执行。

时间:            0                 10              20               30

执行语句:a=0,b=1        d=0       c=1,a=1         d=1

由于initial语句在仿真时只执行一次,因此一般被用于仿真文件中的初始化、信号监视、生成仿真波形等。其他变量初始化方法:

  • 在变量声明时初始化:reg clock = 1'b0;
  • 在端口数据声明时初始化:input   reg  a=1'b1;
  • ANSI C风格端口声明初始化:module adder(input wire in1=1'b1,  output reg out); 

7.1.2 always语句

always语句从仿真0时刻开始按顺序执行其中的行为语句;在最后一条执行完成够,再次从头开始执行,如此循环往复,直至整个仿真结束。

moudle clock_gen(output reg clock);
    initial
        clock = 1'b0;
    always
        #10 clock = ~clock;
    initial
        #1000 $finish;
endmodule

always语句只有在断电($finish)和终端($stop)语句作用下才能停止。

7.2 过程赋值语句

过程赋值语句的更新对象是寄存器、整数、实数和时间变量。与连续赋值语句不同,连续赋值语句总是处于活跃状态,只要操作数发生变化就会重新计算重新赋值,但是过程赋值语句只有在执行到的时候才会起作用。语法如下:

assignment ::= variable_lvalue = [ delay_or_event_control ]
                expression

过程赋值语句的左侧值的类型可以是:

  • reg,整型数、实型数、时间寄存器变量或者存储器单元。(连续赋值左侧位wire型)
  • 上述类型的位选a[2]、域选a[4:2]、拼接。

7.2.1 阻塞赋值语句

阻塞赋值语句的赋值符号为:“ = ”,在串行块语句中的阻塞语句按照顺序执行,即会对后面的赋值语句阻塞执行,但是不会阻塞其后的并行块中的语句的执行。这里将在串行块和并行块中进行讨论。

注意,在对寄存器类型变量进行过程赋值时,如果赋值符两侧的位宽不相等,则采用以下原则:

  • 如果左侧表达式的位宽较宽,则将保留从最低位开始的右侧值,把超过左侧位宽的高位丢弃。
  • 如果左侧位宽大于右侧位宽,则从地位开始赋值,不足的高位补0。

7.2.2 非阻塞赋值语句

非阻塞赋值语句赋值符号为:“<=”,允许赋值调度,不会阻塞位于同一个顺序块中其后语句的执行。

为区分阻塞赋值与非阻塞赋值语句作用,将对以下例子进行分析说明:

reg x, y, z;
reg [15:0] reg_a, reg_b;
integer count;    //整型变量
initial begin
    x=0; y=1; z=1;
    count = 0;
    reg_a = 16'b0;
    #10 reg_b = reg_a;
    
    reg_a[2] <= #15 1'b1;
    reg_b[15:13] <= #10 {x, y, z};
    count = count + 1;
end

 首先x=0到reg_b=reg_a之间语句是从仿真0时刻开始顺序执行。由于阻塞赋值10时刻执行reg_b = reg_a,之后执行同时执行    

reg_a[2] <= #15 1'b1;
reg_b[15:13] <= #10 {x, y, z};
count = count + 1;

上面的reg_a[2] <= #15 1'b1;非阻塞赋值对后面的语句执行时间上没影响。

非阻塞赋值来避免竞争

非阻塞赋值可以用来为常见的硬件电路行为建立模型,例如一个时间发生后,多个数据并发传输行为。为区分阻塞与非阻塞,这里用上升沿交换两个寄存器的值进行说明:

//阻塞赋值的两个并行的always块
always @(posedge clock)
        a = b;
always @(posedge clock)
        b = a;

//非阻塞赋值两个并行的always块
always @(posedge clock)
        a <= b;
always @(posedge clock)
        b <= a;

阻塞赋值中:由于其是顺序执行,此时谁先执行就产生了竞争的情况,具体执行的先后顺序由使用的仿真器决定。因此这段代码不能达到a和b值交换的目的,而是使得两者具有相同的值(在时钟上升沿到来之前的值),具体哪一个与仿真器有关。

非阻塞赋值中:其是并行执行的,其赋值机制可以避免竞争:当上升沿到来时,仿真器首先读取右侧的值,并保存在临时变量中;在进行赋值时,仿真器将这些临时变量中保存的值赋值给左侧变量。这样就将读和写操作分开了,达到了交换数据的目的。

阻塞赋值达到非阻塞赋值的目的

上面由于阻塞赋值时(与C语言数据交换类似),并没有将变量本身的原值进行保存,这样在顺序执行的时候变量中的原值就会被冲掉,从而无法获取原值,导致无法进行交换。这是需要将变量的原值进行保存,这样阻塞赋值就可以达到非阻塞赋值的目的了。
 

always @(posedge clock)
begin
    tempa = a;
    tempb = b;
    a = tempb ;
    b = tempa ;
end 

综上,在数字电路设计中,如果某事件发生后将产生多个数据的并发传输,最好使用非阻塞赋值来描述这种情况。非阻塞赋值由于并发执行及其执行特性可以很好的避免竞争风险。

非阻塞赋值的典型应用:流水线建模和多个互斥数据传输的建模

非阻塞赋值由于操作步骤分为了两步,在仿真时占用的内存量将会增加,仿真速度将会降低。

7.3 时序控制

7.3.1 基于延迟的时序控制

基于延时的时序控制出现在表达式中,它指定了语句开始执行到执行完成之间的时间间隔。定义:

delay3 ::= # delay_value | #(delay_value[,delay_value[,delay_value]])
delay2 ::= # delay_value | #(delay_value[,delay_value])
delay_value ::= unsigned_number       | parameter_identifier (数值)
        | specparam_identifier(标识符)| mintypmax_expression (表达式)

常规延迟控制

常规延迟控制位于赋值语句的左边,如:#10 y=10;    #y x=y; 

内嵌赋值延迟控制

延迟控制嵌入了赋值语句,处于赋值符的右侧,如:y= #10 x+1;

内嵌赋值和常规延迟控制的区别:常规延迟是推迟整个语句延迟;内嵌赋值延迟是仿真器首先立即执行计算右侧表达式的值,并将其值保存在临时变量中,在指定推迟时间后再将值赋给左侧变量。

零延迟控制

尽量不要使用延迟控制。再同一仿真时刻,不用的always和initial块中的过程语句均有可能被同时计算,但执行顺序是不确定的,与使用的仿真器类型有关。此时零延迟控制可以保证带零延迟控制的语句将再执行时刻相同的多条语句中最后执行,从而避免发生竞争。但是都带有零延迟时,其先后执行顺序也将是不确定的。

7.3.2 基于事件的时序控制

再Verilog中,事件是指某一个寄存器或者线网变量的值发生变化。事件可以用来触发声明语句或者块语句执行。

常规事件控制

控制符号使用 @ 来说明,语句继续执行的条件:全部获知指定信号值发生变化、上升沿、下降沿。 * 表示块内的全部变量;posedge表示上升沿; negedge表示下降沿。

@(clock)、@(posedge clock)、@(negedge clock)

命名事件控制

用户可以声明 event(事件)类型的变量,触发该变量,并且识别该事件是否已经发生。

命名事件关键词 event声明,它不能保存任何值。事件的触发用符号 -> 表示;判断事件是否发生使用符号@来识别事件变量。

//本例描述再最后一组数据到达以后,把数据存储再缓存器中的行为
event received_data;    //定义一个事件控制变量
always @(posedge clock)
begin
    if(last_data_packet) ->  received_data;  //最后一组数据到达,且触发事件控制变量
end
always @(received_data)
    data_buf = {data_pkt[0], data_pkt[1], data_pkt[2], data_pkt[3]}
                

OR事件控制

当有多个信号或事件发生任意一个变化都能够触发语句或者语句块的执行。如:

always @(reset or clock or d)

有时or也可以使用“ ,” 来代替,如:

always @(reset, clock, d)

当所有变量任意一个值发生变化时都会触发,or和“ ,”将会十分繁琐,故用@*或者@(*),表示对语句块中的所有变量均敏感。如:

always @(*)

7.3.3 电平敏感时序控制

电平敏感时序控制,后面的语句和语句块需要等待某个条件为真时才能执行。使用关键字wait表示等待电平敏感的条件为真。如:

always 

        wait(count_enable) #20 count = count+1;

上面语句表示只有当count_enable=1时,没20个时间单位进行一次自加。

7.4 条件语句

if-else,使用基本和c类似。但是要注意不能省略else,否则将会产生Latch,相关资料请参考:

二、8【FPGA】Verilog中锁存器(Latch)原理、危害及避免_追逐者-桥的博客-CSDN博客FPGA开发中,使用Verilog编程语言编程时,产生锁存器(Latch)原理、危害及避免https://blog.csdn.net/ARM_qiao/article/details/124309796

7.5 多条分支语句

再多路分支语句中条件语句中的 if-else if也可以实现其功能,但是其是从头开始判断条件是否成立,并非并发判断。而case语句可以实现多条语句并行判断。

7.5.1 多路分支

关键字为:case、endcase、default

case语句中不允许嵌套,其功能实现也与C中相似。其也会产生Latch故请参考上面的链接

7.5.2 casex和casez关键字

除了上面case以外,还有两个变形:

  • casez:将条件表达式或侯选项表达式中的z作为无关值,所有值为z的位也可以用“?”来代表
  • casex:将条件表达式或侯选项表达式中的x作为无关值。
//casex用法
reg [3:0] encoding;
integer state;
casex(encoding)
    4'b1xxx : next_state = 3;
    4'bx1xx : next_state = 2;
    4'bxx1x : next_state = 1;
    4'bxxx1 : next_state = 0;
    default : next_state = 0;
endcase

如果encoding的值为4’吧0xz,则执行next_state = 3。

7.6 循环语句

循环语句有:while、for、reoeat、forever,这些语法与C语言中的类似。但是循环语句为行为级建模,因此只能再always或initial块中使用,且可包含延迟表达式。

7.6.1 while

循环执行,判断条件值为假时结束。如:

while (count < 128)    如果含有多条语句时使用begin-end组合成块,当count>=128时结束。

7.6.2 for

有三部分组成:for(初识条件;终止条件;控制变量过程赋值)

for(count=0; count<128; count=count+1),多条语句依然用begin-end。

for(i=0; i<32; i=i+1)   state[i]=i;   对数组或存储器循环赋值。

7.6.3 repeat

不想while通过判断逻辑式是否为真来确定是否继续执行,repeat是通过给定一个常量、一个变量或者一个信号,执行固定次数的循环。语法如:

repeat(10)  说明以下语句执行10次,多条语句使用begin-end。

7.6.4 forever

表示永久循环,不需要判断表达式,会一直执行下去,除非遇到系统任务 $finish为止。类似于while(1)循环语句。如果需要从forever循环中退出,可以使用disable语句。

通常使用时为使循环并非一直执行下去,需要与时序控制结构结合使用。如:

forever #10 clock = ~clock;

7.7 循环块和并行块

块语句的作用是将多条语句合并成一组,使其可以像一条语句一样调用。

7.7.1 块语句的类型

顺序块

关键字begin-end声明,用于多条语句组成顺序块,前面已经经常用到,具有的特点如下:

  • 顺序块中语句是一条一条按顺序执行的,前一句执行完后面的语句才可执行,除带有内嵌延迟控制的非阻塞赋值语句
  • 如果语句包括延迟或事件控制,那么延迟总是相对于前面语句执行完成的仿真事件的。

并行块

关键字fork-join声明,具有以下特点:

  • 并行块中的语句是并发执行的
  • 语句执行的顺序由各自语句中的延迟或事件控制决定,且开始时间都是从0开始。

我们可以将fork看成将一个执行流分成多个并发执行独立的执行流,而join是将并发执行流合成一个执行流。

在并行块中,如果两条语句在同一时刻对同一变量产生影响,那么将会引起隐含的竞争,这种情况需要避免。

顺序块与并行块的区别,这里使用一个简单的例子:

  1. //顺序块
    begin 
        x = 1'b1;        //0时刻执行
        #10 y = 1'b1;    //10时刻执行
        #15 y = 1'b1;    //25时刻执行
    end
    //并行块
    fork 
        x = 1'b1;        //0时刻执行
        #10 y = 1'b1;    //10时刻执行
        #15 y = 1'b1;    //15时刻执行
    join
    

7.7.2 块语句的特点

嵌套快

顺序块和并行块是不能自己嵌套自己的,但是顺序块可以和并行块混合使用。如:

begin ... fork ... join ... end    顺序执行改并发执行。

命名块

可以对块进行命名,命名的块称为命名块

  1. 可以声明局部变量
  2. 可以在层次设计中,通过层次名引用对块中的变量进行访问
  3. 命名块可以被禁用,例如停止执行$finish
//命名块
module top;

initial 
begin : block1
    integer i;    //整形变量i是block1命名块的静态本地变量
                  //可以通过层次名top.block1.i被其他模块访问
end
initial
fork : block2
    reg i;        //寄存器变量i是block2命名块的静态本地变量
                  //可以通过层次名top.block2.i被其他模块访问
join

命名块的禁用

Verilog通过关键字disable提供了一种终止命名块的方法。disable可以用来从循环中推出、处理错误条件以及根据控制某些代码段是否被执行。对块语句的禁用导致紧接在块后面的那条语句也被禁用。优点类似于C中的break推出循环,但break只能跳出当前循环,disable可以禁用任意一个命名模块。

//功能从向量地位开始查找第一个值为1的位数
reg [15:0] flag;
integer i;
initial
begin
    flag = 16'b 0010_0000_0000_0000;
    i = 0;
    begin : block1
        while(i<16)
        begin
            if(flag[i])
            begin
                $display("number = %d", i);
                disable block1;        //当找到标志位时,禁用命名块block1
            end
            i = i+1;
        end
    end
end

7.8 生成块

对于向量中的多个位进行操作时,或者当进行多个模块的实例引用的重复操作时,或者在根据参数的定义来确定程序中是否应该包括某段Verilog代码的时候,使用生成语句能够大大简化程序的编写过程。

生成语句能够控制变量的声明、任务、函数的调用,还能对实例引用进行全面的控制。生成代码必须在模块中声明声明的实例范围,使用的关键generate - endgenerate来指定作用范围。

        生成实例可以是以下的一种或多种类型:

  • 模块
  • 用户自定义原语(UDP)(第十二章会进一步说明)
  • 门级原语
  • 连续赋值语句
  • initial和always语句

如果生成实例有标识名,则可以通过层次命名规则引用,允许在生成范围内有以下列数据类型:

  • net(线网)和reg(寄存器)
  • integer(整数型)、real(实整型)、time(时间型)、realtime(实数时间型)
  • event(事件)

如果生成数据类型具有唯一标识名,可以被层次引用,还可以使用defparam声明的参数重新定义。

任务和函数的声明也可以初选在生成范围内,但是不能出现在循环生成中。生成任务和函数有唯一标识符名称,可以被层次引用。

不允许出现在生成范围中的模块项声明:

  • 参数、局部参数
  • 输入输出和输入输出声明
  • 指定块

生成块的主要用法主要有三种:循环、条件、分支生成

7.8.1 循环生成语句

循环生成语句允许使用者对下面的模块及模块项进行多次实例引用:

  • 变量声明
  • 模块
  • 用户自定义原语、门级原语
  • 连续赋值语句(assign)
  • initial和always块
//生成两条N位总线变量的按位异或
module bitwise_xor(out, i0, i1);
    paramter N = 32;
    output [N-1:0] out;
    input  [N-1:0] i0, i1;
    genvar j;         //声明一个临时循环变量,只能用于生成块的循环计算,在设计块中并不存在
    generate for(j=0; j<N; j=j+1)
    begin : xor_loop
        xor g1(out[j], i0[j], i1[j]);     //可以层次引用,如:bitwise_xor.xor_loop[5].g1
        //always @(i0[j] or i1[j])  out[j]=i0[j]^i1[j];
    end
    endgenerate
endmodule

7.8.2 条件生成语句

条件生成语句允许使用者对下面的模块及模块项进行多次实例引用:

  • 模块
  • 用户自定义原语、门级原语
  • 连续赋值语句(assign)
  • initial和always块
generate
    if (a0 < 8)||(a1 < 8)
        cla #(a0,a1) m0(p, a0, a1);
    else
        tree #(a0,a1) m1(p, a0, a1);
endgenerate

7.8.3 case生成语句

case生成语句允许使用者对下面的模块及模块项进行多次实例引用:

  • 模块
  • 用户自定义原语、门级原语
  • 连续赋值语句(assign)
  • initial和always块

用法和if-else类似。

7.9 行为级建模实际例子

接下来将用下面三个例子对上面知识点进行复习与应用

7.9.1 四选一多路选择器

//四选一多路选择器
module mux4_1 (out, i0, i1, i2, i3, s1, s0);
    output out;
    input i0, i1, i2, i3;
    input s1, s0;
    reg out;
    always @(*)
    begin
        case({s1, s0})
        2'b00 : out = i0;
        2'b01 : out = i1;
        2'b10 : out = i2;
        2'b11 : out = i3;
        default : out = 1'bx;
        endcase
    end
endmodule

7.9.2 四位计数器

//四位二进制计数器
module counter(Q, clock, clear);
    output [3:0] Q;
    input clock, clear;
    reg [3:0] Q;
    always @(posedge clear or negedge clock)
    begin
        if(clear)
            Q <= 4'd0;
        else
            Q <= Q+1;
    end
endmodule

7.9.3 交通信号灯控制器

项目功能:

该交通信号控制器用于控制一条主干道与一条乡村公路的交叉楼的交通,具体功能为:

  • 由于主干道上来往的车辆很多,因此控制主干道的交通信号具有最高优先级,在默认情况下主干道的绿灯点亮;
  • 乡间公路间断性地有车经过,有车来时乡村公路的交通灯必须变为绿色,只需维持一段足够长的时间,以便让车通过。
  • 只要乡村公路上不在有车辆,那么乡村公路上的绿灯马上变为黄灯,然后变为红灯;同时,主干道的绿灯重新点亮。
  • 一个传感器用于监视乡村公路上是否有车等待,他向控制器输入信号X;如果X=1,则表示有车等待,否则X=0;
  • 当S1状态转换道S2状态、从S2转到S3、从S3转到S4、S4转S0时,具有一定延迟,这些延迟必须控制。

 功能框图绘制与端口信号

输入输出信号描述
信号位宽类型信号描述
clock1bit输入信号工作时钟信号,频率50MHz
x1bit输入信号触发信号,支路有车时触发置1
clear1bit输入信号清楚信号灯状态信号
hwy2bit输出信号主路信号灯(G/Y/R)三种状态
cntry2bit输出信号支路信号灯(G/Y/R)三种状态
hwy、cntry00RED
01YELLOW
10GREEN

波形图

相关程序块和激励块代码由于较长,本人以资源的形式进行上传,资源链接如下:

【FPGA】实践项目:十字路口交通信号灯控制-智慧交通文档类资源-CSDN下载由于主干道上来往的车辆很多,因此控制主干道的交通信号具有最高优先级,在默认情况下主干道的绿灯点亮;更多下载资源、学习资料请访问CSDN下载频道.https://download.csdn.net/download/ARM_qiao/85242874这里给除本人的RTL视图及仿真的时序图供大家进行参考:

 

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

追逐者-桥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值