此笔记仅作为Verilog临时速成的主观简单记录,对于Verilog的语法和性质等并没有进行完备的记录、对于Verilog更深入的内容也并不记载,同时并不保证笔记的绝对正确

Verilog与C相似,很多语法结构和设计思想都可以参考C

Verilog是一门硬件描述语言,虽然与C有很多相似,但在基本设计思想上与C有本质不同
设计代码时应当时刻谨记我们在描述硬件而非设计程序

项目结构

设计方法

Verilog项目一般采用自顶向下的设计方法,即先定义顶层模块功能,进而分析要构成顶层模块的必要子模块;然后进一步对各个模块进行分解、设计,直到到达无法进一步分解的底层功能块。Verilog与C有许多相似之处,同样地,顶层模块一般可以视作C代码的main函数,编译器将由顶层模块开始组织项目

模块结构

简单而言,Verilog模块内部由模块定义声明、内部信号声明和各种逻辑语句、赋值语句以及其他结构构成

基本语法

基本

Verilog语句以分号结尾,多行语句一般用beginend包住
代码使用//进行单行注释、用/**/进行多行注释
单个模块使用moduleendmodule包住

编译指令

`define、 `undef

`define用于编译阶段文本替换,与C的#define相似
`undef用来取消之前的宏定义

`include

用于编译阶段的文件包含,与C的#include相似

`timescale

用于定义时延、仿真的单位和精度

`timescale      time_unit / time_precision

time_unit表示时间单位,time_precision表示时间精度,它们均是由数字以及单位s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成
时间精度大小不能超过时间单位大小
可以使用#n来实现n个时间单位的时延

数值表示

Verilog有0、1、x或X(未知)、z或Z(高阻态)四种电平逻辑

Verilog允许使用下划线_来分隔多位数字增加可读性

Verilog在整数声明时有四种基数格式:十进制('d'D)、十六进制('h'H)、二进制('b'B)、八进制('o'O
数值可不指明位宽
负数通常在位宽前表示

示例

4'b1011         // 4bit 数值
32'h3022_c0de   // 32bit 的数值
'd100            //一般会根据编译器自动分频位宽,常见的为32bit
-6'd15             //负数

Verilog在实数声明时使用十进制或科学计数法表示

Verilog里字符串是由双引号包起来的字符队列
字符串不能多行书写,即字符串中不能包含回车符
字符串本质是一系列的单字节ASCII字符队列

数据类型

interger(整数)

integer var_name ;

若将实数值赋值给整数变量,将只保留其整数部分

real(实数)

real var_name ;

可用十进制或科学计数法表示

time(时间)

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

示例

time       current_time ;
initial begin
       #100 ;
       current_time = $time ; //current_time 的大小为 100
end

parameter/localparam(参数)

parameter    name = data;
localparam    name = data;

参数用于表示常量,其只能赋值一次
局部参数用localparam声明,其与parameter不同之处在于其只能在本模块中被调用

数组

type    var_name [n0:m0][n1:m1]… ; //声明
var_name [a][b]… ; //访问

Verilog允许声明多维数组

wire(线网)

wire var_name ;

wire类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动
如果没有驱动元件连接到wire型变量,缺省值一般为高阻态 "Z"

reg(寄存器)

reg    var_name ;

寄存器用来表示存储单元,其会在被改写前一直保持旧值
integerrealtime本质为寄存器类型

向量

[n:m] //位宽=n-m+1

当位宽大于1时wirereg可以声明为向量形式
n和m可以为表达式
Verilog允许我们指定使用某一位或向量若干相邻位
Verilog允许我们指定当前位前/后若干位的选择
Verilog允许使用大括号将向量合成新的向量

示例

reg [3:0]      counter ;    //声明4bit位宽的寄存器counter
wire [32-1:0]  gpio_data;   //声明32bit位宽的线型变量gpio_data
wire [8:2]     addr ;       //声明7bit位宽的线型变量addr,位宽范围为8:2
reg [0:31]     data ;       //声明32bit位宽的寄存器变量data, 最高有效位为0

//表达式可变向量域
reg [31:0]     data1 ;
reg [7:0]      byte1 [3:0];
integer j ;
always@* begin
    for (j=0; j<=3;j=j+1) begin
        byte1[j] = data1[(j+1)*8-1 : j*8];
        //把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]
    end
end

//下面 2 种赋值是等效的
A = data1[31-: 8] ;
A = data1[31:24] ;
//下面 2 种赋值是等效的
B = data1[0+ : 8] ;
B = data1[0:7] ;

//向量合成
wire [31:0]    temp1, temp2 ;
assign temp1 = {byte1[0][7:0], data1[31:8]};  //数据拼接
assign temp2 = {32{1'b0}};  //赋值32位的数值0  

存储器

reg    var_name [n:m] ;

存储器本质为寄存器数组,可用于描述RAM、ROM的行为

字符串

reg [0:strlen*8-1] str ;
initial begin
    str = "your_string" ;
end

字符串保存在寄存器变量中,每个字符占8 bit
若长度溢出则直接截断,若长度不足则补零
特殊字符需要使用前缀来转义

逻辑操作

Verilog 中提供了大约 9 种操作符,分别是算术、关系、等价、逻辑、按位、归约、移位、拼接、条件操作符,大部分与C相似

  • 算术:乘*、除/、加+、减-、求幂**、取模%
  • 关系:大于>,小于<,大于等于>=,小于等于<=
    • 关系操作符的正常结果有2种,真(1)或假(0),如果操作数中有一位为x或z,则关系表达式的结果为x
  • 等价:逻辑相等==,逻辑不等!=,全等===,非全等!==
    • 等价操作符的正常结果有2种:为真(1)或假(0)
    • 逻辑相等/不等操作符不能比较x或z,当操作数包含一个x或z,则结果为不确定值
    • 全等比较时,如果按位比较有相同的x或z,返回结果也可以为1,即全等比较可比较x或z
  • 逻辑:逻辑与&&,逻辑或||,逻辑非!
    • 如果一个操作数不为0,它等价于逻辑1
    • 如果它任意一位为x或z,它等价于x
    • 如果任意一个操作数包含x,逻辑操作符运算结果不一定为x
  • 按位:取反~,与&,或|,异或^,同或~^
  • 归约:归约与&,归约与非~&,归约或|,归约或非~|,归约异或^,归约同或~^
    • 归约其实意为从最左位开始与每位右一位进行逻辑运算,可用于进行是否全1/全0等多位判断、归约操作
  • 移位:左移<<,右移>>,算术左移<<<,算术右移>>>
    • 算术右移会根据最左位决定补1/补0来保证补码的正确性,其余操作符一律补0
  • 拼接:大括号{}
  • 条件:expression ? true_expression : false_expression
    • 等价于C的三目运算符

过程结构、时序控制

assign语句

assign target = expression ;

assign语句称作连续赋值语句
之所以称为连续赋值语句是指其总是处于激活状态,只要表达式中的操作数有变化,立即进行计算和赋值
赋值目标必须是wire
assign语句中没有beginend

always语句

always @(signal_condition) //星号(*)表示全体信号
    expression ;

always语句块又称过程块
always语句本身不是单一的有意义的一条语句,而是和下面的语句一起构成一个语句块,称之为过程块,过程块中的赋值语句称过程赋值语句
该语句块不是总处于激活状态,当满足激活条件时才能被执行,否则被挂起,挂起时即使操作数有变化,也不执行赋值,赋值目标值保持不变,激活条件分为边沿敏感(上升沿或下降沿)和电平敏感(目标电平发生变化时激活)
常用边沿敏感条件有上升沿posedge、下降沿negedge,电平敏感条件可用逗号或or并列
赋值目标必须是reg
always语句中还可以使用循环、分支等语句使其功能更加强大

initial语句

initial begin
    statement ;
    …
end

initial语句从0时刻开始执行,只执行一次
多个initial块之间是相互独立的
initial语句理论上来讲是不可综合的,多用于初始化、信号检测等

赋值

由于硬件设计的对时序要求的特殊性,Verilog的赋值分为阻塞赋值和非阻塞赋值

阻塞赋值=按语句顺序执行
非阻塞赋值<=则所有语句并行执行

示例

begin
    m = a*b;
    y = m;        //y=a*b
end

begin
    m <= a*b;
    y <= m;        //y=old_m
end

设计组合电路时常用阻塞赋值,设计时序电路时常用非阻塞赋值
不建议在一个always块中混合使用阻塞赋值和非阻塞赋值

分支语句

if语句

if (condition1)    true_statement1 ;
else of (condition2) true_statement1 ;
else default_statement ;

case语句

case(case_expr)
    condition1 : true_statement1 ;
    condition2 : true_statement2 ;
    …
    default : default_statement ;
endcase

循环语句

Verilog 循环语句有4种类型,分别是whileforrepeatforever循环
循环语句只能在alwaysinitial块中使用,但可以包含延迟表达式

while循环

while (condition) begin
    …
end

for循环

for(initial_assignment; condition ; step_assignment)  begin
    …
end

repeat循环

repeat (loop_times) begin
    …
end

repeat的功能是执行固定次数的循环,循环的次数必须是一个常量、变量或信号
如果循环次数是变量信号,则循环次数是开始执行循环时变量信号的值
即便执行期间,循环次数代表的变量信号值发生了变化,执行次数也不会改变

forever循环

forever begin
    …
end

forever表示永久循环,不包含任何条件表达式,一旦执行便无限的执行下去
系统函数$finish可退出循环

函数和任务

函数

function [range-1:0]     function_id ;
input_declaration ;
 other_declaration ;
procedural_statement ;
endfunction

function_id(input1, input2, …);

函数在声明时,会隐式的声明一个宽度为range、 名字为function_id的寄存器变量,函数的返回值通过这个变量进行传递
当该寄存器变量没有指定位宽时,默认位宽1
函数通过指明函数名与输入变量进行调用,函数结束时,返回值被传递到调用处

Verilog中,一般函数的局部变量是静态的,若函数发生并发调用则会产生难以预测的结果
可以在function后加上automatic关键字来说明此类函数在调用时自动分配新的内存空间,也可以理解为此类函数是可并发调用、可递归的

任务

task       task_id ;
    port_declaration ;
    procedural_statement ;
endtask

task_id(input1, input2, …,outpu1, output2, …);

任务中使用关键字inputoutputinout对端口进行声明
inputinout型端口将变量从任务外部传递到内部,outputinout型端口将任务执行完毕时的结果传回到外部

进行任务的逻辑设计时,可以把input声明的端口变量看做wire型,把output声明的端口变量看做reg
但是不需要用regoutput端口再次说明。

output信号赋值时也不要用关键字assign
为避免时序错乱,建议output信号采用阻塞赋值

函数和任务的异同

和函数一样,任务可以用来描述共同的代码段,并在模块内任意位置被调用,让代码更加的直观易读。函数一般用于组合逻辑的各种转换和计算,而任务更像一个过程,不仅能完成函数的功能,还可以包含时序控制逻辑
下面对任务与函数的区别进行概括:

比较点函数任务
输入函数至少有一个输入,端口声明不能包含 inout 型任务可以没有或者有多个输入,且端口声明可以为 inout 型
输出函数没有输出任务可以没有或者有多个输出
返回值函数至少有一个返回值任务没有返回值
仿真时刻函数总在零时刻就开始执行任务可以在非零时刻执行
时序逻辑函数不能包含任何时序控制逻辑任务不能出现 always 语句,但可以包含其他时序控制,如延时语句
调用函数只能调用函数,不能调用任务任务可以调用函数和任务
书写规范函数不能单独作为一条语句出现,只能放在赋值语言的右端任务可以作为一条单独的语句出现语句块中

模块内子程序出现下面任意一个条件时,则必须使用任务而不能使用函数

  • 子程序中包含时序控制逻辑,例如延迟、事件控制等
  • 没有输入变量
  • 没有输出或输出端的数量大于1

门原语

Verilog语言提供已经设计好的门,称为门原语(共12个)
门原语包括逻辑门(andornotxornandnorxnor)、缓冲器(buf)、三态门(bufif1notif1bufif0notif0

门原语的调用类似于模块调用,此处不赘述

模块

模块定义声明

module 模块名 ([端口列表);
             [端口信号声明]; //[输入/输出属性] [数据类型] [位宽] [名称]
             [参数声明]; //[]
             [语句];
endmodule
  • 模块名应符合命名规则(与C类似),根据代码规范最好与文件名一致(一文件一模块)
  • 端口列表指电路的输入/输出信号名称列表,信号间用逗号隔开
  • 端口信号声明包括端口信号的属性、数据类型、位宽和信号名
    • 属性有inputoutputinout
    • 类型常用的有wirereg
    • 位宽用[n:m]表示,位宽=n-m+1且默认为1,根据代码规范通常规定n>m
    • 数据类型默认wire
  • 参数声明需要说明参数名及其初值
  • 端口信号声明可以代替端口列表直接写在括号内

示例

module full_adder (A,B,CIN,S,COUT);
    input [3:0] A,B;
    input CIN;
    output reg [3:0] S;
    output COUT;
endmodule

模块例化

在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化
可以类比理解为C++类的实例化

module_name name (ports_list);

例化模块需要进行端口映射,端口映射有命名法和顺序法两种方法

命名法
module_name name (.port_name(signal_name),…,.port_name(signal_name));
顺序法
module_name name (signal_name,…,signal_name);    //按照原模块端口定义顺序

值得注意的是,Verilog规定,在上层设计中若信号是从子模块输出,则不能使用reg型而应使用wire

示例

示例1:4bit ALU

其实是没写过其他项目,显然没什么可参考性()

alu_4.v

`timescale 1ns / 1ps

module alu_1(
    input A,
    input B,
    input cin,
    input[2:0] M,
    output S,
    output C
    );
    reg result=1'b0;
    reg result1=1'b0;
    always @*
    begin
       case(M)
        3'b000:
        begin
            result=A&B;
            result1=~result;
        end
        3'b001:
        begin
            result=A|B;
            result1=~result;
        end
        3'b010:
        begin
            result=~A;
            result1=~result;
        end
        3'b011:
        begin
            result=~B;
            result1=~result;
        end
        3'b100:
        begin
            result=A^B;
            result1=~result;
        end
        3'b101:
        begin
//            result=(A^B)^cin;
//            result1=((A&B)|(A^B))&cin;
            {result1,result}=A+B+cin;
        end
        3'b110:
        begin
//            result=(A^B)^cin;
//            result1=(~A&(B^cin))|(B&cin);   
            {result1,result}=A-cin-B;
        end
        default:
        begin
            result=0;
            result1=~result;
        end
        endcase
    end
    assign S=result;
    assign C=result1;
endmodule

module alu_4(
    input[3:0] A,
    input[3:0] B,
    input[2:0] M,
    output[3:0] S,
    output C
);
    wire [2:0] Cn;
    alu_1 a1(A[0],B[0],0,M,S[0],Cn[0]);
    alu_1 a2(A[1],B[1],Cn[0],M,S[1],Cn[1]);
    alu_1 a3(A[2],B[2],Cn[1],M,S[2],Cn[2]);
    alu_1 a4(A[3],B[3],Cn[2],M,S[3],C);
