Verilog 学习笔记(5)——Verilog 语言基础(3)

Verilog 学习笔记(5)——Verilog 语言基础(3)

Contents

行为描述

三种描述方式中的最后一种行为描述。理论上所有的电路综合到最后都是门级的,而门级显然是纯组合逻辑可以完成的。但是为了描述方便,所以有了行为描述和结构化描述。本章介绍一些行为描述的基本语句和用法,用来从行为上去描述电路。

initial 和always

在行为描述中,主要的语句类型分为 initial 和 always。注意与数据流描述所用的数据类型不同,一般所描述的信号不是 wire(线网)类型,而是 reg(寄存器)类型。

initial

initial 在0仿真时间执行,并且只执行一次,一般用来对模块进行初始化。举个例子:

initial
    Clk=0;                  //在时间0时刻时钟信号Clk赋值0

always

always 语句可以执行多次,既可以用来描述组合逻辑也可以用来描述时序逻辑。同时 always 语句一般有两种控制方式:事件(@)或延时(#)(实际上还有等待语句)。举个例子:

always@(*)
    begin
        sum=a^b^cin;
        cout= a&b | a&cin | b&cin;
    end

这个语句就是以事件控制的组合逻辑的 always 块的基本写法,其中括号内为敏感列表,通俗地讲就是列表内所表示的信号发生改变时就执行块内的语句。括号内为 * 即表示所有信号都在敏感列表内,只要有一个信号改变就执行块内语句。同时和 C 语言不同的是,verilog 里语句块并不是用大括号来表示,而是使用 beginend。 以上组合逻辑同时也可以用 assign 表示,如下:

assign sum=a^b^cin;
assign cout= a&b | a&cin | b&cin;

区别在于数据类型 always 中 sum、cout 要使用 reg ,即使没有信号驱动寄存器也会保存最后的信号,而 assign 为 wire 型,需要持续驱动。

此外,always 语句还可以用来描述时序逻辑,如下:

always@(posedge Clk or negedge rst)
    begin
        if(~rst)
            T<=~T;          //时钟信号上升沿T取反
        else
            T<=0;           //复位信号时将T置0
    end 

其中 posedge Clk指时钟信号上升沿,同样 negedge 指下降沿,若多个信号触发则在信号之间用 or 隔开。<=是非阻塞赋值,关于阻塞赋值和非阻塞赋值在下面介绍。

延时在语言基础(1)中有提及,大致就是#后跟数字为延时,这里不再赘述。

过程赋值

initial 和 always 语句块里的赋值语句称为过程赋值语句。赋值对象只能是寄存器变量。

阻塞赋值

阻塞赋值中使用和连续赋值一样的=进行赋值,=右边的表达式计算与=左边变量被赋值为连续的两个操作,中间不能插入其他操作,由于这个过程执行前其他语句不能执行,相当于阻塞住了,故称为阻塞赋值。

非阻塞赋值

非阻塞赋值中使用<=进行赋值,与阻塞赋值不同的是,非阻塞赋值先将过程块中的所有右侧计算完成并寄存,再赋给左值,并不会等待前一条赋值完成再对下一条进行计算。

阻塞赋值与非阻塞赋值在应用中的区别

举个例子,以下为两个分别使用阻塞赋值与非阻塞赋值的代码块:

always@(posedge Clk)
    begin
        a1=b1;
        b1=a1;
    end
                //阻塞赋值
always@(posedge Clk)
    begin
        a2<=b2;
        b2<=a2;
    end
                //非阻塞赋值

在上面的代码中,阻塞赋值先将 b1 的值赋给 a1,再将已经被赋值的 a1 的值赋给 b1,实际的效果就是 b1 的值赋给 a1 而自己不变。非阻塞赋值先把赋值前的 b2 和 a2 的值分别寄存,再分别赋值给 a2 和 b2,所实现的功能就是 a2 和 b2 的值互换。

实际操作中一般组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值就行。

过程连续赋值

还有一类赋值语句比较特殊,称为过程赋值语句。类型主要有以下两种:

  • assign 与 deassign:在过程语句块中强制为寄存器变量赋值并释放;
  • force 与 release:在过程语句块中对寄存器和线网进行强制赋值和释放。

举个例子,一个带异步清零端的 D 触发器的清零部分:

always@(Clr)
    begin
        if(~Clr)
            assign Q=0;         //D的值对Q无效,在清零信号持续期间Q强制连续赋值0
        else
            deassign Q;         //释放Q

语句组

除了上文提到的begin…end语句组,还有fork…join语句组,区别在于前者是顺序执行的,后者是并行执行的。

举个例子:

begin
    #5 a=b;
    #10 c=d;
end

fork
    #5 w=x;
    #10 y=z;
join

前者在时刻5执行a=b时刻15(5+10)执行c=d,后者在时刻5执行w=x时刻10执行y=z

编程语句

为了更方便地描述电路的行为,verilog 中也有一些编程语句。

if-else

前文已经用到的比较多了,大致的写法就是

if(condition)
    begin
        ...
    end
else
    begin
        ...
    end

注意的是,使用时尽量避免引入不必要的寄存器而导致的功能错误,也就是说要保证所有的情况下需要的变量都有确定的状态。若只写 if 语句而不确定 else 的语句,当综合时由于 else 的情况未确定就会为该变量增加一个寄存器以保持该变量的值不变,可能造成功能的错误。在下面的 case 语句中也是一样的,在需要的状态后加 default 确定剩下状态。

case

case 语句与 C 中的 switch 语句较为类似,可以实现类似于选择器的效果。举个例子,一个四选一的选择器:

module Mux(input [3:0] in,input [1:0] sel,output reg b);    
                                        //注意声明输出默认为wire,需要改为reg
    always@(*)
        case(sel)
            2'b00:b=a[0];
            2'b01:b=a[1];
            2'b10:b=a[2];
            2'b11:b=a[3];
        endcase

endmodule

其中 case 后的括号是选择控制信号,下面跟的是不同控制信号下执行的语句。与 C 中不同的是,这些语句是并行的,所以不需要跳出。最后 endcase 收尾。

在有一些情况下,输入信号只有某些位有用,这个时候为了简化状态,可以使用 casez 语句。比如一个8位优先译码器(输出右往左第一个信号1的位置,如10100000就输出6,全0输出0),如果使用 case 语句就需要$2^8$个状态,显然是费力而没有必要的,下面给出用 casez 的方案。

module top_module(
    input [7:0] in,
    output reg [2:0] pos
);

    always@(*)
        casez(in)
        8'bzzzzzzz1:pos=3'd0;
        8'bzzzzzz10:pos=3'd1;
        8'bzzzzz100:pos=3'd2;
        8'bzzzz1000:pos=3'd3;
        8'bzzz10000:pos=3'd4;
        8'bzz100000:pos=3'd5;
        8'bz1000000:pos=3'd6;
        8'b10000000:pos=3'd7;
        default:pos=0;
        endcase

endmodule

其中的 z 也可以换成

for

在某些需要使用重复的语句的时候,为了避免反复编写的麻烦,可以使用 for 语句。

比如当有一个100位的输入需要倒序输出时,需要赋值100次,这个情况下可以使用 for 来简化语句。

module top_module(
    input [99:0] in,
    output [99:0] out
);

    integer i;

    always@(*)
        for(i=0;i<100;i=i+1)
            out[i]=in[99-i];

endmodule

基本写法和 C 中一致,但是循环变量注意定义在循环外(据说定义在循环里 ISE 综合会警告,仿真会错误),并且整形是 integer 不是 int。

其他

此外还有一些语句,如 forever(一直循环执行) 、repeat(n)(重复执行 n 次)、while(condition)(为真时循环执行)和上面的比较类似或者比较简单就不再介绍了,感兴趣的可以自己了解。

生成块

前面有提到过 Verilog 作为硬件描述语言与编程语言的区别,并且有提到其实循环是不能或者很难综合的,上面的循环语句本质是将该语句重复数遍,再将这些重复的语句进行综合。比如 for 就是重复了数次语句块的所有语句的综合,if 和 case 就是将所有的条件下的电路都综合出来,再根据判断条件选择哪块电路。在某些情况下这是对资源的浪费。而生成块则是动态生成语句,在 for 里就是将单独的 for 内语句实例化,再进行相应操作,这样就不需要在循环的参量改变时重新综合,再条件语句中就是根据条件只综合需要的状态,而不需要综合多余的状态。

如上面的语句用生成块就可以写成:

genvar i;

generate
    for (i=0; i<100; i = i+1) 
        begin: my_block_name
            assign out[i] = in[99-i];
        end
endgenerate

注意生成块需要命名,并且循环的变量不用 integer 而是 genvar。

牛刀小试

在已经给出一个 bcd 一位全加器的情况下,编写100位 bcd 全加器。

bcd 全加器:

module bcd_fadd {
    input [3:0] a,
    input [3:0] b,
    input     cin,
    output   cout,
    output [3:0] sum );

顶层端口声明:

module top_module( 
    input [399:0] a, b,
    input cin,
    output cout,
    output [399:0] sum );

传送门:bcdfadd100

参考解答:

module top_module( 
    input [399:0] a, b,
    input cin,
    output cout,
    output [399:0] sum );

    wire [399:0] c;
    genvar i;

    bcd_fadd a0(
        .a(a[3:0]),
        .b(b[3:0]),
        .cin(cin),
        .cout(c[0]),
        .sum(sum[3:0])
        );

    generate
        for(i=4;i<400;i=i+4)
            begin:bcdf
            bcd_fadd adder(
                .a(a[i+3:i]),
                .b(b[i+3:i]),
                .cin(c[i-4]),
                .cout(c[i]),
                .sum(sum[i+3:i])
            );
            end
    endgenerate

    assign cout=c[396];

endmodule

 

点赞 1

No Comments

Add your comment