Verilog似乎也挺有趣——程序员也可以电路设计

最近沉迷科研,为了在项目组中发光发热,我跑来学习电路设计语言Verilog
速成教程,看好了您内👇


基本结构

Verilog程序包括四个部分:端口定义,I/O说明,内部信号声明和功能定义。

1. 模块的端口声明了模块得输入输出口。 其格式如下:

module 模块名(1,2,3,...)

模块的端口表示的是模块得输入和输出端口,
在引用模块的时候,其端口可以用两种方式连接。

  • 在引用时严格按照模块定义的端口顺序来连接,不用标明原模块定义时规定的端口名
模块名(连接端口1信号名,连接端口2信号名,连接端口3信号名,...)
  • 在引用时用 " . " 符号,标明原模块是定义时规定的端口名
模块名(.端口1(连接信号1),.端口2(连接信号2),.端口3(连接信号3),...)

2. I/O说明的格式

输入口:
input[信号位宽-1:0] 端口名1;
input[信号位宽-1:0] 端口名2;
...
输出口:
output[信号位宽-1:0] 端口名1;
output[信号位宽-1:0] 端口名2;
...
输入/输出口:
inout[信号位宽-1:0] 端口名1;
inout[信号位宽-1:0] 端口名2;
...

3. 内部信号说明

reg[信号位宽-1:0] R变量1,R变量2,...;
wire[信号位宽-1:0] W变量1,W变量2,...;

wire表示直通,即输入有变化,输出马上无条件地反映(如与、非门的简单连接);
reg表示一定要有触发,输出才会反映输入的状态。

reg相当于存储单元,wire相当于物理连线;
reg表示一定要有触发,没有输入的时候可以保持原来的值,但不直接与实际的硬件电路对应。

两者的区别:

  • 寄存器型数据保持最后一次的赋值,而线型数据需要持续的驱动
  • wire使用在连续赋值语句中,而reg使用在过程赋值语句(initial ,always)中
  • wire若无驱动器连接,其值为z,reg默认初始值为不定值 x

在连续赋值语句中,表达式右侧的计算结果可以立即更新表达式的左侧
在理解上,相当于一个逻辑之后直接连了一条线,这个逻辑对应于表达式的右侧,而这条线就对应于wire。
在过程赋值语句中,表达式右侧的计算结果在某种条件的触发下放到一个变量当中,而这个变量可以声明成reg类型的。
根据触发条件的不同,过程赋值语句可以建模不同的硬件结构:如果这个条件是时钟的上升沿或下降沿,那么这个硬件模型就是一个触发器;如果这个条件是某一信号的高电平或低电平,那么这个硬件模型就是一个锁存器;如果这个条件是赋值语句右侧任意操作数的变化,那么这个硬件模型就是一个组合逻辑。

对组合逻辑输出变量,可以直接用assign;
即如果不指定为reg类型,那么就默认为1位wire类型,故无需指定1位wire类型的变量。
当然专门指定出wire类型,可能是多位或为使程序易读。
wire只能被assign连续赋值,reg只能在initial和always中赋值。

  • 输入端口可以由wire/reg驱动,但输入端口只能是wire
  • 输出端口可以是wire/reg类型,输出端口只能驱动wire
  • 若输出端口在过程块中赋值则为reg型,若在过程块外赋值则为net型(wire/tri)
  • 用关键词inout声明一个双向端口, inout端口不能声明为reg类型,只能是wire类型。

默认信号是wire类型,reg类型要申明。
这里所说的默认是指输出信号申明成output时为wire类型,如果是模块内部信号,必须申明成wire或者reg。
对于always语句而言,赋值要申明成reg,连续赋值assign的时候要用wire。

模块调用时,信号类型确定方法总结如下:

  • 信号可以分为端口信号和内部信号。出现在端口列表中的信号是端口信号,其它的信号为内部信号。
  • 对于端口信号,输入端口只能是net类型。输出端口可以是net类型,也可以是register类型。若输出端口在过程块中赋值,则为register类型;若在过程块外赋值(包括实例化语句),则为net类型。
  • 内部信号类型与输出端口相同,可以是net或register类型。判断方法也与输出端口相同。若在过程块中赋值,则为register类型;若在过程块外赋值,则为net类型。
  • 若信号既需要在过程块中赋值,又需要在过程块外赋值。这种情况是有可能出现的,如决断信号。这时需要一个中间信号转换。

