Skip to content

学习Verilog的基本语法.

电路的描述方式

verilog是描述电路的语言。为了实现系统的逻辑功能,在设计系统时可以采用多种描述方式进行建模。verilog通常使用三种不同的风格描述电路。在设计电路的时候可以任意使用一种或混合使用多种描述方式来描述电路。经过综合工具综合后的结果一般都是门级结构描述。

  1. **结构描述方式:**调用其他已经定义好的低层模块对整个电路功能进行描述,或者直接调用verilog内部基本的门级元件描述电路的结构。
  2. **数据流描述方式:**设计者从数据在各存储单元之间进行流动和运算的角度对电路的功能进行描述。设计者可以使用verilog提供的高层次运算符,例如 +等直接对数据进行高层次的数学逻辑运算建模,而不用关心具体的门级电路结构。
  3. **行为描述方式:**直接根据电路的外部行为进行建模,与具体的硬件电路无关。

信号声明方式

数字表示方法

整形数字的定义格式为

[-][<位宽>]'<进制><数值>

其中进制的标识符分别为:二进制(b或B)、八进制(o或O)十进制(d或D)、十六进制(h或H)。若省略进制则默认为十进制。

负号的标识需要在位宽的前面

数值还可以使用符号表示不定态

  • x(X)表示未知状态
  • z(Z,?)表示高阻态

一个 xz可以分别表示十六进制中的四位,八进制中的三位,二进制的一位

变量数据类型

verlog中的常量有三种类型

  1. parameter
  2. wire
  3. reg
  • parameter类型

    parameter用于定义在不变的值,例如状态机中的状态、数学运算中的常数等。常量的类型包括三种:

    • 整数型常量 (可逻辑综合)
    • 实数型常量 (用于逻辑仿真)
    • 字符串型常量 (用于逻辑仿真)

    声明方式如下:

    verilog
    parameter a=4'b10x0			// 4位二进制,第二位为不定值
    parameter b=12'dz			// 12位十进制,全位为高阻态
    parameter c=8'h4x			// 8位十六进制,低4位为不定值
  • wire类型

    线网类型代表各个模块之间的物理连线,不能存储逻辑值,它的值由它的驱动元件决定,线网类型的默认值为高阻态z。verilog中所有的输入输出信号类型默认均为wire类型。

    verilog
    wire [位宽] 线网名称;
    
    // 声明例子
    wire a;				// 定义一个 1位 的wire
    wire [7:0] b;		// 定义一个8 位的wire
    wire [4:1] c, d;	// 定义一个4 位的wire
  • reg类型

    寄存器类型表示一个抽象的数据存储单元,它具有状态保存作用。寄存器类型只能在initialalways内部被赋值。在赋值前默认值为x。对于reg类型,赋值语句的作用就像改变一组触发器的存储单元的值。可以用各种构造控制改变值的时机,如上升沿、下降沿等。单个reg类型的声明格式如下,其中的 n 代表寄存器的位宽。

    verilog
    reg [位宽] 寄存器名称;
    reg [3:0] a;		// 声明一个 4 位的寄存器
    reg [4:1] b;		// 声明一个 4 位的寄存器

$ git config --global init.defaultBranch main$ git branch -M main # 如果是已经新建了的仓库,修改当前的分支名称为 mainshell

verilog
reg[msb:lsb] name;

上面的msblsb分别代表向量的最高有效位和最低有效位。msblsb必须为常数、常数表达式或者parameter参数。

变量数组

线网和变量都可以声明为一个数组,数组中的每个元素可以是标量也可以是向量。数组的定义格式为:

verilog
数据类型 [位宽] 变量名称[地址范围]
// 例子
wire bus[5:0]; 			//声明 6个位宽为 1 的wire 类型数组
reg [7:0] data[3:0]; 	// 声明4个位宽为7 的reg 类型数组

通过使用下标可以访问数组中特定的单元

verilog
data[2] = 0;
data[3] = 8'hff;

助记:

位宽写在wire或者reg关键字后面则表示位宽,变量名后面则表示数组大小。如果位宽不写,则默认为1。

