[Verilog1]语言结构_数据类型_运算符与表达式_赋值语句

Verilog语言结构,数据类型,运算符与表达式,赋值语句

整理复习一下常用的Verilog相关知识。共分为三节进行更新。

Verilog语言概述

  • Verilog HDL 语言具有下述描述能力:设计的行为特性、设计的数据流特性、设计的结构组成以及包含响应监控和设计验证方面的时延和波形产生机制。所有这些都使用同一种建模语言。此外,Verilog HDL 语言提供了编程语言接口,通过该接口可以在模拟、验证期间从设计外部访问设计,包括模拟的具体控制和运行。 Verilog HDL 语言不仅定义了语法,而且对每个语法结构都定义了清晰的模拟、仿真语义。因此,用这种语言编写的模型能够使用Verilog 仿真器进行验证。语言从C 编程语言中继承了多种操作符和结构。Verilog HDL 提供了扩展的建模能力,其中许多扩展最初很难理解。但是,VerilogHDL 语言的核心子集非常易于学习和使用,这对大多数建模应用来说已经足够。当然,完整的硬件描述语言足以对从最复杂的芯片到完整的电子系统进行描述。

Verilog 模块的结构

  • Verilog HDL的基本设计单元是“模块(block)”一个模块是由两部分组成的,一部分描述接口;另
    一部分描述逻辑功能,即定义输入是如何影响输出的。
  • 模块中的input、output行说明接口的信号流向,而assign语句、always语句块或者原件例化的方式描述模块的逻辑功能。
  • Verilog HDL结构完全嵌在module和endmodule声明语句之间,每个Verilog程序包括4个主要部分:端口定义、I/O说明、信号类型声明和功能描述,下文将会对各个部分进行详细介绍。
/*定义了一个compare比较器。对两比特大小的数a、b进行比较*/
module compare (equal,a,b);   //定义模块名为compare,且模块具有三个端口
    output equal;  //声明输出信号equal
    input [1:0] a,b;  //声明输入信号a,b
    assign equal=(a==b)?1:0;  //如果a、b 两个输入信号相等,输出为1。否则为0
endmodule

模块的端口定义

  • 模块的端口声明了模块的输入和输出口。其格式如下:
module module_name(port1,port2,……);

I/O说明

  • 可单独定义输入输出口,也可将其合并在模块声明中一起书写。格式如下:
input port1,port2,……,portN;//共有N个输入端口
output port1,port2,……,portN;//共有N个输出端口
//////或者写在端口声明语句中//////
module module_name(
    input InPort1,
    input InPort2,
    output OutPort1,
    output OutPort2
);

信号类型声明

  • 它是说明逻辑描述中所用信号的数据类型及函数声明。其中,对于端口信号的缺省定义类型为wire(连线)型。例如,下面代码是将输出一个端口定义为reg寄存器类型:
reg[7:0] output; // 定义output的数据类型为reg(寄存器)型,位宽为8位

逻辑功能定义

  • 逻辑功能是模块中最重要的部分。有3种方法可在模块中描述逻辑。

"assign"语句

  • 即使用表达式的方式定义关系,常用于组合逻辑的实现。如:
assign y = (a & (~ s)) | (b & s);//将a与s非的结果和b与s的结果做相或运算,并持续赋值给y

元件例化(instantiate)

  • 即调用实例元件或门的方法,且要求元件名唯一。如:
or(y,as,bs) //元件例化或门,并将as与bs做或运算,将结果赋给y

"always"块语句

  • "always"既可用于时序逻辑也可用于组合逻辑,如:
always @(a,b,s)  begin   //a,b,s为敏感信号列表
    if(!s) y = a;   //如果s为假,则y=a
    else y = b;   //否则,y=b
end
  • 需要注意,verilog模块内的各个逻辑功能是并行执行,即如果将上述定义的“assign语句”、元件例化、"always"块放到同一个模块文件中,几个例子将会同时执行,书写的先后次序不会影响逻辑实现的功能,即并发。而在"always"块内,语句却是顺序执行的,代码根据书写的先后顺序进行执行。

基础数据类型

常量

  • 值不能改变的量即为常量

数字

  • 在Verilog HDL中,整型常量即整常数有以下四种进制表示形式:
  1. 二进制整数(b或B)。
  2. 十进制整数(d或D)。
  3. 十六进制整数(h或H)。
  4. 八进制整数(o或O)。
  • 数字表达方式有以下三种:
  1. <位宽><'><进制><数字>这是一种全面的描述方式。
  2. <'><进制><数字>在这种描述方式中,数字的位宽采用缺省位宽(这由具体的机器系统决定,但至少32位)。
  3. <数字>在这种描述方式中,采用缺省进制十进制。如:6'b100110其中撇号(')之前的数字表示该整型常量的位宽,撇号(')之后紧跟的是表示进制的描述符。
8'b11000101   //位宽8位的二进制数11000101
8'hc5   //位宽8位的十六进制数C5
197   //十进制数197

X和Z值

  • 在数字电路中,x代表不定值,z代表高阻值。在使用case表达式时建议使用这种写法,以提高程序的可读性。如:
4'b10x0  //位宽为4的二进制数,第二位为不定值
4'b101z  //位宽为4的二进制数,第一位为高阻态

负数

  • 一个数字可以被定义为负数,只需在位宽表达式前加一个减号,减号必须写在数字定义表达式的最前面。如:
-8'd5

下划线

  • 使用下划线可以用来分隔开数的表达以提高程序可读性。如:
16'b1010_1011_1111_1010

参数(parameter)型

  • 用parameter来定义一个标识符代表一个常量,称为符号常量。定义格式如下:
/*   parameter 参数名1=表达式,参数名2=表达式, …,参数名n=表达式;   */
parameter sel = 8, code = 8'hA3;   //定义了常量sel和code

本地参数(localparam)型

  • 用localparam来定义一个标识符代表一个本地私有常量,称为本地符号常量。该常量只可通过内部定义,无法通过外部传入,定义格式如下:
/*   localparam 参数名1=表达式,参数名2=表达式, …,参数名n=表达式;   */
localparam sel = 8, code = 8'hA3;   //定义了常量sel和code
  • 需要注意的是,Parameter 可以用于模块间的参数传递,而 localparam 仅用于本模块内使用,不能用于参数传递。 Localparam 多用于状态机状态的定义。

变量

  • 即在程序运行过程中其值可以改变的量、

wire型

  • 网络型变量,wire型数据常用来表示用于以assign关键字指定的组合逻辑信号。wire型信号可以用作任何方程式的输入,也可以用作“assign”语句或实例元件的输出。如:
wire [7:0] in, out;   // 定义了两个8位wire型向量in,out
assign out = in;  //将in的值持续的赋值给out

reg型

  • 寄存器型变量,寄存器是数据储存单元的抽象,通过赋值语句可以改变寄存器储存的值,reg的初始值为不定值。特别的,reg型只表示被定义的信号将用在“always”块内。如:
reg [7:0] Data; // 定义Data为8位宽的reg型向量

memory型

  • 若干个相同宽度的向量构成数组,reg型数组变量即为memory型变量,即可定义存储器型数据。如:
reg [7:0] Memory [1023:0]; //定义了一个1024字节、字节宽度为8位的名为Memory存储器
Memory[8] = 1; // Memory存储器中的第8个单元赋值为1

运算符与表达式

算数运算符

  • 加(+)减(-)乘(*)除(/)取余(%)。当操作数中有一个不定值X时,整个结果也为不定值X

位运算符

  • 按位取反(~)按位与(&)按位或(|)按位异或(^)按位同或(^~)。
  • 需要注意的一点是两个不同长度的数据进行位运算时,会自动地将两个操作数按右端对齐,位数少的操作数会在高位用0补齐。
  • 举例如下:
A = 5'b11001;
B = 5'b10101;
~ A ;  //5'b00110
A & B;  //5'b10001
A | B;   //5'b11101 
A ^ B;  //5'b01100

逻辑运算符

  • 逻辑与(&&)逻辑或(||)逻辑非(!)。
  • 举例如下:
!A;  //A的非
A&&B  //A和B的与
A||B  //A和B的 或

关系运算符

  • 小于(<)小等于(<=)大于(>)大等于(>=)
  • 在进行关系运算时,如果声明的关系是假,则返回值是0;如果声明的关系是真,则返回值是1;如果某个操作数的值不定,则其结果是模糊的,返回值是不定值。

等式运算符

等于(==)不等于(!=)强等于(===)强不等于(!==)。在普通的等于不等于中,由于操作数中存在不定值X或高阻态Z,结果可能为不定值X。但在强等于强不等于中,要求所有操作数完全一致,结果才是1,否则结果为0,在case表达式中常使用强等于。如:

a = 5'b11x01;
b = 5'b11x01;
a == b  //结果为不定值x
a === b  //结果为1。

移位运算符

  • 左移运算(<<)右移运算(>>)。
  • 表示把操作数A右移或左移n位,同时用0填补移出的位。如:
A = 5'b11001;
A >> 2;  //值为5'b00110,移除的位补0

位拼接运算符

  • 位拼接运算符{}。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。
  • 当在使用位拼接表达式时不允许存在没有指明位数的信号。这是因为在计算拼接信号的位宽的大小时必需知道其中每个信号的位宽。
  • {信号1的某几位,信号2的某几位,..,..,信号n的某几位}。如:
input [7:0] A, B;
input Cin;
output [7:0] Sum; // 和输出
output Cout; // 进位输出
assign {Cout, Sum} = A + B + Cin; // 进位与和拼接在一起
  • 位拼接还可以用重复法来简化表达式。如:
{4{w}} //这等同于{w,w,w,w}
{b,{3{a,b}}} //这等同于{b,a,b,a,b,a,b}
//需要注意,在重复表达式中的4和3,务必为常数表达式

条件运算符

  • 信号 = 条件?表达式1:表达式2;
  • 即当条件成立时,信号取表达式1的值,反之取表达式2的值。如:
Out = Sel ?In0 :In1;  //二选一选择器,即Sel=1时,Out=In0;Sel=0时,Out=In1

缩减运算符

  • b = &a; // 等效于b = ((a[0] & a[1]) & a[2]) & a[3];。如:
A = 5'b11001;
&A;  //值等于0, 只有A的各位都为1时,其与缩减运算的值才为1
|A; //值等于1, 只有A的各位都为0时,其或缩减运算的值才为0

位切片运算符

确切位置的位切片

  • 如果我们已经确定需要将一个多位数值的某几位赋值给另一个变量,可使用:,对其进行位切片
reg [7:0] a = 8'b1010_0101;
wire [3:0] b;
assign b[3:0] = a[7:4] ;//将a的高四位赋值给b

不确定位置的位切片

  • 有时候,我们需要切片的位置,并不是一个确定的位置,而是需要将某位输入经过一定运算后得到。但对于:运算符,要求:两边的数字均为一个确定值。故我们需要使用:+:-运算符进行不确定位置的切片,具体语法如下:
reg [31:0] a =32'haabbccdd; 
wire [3:0] b,c;
reg [2:0] byte_addr;//外部输入的byte地址
//从输入地址的byte的第4位开始,向上截取4位赋值给b
assign b[3:0] = a[byte_addr+4 +: 4]
//从输入地址的byte的第7开始,向下截取4位赋值给b
assign c[3:0] = a[byte_addr+7 -: 4]

运算符优先级

运算符 优先级
运算符 最高优先级
!, ~
*, /, %
+, -
<<, >>
<, <=, >, >=
==, !=, ===, !==
&
^, ^~
|
&&
||
?: 最低优先级

赋值语句

连续赋值语句

  • assign为连续赋值语句,它对于对wire型变量进行持续赋值。比如:assign c = a & b;在该赋值语句中,a、b、c三个变量皆为wire型变量,a和b信号的任何变化,都将随时反映到c上来,因此称为连续赋值方式。

过程赋值语句

过程赋值语句用于对寄存器类型(reg)的变量进行赋值,其又分为阻塞和非阻塞两种赋值方式:

非阻塞赋值方式

  • 赋值符号为“<=”,如b <= a;。非阻塞赋值在块结束时才完成赋值操作,即b的值并不是立即就改变的,而要等到该always代码块结束时才将值赋值给目标对象。所以,若是在一个always代码块中对一个变量进行多次非阻塞赋值,则只有最后一次赋值将生效。

阻塞赋值方式

  • 赋值符号为“=”,如b = a; 。阻塞赋值在该语句结束时就完成赋值操作,即b的值在该赋值语句结束后立刻改变。 如果在一个块语句中,有多条阻塞赋值语句,那么在前面的赋值语句没有完成之前,后面的语句就不能执行,就像被阻塞(blocking)了一样,因此称为阻塞赋值方式。
    在描述组合逻辑的always块中用阻塞赋值,则综合成组合逻辑的电路结构;在描述时序逻辑的always块中用非阻塞赋值,则综合成时序逻辑的电路结构。
always @( posedge clk ) begin   //非阻塞赋值
    b<=a;  //在always代码块结束时,a的值赋给b,b的值赋给c
    c<=b;  //赋值过程同时进行更新,实现数据移位操作
end
//////////////////////////////////////////////////////
always @(posedge clk) begin   //阻塞赋值
    b=a;//首先将a的值赋值给b,覆盖原有b的数据
    c=b;//再将b更新后的值赋值给c
end

阻塞与非阻塞赋值使用要点

  1. 当对时序电路建模时,用非阻塞赋值。
  2. 当对锁存器电路建模时,用非阻塞赋值。
  3. 当用always块建立组合逻辑模型时,用阻塞赋值。
  4. 在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值。
  5. 在同一个always块中不要既用非阻塞赋值又用阻塞赋值。
  6. 不要在一个以上的always块中为同一个变量赋值

块语句

顺序块

  1. 块内的语句是按顺序执行的,即只有上面一条语句执行完后下面的语句才能执行。
  2. 每条语句的延迟时间是相对于前一条语句的仿真时间而言的。
  3. 直到最后一条语句执行完,程序流程控制才跳出该语句块。格式如下:
parameter d=50; //声明d是一个参数
reg [7:0] r; //声明r是一个8位的寄存器变量
begin //由一系列延迟产生的波形
    #d r = 'h35;
    #d r = 'hE2;
    #d r = 'h00;
    #d r = 'hF7;
    #d -> end_wave; //触发end_wave事件
end

并行块

  1. 块内语句是同时执行的,即程序流程控制一进入到该并行块,块内语句则开始同时并行地执行。
  2. 块内每条语句的延迟时间是相对于程序流程控制进入到块内时的仿真时间的。
  3. 延迟时间是用来给赋值语句提供执行时序的。
  4. 当按时间时序排序在最后的语句执行完后或一个disable语句执行时,程序流程控制跳出该程序块。
fork
    #50 r = 'h35;
    #100 r = 'hE2;
    #150 r = 'h00;
    #200 r = 'hF7;
    #250 -> end_wave; //触发事件end_wave.
join

参考文献:
《Verilog经典教程(第三版)》——夏闻宇