HDL的出现

  • 数字电路的发展特别迅速,一开始,设计人员通过真空管和晶体管设计数字电路,而后他们将逻辑门安置在单个芯片上,发明了集成电路,第一代集成电路 (Integrated Circuit, IC)。随着集成的逻辑门数越来越高,设计过程变得越来越复杂,设计人员就希望某些设计阶段可以自动完成,于是就出现了电子设计自动化 (Electronic Design Automation, EDA)。
  • 类比C语言的出现,在硬件设计领域,设计人员也希望使用一种标准的语言进行硬件设计,在这种情况下,便出现了硬件描述语言 (Hardware Description Languages, HDL)。其中比较著名的就是Verilog HDL。在当时,设计者可以使用HDL在寄存器传输级 (Register Transfer Level, RTL)对电路进行描述,只需说明数据流是如何在寄存器之间进行移动及如何被处理的,而构成电路的逻辑门和连接数据由逻辑综合工具自动从RTL描述中提取出来。
  • 有了HDL以后,设计大规模集成电路便可以变成如下过程: 编写设计要求说明 -> 行为级描述(用HDL说明电路的功能,性能等问题) -> RTL级描述(对实现电路功能的数据流进行描述)->……前三个过程需要设计者完成,而后的过程则有逻辑综合工具(比如将RTL描述转换为门级网表等)完成。
  • 总结起来HDL的优点:使设计者可以在非常抽象的层次进行电路描述,而不需要画什么门电路这些。另外,通过HDL描述,可以在设计早期发现错误,及时改正,和编程改bug一回事

设计方法和模块简介

  • 设计方法:设计电路两种方法:自顶向下和自底向上,两者在设计中综合使用。自顶向下:首先定义顶层模块,再分析顶层模块由哪些子模块构成,再对每个子模块进行分解。比如说用T触发器搭建起顶层模块,再用D触发器和非门搭建T触发器。 自底向下:用与或门实现D触发器,D触发器和非门实现T触发器,T触发器实现顶层模块。一般设计就是两种方法在D触发器的级别进行汇合,一个从上到下,一个从下到上,在某个级别上进行汇合。

  • 模块:模块代表一种基本的功能块,如果一种功能在多个地方都要用到,就把它写成一个模块,以便代码重用模块通过接口(输入输出端口)被高层模块调用,但隐藏了内部实现。

  • 1
    2
    3
    4
    5
    module 模块名 (端口1, 端口2,....);
    /*模块内容*/
    endmodule
  • 用Verilog可以同时进行多个层次的描述。

  • 行为级:只注重实现算法,不关心硬件实现,在此层次上写的代码类似C语言

  • 数据流级:关心的是数据如何在各个寄存器间流动,和如何处理这些数据

  • 门级:在此层次上通过调用与或非门和他们互相用wire变量连接来构成电路描述,类似画门级逻辑电路图

  • 注:RTL描述后来引申出来多指行为级和数据流级的混合描述

  • 为什么一个东西可以有很多种写法!!因为它描述的层次有很多种啊!!这也就是为什么多路选择器可以用那么多种方法写的原因

  • 模块例化:模块声明更像C++中的一个类,通过类来创建实际对象的过程叫做实例化,对模块的调用也要通过例化进行。不能在一个模块声明中包含另一个模块的声明,只能将另一个模块实例化后,在这个模块声明中调用另一个模块。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    module ripple_carry_counter(q, clk, reset);
    output [2:0] q;
    input clk, reset;
    T_FF tff0(q[0], clk, reset);//生成了3个T_FF的实例,每个实例都传递一组信号
    T_FF tff1(q[1], clk, reset);
    T_FF tff2(q[2], clk, reset);
    endmodule
    module T_FF(q, clk, reset);//下面是对模块T_FF的定义
    output q;
    input clk, reset;
    wire d;
    D_FF dff0(q,d,clk,reset);//这次又例化了D触发器,说是例化,倒不如直接说调用
    not n1(d,q);//非门的调用
    endmodule