运算符

verilog中支持的运算符与c语言中的运算符基本一致,但是由于verilog支持不定值的赋值方式,所以某些运算符的运算结果可能会不太一样。而且还有verilog中的所特有的运算符这里也会列出来。

算术运算

  1. 在进程整数除法运算时,结果要略去小数的部分只取整数部分。
  2. 在进行取模运算时结果的符号位与第一个操作数的符号位一致。

注意:当所运算的操作数中含有不确定的值x或者z时算数运算的结果也是不确定的

逻辑运算

  1. ! && ||这三个逻辑运算符只有作用于 全 0 的操作数时才认为该操作数具有逻辑零的值,操作数中有任意多个1则认为该操作数为1,当操作数中含有x时则返回结果仍然为 x
  2. 判断逻辑相等和不相等分为两种 ,逻辑相等== !=和逻辑全等=== !==,当进行相等运算时,两个操作数必须逐位相等,比较结果才为1(真),如果任意一个操作数含有不定态(X)或高祖态(Z),其相等比较的结果就会是不定值。进行全等运算时,对不定或高阻状态也进行比较,当两个操作数完全一致时,其结果才为1,否则为0.

位运算

  • 在使用~ & | ^ ^~等位运算符时,若两个操作数的位宽不相等,则会自动将操作数在右端对齐,位数少的操作数相应的高位会使用0进行填充。

  • 移位操作,移位操作分为两种,移位运算符 >><<算术移位运算符<<<,>>>

    对于左移操作中出现的空位,都使用 0 开进行填充,在进行右移操作时,算术移位会使用符号位进行填充

    需要注意>>>作用于有符号数,左边会补符号位;<<<作用于有符号数,右边补0,符号位也被移走;

    注意事项:

    左移会导致位宽增加,右移位宽不变。

拼接与缩位

下面列举verilog中所特有的运算符。

  • 拼接运算符:可以将两个或多个信号的某些为进行拼接表示一个整体信号。

    verilog
    // 例子
    {a, b[3:0], w, 3'b101};
    
    // 将信号复制再进行拼接
    {4{w}} // 等同于 
    {w, w, w, w};
  • 缩位运算符:缩位运算符的符号与位运算符的符号一样,但缩位运算符是针对当个操作数的,对当个操作数从左到右按照位的顺序两两进行相应的运算。

    verilog
    a = 4'b0101;
    
    &a			//  0
    |a 			// 1
    ^a			// 0

程序的基本结构

结构化描述

verilog
// 这里演示全加器的实现
// 可以按照以下格式声明端口
// 输入输出方向 信号类型[位宽] 信号名;
module fadder
(
	i_A, i_B, i_Cin,
    o_S, o_C
);
	input i_A, i_B, i_Cin; // 输入端口
	output o_S, o_C; // 输出端口
	parameter s_delay=1, c_delay=1; // 定义两个参数,参数也可以代表常量
	assign #s_delay o_S = i_A ^ i_B ^ i_Cin; // 延时s_delay 
    assign #c_delay o_C = (i_A ^ i_B) & i_Cin | (i_A & i_B) // 延时c_delay 

endmodule

当需要调用已经写好的模块或者IP时(类似于函数调用),需要例化模块,并传递信号。

例化模块的基本语法如下:

verilog
模块名 实例名 (.端口名1(连线1), .端口名2(连线2), ...);

注意事项:

在顶层文件使用被调用模块的端口时,如果端口声明为input,则向该端口传递的信号可以是wire或者reg类型。如果端口声明为output,则向该端口传递的信号必须是wire类型。

一个完整地例化模块调用过程:

verilog
module fadder_delay_1
(
	i_A, i_B, i_Cin,
    o_S, o_C
)
	// 实例化一个模块并设置其中的参数
    // 第一种方式
    fadder fadder_delay_1_ins(.i_A(i_A), .i_B(i_B), .i_Cin(i_Cin), .o_S(o_S), o_C(o_C));
    defparam fadder_delay_1_ins.s_delay=2, fadder_delay_1_ins.c_delay=3;
    
    // 第二种方式
    fadder #(2,3) 
    	fadder_delay_1_ins(.i_A(i_A), .i_B(i_B), .i_Cin(i_Cin), .o_S(o_S), o_C(o_C));
    
    //第三种方式
    fadder #(.s_delay(2),.c_delay(3))
    	fadder_delay_1_ins(.i_A(i_A), .i_B(i_B), .i_Cin(i_Cin), .o_S(o_S), o_C(o_C));
endmodule

数据流描述

在使用数据流描述电路时通常使用连续赋值语句实现。连续赋值语句必须以 assign开头,出现在和门单元实例化相同的代码层次,它的结构为

verilog
assign [delay] wire_type_var=expression;

使用数据流的方式描述一个4位的加法器

verilog
module adder(
    input [3:0] a, input [3:0] b, input cin, output [3:0] sum, output cout;
)
    
    assign {cout,sum} = a+b+cin;
endmodule

在进行逻辑综合时,综合器会自动优化综合出来的门电路。

行为描述

行为描述一般使用initialalways过程块结构。

  • initial过程块

    主要用于仿真测试,用来对变量进行初始化或者产生激励波形,过程块中的内容只会被执行一次,一个module可以有多个initial块结构,各个initial过程块之间并行运行。若块中的语句没有添加延时控制,则可以延时指定的时钟周期后再执行后续指令。

    initial过程块不能进行逻辑综合

    verilog
    initial
    begin
    	#延时数 行为语句; 
        #延时数 行为语句; 
        ..
    end
    
    // 例子
    reg[3:0] a;
    
    initial begin 
    a=4'b0000; 		// 缺省为在0时刻执行该代码
    #5 a=4'b0001 	// 经过5个时间单位后执行赋值
    #5 a=4'b0010 	// 经过 10 个时间单位后执行赋值
    end
  • always 过程块,always 过程块会一致被重复执行,多个always过程块并行执行,与书写顺序无关。

    @中包含的敏感信号可为电平触发或者逻辑变量触发,使用电平触发时可以使用 posedgenegedge表示上升沿和下降沿。

    always过程块中执行的赋值语句,等号左边必须为reg类型。

    verilog
    always @ (敏感信号列表) // always @(*) 代表任意信号变化(电平变化)时都会发生的操作
    begin
        行为语句1;
        行为语句2;
        ...
    end
    
    // 例子
    always @ (posedge clk)					// 只在上升沿触发执行
    always @ (posedge clk or negedge clk)	// 在上升沿和下降沿都执行

赋值方式

由于verilog程序所描述的是电路结构,具有天然的并行特性,因此对变量的赋值于软件编程语言存在非常大的不同。

使用准则:

  1. 在描述组合逻辑的 always 块中使用阻塞赋值 =, 综合成组合逻辑电路结构,这种电路的输出结果只和输入的电平变化有关。
  2. 在描述时序逻辑的 always块中使用非阻塞赋值 <=, 综合成时序逻辑电路结构,这种电路的结构往往和触发沿有关,当触发沿满足条件时才发生变化。

注意事项:

  1. 在同一个 always 块中不能混用阻塞赋值和非阻塞赋值
  2. 不允许在多个 always 块中对同一个变量进行赋值

下面通过两个例子帮助理解两种赋值方式的不同。

阻塞赋值

所谓阻塞的概念是指,在同一个always块中,后面的赋值语句是在前一句赋值语句结束后才开始赋值的。例如下面的示例

verilog
always @(posedge CLK_i or negedge RSTn_i)
  begin
      if(!RSTn_i)
        begin
          a = 1;
          b = 2;
          c = 3;
        end
      else 
        begin
          a = 0;
          b = a;
          c = b;
        end
  end

在复位结束后,有 a = 1; b = 2; c = 3;

第一个上升沿到来的时候,有a = 0; b = 0; c = 0;,而后的结果就不会再发生变化了。

非阻塞赋值

所谓非阻塞的概念是指,在计算非阻塞赋值的RHS以及更新LHS期间,允许其他的非阻塞赋值语句同时计算RHS和更新LHS。

右边表达式是同一时间计算出来的,并同时更新左边的表达式, 即右边表达式通过组合逻辑计算出来后,马上所存到DFF中

备注: RHS - > 右边的表达式; LHS - > 左边的表达式;

  • always开始时,先计算右边表达式的值

  • always结束后才更新左边表达式的值,只能下次always执行时才能感知到变量的变化

将阻塞赋值的例子修改为:

verilog
always @(posedge CLK_i or negedge RSTn_i)
  begin
      if(!RSTn_i)
        begin
          a <= 1;
          b <= 2;
          c <= 3;
        end
      else 
        begin
          a <= 0;
          b <= a;
          c <= b;
        end
  end

复位结束后,值与阻塞赋值相同,有 a = 1; b = 2; c = 3;

当第一个上升沿到达时,有a = 0; b = 1; c = 2;

当第二个上升沿到达时,有a = 0; b = 0; c = 1;

当第三个上升沿到达时,有a = 0; b = 0; c = 0;,而后的结果就不会再发生变化了。

可以看到,当使用非阻塞赋值时,右侧的表达式的值是在同一时刻确定的,而阻塞赋值则存在一个先后次序关系。

条件判断

条件判断语句属于过程描述语句,不能单独用来描述组合逻辑电路,ifcase语句只能出现在always语句内,如果需要使用条件判断语句描述组合逻辑电路,可以在always @(*)中进行书写,在综合时会生成多路选择器。

verilog
always @(*) begin
    if (w1)
        // expression
    else
        // expression
        
    case (w2)
        a1: //...
        a2: // ...
    endcase
            
end

如果只含有一个判断变量可以使用三元表达式进行描述,在综合的时候会生成 一个 2选1 的多路选择器。

verilog
assign w1 = (c1) ? a1 : a2; // c1 为真则选择 a1 输出,否则选择 a2

注意(避免生成Latch):

使用条件判断语句描述组合逻辑电路时需要对所有可能的情况都要进行描述描述,因为verilog对于没有指定条件的输出采用的”维持输出不变“的原则,此时就需要电路具有状态保持功能,但是组合逻辑电路中并没有”记忆原件“,因此就会在综合的时候为组合逻辑电路加上锁存器(Latch)以”记住“此时的状态。

单分支

逻辑判断使用if () ... else if () ... else 语句表示。具体的语法和c 语言的差不多,这是在书写多条语句的时候需要使用 begin ... end括起来。

verilog
if (condition)
    expression;

if (condistion1)
    expression1;
else
    expression2;

if (condistion1)
    expression1;
else if (condistion2)
    expression2;
...
else
    expression3;

多分支

多条件判断时可以使用 case 语句,它的使用格式是

verilog
always @(*)
    begin
        case (var)
            condition1: expression1;
            condition2: expression2;
            defualt: defualt_expression;
		endcase
    end

// 支持不定态的casex
always @(*)
    begin
        casex (in)
            4'bxxx1: pos <= 2'd0;
            4'bxx10: pos <= 2'd1;
            4'bx100: pos <= 2'd2;
            4'b1000: pos <= 2'd3;
            default: pos <= 2'd0;
    	endcase
    end

尽量避免使用 casex语句,可以使用casez语句。

注意:

  1. 分支表达式的值互不相同
  2. 所有表达式的位宽必须相等,不3能使用 'bx 代替 n'bx
  3. 当不需要考虑 x 和 高阻时 使用 casex ... endcase
  4. 当不需要考虑 高阻时 使用 casez ... endcase
  5. 0 != x or z

循环语句

软件开发的高级语言大多提供了forwhile等循环流程控制语句。verilog中也提供了for语句,但是在HDL语言中,循环执行的操作一般通过时序逻辑电路完成,for语句一般只用在testbench 中进行数据生成,在RTL编程中很少进行使用。

主要原因就是for循环会被综合器做并行展开,每个变量独立占用寄存器资源,如果不能很好地考虑运行逻辑,会造成资源的极大浪费。

常见使用格式如下:

verilog
reg [3:0] counter ;
initial begin
    counter = 'b0 ;
    while (counter<=10) begin
        #10 ;
        counter = counter + 1'b1 ;
    end
end

生成语句

  1. 批量生成语句

2001标准中新加入的generate语句可以批量例化多个模块,并设置其中的连线。genvar声明了generatate块中使用的变量名。

具体格式如下:

verilog
genvar i; // 标识变量

generate
    for(init; condistion; next)begin:module_name
        // expression
    end
endgenerate

可以看到for中的表达式和C语言中时一样的,只是关键字begin后面需要跟一个module_name,该名称指明了generate所生成模块的名称,可以通过module_name[index]的方式的访问实例化生成的模块。

例如:

verilog
genvar i;
        
generate 
    for (i = 1; i <= 510; i=i+1) begin :ut
        relu110 u(.l(q[i+1]), .c(q[i]), .r(q[i-1]), .n(q_next[i])); 
    end
endgenerate

注意:不能将generate for语句放在 always语句中

  1. 条件生成语句

条件生成语句包括 generate if generate case两种。

generate if语句的格式

verilog
generate
    if (condition) begin: Name1
		//expression
	end else begin: Name2
		//expression
	end
endgenerate

case 语句的格式

verilog
generate
	case (<constant_expression>)
     	<value>: begin: <label_1>
                 <code>
              end
     	<value>: begin: <label_2>
                 <code>
              end
     	default: begin: <label_3>
                 <code>
              end
	endcase
endgenerate

注意: 这段表达是在编译时生效的,因此 condition 应该满足以下几个条件

  • 条件表达式必须是参数或宏定义。

  • 条件表达式必须在编译时求值。

  1. 生成语句的嵌套

generate for 语句中可以嵌套ifcase语句用于判断,这样可以提升生成语句的灵活性。例如:

verilog
genvar i;

generate 
    for (i = 0; i <= 511; i=i+1) begin :ut
        if (i == 0)
            relu110 u(.l(q[i+1]), .c(q[i]), .r(1'b0), .n(q_next[i]));
        else if (i == 511)
            relu110 u(.l(1'b0), .c(q[i]), .r(q[i-1]), .n(q_next[i]));
        else
            relu110 u(.l(q[i+1]), .c(q[i]), .r(q[i-1]), .n(q_next[i])); 
    end
endgenerate

属性语法

属性语法不直接影响模块的逻辑行为,但它们可以提供有关模块和其实例的信息,以帮助综合工具和其他工具正确处理设计。

verilog
// 告诉综合工具保留指定的信号或模块,即使它们似乎没有被使用, 方便进行调试查看
(* KEEP="TRUE"*)

// 告诉综合工具不要修改指定的信号或模块,并且不要尝试进行优化。
(* DONT_TOUCH="TRUE"*)

函数

verilog
function integer func1(integer pa) 
	begin
        // expresion

        // 这是返回值
        func1 = //expression; 
	end
endfunction

常见技巧

  1. 变量索引与部分选择

part-select 即引用数组中一个元素的某几位。

例:reg [9:0] a[9:0] 引用第0个元素的后四位:a[0][0+:4]a[0][3-:4],其中 -:+: 左边可以是常数或者变量,右边必须是常数。(这个会很有用,可以省很多时间写部分索引)

  1. 1’sb1表示 -1,1’sb0 表示 0
  2. 单纯的十进制数为 32bit interger 有符号数
  3. ±运算赋值运算 所有操作数是有符号数,那么结果才是有符号数。

编程规范:

总结verilog的编程规范,一共10条。

  1. 不要太多的 if—else 嵌套
  2. 不要编写过于庞大的状态机
  3. 复杂的状态机,可以采用 2 段式以上的实现
  4. 尽量使用时序逻辑完成编程
  5. 使用组合逻辑不要过于庞大
  6. 复杂代码拆分简单模块
  7. 复杂计算,增加流水设计
  8. 高速模块和低速模块搭配使用
  9. case 语句一定有 default
  10. 组合逻辑有 if 必须有 else 防止产生锁存器,锁存器所以出毛刺

最新更新: