一、语法

1.模块的定义方法

module AndGate(
        input i1,
        input [7:0] i2,
        output reg [7:0] i3 [0:15], 
        //声明了一个8位的16单元存储器,其中前面的中括号内为位宽,后面的中括号内为存储器。
        //i3[2]访问i3中第3个元素
        output i4 //最后没有逗号!
    );
    assign i4 = i1 & i2;
endmodule

而这样定义无法合成只能这样做
module AndGate(
        input a,input b,output c
    );
endmodule
module AndGate(i1,i2,o);
    input i1;
    input i2;
    output o;
    //或者 intput i1,i2;
endmodule

reg[31:0] r1, r2; 这个[31:0]仅仅对应了r1,不对应r2。

关于[3:0] 和 [4:7]定义位宽的区别?
两者位宽均为4。但[3:0] 的最低位是0,最高位是3;[4:7]的最低位是7,最高位是4,数据存储顺序不同。 比如4‘b0001在 a[3:0] 存储是 a[0]=1,a[1]=0,a[2]=0,a[3]=0。在 b[4:7] 存储是 b[4]=0,b[5]=0,b[6]=0,b[7]=1。

2. 常用数据类型

2.1 wire

wire a;

wire型数据属于线网nets型数据,通常用于表示组合逻辑信号,可以将它类比为电路中的导线。它本身并不能存储数据,需要有输入才有输出(这里输入的专业术语叫驱动器),且输出随着输入的改变而即时改变。一般使用assign语句对wire型数据进行驱动(assign语句将在下一节中进行讲解)。

wire型的数据分为标量(1位)和向量(多位)两种。可以在声明过程中使用范围指示器指明位数,如wire [31:0] a;。冒号两侧分别代表最高有效位(MSB, Most Significant Bit)和最低有效位(LSB, Least Significant Bit)。

  • 在访问时,可以使用形如a[7:4]的方式取出a的第7-4位数据!!!

一般在使用wire型数据前应先声明它。但如果在模块实例的端口信号列表中使用了一个未声明的变量,则会将其默认定义为1位的wire变量。

需要注意的是,信号变量与C语言中的变量有所不同,不能像C语言一样随意赋值,一般需要按照组合逻辑的规则进行操作。比如,对于wire型变量a,assign a = a + 1是不合法的。

使用 a[7:3]获取a的7-3位。
只能assign赋值,assign语句不能在always和initial块中使用。
assign语句经常与三目运算符配合使用建模组合逻辑。
一般来说,assign语句综合出来的电路是右侧表达式化简后所对应的逻辑门组合。

2.2 reg

它必须在always块内赋值

我们可以通过对reg型变量建立数组来对存储器建模,例如reg [31:0] mem [0:1023];,其中前面的中括号内为位宽,后面的中括号内为存储器数量。这种写法在我们开始搭建CPU后会用到。我们可以通过引用操作访问存储器型数据元素,类似于位选择操作,例如mem[2]就是访问mem中的第3个元素。

2.3 数字面量

Verilog中的数字字面量可以按二进制(b或B)、八进制(o或O)、十六进制(h或H)、十进制(d或D)表示。 数字的完整表达为<位宽>'<进制><值>,如10'd100。省略位宽时采用默认位宽(与机器有关,一般为32位),省略进制时默认为十进制,值部分可以用下划线分开提高可读性,如16'b1010_1011_1111_1010。 Verilog中除了普通的数字以外,还有两个特殊的值:x和z。x为不定值,当某一二进制位的值不能确定时出现,变量的默认初始值为x。z为高阻态,代表没有连接到有效输入上。对于位宽大于1的数据类型,x与z均可只在部分位上出现。

正确的定义有:

'h20
8'o100
4'b10x0
4'b101z
-8'd5

不正确的定义有:

2b10
3'101
32'd-100
8'b_0011_1010

2.4 integer

integer数据类型一般为32位,与C语言中的int类似,默认为有符号数,在我们的实验中主要用于for循环(将在本章后面提到)和状态存储。

2.5 parameter

parameter类型用于在编译时确认值的常量,通过形如parameter 标识符 = 表达式;的语句进行定义,如:parameter width = 8;。在实例化模块时,可通过参数传递改变在被引用模块实例中已定义的参数(模块的实例化将在后面的章节进行介绍)。parameter虽然看起来可变,但它属于常量,在编译时会有一个确定的值。

