目录
1 Verilog 设计方法
Verilog 的设计多采用自上而下的设计方法(top-down)。即先定义顶层模块功能,进而分析要构成顶层模块的必要子模块;然后进一步对各个模块进行分解、设计,直到到达无法进一步分解的底层功能块。这样,可以把一个较大的系统,细化成多个小系统,从时间、工作量上分配给更多的人员去设计,从而提高了设计速度,缩短了开发周期。
2. 基础语法
2.1 格式
Verilog 是区分大小写的。
格式自由,可以在一行内编写,也可跨多行编写。
每个语句必须以分号为结束符。空白符(换行、制表、空格)都没有实际的意义,在编译阶段可忽略。例如下面两中编程方式都是等效的。
2.2 数值表示
数值种类
0 1 x z
表示方法
合法的基数格式:4'b1011,32'h3022_c0de,'d100,'O332
负数-------6d15
,-15
。
实数------20.123,1.2e4【12000】
- 字符串表示方法
字符串是由双引号包起来的字符队列。字符串不能多行书写,即字符串中不能包含回车符。Verilog 将字符串当做一系列的单字节 ASCII 字符队列。例如,为存储字符串 "www.runoob.com", 需要 14*8bit 的存储单元。例如:
reg [0: 14*8-1] str ;
initial begin
str = "www.runoob.com";
end
2.3 数据类型
wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。
寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。
整数,实数,时间寄存器变量等数据类型实际也属于寄存器类型。
- 整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数。
- 实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。
- Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。
-
数组
在 Verilog 中允许声明 reg, wire, integer, time, real 及其向量类型的数组。integer flag [7:0] ; //8个整数组成的数组
reg [3:0] counter [3:0] ; //由4个4bit计数器组成的数组
wire [7:0] addr_bus [3:0] ; //由4个8bit wire型变量组成的数组
wire data_bit[7:0][5:0] ; //声明1bit wire型变量的二维数组
reg [31:0] data_4d[11:0][3:0][3:0][255:0] ; //声明4维的32bit数据变量数组
存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为。
参数用来表示常量,用关键字 parameter 声明,只能赋值一次。
- 字符串
字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。
字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。
有一些特殊字符在显示字符串中有特殊意义,例如换行符,制表符等。如果需要在字符串中显示这些特殊的字符,则需要在前面加前缀转义字符 \ 。
2.4 表达式
操作符------
3. 编译指令
以反引号 ` 开始的某些标识符是 Verilog 系统编译指令。
编译指令 | 含义 |
---|---|
`define | 宏定义,替换文本 |
`undef | 取消宏定义 |
`ifdef, `ifndef, `elsif, `else, `endif | 条件编译指令 |
`ifdef MCU51
parameter DATA_DW = 8 ;
`elsif WINDOW
parameter DATA_DW = 64 ;
`else
parameter DATA_DW = 32 ;
`endif
例子中,如果定义了 MCU51,则使用第一种参数说明;如果没有定义 MCU、定义了 WINDOW,则使用第二种参数说明;如果 2 个都没有定义,则使用第三种参数说明。
编译指令 | 含义 |
---|---|
`include | 使用 `include 可以在编译时将一个 Verilog 文件内嵌到另一个 Verilog 文件中,作用类似于 C 语言中的 #include 结构。该指令通常用于将全局或公用的头文件包含在设计文件里。文件路径既可以使用相对路径,也可以使用绝对路径。 |
`timescale | 在 Verilog 模型中,时延有具体的单位时间表述,并用 `timescale 编译指令将时间单位与实际时间相关联。`timescale 的时间精度设置是会影响仿真时间的。时间精度越小,仿真时占用内存越多,实际使用的仿真时间就越长。所以如果没有必要,应尽量将时间精度设置的大一些。 |
- `timescale
该指令用于定义时延、仿真的单位和精度,格式为:
`timescale time_unit / time_precision
time_unit 表示时间单位,time_precision 表示时间精度,它们均是由数字以及单位 s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成。时间精度可以和时间单位一样,但是时间精度大小不能超过时间单位大小,例如下面例子中,输出端 Z 会延迟 5.21ns 输出 A&B 的结果。
在编译过程中,`timescale 指令会影响后面所有模块中的时延值,直至遇到另一个 `timescale 指令或 `resetall 指令。
编译指令 | 含义 |
---|---|
`default_nettype | 该指令用于为隐式的线网变量指定为线网类型,即将没有被声明的连线定义为线网类型。 |
`resetall | 该编译器指令将所有的编译指令重新设置为缺省值。`resetall 可以使得缺省连线类型为线网类型。当 `resetall 加到模块最后时,可以将当前的 `timescale 取消防止进一步传递,只保证当前的 `timescale 在局部有效,避免 `timescale 的错误继承。 |
`celldefine, `endcelldefine | 这两个程序指令用于将模块标记为单元模块,他们包含模块的定义。例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。 |
`unconnected_drive, `nounconnected_drive | 在模块实例化中,出现在这两个编译指令间的任何未连接的输入端口,为正偏电路状态或者为反偏电路状态。 |
`celldefine
module (
input clk,
input rst,
output clk_pll,
output flag);
......
endmodule
`endcelldefine
4. 连续赋值
assign LHS_target = RHS_expression ;
-
LHS_target 必须是一个标量或者线型向量,而不能是寄存器类型。
-
RHS_expression 的类型没有要求,可以是标量或线型或存器向量,也可以是函数调用。
-
只要 RHS_expression 表达式的操作数有事件发生(值的变化)时,RHS_expression 就会立刻重新计算,同时赋值给 LHS_target。
wire A, B ;
wire Cout = A & B ;
5. 过程结构
一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用 。
这些语句在模块间并行执行 ,与其在模块的前后顺序没有关系。
但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。
每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。
- initial
initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。
initial 理论上来讲是不可综合的,多用于初始化、信号检测等。
- always
与 initial 语句相反,always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。
由于循环执行的特点,always 语句多用于仿真时钟的产生,信号行为的检测等。
6 过程赋值
- 阻塞赋值
阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。
阻塞赋值语句使用等号 = 作为赋值符。
前面的仿真中,initial 里面的赋值语句都是用的阻塞赋值。 - 非阻塞赋值
非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。
非阻塞赋值语句使用小于等于号 <= 作为赋值符。
7 时序控制
基于时延的时序控制出现在表达式中,它指定了语句从开始执行到执行完毕之间的时间间隔。
时延类型 | 解释 |
---|---|
常规时延 | 遇到常规延时时,该语句需要等待一定时间,然后将计算结果赋值给目标信号。 |
内嵌时延 | 遇到内嵌延时时,该语句先将计算结果保存,然后等待一定的时间后赋值给目标信号。 |
当延时语句的赋值符号右端是常量时,2 种时延控制都能达到相同的延时赋值效果。
当延时语句的赋值符号右端是变量时,2 种时延控制可能会产生不同的延时赋值效果。
一般时延赋值方式:遇到延迟语句后先延迟一定的时间,然后将当前操作数赋值给目标信号,并没有"惯性延迟"的特点,不会漏掉相对较窄的脉冲。
内嵌时延赋值方式:遇到延迟语句后,先计算出表达式右端的结果,然后再延迟一定的时间,赋值给目标信号。
8 语句块
类型 | 内容 |
---|---|
顺序块 | 顺序块用关键字 begin 和 end 来表示。顺序块中的语句是一条条执行的。当然,非阻塞赋值除外。顺序块中每条语句的时延总是与其前面语句执行的时间相关。 |
并行块 | 并行块有关键字 fork 和 join 来表示。并行块中的语句是并行执行的,即便是阻塞形式的赋值。并行块中每条语句的时延都是与块语句开始执行的时间相关。 |
嵌套块 | 顺序块和并行块还可以嵌套使用。 |
命名块 | 我们可以给块语句结构命名。命名的块中可以声明局部变量,通过层次名引用的方法对变量进行访问。 |
9 循环
-
while 循环
while (condition) begin
...
end -
for 循环
for(initial_assignment; condition ; step_assignment) begin
...
end -
repeat 循环
repeat (loop_times) begin
...
end
repeat 的功能是执行固定次数的循环,它不能像 while 循环那样用一个逻辑表达式来确定循环是否继续执行。repeat 循环的次数必须是一个常量、变量或信号。如果循环次数是变量信号,则循环次数是开始执行 repeat 循环时变量信号的值。即便执行期间,循环次数代表的变量信号值发生了变化,repeat 执行次数也不会改变。
-
forever 循环
forever begin
...
end
forever 语句表示永久循环,不包含任何条件表达式,一旦执行便无限的执行下去,系统函数 $finish 可退出 forever。
forever 相当于 while(1) 。
通常,forever 循环是和时序控制结构配合使用的。
// 产生一个时钟
reg clk ;
initial begin
clk = 0 ;
forever begin
clk = ~clk ;
#5 ;
end
end
10 函数
函数只能在模块中定义,位置任意,并在模块的任何地方引用,作用范围也局限于此模块。函数主要有以下几个特点:
1)不含有任何延迟、时序或时序控制逻辑
2)至少有一个输入变量
3)只有一个返回值,且没有输出
4)不含有非阻塞赋值语句
5)函数可以调用其他函数,但是不能调用任务
- 声明格式
function [range-1:0] function_id ;
input_declaration ;
other_declaration ;
procedural_statement ;
endfunction
函数在声明时,会隐式的声明一个宽度为 range、 名字为 function_id 的寄存器变量,函数的返回值通过这个变量进行传递。当该寄存器变量没有指定位宽时,默认位宽为 1。
函数通过指明函数名与输入变量进行调用。函数结束时,返回值被传递到调用处。
-
函数调用格式如下:
function_id(input1, input2, ...);
下面用函数实现一个数据大小端转换的功能。
module endian_rvs
#(parameter N = 4)
(
input en, //enable control
input [N-1:0] a ,
output [N-1:0] b
);
reg [N-1:0] b_temp ;
always @(*) begin
if (en) begin
b_temp = data_rvs(a);
end
else begin
b_temp = 0 ;
end
end
assign b = b_temp ;
//function entity
function [N-1:0] data_rvs ;
input [N-1:0] data_in ;
parameter MASK = 32'h3 ;
integer k ;
begin
for(k=0; k<N; k=k+1) begin
data_rvs[N-k-1] = data_in[k] ;
end
end
endfunction
endmodule
- 常数函数
常数函数是指在仿真开始之前,在编译期间就计算出结果为常数的函数。常数函数不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。
这种函数能够用来引用复杂的值,因此可用来代替常量。
- automatic 函数
在 Verilog 中,一般函数的局部变量是静态的,即函数的每次调用,函数的局部变量都会使用同一个存储空间。若某个函数在两个不同的地方同时并发的调用,那么两个函数调用行为同时对同一块地址进行操作,会导致不确定的函数结果。
Verilog 用关键字 automatic 来对函数进行说明,此类函数在调用时是可以自动分配新的内存空间的,也可以理解为是可递归的。因此,automatic 函数中声明的局部变量不能通过层次命名进行访问,但是 automatic 函数本身可以通过层次名进行调用。
例子-数码管译码
module digital_tube
(
input clk ,
input rstn ,
input en ,
input [3:0] single_digit ,
input [3:0] ten_digit ,
input [3:0] hundred_digit ,
input [3:0] kilo_digit ,
output reg [3:0] csn , //chip select, low-available
output reg [6:0] abcdefg //light control
);
reg [1:0] scan_r ; //scan_ctrl
always @ (posedge clk or negedge rstn) begin
if(!rstn)begin
csn <= 4'b1111;
abcdefg <= 'd0;
scan_r <= 3'd0;
end
else if (en) begin
case(scan_r)
2'd0:begin
scan_r <= 3'd1;
csn <= 4'b0111; //select single digit
abcdefg <= dt_translate(single_digit);
end
2'd1:begin
scan_r <= 3'd2;
csn <= 4'b1011; //select ten digit
abcdefg <= dt_translate(ten_digit);
end
2'd2:begin
scan_r <= 3'd3;
csn <= 4'b1101; //select hundred digit
abcdefg <= dt_translate(hundred_digit);
end
2'd3:begin
scan_r <= 3'd0;
csn <= 4'b1110; //select kilo digit
abcdefg <= dt_translate(kilo_digit);
end
endcase
end
end
/*------------ translate function -------*/
function [6:0] dt_translate;
input [3:0] data;
begin
case(data)
4'd0: dt_translate = 7'b1111110; //number 0 -> 0x7e
4'd1: dt_translate = 7'b0110000; //number 1 -> 0x30
4'd2: dt_translate = 7'b1101101; //number 2 -> 0x6d
4'd3: dt_translate = 7'b1111001; //number 3 -> 0x79
4'd4: dt_translate = 7'b0110011; //number 4 -> 0x33
4'd5: dt_translate = 7'b1011011; //number 5 -> 0x5b
4'd6: dt_translate = 7'b1011111; //number 6 -> 0x5f
4'd7: dt_translate = 7'b1110000; //number 7 -> 0x70
4'd8: dt_translate = 7'b1111111; //number 8 -> 0x7f
4'd9: dt_translate = 7'b1111011; //number 9 -> 0x7b
endcase
end
endfunction
endmodule