千字长文带你梳理Verilog怎么写

Verilog梳理

诸位看官请确保阅读本文之前已经学过了数字逻辑基本的知识。本文梳理的内容基于《计算机组成与设计:硬件与软件接口》这本书,侧重点在于最终设计出CPU。

1. Verilog的数据类型和操作类型

两种基本数据类型datatypes

wire表示一个组合信号(specifies a combinational signal)

reg-用于存储一个数据 (a register)

//下面展示一些基本定义方式
//32位宽
reg [31:0]
wire[31:0]

//访问指定连续位
reg[starting bit: ending bit]

//定义寄存器案例
reg [31:0] registerfile[0:31];


1个reg或wire可能取值

0 或 1

X:未知

Z:高阻态(不讨论)

不同进制的定义

4'b0100 //4位二进制数,也可以表示为4'd4; b, d, h对应二进制、十进制和16进制
//binary2, octal8, hexadecimal 16, decimal 10

操作符

从C语言继承单目运算符和双目运算符和条件运算符?:。

有自己的操作符:所有位进行对应操作

& | ^ ,如 ^A返回A中所有位异或的结果

拼接运算符:{}

{16{2'b01}} //创建了一个32位的数,形式为010101...01
{A[31:16], B[15:0]} //创建了一个值,upper16位来自A,lower16位来自B

2. Verilog程序的结构

  • initial结构(对reg型变量进行初始化)
  • 连续赋值语句(只用于组合逻辑)
  • always结构(sequential时序和combinational组合逻辑都可以用)
  • 模块实例化(个人补充:有点基于类定义对象)

3.Verilog构造复杂的组合逻辑

连续赋值continuous assignment在assign关键字中反映出来,就像一个组合逻辑的函数一样使用(output被连续赋值,input值的一个改变会立即在output值处体现)。wires只能通过连续赋值来赋值。(博主注:这句话还有待讨论,请自行判断)。

下面给出一个用连续赋值来实现半加法器的例子

module half_adder(A, B, Sum, Carry):
	input A, B; //两个1位输入
	output Sum, Carry; //两个1位的输出
	assign Sum = A^B; 
	assign Carry = A & B; 
endmodule
	

但是,对于更复杂的结构,用assign显得很蠢(原文tedious & awkward)。于是,这时候可以使用always块(block)来描述一个组合逻辑的module。always block中很重要的就是由@引导的敏感列表(sensitivity list)。

如果敏感列表中任一个信号干煸,always块会被不断地reevaluate。如果always块中有很多的statements,那么用begin,end把它框起来,就像C语言中的括号一样。

Reg或许只能在(博主注:may only be?原文是?这里怎么大家也要自己分辨一下)always block里面赋值,赋值方式是过程赋值procedural assignment(不同于之前assign这种连续赋值continuous assignment方式)。有两种不同的过程赋值,一种是=,一种是<=, 前者是阻塞赋值,后者是非阻塞赋值(nonblocking), always模块中的非阻塞赋值右侧是同时执行的(simultaneously)。

在always模块中只有reg能被赋值,当我们想用always块的时候,一定要确保reg没有synthesize into a register(博主注:这里怎么翻译没太想好)。原书这里提出了一些编程的好习惯,感兴趣可以去进一步阅读。

下面给出always模块的两个例子。其中的case就像C语言中的switch。

//32位输入的4选1多路选择器,用case语句实现
module Mult4to1(In1, In2, In3, In4, Sel, Out):
    input [31:0] In1, In2, In3, In4; 
    input [1:0] Sel; //select signal
    output reg [31:0] Out; 
    always @(In1, In2, In3, In4, Sel)
    case(Sel)
        0: Out <=In1;
        1: Out <=In2;
        2: Out <=In3;
        default: Out <=In4;
    endcase
endmodule
//一个mips ALU(用Verilog的行为级语言表述)
module MIPSALU(ALUctl, A, B, ALUOut, Zero):
    input [3:0] ALUctl;
    input [31:0] A, B;
    output reg[31:0] ALUOut;
    output Zero;
    assign Zero = (ALUOut==0);
    always @(AlUctl, A, B) begin
        case(ALUct1)
            0: ALUOut <= A&B;
            1: ALUOut <= A|B;
            2: ALUOut <= A+B;
            6: ALUOut <= A-B;
            7: ALUOut <= A<B?1:0;
            12: ALUOut <= ~(A|B);
            default: ALUOut<=0; //default to 0, should not happen
        endcase
    end
endmodule

下面给出MIPS ALU control的一个小片段

module ALUControl(ALUOp, FuncCode, ALUCtl);
	input [1:0] ALUOp;
	input [5:0] FuncCode;
	output [3:0] reg ALUCtl;
	
	always case(FuncCode)
	32: ALUOp <=2; //add
	34: ALUOp <=6; //substract
	36: ALUOp <=0; //and
	37: ALUOp <=1; //or
	39: ALUOP <=12; //nor
	42: ALUOp <=7; //slt
	default: ALUOp <=15; //should not happen
	endcase
endmodule

4-Clocks

下面来简单介绍一下时钟verilog怎么写。如果概念不清楚的同学,请自己补一下数字逻辑中的有关知识。

自我考核时间,你是否能说出以下概念的意义
边沿触发,时钟周期,上升沿,下降沿,同步,异步,建立时间setup time,保持时间hold time

下面对后面两个概念进行一个说明:

setup time:the minimum time that the input to a memory device must be valid before the clock edge(输入在时钟沿到来前必须保持的时间)

hold time: 输入在时钟沿之后必须保持的时间。

时钟周期必须大于等于:Tprop(信号在触发器传递的时间,也叫clock to Q) + Tcombinational(关键路径最长时延)+Tsetup(时钟建立时间)+Tskew(时钟扭曲时间,指的是时钟信号到不同地方不同时间带来的影响时间)。

在CPU设计中,有关技术常用于寄存器堆即register file。

存储元件Memory Elements

Flip-flops触发器

下面给出一个上升沿触发的D触发器的例子

module DFF(clock, D, Q, Qbar):
    input clock, D;
    output reg Q;
    output Qbar;
    assign Qbar=~Q;
    always @(posedge clock)
        Q=D;
endmodule
Latches锁存器
Registers寄存器
具体例子
//A specification of a clock
reg clock;
always 
#1 clock=1; #1 clock=0;
//一个小的sample,上升沿时寄存器A写入b的值
reg [31:0] A;
wire [31:0] b;

always @(posedge clock) A<=b;
//行为级语言的MIPS registers file,上升沿触发
module registerfile(Read1, Read2, WriteReg, WriteData, RegWrite, Data1, Data2, clock);
    input [5:0] Read1, Read2, WriteReg;//用于读或写的寄存器编号
    input [31:0] WriteData;//data to write
    input RegWrite, clock;//写控制信号
    output [31:0] Data1, Data2; //the register values read
    reg [31:0] RF [31:0]; //32个32位寄存器
    
    assign Data1 = RF[Read1];
    assign Data2 = RF[Read2];
    
    always begin 
        @(posedge clock) if (RegWrite) RF[WriteReg] <= WriteData;
    end
endmodule

    
    

5-Finite-State Machines有限状态机FSM

这是又一个很重要的概念。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dFtmT7YN-1652006634991)(C:\Users\Eliza\AppData\Roaming\Typora\typora-user-images\image-20220508183302195.png)]

下面给出上面的两状态交通灯的verilog代码。NSlite=1当切仅当NS方向的灯是绿的,EWlite同理。NScar=1当且仅当有车经过NS方向的路,EWcar同理。NSgreen=1当且仅当NS方向交通灯是绿的,EWgreen同理。

下面给出对应的verilog代码

module TrafficLite(EWCar, NSCar, EWLite, NSLite, clock);
    input EWCar, NSCar, clock;
    output EWLite, NSLite;
    reg state;
    initial state=0; 
    
    assign NSLite = ~state;
    assign EWLite = state;
    
    always @(posedge clock)
        case (state)
            0: state = EWCar;
            1: state = NSCar;
        endcase
endmodule

6- 补充在设计CPU时候适用的trick

  1. assign语句左边的信号必须为wire类型,always中被复制的信号必须是reg类型

  2. 用#来说明参数

    module mux2 #(parameter width = 8) (out, s, d0, d1);
    
  3. mux2_tb是上面对应的测试文件,其中

    `timescale 10ns/1ns  //预编译命令,时间单位/时延精度
    
    module mux2_tb;
        
    ...
        mux2 #{32} dut(out, s, d0, d1); //实例化
        
        initial
            $monitor("$s=%d", s); //monitor用于检测指定变量,只要这些变量发生了变化,就会立即显示对应的输出语句
        
       	initial
            begin
                #5 $display("in=%d;out=%8b", in, out) //显示输出对应信号,自动换行	
                #5 $stop;
            end
    endmodule
    
  4. always@(*) 表示所有输入信号都是敏感信号

  5. 端口说明也可以写在端口列表中

  6. 也可以用名称关联写法来实例化

    mux2 #(.width(width)) muxh(.out(t2), .s(s[0]), .d0(d2), .d1(d3));
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值