parameter可以用于搭建数据位宽可变的模块等。

3. 常用运算符

  • Verilog中没有自增、自减运算符。
  • 逻辑右移运算符>>与算术右移运算符>>>

它们的区别主要在于前者在最高位补0,而后者在最高位补符号位。

在Verilog HDL中,wire、reg等数据类型默认都是无符号的。当你希望做符号数的操作时,你需要使用$signed()。

16'hf000 >> 4 =
signed(16'hf000) >>> 4
如果要把上面当作有符号数,必须signed($signed(16'hf000) >>> 4)
因为这里自动将存储的操作数认为是无符号数。
  • 相等比较运算符=====!=!==

==!=可能由于不定值x和高阻值z的出现导致结果为不定值x,而===!==的结果一定是确定的0或1(x与z也参与比较)。

  • 阻塞赋值=和非阻塞赋值<=

不同于assign语句,这两种赋值方式被称为过程赋值,通常出现在initial和always块中,为reg型变量赋值。这种赋值类似C语言中的赋值,不同于assign语句,赋值仅会在一个时刻执行。由于Verilog描述硬件的特性,Verilog程序内会有大量的并行,因而产生了这两种赋值方式。这两种赋值方式的详细区别我们会在之后的小节内介绍,这里暂时只需记住一点:为了写出正确、可综合的程序,在描述时序逻辑时要使用非阻塞式赋值<= 。

  • 位拼接运算符{}

这个运算符可以将几个信号的某些位拼接起来,例如{a, b[3:0], w, 3'b101};;可以简化重复的表达式,{4{w}}等价于{w,w,w,w};还可以嵌套,{b, {3{a, b}}}等价于{b, {a, b, a, b, a, b}},也就等价于{b, a, b, a, b, a, b}。

  • 缩减运算符

运算符&(与)、|(或)、^(异或)等作为单目运算符是对操作数的每一位递推运算,如对于reg[31:0] B; &B代表将B的每一位与起来得到的结果。

4. 时序逻辑建模常用语法

4.1 always块

always @(posedge clk)
begin 
//do something
end
  1. always@(*)或者always @*表示对其后面的语句块中出现的所有输入变量的变化是敏感的,也就是对语句块中赋值符号右边的变量值的变化敏感,此题中always@(*)语句块中右值并不是变量,也就不会发生变化。

2

  1. always @(a)表示当变量a发生变化时,执行之后的语句;若always @(posedge a)表示当a达到上升沿,即从0变为1时触发条件,下降沿不触发。多个条件使用,or隔开,即always @(negedge a,posedge b or c)

4.2 initial块

initial块后面紧跟的语句或顺序语句块在硬件仿真开始时就会运行,且仅会运行一次,一般用于对reg型变量的取值进行初始化。initial块通常仅用于仿真,是不可综合的。下面的代码用于给寄存器a赋初始值0:

reg a;
initial begin
    a = 0;
end

4.3 if

为了避免意料之外的锁存器的生成而导致错误,建议大家为所有的if语句都写出相应的else分支。

always @ * begin
    if (a > b) begin
        out = a;
    end
    else begin
        out = b;
    end
    1
end

简略写法

if( ? ) pass = 1;
else pass = 2; 

4.4 case

Verilog中的case语句与C语言的写法略有区别,详见下方的示例。case语句同样只能出现在顺序块中,其中的分支也只能是语句或顺序块。与C语言不同,case语句在分支执行结束后不会落入下一个分支,而会自动退出。举例如下:

需要指出的是,case语句进行的是全等比较,也就是每一位都相等(包括x和z)才认为相等。另外,还有casex和casez两种语句,我们的课程涉及不多,感兴趣的同学可以自行查阅相关资料。

always @(posedge clk) begin
  case(data)
      0: out <= 4;
      1: out <= 5;
      2: out <= 2;
      3: begin
          out <= 1;
      end
      default: ;
  endcase
end

4.5 for

注意Verilog没有自增自减运算符

module demo(
  input a,
  output b
);
    integer i;
    always @(*) begin
        for(i=0;i<7;i=i+1) begin 
            //do something
        end
    end
endmodule

4.6 while

always @(posedge clk) begin : count1 //命名顺序块,建模时序逻辑
    reg[7:0] tempreg;
    tempreg = 1'hff;
    while(tempreg) begin
        if(tempreg[0]) count = count +1; //取最低位
        tempreg = tempreg >> 1;
    end

end 

4.7 时间控制语句

时间控制语句通常出现在测试模块中,用来产生符合我们期望变化的测试信号,比如每隔5个时间单位变更一次信号等。这个语句通过关键字#实现延时,格式为"#时间",当延时语句出现在顺序块中时它后面的语句会在延时完毕后继续执行。举例如下:

#3;         //延迟3个时间单位
#5 b = a;   //b为reg型,延迟5个时间单位后执行赋值语句
always #5 clk = ~clk;   //每过5个时间单位触发一次,时钟信号反转,时钟周期为10个时间单位
assign #5 b = a;        //b为wire型,将表达式右边的值延时5个时间单位后赋给b

5. 模块的典型内部结构

一个模块的典型结构可以大致划分为三个部分:组合逻辑、时序逻辑和对其他模块的引用。

对其他模块的引用---模块实例化

module Sample(
    input a,
    output b
    );
引用
reg a,b;
Sample uut(.a(x),.b(y));

6. 阻塞赋值和非阻塞赋值

6.1非阻塞赋值

处在一个always块中的非阻塞赋值是在块结束时同时并发执行的。对于ISim,在每一条非阻塞赋值执行前,仿真器“按下快门”保存下了在“<=”右边参与运算的变量值。在块结束进行赋值时,对于“<=”左边被赋值的变量,都是用“快照”中的值参与运算的。

当阻塞赋值和非阻塞赋值同时存在,则阻塞赋值将其划分成了几部分,每部分遵循非阻塞赋值的特性

7. 宏定义的使用

注意,引用宏名时也必须在宏名前加上符号“反引号”表明该名字是经过宏定义的名字。

`define WORDSIZE 8
reg[1:`WORDSIZE] data;

二、模拟仿真

1.生成仿真模板文件

新建测试模块:工程管理视窗单击右键 -> New source -> Verilog Test Fixture

在自动生成的文件中编写测试代码, 工程管理视窗选择Simulation,双击下方Behavioral Check Syntax,检查通过后,再双击Simulate Behavioral Model即可使用ISim进行仿真

下面是生成的模板代码

module test;

    // Inputs
    reg [31:0] A;

    // Outputs
    wire [7:0] O1;
    wire [7:0] O2;
    wire [7:0] O3;
    wire [7:0] O4;

    // Instantiate the Unit Under Test (UUT)
    splitter uut (
        .A(A), 
        .O1(O1), 
        .O2(O2), 
        .O3(O3), 
        .O4(O4)
    );

    initial begin
        // Initialize Inputs
        A = 0;

        // Wait 100 ns for global reset to finish
        #100;

        // Add stimulus here

    end

endmodule

2.综合

在View的Implement选项中,选择Synthesize - XST,然后选择View RTL Schematic,来综合得到电路图

三、易错地方

3.1 有符号数的泪

前提:在一个算术式子中,如果有无符号数存在,就默认向无符号数转化。

所以我们进行算术右移时,要写成 assign C = signed(signed(A)>>>B);

$signed()可以屏蔽外部表达式的符号性传递。

其中

assign C=(ALUOp==3'b101):signed(A)>>>B:$signed(A+B); 可以得到正确的位移结果。

但时,assign C=(ALUOp==3'b101):signed(A)>>>B:A+B; 就无法得到正确的位移结果。

另外,assign C=(ALUOp==3'b101):signed(A)>>>B:0; 也可以得到正确的位移结果。

但是,assign C=(ALUOp==3'b101):signed(A)>>>B:32'b0; 就不能得到正确的位移结果。

涉及有符号数的运算,不推荐使用三目运算符,推荐使用always@(*)的方式?那么result在写代码时应写为reg型。

我认为是因为,三目运算符中的有符号数据容易受其他数据影响从而变为无符号数,而always块里不会受干扰。

其实always块里也是会受干扰的,只不过因为在always块里我们习惯用if或者case代替三目运算符,不构成表达式的嵌套,所以说不会出现影响。

signed的行为其实没有那么迷惑,它是一个系统函数,其作用是计算其内部的表达式,并且把结果强制转换为一个有符号的值。返回值的位宽仅取决于传入的表达式,返回值的符号性与外界无关

也就是说,我们完全可以把signed(*#$%^&...)看作一个大号的有符号的常数。

例如:(假设A为16位普通reg型变量)

  • signed(A>>>B) 的返回值是一个有符号的16位的数值,可以视为16'shxxxx(s为有符号常数标志,可以加在进制标志前面)
  • signed (A+32'hxxxx_xxxx)的返回值是一个有符号的32位数值,可以被看作32'shxxxx_xxxx
  • signed (1'b0?A:A>>>2)的返回值是一个有符号的16位数值,也可以视为16'shxxxx。注意其中的A>>>2仍然按照无符号数规则在A的最高位进行补零,之后再被强制转换成有符号数。此外,移位运算不影响位宽。

至于剩下的部分,就是表达式的符号性推导与传递了~

3.2 位拼接

位拼接运算符{}

这个运算符可以将几个信号的某些位拼接起来,例如{a, b[3:0], w, 3'b101};;可以简化重复的表达式,{4{w}}等价于{w,w,w,w};还可以嵌套,{b, {3{a, b}}}等价于{b, {a, b, a, b, a, b}},也就等价于{b, a, b, a, b, a, b}。

0默认为32位的数据

{{16{imm[15]}},imm} 表示将16位的imm符号扩展到32位。

高位零扩展 {{16'b0},imm}或者 {{16{1'b0}},imm}

{imm,{16'b0}}将imm加载到高位,低位补0.

注意以上数据都加了 {}

如果要看为符号数可以加 $signed({{16{imm[15]}},imm})

3.3 格雷码

更通俗点,就是原二进制码逻辑右移一位之后与原二进码对应位进行异或运算后即为格雷码。

Output = (temp >> 1) ^ temp;

也可以使用case语句进行输出。

结合parameter或者`define语句

parameter State1 = 4'b0001;
`define State1 4'b0001 //没有分号
Decimal Binary Gray
0 0000 0000
1 0001 0001
2 0010 0011
3 0011 0010
4 0100 0110
5 0101 0111
6 0110 0101
7 0111 0100
8 1000 1100
9 1001 1101
10 1010 1111
11 1011 1110
12 1100 1010
13 1101 1011
14 1110 1001
15 1111 1000

3.4 Verilog的异步复位

  • 复位时回到起始状态(敏感信号为时钟复位信号,注意同步复位和异步复位的区别)

同步复位always@(posedge clk)+if(reset)+复位操作;异步复位always@(posedge clk or posedge reset)+ if(reset)+复位操作;其中,异步复位敏感条件中添加复位信号就代表在复位信号发生变化时便执行always块中的操作,不必等待时钟上升沿,从而实现异步复位。

3.5 Moore型状态机

initial 块内只初始化输出reg类型的数据和自己定义的integer类型和reg类型
也可以在定义时进行初始化。

另外,在有清零信号时,我们只需要重置输出(reg类型)、reg和integer,不用重置输入input。

output reg Overflow = 0;
integer state = 0;

img

3.6 testbench

在自己编写 testbench 进行测试时,应尽可能避免在读取数据的时钟上升沿同时改变输入信号,可以比上升沿稍提前些改变输入使得上升沿到来时输入信号稳定,或者在时钟下降沿时改变输入信号(使输入和时钟刚好相差半个周期)从而避免上述现象。(不知道评测机采用的 testbench 是否总能保证时钟上升沿时输入信号稳定)

样例提供的波形中,输入数据是恰好在时钟上升沿变化还是在时钟上升沿之前的一小段时间变化的?

答:上升沿之前,他应该会确保采样时间内输入是稳定的

`timescale 1ns/1ps

module test;

    reg clk;
    reg reset;
    reg [7:0] in;

    wire result;

    BlockChecker uut(
        .clk(clk),
        .reset(reset),
        .in(in),
        .result(result)
    );

    always #5 clk<=~clk;

    initial begin

        clk<=0;
        reset<=1;
        in<="a";
        #10;
        reset<=0;

        #10;
        in<="b";
        #10;
        in<="e";
        #10;
        in<="g";
        #10;
        in<="i";
        #10;
        in<="n";
        #10;
        in<=" ";
        #10;
        in<=" ";
        #10;
        in<=" ";
        #10;
        in<=" ";
    end

endmodule

gmm