Skip to content

Verilog 基础

约 2653 个字 188 行代码 预计阅读时间 16 分钟

Note

Verilog 代码与同学们之前学习的 C 语言有很多相似之处,比如自顶向下设计思想、模块化编程、循环语句、条件语句、多路分支语句等,但是又与 C 语言大不相同,因为这是一个硬件描述语言,最后所有代码都将转化为各种门与电路的相连,无论何时同学们都应记住这一点,将 Verilog 代码当作真实的硬件电路去设计,而不是将 C 语言编程的那一套照搬到 Verilog 编程中去。

自顶向下设计.png

标识符 与 关键字

标识符可以是任意一组字母、数字、$ 符号和 _ (下划线)符号的合,但标识符的 第一个字符必须是字母或者下划线,不能以数字或者美元符开始 ,标识符也区分大小写。

常量

Verilog HDL 中有下列四种基本的值来表示硬件电路中的电平逻辑:

  • 0:逻辑 0 或 "假"
  • 1:逻辑 1 或 "真"
  • x 或 X:未知
  • z 或 Z:高阻

整型数的定义

  • 如果定义的长度比为常量指定的长度长,通常在左边填0 补位。但是如果数最左边一位为x 或z ,就相应地用x 或z 在左边补位。例如
  • 10'b10 左边添0占位, 0000000010
  • 10'bx0x1 左边添x占位, x x x x x x _ x 0 x 1
  • 如果长度定义得更小,那么最左边的位相应地被截断。例如
  • 3 'b1001 _ 0011 与 3 'b011 相等
  • 5 'H0FFF 与5 'H1F 相等 //注意,位宽5代表5位二进制,而不是五位16进制!!!

字符串型的定义

一个字符用8为ASCII值表示

1
2
3
reg [1:8*14] Message;
...
Message = "Internal Error"

数据类型

在我们的正常使用了,可以无脑定义变量类型为 wire ,如果报错了再改为 reg

线网类型 net type

线网类型用于对结构化器件之间的物理连线的建模。如器件的管脚,内部器件如与门的输出等。

由于线网类型代表的是物理连接线,因此它不存贮逻辑值。必须由器件所驱动。通常由assign进行赋值。如 assign A = B ^ C;

当一个 wire 类型的信号没有被驱动时,缺省值为 Z(高阻)。

信号没有定义数据类型时,缺省为 wire 类型。

此外,对wire的赋值不能在always / initial 语句中。

寄存器类型 reg type

reg 是最常用的寄存器类型,寄存器类型通常用于对存储单元的描述,如D型触发器、 ROM等。存储器类型的信号当在某种触发机制下分配了一个值,在分配下一个值之时保留原值。但必须注意的是, reg 类型的变量,不一定是存储单元,如在always 语句中进行描述的必须用reg 类型的变量。

1
2
3
reg [msb:lsb] reg1,reg2, ... ,regN //没有定义范围,缺省值为1位寄存器
reg [3:0] Sat  //Sat 是4位寄存器
reg Cnt;        //Cnt 是1位寄存器
  • 寄存器类型的值可取负数,但若该变量用于表达式的运算中,则按无符号类型处理,如:
1
2
3
4
reg A;
...
A = -1;
...

则 A 的二进制位 1111 , 在运算中 , A 总按无符号数 15 来看待

  • 用寄存器数组类型来建立存储器的模型,如对2个8位的RAM的建模如下:
1
2
3
4
5
reg [7:0] Mem[0:1];  //书写规范建议:对数组类型,最好按降序方式([7:0])
...
Mem[0] = `h55;       //对存储单元的赋值必须一一赋值
Mem[0] = `haa;
... 

对向量的一些操作

1
2
3
4
5
6
//实现倒置了
reg [3:0] data = 4'b1011; // data[3:0] = 4'b1011; 
reg [3:0] data_low = data[0:3] ; // data_low[3:0] = 4'b1101