基本概念

  • 注释:同C语言

  • 数字声明:对于verilog中出现的数字,包括指明位数的数字和不指明位数的数字

  • 1
    2
    3
    4
    5
    6
    //<size>'<base format><number>
    4'b1111 //4位二进制数,h十六进制,o八进制,d十进制,可以用大写字母
    12'habc //12位16进制数
    16'd255 //16位十进制数
    //不指明位数的数字
    23456 //默认为十进制数,位宽与使用的计算机有关
  • 标识符和关键字:标识符区分大小写,由字母,数字,下划线,美元符$组成,以字母或者下划线开头

    1
    reg value;//reg是关键字,value是标识符
  • 变量数据类型:

  • wire类型:

    1
    2
    3
    1.相当于一条线
    2.需要有驱动源,由驱动源连续驱动,连续地拥有与其连接器件的输出值,随值变化而变化,不能长期保存
    3.wire a;//声明方式 没有对变量a或者端口a声明,默认为wire型
  • reg类型:

    1
    2
    3
    4
    1.中文叫寄存器类型,但不要与时序电路中的寄存器混淆
    2.表示存储元件,保持原有的值,直到被改写
    3.与wire不同,不需要驱动源,仅仅是保存数值的变量,任意时刻都可以通过赋值来改变
    4.reg a//声明能保持数值的变量a
  • 向量:wire [7:0] a; reg [3:0] a; 类似这种形式叫向量,3:0表示位宽为4的总线,也就是a有4位

  • 数组:reg [3:0] a[2:0]; 不要混淆向量,这种表示3个位宽为4的寄存器变量组成的数组

  • 参数:通过关键字parameter来定义常数,常数只在初始化时被赋值

    1
    parameter port_id = 25;

模块和端口

  • 前面我们知道了模块如何定义和调用(例化引用),现在做进一步阐述

  • 一个模块由多个不同的部分组成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module 模块名 (端口列表);
    端口声明
    参数声明//可选
    -----------------------------
    wire reg类型对象的声明
    低层模块实例
    数据流语句(assign)
    always块(包含行为语句)
    任务与函数//不学
    ------------------------------
    endmodule
  • 端口:端口是模块与外界环境交互的接口,模块内部是不可见的,对模块的例化调用只能通过端口进行

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //端口列表
    module fulladd4(sum, c_out, a, b, c_in);//有端口列表
    module Top;//无端口列表模块
    //端口声明关键字
    input //输入端口
    output //输出端口
    inout //输入/输出双向端口
    //D触发器的端口声明和变量声明
    module DFF(q, d, clk, reset);
    output q;
    input d, clk, reset;
    reg q;//输出端口q保持值,因此被声明为reg类型变量
    ....
    ....
    endmodule
  • 注:所有端口隐含地声明为wire类型,如果输出类型的端口需要保持数值,则必须声明为reg

  • 比如上面的D触发器的输出端口q需要保持它的值,直到下一个时钟边沿

  • 端口连接规则:将一个端口看成由相互链接的两个部分组成,一部分位于模块内部,另一部分位于模块外部。当在一个模块中调用(实例引用)另一个模块时,端口之间的连接必须遵守一些规则。

  • 001

输入端口:从模块内部来讲,输入端口必须为线网数据类型,从模块外部来看,输入端口可以连接到线网或者reg数据类型的变量。

输出端口:从模块内部来讲,输出端口可以是线网或者reg数据类型,从模块外部来看,输出必须连接到线网类型的变量,而不能连接到reg类型的变量。

输入/输出端口(必须为wire):从模块内部来讲,输入/输出端口必须为线网数据类型;从模块外部来看,输入/输出端口也必须连接到线网类型的变量。

1
2
3
4
5
6
7
8
9
10
module TOP;
//声明连接变量
reg [3:0] a,b;
reg cin;
reg [3:0] sum;
wire cout;
//调用fulladd4,本模块中命名为fa0
fulladd4 fa0(sum,cout,a,b,cin);
//非法连接,输出端口SUM被连接到reg类型的变量上去了

未连接端口:Verilog允许模块实例的端口保持未连接的状态。例如,如果模块的某些输出端口只用于调试,那么这些端口可以不与外部信号连接。

module fulladd4 fa0(sum, , a, b, c_in);//输出端口c_out没有连接

位宽匹配:在对模块进行调用的时候,verilog允许端口的内、外两个部分具有不同的位宽。一般情况下,verilog仿真器会对此警告。

  • 例化中端口与外部信号的连接:两种方法,不可混用

  • 注:之前觉得例化语句很难写,其实就是被误导了,无非首先把调用的模块看作类,由类生成一个对象,对象中传递相应的参数(也就是端口和外部信号连接)进去

  • 1
    2
    3
    4
    5
    fulladd4 fa0(sum1, cout1, a1, b1, cin1);
    //连接的信号与模块声明时的位置保持一致,这是方法1
    fulladd4 fa0(.cout(cout1), .sum(sum1), .b(b1), .cin(cin1), .a(a1));
    //这种一目了然,把cout1传给cout,并且顺序无所谓了

门级建模

  • verilog可以用多个不同的抽象层次描述硬件电路,在门级抽象层次下,电路是用门来描述的,如用not,and,nand来描述

  • 1
    2
    3
    wire out,in1,in2;
    and a1(out, in1, in2 );
    nor nor1(out, in1, in2);
  • 实例:四选1多路选择器:首先按照数电的设计流程,画出门电路图

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//如图,确定有几个线网变量,有几个门,输入输出端口是哪些,内部变量是哪些
module mux4_to_1(out,i0,i1,i2,i3,s1,s0);
output out;
input i0,i1,i2,i3;
input s1,s0;
wire s1n,s0n;
wire y0,y1,y2,y3;
not(s1n,s1);
not(s0n,s0);//注意顺序
and(y0,i0,s1n,s0n);//输出在前,输入在后
and(y1,i1,s1n,s0);
and(y2,i2,s1,s0n);
and(y3,i3,s1,s0);
or(out,y0,y1,y2,y3);
endmodule

数据流建模

  • 可以看到门建模虽然直观,但门一多就特繁琐,所以寻求更高层次的建模

  • 目前普遍借助计算机辅助设计工具,自动将电路的数据流设计直接转换为门级结构,这个过程叫逻辑综合

  • 先讲更抽象的建模方式-数据流建模(根据数据在寄存器间的流动和处理过程来描述电路)

  • 连续赋值语句:数据流建模的基本语句,用于对wire类型的赋值。

    1
    2
    3
    assign out = i1 & i2;
    1.左值只能是wire类型,右值可以是wire也可以是reg
    2.连续赋值语句,总在激活状态,一旦任意一个操作数发生变化,该语句便会执行,并不会顺序执行到该处,才执行
  • 表达式、操作数和操作符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    表达式:操作数和操作符的结合
    操作符:算数,逻辑,关系,等价,按位,移位,条件,拼接
    //按位
    x = 4'b1010,y = 4'b1101
    x | y = 4'b1111 (按位或) 注:x || y = 1
    x ^ y(异或) x ^~ y(按位同或)
    //移位
    x = 4'b1100
    y = x >> 1 == 4'b0110 //右移一位,高位补0
    //拼接 两个操作数必须确定位宽
    a = 1'b1, b = 2'b00 , c = 2'b10, d = 3'b110
    y = {b,c} = 4'b0010
    y = {a, b[0],c[1]} = 3'b101
    //条件
    assign out = (a == 3)? 4'b1 : 4'b0;
    //优先级 单目取反,取负->乘除模->加减移位->关系等价->逻辑->条件
  • 四选1数据选择器的数据流级实现(对外端口不变,内部实现改变)

1
2
3
4
5
6
7
8
9
10
11
12
module mux4_to_1(out,i0,i1,i2,i3,s0,s1);
output out;
input i0,i1,i2,i3;
input s1,s0;
assign out = s1 ? (s0 ? i3:i2):(s0 ? i1 : 10);
endmodule
/*思路*/
/*
四选1数据选择器就是根据s0,s1的值选那路端口输出给out,s1为1的时候,肯定是i3或者i2,取决于s0,s1为0的时候,肯定是i1或i0,取决于s0
*/
  • 四位全加器
1
2
3
4
5
6
7
8
9
module fulladd4(sum, cout, a, b, cin);
output [3:0]sum;
output cout;
input [3:0]a,b;
input cin;
assign {cout,sum} = a + b + cin;
endmodule

行为级建模

  • 行为级建模语句:always结构化过程语句,其他所有行为语句只能出现在always中

  • verilog的各个执行流程是并发执行的,每个always语句都是一个独立的执行过程。always中的所有行为语句构成了一个alway语句块,该语句块从仿真0时刻顺序执行其中的行为语句,在没有条件限制时,一直会反复执行直到仿真结束,类似无限循环

  • 过程赋值语句:

    1
    2
    3
    4
    5
    1.用于reg类型,其变量被赋值后,其值将保持不变,直到再次执行该语句时,才有可能被重新赋值。
    2.过程语句分阻塞赋值和非阻塞赋值,另外,它是行为语句,所以要写在always
    3.阻塞赋值:在always中顺序执行,待阻塞语句1执行后,执行阻塞语句2
    4.非阻塞赋值:在always中的其他语句执行完成后,共同并发执行,常在当前仿真时刻的最后一个时间步
    5.不要在同一个always中混用
  • 使用非阻塞语句避免竞争

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    always @(posedge clk)
    a = b;
    always @(posedge clk)
    b = a;
    //两个always语句块并发执行,最终结果两者值相同,达不到交换效果
    always @(posedge clk)
    a <= b;
    always @(posedge clk)
    b <= a;
    //时钟沿到来时,仿真器读取操作数的值,存入临时变量,待alway结束时,同时进行赋值,实现交换
    //改进阻塞语句实现交换
    always @(posedge clk)
    begin
    tmp_a = a;
    tmp_b = b;
    a = tmp_b;
    b = tmp_a;
    end
    //非阻塞缺点:仿真速度下降,内存使用量增加

    非阻塞复制的要点在于在时钟到来时,已经读取和计算出了表达式的值(旧值),保存在临时变量中,待其他语句完成后最后,统一把旧值赋给变量

  • 触发always语句

    1
    2
    always @(posedge clk, negedge rst)
    //只要参数发生变化,则执行always
  • begin-end:顺序语句块,里面的语句(除非阻塞赋值外)都顺序执行

  • if-else 同c

  • 多路分支

    1
    2
    3
    4
    5
    6
    7
    case(条件表达式)
    选项1:表达式;
    选项2:表示式;
    ...
    default:表达式;
    endcase
    //首先计算条件表达式的值,再与选项比对,和哪个值相同,就执行哪个分支语句或者表达式,分支语句也可以是begin-end语句块
  • case语句易于实现多路选择器

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    module mux_4_to_1(out, i0, i1, i2, i3, s1, s0);
    output out;
    input i0,i1,i2,i3;
    input s1,s0;
    reg out;
    always @(s1,s0,i0,i1,i2,i3)
    case({s1,s0})
    2'd0 : out = i0;
    2'd1 : out = i1;
    2'd2 : out = i2;
    2'd3 : out = i3;
    default : out = i0;
    endcase
    endmodule
  • 循环(只能在always中用)

    1
    2
    3
    4
    5
    6
    7
    while(cnt < 128)
    begin
    ...
    end
    for(i = 0; i<32; i = i + 2)
    ...