4. 功能定义

  • assign声明语句,如:assign q = a & b
    这种方法的句法很简单,只需要写一个assign。后面加一个方程式即可。
  • 用实际元件,如:and #2 u1( q , a , b , c )
    采用实际元件的方法像在电路图输入方式下调用库元件一样,键入元件的名字和相连的引脚即可。这表示在设计中用到一个跟与门一样的名为u1的与门,其输入端为a、b,输出为q。输出延迟为2个单位时间。要求每个是实际元件的名字必须是唯一的,以避免与其他调用与门的实际混淆。
  • 用always块,如:
always @ (posedge clk or posedge clr);
begin
    if (clr) q <= 0;
    else if (en) q <= d;
end

采用assign语句是描述组合逻辑最常用的方法之一。而always块即可用于描述组合逻辑,也可描述时序逻辑。用always块的例子生成了一个带有异步清零端的D触发器。
always块可用很多描述手段来表示逻辑,例如上例就用了 " if … else … " 语句来表述逻辑关系。
如按照一定的风格来编写always块,可以通过综合工具把源代码自动综合成用门级结构表示的组合或时序逻辑电路。

要点总结

  • 在Verilog模块中所有过程块(如:initial块,always块),连续赋值语句,实例引用语句都是并行的
  • ta门表示的是一种通过变量名互相连接的关系
  • 在同一模块中这三者出现的先后次序没有关系
  • 只有连续赋值语句assign和实例引用语句可以独立于过程块而存在于模块的功能定义部分

数据类型 ( 部分 )

1. 数字
(1) 整数

<位宽><进制><数字>
<进制><数字>  //默认32位宽
<数字>  //默认32位宽十进制

(2) x和z值
x表示不定值,z表示高阻态。

4'b10x0      //位宽为4的二进制数从低位起第2位为不定值
4'b101z      //位宽为4的二进制数从低位起第1位为不定值
12'dz        //位宽为12的十进制数,其值为高阻态
12'd?        //位宽为12的十进制数,其值为高阻态
8'h4x        //位宽为8的十六进制数从,其低4位为不定值

(3) 负数
一个数字可以被定义为负数,只需要在位宽表达式前加一个减号,减号必须写在数字定义表达式的最前面。
(4) 下画线
下画线可以用来分隔开数的表达式以提高程序可读性。ta不可以用在位宽和进制处,只能用在具体的数字中间。

2. 参数parameter

parameter 参数名1=表达式 , 参数名2=表达式 , 参数名3=表达式 , ... , 参数名n=表达式;

3. memory型
memory型数据是通过扩展reg型数据的地址范围来生成的。其形式如下:

reg[n-1:0] 存储器名[m-1:0]
reg[n-1:0] 存储器名[m:1]

运算符 ( 部分 )

取反运算符的运算规则

~结果
10
01
xx

按位与的运算规则

&01x
0000
101x
x0xx

按位或的运算规则

&01x
001x
111x
xxxx

按位异或的运算规则

&01x
001x
110x
xxxx

按位同或的运算规则

&01x
010x
101x
xxxx

相等运算符(结果有三种:0,1,x)

==01xz
010xx
101xx
xxxxx
zxxxx

全等运算符(结果只有两种:0,1)

==01xz
01000
10100
x0010
z0001

位拼接运算符

{a,b[3:0],w,3'b101} 
<=> {a,b[3],b[2],b[1],b[0],w,1'b1,1'b0,1'b1}

{4{w}}
<=> {w,w,w,w}

{b,{3{a,b}}}
<=> {b,a,b,a,b,a,b}

缩减运算符

reg[3:0] B;
reg C;
C = &B;
<=> C = (((B[0]&B[1])&B[2])&B[3])

赋值语句

非阻塞赋值方法(b<=a)

  • 在语句块中,上面语句所赋的变量值不能立即就为下面的语句所用
  • 块结束后才能完成这次赋值操作,而所赋得变量值是上一次赋值得到得
  • 在编写可综合的时序逻辑模块时,这是最常用的赋值方法

注意:非阻塞赋值符 " <= " 与小于等于符 " <= " 看起来是一样的,但意义完全不同,小于等于时关系运算符,用于比较大小,而非阻塞赋值符用于赋值操作。

阻塞赋值方法(b=a)

  • 赋值语句执行完后,块才结束
  • b的值在赋值语句执行完后立即就改变
  • 在时序逻辑中使用,可能会产生意想不到的结果

块语句的特点

1. 嵌套块
块可以嵌套使用,顺序块和并行块可以混合在一起使用。

initial
begin
    x = 1'b0;
    fork
        #5 y = 1'b1;
        #10 z = {x,y};
    join
    #20 w = {y,x};
end

2. 命名块
块可以具有自己的名字,这称为命名块。命名块的特点是:

  • 命名块中可以声明局部变量
  • 命名块是设计层次的一部分,命名块中声明的变量可以通过层次名引用进行询问
  • 命名块可以被禁用,例如停止其执行
module top;
initial
begin: block1       //名字为block1的顺序命名块
    integer i;      //整型变量i是block1命名块的静态本地变量
    ...             //可以用层次名top.block1.i被其它模块访问
end

initial
fork: block2        //名字为block2的并行命名块
    reg i;          //寄存器变量i是block2命名块的静态本地变量
    ...             //可以用层次名top.block2.i被其它模块访问
join

3. 命名块的禁用
Verilog通过关键字disable提供了一种中止命名块执行的方法。disable可以用来从循环中退出,处理错误条件以及根据控制信号来控制某些代码段是否被执行。
对块语句的禁用导致紧接在块后面的那条语句被执行。
下面的代码的功能是在一个标志寄存器中查找第一个不为零的位:

//从矢量标志寄存器的低有效位开始查找第一个值为1的位
reg[15:0] flag;
integer i;

initial
begin
    i = 0;
    flag = 16'b0010_0000_0000_0000;
    begin: block1
        while (i < 16)
        begin
            if (flag[i])
            begin
                $ display("Endounter a TRUE bit at element number %d,"i);
                disable block1;
            end
            i = i + 1;
        end
    end
end

生成块

Verilog中的生成块应该怎样理解?

生成语句可以动态地生成Verilog代码。
当对矢量中的多个位进行重复操作时,或者当进行多个模块的实例引用的重复操作时,或者在根据参数的定义来确定程序中是否应该包括某段Verilog代码的时候,生成语句的使用方便了参数化模块的生成

生成语句能够控制变量的声明、任务或函数的调用,还能对实例引用进行全面的控制。
关键字generate-endgenerate来指定生成的实例范围。
生成实例可以是以下的一个或多个类型:

  1. 模块
  2. 用户定义原语
  3. 门级原语
  4. 连续赋值语句
  5. initial和always块
  • 代码设计中可以多次有条件地调用(实例引用)生成的实例和生成的变量声明,而且生成的实例具有唯一的标识名,可以用层次命名规则引用。

  • 为了支持结构化的元件和过程块语句的相互连接,Verilog语言允许在生成范围内声明下列数据类型:

    1. net(线网),reg(寄存器)
    2. integer(整型数),real(实型数),time(时间型),realtime(实数时间型)
    3. event(事件)
  • 而且,生成的数据类型具有唯一的标识名,也可以被层次引用。生成范围中可以定义使用按照秩序或者参数名赋值的参数重新定义,或者使用defparam声明的参数(需在同一个生成范围内或者生成范围的层次化实例中)重新定义。

  • 任务和函数的声明也允许出现在生成范围之中,但是不允许出现在循环生成中。生成任务和函数同样具有唯一的标识符名称,可以被层次引用。

  • 不允许出现在生成范围之中的模块项声明包括:

    1. 参数、局部参数
    2. 输入、输出和输入/输出声明
    3. 指定块
  • 在Verilog中有3中创建生成语句的方法:

    1. 循环生成
    2. 条件生成
    3. case生成

总结
生成块的主要作用:

  • 实例引用(有条件地调用,即只引用需要用到的实例),可以理解为一种静态展开行为,在仿真开始前,编译器就将代码展开,把行为预先确定下来,不会例化不需要的逻辑分支而生成电路
  • 利用标识后唯一的标识名,可以对生成语句中的变量进行层次化引用
循环生成语句
  • 关键字:generate-for
  • 关键字genvar用于声明生成变量(定义for的索引变量),生成变量只能用在生成块中,在确立后的仿真代码中,生成变量是不存在的
  • for循环体的begin后的标识名是赋予循环生成语句的名字,目的在于通过它对循环生成语句中的变量进行层次化引用
//本模块生成两条N位总线变量进行按位异或
module bitwise_xor(out,i0,i1);
//参数声明语句,参数可以重新定义
parameter N=32;
//默认总线位宽为32位

//端口声明语句
output[N-1:0] out;
input[N-1:0] i1,i0;

//声明一个临时循环变量
//该变量只用于生成块的循环计算
//Verilog仿真时该变量在设计中并不存在
genvar j;

//用一个单循环生成按位异或的异或门(xor)
genarate
    for (j=0;j<N;j=j+1)
    begin: xor_loop
        xor g1(out[j],i0[j],i1[j]);
    end
endgenerate

//另一种编写方式
//异或门可以用always模块来代替
//reg[N-1:0] out;
//genarate
//    for (j=0;j<N;j=j+1)
//    begin: xor_loop
//        always @(i0[j] or i1[j]) out[j]=i0[j]^i1[j];
//    end
//endgenerate

endmodule
条件生成语句
  • 关键字:generate-if
  • if 的条件都为常量条件,根据不同的条件引用不同的实例(这里与普通的if不同,普通的if会例化所有的分支结构生成电路)
//本模块生成一个参数化乘法器
module multiplier(product,a0,a1);
//参数声明,该参数可以重新定义
parameter a0_width=8;
parameter a1_width=8;

//本地参数声明
//本地参数不能用参数重新定义(defparam)修改
//也不能在实例引用时通过传递参数语句,即 #(参数1,参数2,...)的方法修改
localparam product_width=a0_width+a1_width;

//端口声明语句
output[product_width-1:0] product;
input[a0_width-1:0] a0;
input[a1_width-1:0] a1;

//有条件地调用(实际引用)不同类型地乘法器
//根据参数a0_width和a1_width,在调用时引用相对应的乘法器实例
generate
    if (a0_width<8) || (a1_width<8)
        cal_multiplier #(a0_width,a1_width) m0(product,a0,a1);
    else
        tree_multiplier #(a0_width,a1_width) m0(product,a0,a1);  
endgenerate

endmodule
case生成语句
  • 关键字:generate-case
  • 只会选择case的一路分支进行实例例化
//本模块生成N位加法器
module adder(co,sum,a0,a1,ci);
//参数声明,该参数可以重新定义
parameter N=4;

//端口声明语句
output[N-1:0] sum;
output co;
input[N-1:0] a0,a1;
input ci;

//根据总线的位宽调用相应的加法器
//参数N在调用的时候可以重新定义
//不同位宽的加法器根据不同N来决定
generate
    case(N)
        1: adder_1bit adder1(co,sum,a0,a1,ci); //1位加法器
        2: adder_2bit adder2(co,sum,a0,a1,ci); //2位加法器
        //默认情况下选用位宽为N的超前进位加法器
        default: adder_cla #(N) adder3(co,sum,a0,a1,ci);
    endcase
endgenerate

endmodule

结构语句

Verilog语言中的任何过程模块都是从属于一下四种结构的说明语句:

  • initial说明语句
  • always说明语句
  • task说明语句
  • function说明语句

在这四种结构之下,又有

  • begin … end 顺序块
  • fork … join 并行块

一个程序模块可以有多个initial和always过程块。每个initial和always说明语句在仿真的一开始同时立即开始执行。initial语句只执行一次,而always语句则是不断地重复执行,直到仿真过程结束。
但是always语句后跟着的过程块是否运行,则要看ta的触发条件是否满足,如满足就运行过程块一次,再次满足就再运行一次,直到仿真过程结束。
在一个模块中,使用initial和always语句的次数是不受限制的,ta们是同时开始运行的。
task和function语句可以在程序模块中的一处或多处调用。

initial语句
initial
    begin
        语句1;
        语句2;
        ...
        语句n;
    end
always语句
//沿触发
always @(posedge clock or posedge reset)
    begin
        ...
    end

//电平触发
always @(a or b or c)
    begin
        ...
    end

//对其后面语句块中所有输入变量的变化敏感
always @(*)
    begin
        ...
    end

//wait关键字表示等待电平敏感的条件为真
always
    wait (count_enable) #20 count=count+1;
task语句&&function语句

任务、函数的定义和调用都包括在一个module的内部,ta们一般用于行为级建模,在编写testbench时用的较多,而在写可综合的代码时要少用。

//function的定义
function<返回值类型和位宽> (函数名);
    <入口参量和类型声明>
    <局部变量声明>
    行为语句;
endfunction

定义function时,要注意以下几点:

  • function定义结构不能出现在任意一个过程块(always块或者initial块)的内部
  • function定义不能包括有任何时间控制语句,即任何用#,@或wait来标识的语句
  • 定义function时至少要有一个输入参量
  • 定义function时,在function内部隐式地将函数名声明成一个寄存器变量,在函数体中必须有一条赋值语句对该寄存器变量赋以函数的结果值,以便调用function时能够得到返回的函数值。如果没有指定的返回值的宽度,function将缺省返回1位二进制数
//function的调用
<函数名>  (<输入表达式1>,...,<输入表达式n>);

输入表达式与函数定义结构中的各个输入端口一一对应,这些输入表达式的排列顺序必须与各个输入端口在函数定义结构中的排列顺序一致.
function的调用既可以出现在过程块中又可以出现在assign连续赋值语句之中.
另外,function定义中声明的所有局部变量寄存器都是静态的,即function中的局部寄存器在function的多个调用之间保持他们的值.

任务(task)类似于一般编程语言中的Process(过程),它可以从描述的不同位置执行共同的代码。
通常把需要共用的代码段定义为task,然后通过task调用来使用它。
在task中还可以调用其他的task和function。

//task的定义
task<任务名>;
    端口与类型说明;
    变量声明;
    语句1;
    ...
    语句n;
endtask

在定义一个task时,必须注意以下几点:

  • 任务定义结构不能出现在任何一个过程块内
  • 一个task可以没有输入/输出端口,当然也可以有
  • 一个task可以没有返回值,也可以通过输出端口或双向端口返回一个或多个值
  • 除任务参数外,task还能够引用说明任务的模块中定义的任何变量
//task的调用  
//task调用语句给出传入任务的参数值和接收结果的变量值
<任务名>  (端口1,端口2... ,端口n);

在调用task时,必须注意一下几点:

  • task调用是过程性语句,因此只能出现在always过程块和initial过程块中,调用task的输入与输出参数必须是寄存器类型的
  • task调用语句中的列表必须与任务定义时的输入、输出和双向端口参数说明的顺序相匹配
  • 在调用task时,参数要按值传递,而不能按地址传递(和其他语言的不同)
  • 在一个task中,可也直接访问上一级调用模块中的任何寄存器
  • 可以使用循环中断控制语句disable来中断任务执行,在task被中断后,程序流程将返回到调用task调用的地方继续向下执行

task和function的不同点:

  • function只能与主模块共用一个仿真时间单位,而task可以定义自己的仿真时间单位
  • function不能调用任务,而task可以调用函数
  • function至少需要一个输入变量,而task可以没有或者有很多个任意类型的变量
  • function返回一个值,而task则不返回值。

举例

四选一多路选择器

module mux4_to_1(out,i0,i1,i2,i3,s0,s1)

output out;
input i0,i1,i2,i3;
input s0,s1;

//输出端口被声明为寄存器类型
reg out;

//若输入信号改变,则重新计算输出信号out
//造成输出信号out重新计算的所有输入信号必须写入always@(...)电平敏感列表
always @(s1 or s0 or i0 or i1 or i2 or i3)
    begin
        case ({s1,s0})
            2'b00: out=i0;
            2'b01: out=i1;
            2'b10: out=i2;
            2'b11: out=i3;
        endcase
    end

endmodule

四位计数器

module counter(Q,clock,clear)

output Q;
input clock,clear;

//输出变量被定义成寄存器类型
reg[3:0] Q;

always @(posedge clock or negedge clear)
begin
    if (clear)
        Q <= 4'b0000;
    else
        Q = Q + 1;   
end

endmodule

红黄绿交通灯

module traffic_lights;
    reg clock,red,green,amber;
    parameter on=1,off=0,red_tics=350,
            amber_tics=30,green_tics=200;
    //交通灯初始化
    initial red=off;
    initial amber=off;
    initial green=off;
    
    //交通灯控制时序
    always
        begin
            red=on;
            light(red,red_tics);
            green=on;
            light(green,green_tics);
            amber=on;
            light(amber,amber_tics);
        end
    
    //定义交通灯开始时间的任务
    task light;
        output color;
        input[31:0] tics;
        begin
            repeat(tics)
                @(posedge clock); 
            color=off;
        end  
    endtask
    
    //产生时间脉冲的always块
    always
        begin
            # 100 clock=0;
            # 100 clock=1;
        end
        
endmodule
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值