reg addr[8:0] = 9'h10F; // addr[8:0] = 9'b1_0000_1111 
addr_temp[3:2] = addr[8:7] + 1'b1 ; // addr_temp[3:2] = 2'b11
1
2
3
4
5
6
7
8
9
// [bit+: width] : 从起始 bit 位开始递增,位宽为 width。 
// 下面 2 种赋值是等效的 
A = data1[31-: 8] ; 
A = data1[31:24] ; 

// [bit-: width] : 从起始 bit 位开始递减,位宽为 width。 
// 下面 2 种赋值是等效的 
B = data1[0+ : 8] ; 
B = data1[0:7] ;

参数

参数用来表示常量,用关键字 parameter 声明,只能赋值一次。例如:

1
2
3
parameter      data_width = 10'd32 ; 
parameter      i=1, j=2, k=3 ; 
parameter      mem_size = data_width * 10 ;
在实际编程中,对于一些给定的数值,可以用parameter代替,以增加代码的可读性。

时间

Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。

1
2
3
4
5
time       current_time ; 
initial begin        
    #100 ;        
    current_time = $time ; //current_time 的大小为 100 
end

运算符和表达式

算术运算符

  • 需要注意的是,在计算过程中,结果的位宽可能会扩展,因此需要十分注意结果的位宽是否足够,否则可能存在高位被丢弃的情况。
1
2
3
4
reg [3:0] add = 3'b111 + 3'b101; 
//两个三位的数相加,需要四位的寄存器来保存结果,避免高位被丢弃 
reg [6:0] mul = 3'b111 * 4'b1000; 
//一个3位数与一个4位数相乘,需要3+4=7位的寄存器来保存结果。
  • 算术表达式结果的长度由最长的操作数决定。在赋值语句下,算术操作结果的长度由操作符左端目标长度决定。
1
2
3
4
5
reg [3:0] Arc,Bar,Crt;
reg [5:0] Frx;
...
Arc = Bar+Crt;  //结果长度由Arc决定,为4位,加法操作溢出部分被丢弃
Frx = Bar+Crt;  //结果长度由Frx决定,为6为,加法操作溢出部分存储在Frx[4]中
  • 在Verilog HDL 中定义了如下规则:表达式中的所有中间结果应取最大操作数的长度(赋值时,此规则也包括左端目标)。
wire [4:1] Box,Drt;
wire [5:1] Cfg;
wire [6:1] Peg;
wire [8:1] Adt;
...
assign Adt = (Box+Cfg)+(Drt+Peg); 
/*表达式右端的操作数最长为6 ,但是将左端包含在内时,最大长度为8 。
 *所以所有的加操作使用8位进行。
 *例如: Box 和Cfg 相加的结果长度为8 位。
 */

关系运算符与逻辑运算符

A = 4 ; 
B = 3 ; 
X = 3'b1xx ; 

A > B // 为真 
A <= B // 为假 
A >= Z // 为X,不确定 
-------------- 
A = 4 ; 
B = 8'h04 ; 
C = 4'bxxxx ; 
D = 4'hx ;

A == B        // 为真 
A == (B + 1)  // 为假 
A == C        // 为X,不确定 
A === C       // 为假,返回值为0 
C === D       // 为真,返回值为1 因为可以按位比较都为x 所以为真 
---------------
A = 3; 
B = 0; 
C = 2'b1x ;

 A && B // 为假 
 A || B // 为真 
 ! A // 为假 
 ! B // 为真 
 A && C // 为X,不确定 
 A || C // 为真,因为A为真,无论C为什么都为真 
 (A==2) && (! B) //为真,此时第一个操作数为表达式

按位运算符

按位操作符会对两个操作数按位操作,可以用作筛选、合并。 常用的按位运算符有取反(~),与(&),或(|),异或(^)。 除取反外,其余均为两目运算符,如果2个操作数位宽不相等,则用 0 向左扩展补充较短的操作数

条件运算符

形式:cond_expr ? expr1 : expr2

如果cond_expr 为真(即值为1 ),选择expr1 ;如果cond_expr 为假(值为0 ),选择expr2 。如果cond_expr 为 x或z ,结果将是按以下逻辑expr1 和expr2 按位操作的值: 0 与0 得0 , 1 与1 得1 ,其余情况为x

移位运算符

移位运算符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>), 拼接运算符用大括号 {,} 来表示,用于将多个操作数(向量)拼接成新的操作数(向量),信号间用逗号隔开。 算术左移和逻辑左移时,右边低位会补 0。

逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。

1
2
3
4
5
6
A = 4'b1100 ; 
B = 4'b0010 ; 
A = A >> 2 ;        //结果为 4'b0011 
A = A << 1;         //结果为 4'b1000 
A = A <<< 1 ;       //结果为 4'b1000 
C = B + (A>>>2);    //结果为 2 + (-4/4) = 1, 4'b0001

连接运算符

1
2
3
4
5
wire [7:0] Dbus;
assign Dbus [7:4] = {Dbus[0],Dbus[1],Dbus[2],Dbus[3]};
//以反转的顺序将低端四位赋值给高端四位
assign Dbus = {Dbus[3:0],Dbus[7:4]};
//高四位与低四位交换

由于非定长常数的长度未知,不允许连接非定常常数。例如{Dbus,5}此式非法

移位运算符和连接运算符可以帮助我们灵活拼接得到我们需要的不同位的值

宏定义

1
2
3
4
5
6
// 在代码中出现的DATA_DW将直接被转化为32 
`define DATA_DW 32 
//用`S来代替系统函数$stop; (包括分号) 
`define S $stop; 
//可以用`WORD_DEF来声明32bit寄存器变量 
`define WORD_DEF reg [31:0]

赋值

连续赋值

连续赋值语句是 Verilog 数据流建模的基本语句,用于对 wire 型变量进行赋值。

在 Verilog 中,我们用 assign 关键字来表示连续赋值,这里大家需要区分一件事,连续赋值只能赋值给 wire 型变量,但是 assign 关键字可以赋值给 reg 型变量,这是过程连续赋值。即 assign 不完全等同于连续赋值。

assign LHS_target = RHS_expression 

LHS_target 只能是 wire 型变量,可以是标量或者向量。 RHS_expression 则没有要求, 只要 RHS_expression 表达式的操作数有事件发生(值的变化)时,RHS_expression 就会立刻重新计算,同时赋值给 LHS_target。

因为我们写完代码后,线路已经被焊死,我们无法通过电路本身的电平控制或者设计来重新连接电路,因此大家在实际实验中需要注意 wire 变量仅能连续赋值一次

过程赋值

与连续赋值相对应的就是过程赋值,过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型。 这些变量在被赋值后,其值将保持不变,直到重新被赋予新值。

与连续性赋值不同,过程性赋值存在保持的特性,这其实也是电路中寄存器的特性。 与连续性赋值不同,过程赋值分为阻塞赋值和非阻塞赋值。

  • 阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。阻塞赋值语句使用等号 = 作为赋值符。
  • 非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。 非阻塞赋值语句使用小于等于号 <= 作为赋值符。

我们在实际的操作中, 不要混用阻塞赋值和非阻塞赋值 ,因为这样会导致时序不易控制,造成意向不到的后果。

在设计电路时,always 时序逻辑块中多用非阻塞赋值,always 组合逻辑块中多用阻塞赋值;在仿真电路时,initial 块中一般多用阻塞赋值。

例如:

1
2
3
4
5
6
always @(posedge clk) begin     
    a < = b ; 
end   
always @(posedge clk) begin     
    b < = a; 
end

此时a<=b 与 b<=a 可以相互不干扰的执行,达到交换寄存器值的目的。

结构建模

模块定义结构

1
2
3
module module_name(port_list);
    Declarations_and_Statements
endmodule

实例化语句

1
2
3
4
5
6
7
module and(C,A,B);
    input A,B;
    output C;
    ...
    and A1(T3,A,B);             //采用位置关联
    and A2(.C(T3),.A(A),.B(B)); //采用名字关联
    ...
  • 在实例化中,有些管脚可能没用到,可在映射中采用空白处理,如:
1
2
3
4
5
6
7
DFF d1(
    .Q(QS),
    .Qbar(),    //该管脚悬空
    .Data(D),
    .Preset(), //该管脚悬空
    .Clock(CK)
);

对输入管脚悬空的,则该管脚输入为高阻 Z,输出管脚被悬空的,该输出管脚废弃不用

  • 当端口长度和局部端口表达式长度不同时,端口通过无符号数的右对齐或截断方式进行匹配。
module Child(Pba,Ppy);
    input [5:0] Pba;
    output [2:0] Ppy;
    ...
endmodule

module Top;
    wire [1:2] Bdl;
    wire [2:6] Mpr;
    Child C1(Bdl,Mpr);
endmodule

在该例中,Bdl[2]连接到Pba[0], Bdl[1] 连接到Pba[1],余下的输入端口Pba[5]、 Pba[4]和Pba[3]悬空,因此为高阻态z 。与之相似, Mpr[6]连接到Ppy[0], Mpr[5]连接到Ppy[1], Mpr[4] 连接到Ppy[2],其余截断。

过程结构

initial语句

initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。

如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。

initial语句大多用于仿真时模拟输入电平,或者用于初始化器件的寄存器。

1
2
3
4
5
6
7
8
9
`timescale 1ns/1ns 
module test ;     
    reg  ai, bi ;       
    initial begin         
        bi = 1 ; 
        # 10; 
        ai = 1 ;     
    end   
endmodule

always语句

与 initial 语句相反,always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。

always 语句搭配事件控制符号@可以达到特定条件下执行特定事件的作用。

// 信号clk和rst只要发生变化,就执行q< =d,双边沿D触发器模型 
always @(clk, rst) begin 
    q < = d ;             
end    
// 在信号clk上升沿时刻,执行q< =d,正边沿D触发器模型 
always @(posedge clk) begin 
    q < = d ;   
end 
// 在信号clk下降沿时刻,执行q<=d,负边沿D触发器模型 
always @(negedge clk) begin 
    q < = d ; 
end 
// 任何信号只要发生变化,就执行q< =d, 
always @(*) begin 
    q < = d ; 
end

代码的语句

条件语句、多路分支语句和循环语句,都需要在过程结构中,即都需要在 initial 语句或者 always 语句中。

条件语句

在Verilog HDL中,else 与最近的没有else 的if 相关联

对if 语句,除非在时序逻辑中, if 语句需要有else 语句。若没有缺省语句,设计将产生一个锁存器,锁存器在ASIC设计中有诸多的弊端(可看同步设计技术所介绍)。如下一例:

if T
    Q = D

没有else 语句,当T为1(真)时, D 被赋值给Q,当T为0(假)时,因为没有else 语句,电路保持 Q 以前的值,这就形成一个锁存器。

1
2
3
4
5
6
7
8
case(HES)
    4`b0001,4`b0010,4`b0011: begin 
        LED = 7`b1111001;
    end
    4`b0100,4`b0101,4`b0110: LED = 7`b0000110;
    ...
    default: ; //case的缺省值必须写,防止产生锁存器
    endcase

如果我们希望条件语句写在过程结构外面,可以考虑使用线网连续赋值:

assign a = (b & rst) ? 1'b0 : 1'b1;

循环语句

for(initial_assignment; condition ; step_assignment) begin 
     
end 
module test; 
... 
    initial begin     
        counter2 = 'b0 ;     
        for (i=0; i<=10; i=i+1) begin         
            #10 ;         
            counter2 = counter2 + 1'b1 ;     
        end 
    end 
endmodule
Comments: