【Verilog 语法】~ if-else、case、for、generate、函数 function、任务 task、过程块、位宽计算、阻塞/非阻塞、时间尺度、存储器设计、

1. if-else

1.1 设计要点

  1. 条件语句必须在过程块中使用。所谓过程块语句是指由 initial、always 引导的执行语句集合。除了这两个语句块引导的 begin end 块中可以编写条件语句外,模块中的其他地方都不能编写。
  2. if 语句中的表达式一般为逻辑表达式或者关系表达式。系统对表达式的值进行判断;若为 0,z(高阻),X(不定),按照假处理;若为 1 按照真处理,执行指定的语句;
  3. if(a)等价于 if(a == 1);
  4. if 语句可以·嵌套·使用
  5. end 总是与离它最近的一份 else 配对。

如果 if 语句使用不当,没有 else,可能会综合出来意想不到的锁存器。
在 always 块里面,如果在给定的条件下变量没有被赋值,这个变量将会保持原来的值,也就是说会生成一个锁存器。

需要注意的是,这里说的是可能, 因此,不代表没有 else 就一定会出现锁存器,同时,不代表有 else 就一定不会出现锁存器。这个是根据具体设计来看的。

2. case

2.1 概述

case 语句检查给定的表达式是否与列表中的其他表达式之一相匹配,并据此进行分支。它通常用于实现一个多路复用器。
如果要检查的条件很多,if-else 结构可能不合适,因为它会综合成一个优先编码器而不是多路复用器。

2.2 语法

一个 Verilog case 语句以 case 关键字开始,以 endcase 关键字结束。在括弧内的表达式将被精确地评估一次,并按其编写顺序与备选方案列表进行比较,与给定表达式匹配的备选方案的语句将被执行。一块多条语句必须分组,并在 begin 和 end 范围内。

// Here 'expression' should match one of the items (item 1,2,3 or 4)
case (<expression>)
case_item1 : <single statement>
case_item2,
case_item3 : <single statement>
case_item4 : begin
 <multiple statements>
 end
default : <statement>
endcase

如果所有的 case 项都不符合给定的表达式,则执行缺省项内的语句,缺省语句是可选的,在 case 语句中只能有一条缺省语句。case 语句可以嵌套。
如果没有符合表达式的项目,也没有给出缺省语句,执行将不做任何事情就退出 case 块。

2.3 注意事项

避免锁存器 同 if else,case 应当加上 default,以避免锁存器出现

注意,如果 case 的情况是完备的,可以不加。(完备意为所有情况都设计了)

3. for

3.1 区别与其它语言的for循环

在 C 语言中,经常用到 for 循环语句,但在硬件描述语言中 for 语句的使用较 C 语言等软件描述语言有较大的区别。
for 循环会被综合器展开为所有变量情况的执行语句,每个变量独立占用寄存器资源。简单的说就是:for 语句循环几次,就是将相同的电路复制几次,因此循环次数越多,占用面积越大,综合就越慢。

3.2 注意事项

注意,i 的变化不跟时钟走: 在 Verilog 中使用 for 循环的功能就是,把同一块电路复制多份,完全起不到计数的作用,所以这个 i 的意思是复制多少份你这段代码实现的电路,和时钟没有任何关系。主要是为了提高编码效率。

4. generate

Verilog 中的 generate 语句常用于编写可配置的、可综合的 RTL 的设计结构。它可用于创建模块的多个实例化,或者有条件的实例化代码块。然而,有时候很困惑 generate 的使用方法,因此看下 generate 的几种常用用法。

4.1 用法

我们常用 generate 语句做三件事情。一个是用来构造循环结构,用来多次实例化某个模块;一个是构造条件 generate 结构,用来在多个块之间最多选择一个代码块,条件 generate 结构包含 if–generate 结构和 case–generate 形式;还有一个是用来断言。

4.2 注意事项

在 Verilog 中,generate 在建模(elaboration)阶段实施,出现预处理之后,正式模拟仿真之前。因此。generate 结构中的所有表达式都必须是常量表达式,并在建模(elaboration)时确定。例如,generate 结构可能受参数值的影响,但不受动态变量的影响。

generate 循环的语法与 for 循环语句的语法很相似。但是在使用时必须先在 genvar 声明中声明循环中使用的索引变量名,然后才能使用它。genvar 声明的索引变量被用作整数用来判断 generate 循环。genvar 声明可以是 generate 结构的内部或外部区域,并且相同的循环索引变量可以在多个 generate 循环中,只要这些环不嵌套。genvar 只有在建模的时候才会
出现,在仿真时就已经消失了。

在“展开”生成循环的每个实例中,将创建一个隐式 localparam,其名称和类型与循环索引变量相同。它的值是“展开”循环的特定实例的“索引”。可以从 RTL 引用此 localparam 以控制生成的代码,甚至可以由分层引用来引用。
Verilog 中 generate 循环中的 generate 块可以命名也可以不命名。如果已命名,则会创建一个 generate 块实例数组。如果未命名,则有些仿真工具会出现警告,因此,最好始终对它们进行命名。
在这里插入图片描述

5. 函数 function

function 函数的目的返回一个用于表达式的值。verilog 中的 function 只能用于组合逻辑。

5.1 定义函数的语法

function <返回值的类型或范围> <函数名>
 <端口说明语句> <变量类型说明>
begin
<语句> …
end
endfunction

5.2 代码说明

 1 function [7:0] getbyte ;
 2 input [15:0] address ;
 3 begin
 4 <说明语句> //从地址字节提取低字节的程序
 5 getbyte = result_expression ; //把结果赋给函数的返回字节
 6 end
 7 endfunction

① <返回值的类型或范围>这一项为可选项,如果缺失,则返回值为一位寄存器类型数据。

② 从函数的返回值:函数的定义蕴含声明了与函数同名、位宽一致的内部寄存器。例子中,getbyte 被赋予的值就是调用函数的返回值。

③ 函数的调用:函数的调用是通过将函数作为表达式中的操作数来实现的。其调用格式:<函数名> (<表达式> ,…, <表达式>);其中函数名作为确认符。下面的例子中,两次调用 getbyte,把两次调用的结果进行位拼接
运算,以生成一个字。
word = control ? {getbyte(msbyte),getbyte(lsbyte)} : 8’d0 ;

④ 函数使用的规则
1 函数定义不能包含有任何的时间控制语句,即任何用#、@、wait 来标识的语句。
2 函数不能调用“task”。
3 定义函数时至少要有一个输入参数。
4 在函数的定义中必须有一条赋值语句给函数中与函数名同名、位宽相同的内部寄存器赋值。
5 verilog 中的 function 只能用于组合逻辑;

5.3 具体实例

函数功能:实现两个 4bit 数的按位“与”运算。
实验现象:如果函数操作正确,则 led 灯闪烁;如果函数操作不正确,则 led 灯常灭。

module func_ex_01 (
 input clk , //E1 25M
 output led //G2 高电平 灯亮
 );
 ///*counter_01*
 reg [25:0] counter_01 = 26'd0 ;
 always @ (posedge clk)
 begin
 counter_01 <= counter_01 + 1'b1 ;
 end
 /*& function*/
 function [3:0] yu ;
 input [3:0] a ;
 input [3:0] b ;
 begin
 yu = a & b ;
 end
 endfunction
 reg [3:0] reg_a = 4'b0101 ;
 reg [3:0] reg_b = 4'b1010 ;
 wire [3:0] result ;
 assign result = yu(reg_a , reg_b) ;
 //*verify and display*
 assign led = (result == 4'd0) ? counter_01[25] : 1'b0 ;
 endmodule

5.4 注意事项

verilog 中的 function 只能用于组合逻辑

6. 任务 task

任务就是一段封装在“task-endtask”之间的程序。任务是通过调用来执行的,而且只有在调用时才执行,如果定义了任务,但是在整个过程中都没有调用它,那么这个任务是不会执行的。调用某个任务时可能需要它处理某些数据并返回操作结果,所以任务应当有接收数据的输入端和返回数据的输出端。另外,任务可以彼此调用,而且任务内还可以调用函数

6.1 任务定义

任务定义的形式如下:

task task_id; 
 [declaration] 
 procedural_statement 
endtask 

其中,关键词 task 和 endtask 将它们之间的内容标志成一个任务定义,task 标志着一个任务定义结构的开始;task_id 是任务名;可选项declaration 是端口声明语句和变量声明语句,任务接收输入值和返回输出值就是通过此处声明的端口进行的;procedural_statement是一段用来完成这个任务操作的过程语句,如果过程语句多于一条,应将其放在语句块内;
endtask 为任务定义结构体结束标志。

6.1.1 举例说明

下面给出一个任务定义的实例。

task task_demo; //任务定义结构开头,命名为 task_demo 
 input [7:0] x,y; //输入端口说明
 output [7:0] tmp; //输出端口说明
 if(x>y) //给出任务定义的描述语句
  tmp = x; 
 else 
 tmp = y;
endtask

上述代码定义了一个名为“task_demo”的任务,求取两个数的最大值。在定义任务时,有下列六点需要注意:
(1)在第一行“task”语句中不能列出端口名称;
(2)任务的输入、输出端口和双向端口数量不受限制,甚至可以没有输入、输出以及双向端口。
(3)在任务定义的描述语句中,可以使用出现不可综合操作符合语句(使用最为频繁的就是延迟控制语句) ,但这样会造成该任务不可综合。
(4)在任务中可以调用其他的任务或函数,也可以调用自身。
(5)在任务定义结构内不能出现 initial 和 always 过程块。
(6)在任务定义中可以出现“disable 中止语句” ,将中断正在执行的任务,但其是不可综合的。当任务被中断后,程序流程将返回到调用任务的地方继续向下执行。

6.2 任务调用

虽然任务中不能出现 initial 语句和 always 语句语句, 但任务调用语句可以在 initial 语句和 always 语句中使用,其语法形式如下:

task_id[(端口 1, 端口 2, …, 端口 N)];

其中 task_id 是要调用的任务名,端口 1、端口 2,…是参数列表。参数列表给出传入任务的数据(进入任务的输入端)和接收返回结果的变量(从任务的输出端接收返回结果) 。任务调用语句中,参数列表的顺序必须与任务定义中的端口声明顺序相同。任务调用语句是过程性语句,所以任务调用中接收返回数据的变量必须是寄存器类型。下面给出一个任务调用实例。

6.2.1 举例说明

通过 Verilog HDL 的任务调用实现一个 4 比特全加器。

module EXAMPLE (A, B, CIN, S, COUT); 
input [3:0] A, B; 
input CIN; 
output [3:0] S; 
output COUT; 
reg [3:0] S; 
reg COUT; 
reg [1:0] S0, S1, S2, S3; 
task ADD; 
input A, B, CIN; 
output [1:0] C;
reg [1:0] C; 
reg S, COUT; 
begin
S = A ^ B ^ CIN; 
COUT = (A&B) | (A&CIN) | (B&CIN); 
C = {COUT, S}; 
end 
endtask 
always @(A or B or CIN) begin 
ADD (A[0], B[0], CIN, S0); 
ADD (A[1], B[1], S0[1], S1); 
ADD (A[2], B[2], S1[1], S2); 
ADD (A[3], B[3], S2[1], S3); 
S = {S3[0], S2[0], S1[0], S0[0]}; 
COUT = S3[1]; 
end 
endmodule

在调用任务时,需要注意以下几点:
(1)任务调用语句只能出现在过程块内;
(2)任务调用语句和一条普通的行为描述语句的处理方法一致;
(3)当被调用输入、输出或双向端口时,任务调用语句必须包含端口名列表,且信号端口顺序和类型必须和任务定义结构中的顺序和类型一致。需要说明的是,任务的输出端口必须和寄存器类型的数据变量对应。
(4)可综合任务只能实现组合逻辑,也就是说调用可综合任务的时间为“0” 。而在面向仿真的任务中可以带有时序控制,如时延,因此面向仿真的任务的调用时间不为“0” 。

7. 过程块

过程块是行为模型的基础。过程块有两种:

initial 块,只能执行一次
always 块,循环执行

过程块中有下列部件:

过程赋值语句:在描述过程块中的数据流
高级结构(循环,条件语句):描述块的功能
时序控制:控制块的执行及块中的语句。

initial 语句与 always 语句和 begin_end 与 fork_join 是一种高频搭配。

7.1 .initial 语句

initial 语句的格式如下:

initial
 begin
 语句 1;
 语句 2;
 ......
 语句 n;
end

7.1.1 举例说明

[例 1]:

initial
 begin
 areg=0; //初始化寄存器 areg
 for(index=0;index<size;index=index+1)
 memory[index]=0; //初始化一个 memory
end

在这个例子中用 initial 语句在仿真开始时对各变量进行初始化。

[例 2]:

initial
 begin
 inputs = 'b000000; //初始时刻为 0
 #10 inputs = 'b011001; 
 #10 inputs = 'b011011; 
 #10 inputs = 'b011000; 
 #10 inputs = 'b001000; 
end

从这个例子中,我们可以看到 initial 语句的另一用途,即用 initial 语句来生成激励波形作为电路的测试仿真信号。一个模块中可以有多个 initial 块,它们都是并行运行的。
initial 块常用于测试文件和虚拟模块的编写,用来产生仿真测试信号和设置信号记录等仿真环境。

7.2 always 语句

always 语句在仿真过程中是不断重复执行的。
其声明格式如下:

always <时序控制> <语句>

always 语句由于其不断重复执行的特性,只有和一定的时序控制结合在一起才有用。如果一个 always 语句没有时序控制,则这个 always 语句将会成为一个仿真死锁。见下例:

[例 1]:

always areg = ~areg;

这个 always 语句将会生成一个 0 延迟的无限循环跳变过程,这时会发生仿真死锁。如果加上时序控制,则这个 always 语句将变为一条非常有用的描述语句。见下例:

[例 2]:

always #10 areg = ~areg;

这个例子生成了一个周期为 20 的无限延续的信号波形,常用这种方法来描述时钟信号,作为激励信号来测试所设计的电路。

[例 3]:

reg[7:0] counter;
reg tick;
always @(posedge areg) 
 begin
 tick = ~tick;
 counter = counter + 1;
 end

这个例子中,每当 areg 信号的上升沿出现时把 tick 信号反相,并且把 counter 增加 1。这种时间控制是 always 语句最常用的。

always 的时间控制可以是沿触发也可以是电平触发的,可以单个信号也可以多个信号,中间需要用关键字 or 连接,如:

always @(posedge clock or posedge reset) //由两个沿触发的 always 块
begin
……
end
always @( a or b or c ) //由多个电平触发的 always 块
begin
……
end

沿触发的 always 块常常描述时序逻辑,如果符合可综合风格要求可用综合工具自动转换为表示时序逻辑的寄存器组和门级逻辑,而电平触发的 always 块常常用来描述组合逻辑和带锁存器的组合逻辑,如果符合可综合风格要求可转换为表示组合逻辑的门级逻辑或带锁存器的组合逻辑。一个模块中可以有多个 always 块,它们都是并行运行的。

7.2.1 高频用法

always 是一个极高频的语法,always@()用法总结如下:

① always@(信号名) • 信号名有变化就触发事件
例:

always@( clock) 
a=b;

② always@( posedge 信号名) • 信号名有上升沿就触发事件
例:

always@( posedge clock) 
a=b;

③ always@(negedge 信号名) • 信号名有下降沿就触发事件
例:

always@( negedge clock) 
a=b;

④ always@(敏感事件 1or 敏感事件 2or…)
• 敏感事件之一触发事件
• 没有其它组合触发
例:

always@(posedge reset or posedge clear) 
reg_out=0;

⑤ always@(*)
• 无敏感列表,描述组合逻辑,和 assign 语句是有区别的
例:

always@(*) 
b= 1'b0;

7.2.2 assign 和 always@(*)语句区别

1.被 assign 赋值的信号定义为 wire 型,被 always@(*)结构块下的信号定义为 reg 型,值得注意的是,这里的 reg 并不是一个真正的触发器,只有敏感列表为上升沿触发的写法才会综合为触发器,在仿真时才具有触发器的特性。

2.另外一个区别则是更细微的差别:举个例子。

wire a;
reg b;
assign a = 1'b0;
always@(*)
b= 1'b0;

在这种情况下,做仿真时 a 将会正常为 0,但是 b 却是不定态。这是为什么?
verilog 规定,always@()中的是指该 always 块内的所有输入信号的变化为敏感列表,也就是仿真时只有当 always@()块内的输入信号产生变化,该块内描述的信号才会产生变化,而像 always@()b = 1’b0,敏感源是什么呢?1’b0,它始终是不会发生变化的。

8. 位宽计算

N 位二进制数所能表示的数据范围

8.1 有符号数(补码)

-2^(N-1) ~ 2^(N-1)-1
正数范围-1,是因为 0 算在正数范围中。

如,N = 8,则表示范围是:-128 ~ 127.

8.2 无符号数

0~2^N-1
如,N = 8,则表示范围是:0~255.

8.3 有符号定点数

如 N = 8‘ 3Q5 :意思为共 8 位,3 位整数位 5 位小数位。
如果有符号,那么整数位被占去一位。
所以整数部分其实只有 2 位,最大为 4;小数位有 5 位。那么最大值为:
4 + ∑ 2^(-i) (i = 1…5) - 2^(-5) ;
整数 + 所有小数位为 1 时的值 - 精度(最小的小数值)

为什么最大值要减 1 个精度,同样是因为 0 占掉了一个范围。
最小值则为 -4 - ∑ 2^(-i) (i = 1…5)

8.4 N bit 数和 M bit 数相加、相乘后需要多少 bit?

相加相乘后需要的数据位宽,
若无已知数据范围,按照基本规律
相加位宽+1,相乘位宽为 N+M(保证宽度足够)

若已知操作数范围,根据运算操作数所能表示数的绝对值最大值,求出运算结果极限值
例如,两个 8bit 有符号数相乘,其结果需要的位宽是多少?
8bit 有符号数补码所能表示的数据范围是:-128 到 127,具有最大绝对值的数是-128,所以极限情况下是-128*(-128) = 16384 = 2^14.
15bit 有符号数所能表示范围是:-2^14 到 2^14-1,并不能表示 2^14。综上,需要 16bit 位宽。
正好位宽扩大一倍,也就是 8+8。

9 阻塞/非阻塞

这个之前写过,看这篇文章点击查看

10 时间尺度

timescale 是 Verilog HDL 中的一种时间尺度预编译指令,它用来定义模块的仿真时的时间单位和时间精度。格式如下:

` timescale<时间单位>/<时间精度>

注意:用于说明仿真时间单位和时间精度的数字只能是 1、10、100,不能为其它的数字。而且,时间精度不能比时间单位还要大。最多两则一样大。

10.1 举例说明

比如:下面定义都是对的:

`timescale 1ns/1ps
`timescale 100ns/100ns

下面的定义是错的:

`timescale 1ps/1ns

时间精度就是模块仿真时间和延时的精确程序,比如:定义时间精度为 10ns, 那么时序中所有的延时至多能精确到 10ns,而 8ns 或者 18ns 是不可能做到的。在编译过程中,timescale 指令影响这一编译器指令后面所有模块中的时延值,直至遇到另一个 timescale 指令 resetall 指令。

在 verilog 中是没有默认 timescale 的,一个没有指定 timescale 的 verilog 模块就有可能错误的继承了前面编译模块的无效 timescale 参数.

11. 存储器设计

Verilog 中提供了两维数组来帮助我们建立内存的行为模型。具体来说,就是可以将内存定义为一个 reg 类型的数组,这个数组中的任何一个单元都可以通过一个下标去访问。

11.1 数组(内存)定义

reg [wordsize : 0] array_name [0 : arraysize];

例如:

reg [7:0] my_memory[0:255];

其中 [7:0] 是内存的宽度(位宽),而 [0:255] 则是 内存的深度(也就是有多少存储单元),其中宽度为 8 位,深度为 256。地址 0 对应着数组中的 0 存储单元。

11.2 写操作

如果要存储一个值到某个单元中去,可以这样做:

my_memory[address] = data_in;

11.3 读操作

而如果要从某个单元读出值,可以这么做:

data_out = my_memory[address];

读取内存中的某一位或者多位,就要麻烦一点,因为 Verilog 不允许读/写一个位。这时,就需要使用一个变量转换一下:

例如:

 data_out = my_memory[address];
data_out_it_0 = data_out[0];

这里首先从一个单元里面读出数据,然后再取出读出的数据的某一位的值。

========================================================
仅供学习使用,如有错误欢迎指出改正,侵删!

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值