endmodule

alu_4_sim.v

`timescale 1ns / 1ps

module alu_4_sim();
    reg [3:0] A;
    reg [3:0] B;
    reg [2:0] M;
    wire [3:0] S;
    wire C;
    integer i;
    integer j;
    integer k;
alu_4 alu_4_sim (
    .A(A),
    .B(B),
    .M(M),
    .S(S),
    .C(C)
);
initial
begin
    for(i=0,A=4'b0000;i<16;i=i+1)
    begin
        for(j=0,B=4'b0000;j<16;j=j+1)
        begin
            for(k=0,M=3'b000;k<8;k=k+1)
            begin
                #1;
                M=M+3'b001;            
            end
            B=B+4'b0001;
        end
        A=A+4'b0001;
    end
end
endmodule

vivado仿真结果

示例2:汽车尾灯

要求使用JK触发器和数据选择器实现
通过触发器实现移位寄存器,从而实现汽车尾灯左转、右转和闪烁功能

设计得闭眼可见的烂,但蒟蒻如我实在没活了()

main.v

`timescale 1ns / 1ps

module jk(clk, cr, j, k, q);
    input clk, cr, j, k;
    output q;  
    reg q=1'b0;
always@(posedge clk) begin
    if(cr) begin
            q<=1'b0;
        end
    else begin
            case({j,k})
                2'b00: q<=q;
                2'b01: q<=1'b0;
                2'b10: q<=1'b1;
                2'b11: q<=~q;
            endcase
        end
end
endmodule

module right(clk, cr, s0, s1, out);
    input clk, cr, s0, s1;
    output [3:0]out;

    jk j1(clk, cr, s0, s1, out[0]);
    jk j2(clk, cr, out[0], ~out[0], out[1]);
    jk j3(clk, cr, out[1], ~out[1], out[2]);
    jk j4(clk, cr, out[2], ~out[2], out[3]);
endmodule

module left(clk, cr, s0, s1, out);
    input clk, cr, s0, s1;
    output [3:0]out;

    jk j1(clk, cr, s0, s1, out[3]);
    jk j2(clk, cr, out[3], ~out[3], out[2]);
    jk j3(clk, cr, out[2], ~out[2], out[1]);
    jk j4(clk, cr, out[1], ~out[1], out[0]);
endmodule

module stop(clk, out);
    input clk;
    output [3:0]out;

    jk j1(clk, 0, 1, 1, out[3]);
    jk j2(clk, 0, 1, 1, out[2]);
    jk j3(clk, 0, 1, 1, out[1]);
    jk j4(clk, 0, 1, 1, out[0]);
endmodule

module choose(s0, s1 , s2, i0, i1, i2, i3, i4, i5, i6, i7, out);
    input s0, s1, s2, i0, i1, i2, i3 , i4, i5, i6, i7;
    output out;

    reg t=1'b0;
    always@* begin
        case({s0,s1,s2})
            3'b000: t=i0;
            3'b010: t=i1;
            3'b100: t=i2;
            3'b110: t=i3;
            3'b111: t=i4;
            3'b101: t=i5;
            3'b001: t=i6;
            3'b011: t=i7;
        endcase
    end
    assign out=t;
endmodule

module light(    
    input clk,
    input s0,
    input s1,
    input s2,
    output [3:0] lo,
    output [3:0] ro,
    output [3:0] so,
    output [7:0] t
);
    left l0(clk, &lo, 1&s0, 0, lo);
    right r0(clk, &ro, 1&s0, 0, ro);
    stop st0(clk, so);
    choose m0(s0, s1, s2, 0, 0, ro[0], lo[0], 0, 0, so[0], so[0], t[0]);
    choose m1(s0, s1, s2, 0, 0, ro[1], lo[1], 0, 0, so[1], so[1], t[1]);
    choose m2(s0, s1, s2, 0, 0, ro[2], lo[2], 0, 0, so[2], so[2], t[2]);
    choose m3(s0, s1, s2, 0, 0, ro[3], lo[3], 0, 0, so[3], so[3], t[3]);
    choose m4(s0, s1, s2, 0, 0, ro[0], lo[0], 0, 0, so[0], so[0], t[4]);
    choose m5(s0, s1, s2, 0, 0, ro[1], lo[1], 0, 0, so[1], so[1], t[5]);
    choose m6(s0, s1, s2, 0, 0, ro[2], lo[2], 0, 0, so[2], so[2], t[6]);
    choose m7(s0, s1, s2, 0, 0, ro[3], lo[3], 0, 0, so[3], so[3], t[7]);
endmodule

sim.v

`timescale 1ns / 1ps

module light_sim();    
    reg clk;
    reg s0;
    reg s1;
    reg s2;
//    wire [7:0] out;
    wire [3:0] ro;
    wire [3:0] lo;
    wire [3:0] so;
    wire [7:0] t;
//    integer i;
light light_sim(
    .clk(clk),
    .s0(s0),
    .s1(s1),
    .s2(s2),
//    .out(out)
    .lo(lo),
    .ro(ro),
    .so(so),
    .t(t)
);
initial begin
    for(clk=1'b0;1;) begin
        #10;
        clk=~clk;
    end
end

initial begin
    for({s0,s1,s2}=3'b000;1;) begin
        {s0,s1,s2}=3'b000;
        #200;
        {s0,s1,s2}=3'b100;
        #200;
        {s0,s1,s2}=3'b000;
        #200;
        {s0,s1,s2}=3'b001;
        #200;
        {s0,s1,s2}=3'b000;
        #200;
        {s0,s1,s2}=3'b110;
        #200;
        {s0,s1,s2}=3'b000;
        #200;
        {s0,s1,s2}=3'b011;
        #200;
    end
end

endmodule

vivado仿真结果:

踩过的坑:

模块只能在单独的语句块中调用,而不能在其他语句中调用
上层模块从子模块接收的输入变量只接受wire型,不接受reg
Verilog的for循环的初始化和判断不允许留空
寄存器最好声明一个初值,否则仿真伊始会出现x值影响触发器

参考