写在前面:自学笔记,推荐小伙伴一起学习,欢迎提出宝贵意见。参考内容有Verilog菜鸟教程,徐文波版《Xilinx FPGA开发实用教程》;
目录
流控制(if、case、for、while、forever、repeat)
一、Verilog的基本语法
1.1 Verilog模块的基本概念
Verilog程序是由一个或者多个模块组成的,每个模块可以实现特定的功能,在设计时应该优先确定每个模块的功能,之后在进行代码编写。每个模块要进行端口定义,并说明输入、输出口,然后对模块的功能进行描述,即代码的编写。Verilog程序包括4个主要部分:端口定义、I/O说明、内部信号说明、功能定义。下面的代码即为一个模块:
//二选一多路选择器为例
module module_name(out, a, b, sl);
input a, b, sl; //定义输入信号
output out; //定义输出信号
reg out; //定义存储器
always @(sl or a or b) //语法体
if(!sl)
out = a;
else
out = b;
endmodule
/*module_name是模块名字,在定义时替换成合适的名字,字母和下划线,举例:uart_xx;*/
- 以module起始,以endmodule结尾,其内部每个语句和数据定义的最后必须有分号(;)。
- 每个文件只包含一个module,而且module名要小写,并且与文件名保持一致;
- 不要书写空的模块,即:一个模块至少要有一个输入和一个输出;
暂时这么多,之后会补充
写在前面,关于Verilog的代码特点
- 区分大小写;
- 书写自由,一行可以写多个语句,一个语句也可以分行;
- 空白符(换行、制表、空格)都没有实际的意义,在编译阶段可忽略。
1.2 模块内容
在模块的端口声明了模块的输入输出口,其格式如下:
module 模块名(口1, 口2, 口3, ....);
这里只是声明了输入输出口,接下来需要具体指明每个端口是输入还是输出。
1.2.1 I/O说明
输入口—— 模块从外界读取数据的接口,在模块内不可写。
input [ 信号位宽 - 1 : 0 ] 端口 i 名;
input [7:0] a; //说明了一个8位的输入端口 "a"
输出口——模块往外界送出数据的接口,在模块内不可读。
output [ 信号位宽 - 1 : 0 ] 端口 j 名;
output [7:0] out; //说明了一个8位的输入端口 "out"
输入/输出口——可读取数据也可以送出数据,数据可双向流动。
inout [ 信号位宽 - 1 : 0 ] 端口1名;
inout [7:0] in_out; //说明了一个8位的输入/输出端口 "in_out"
注意:如果不定义位宽则默认位1
1.2.2 内部信号说明
线网(wire):wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。(模块内的连接线)
wire [7:0] addr ; //声明8bit位宽的线型变量addr,
寄存器(reg):寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。(模块内的寄存器)
reg [7:0] addr ; //声明8bit位宽的线型变量addr,
reg [15:0] ROMA [7:0] ; //声明了一个存储位宽为16位,存储深度为8的一个存储器。该存储器的地址范围是0到8。(memory型)
常量(parameter):定义常数(常量)。parameter 参数名1 = 数据;
parameter [3:0] S0 = 4'h0;
1.3 数值表示
数值种类
Verilog HDL 有下列四种基本的值来表示硬件电路中的电平逻辑:
- 0:逻辑 0 或 "假"
- 1:逻辑 1 或 "真"
- x 或 X:未知
- z 或 Z:高阻
x 意味着信号数值的不确定(这里不区分大小写),即在实际电路里,信号可能为 1,也可能为 0。z 意味着信号处于高阻状态,常见于信号(input, reg)没有驱动时的逻辑结果。例如一个 pad 的 input 呈现高阻状态时,其逻辑值和上下拉的状态有关系。上拉则逻辑值为 1,下拉则为 0 。
整数数值表示方法
整技术表示格式:[长度] ' 基数 数值;
4'b1011 // 4bit 数值
5'o8 // 5位八进制数
9'd6 // 9位十进制数
32'h3022_c0de // 32bit 的数值
/*不区分大小写;下划线是为了增加可读性; */
//一般直接写数字时,默认为十进制表示,例如下面的 3 种写法是等效的:
counter = 'd100 ; //一般会根据编译器自动分频位宽,常见的为32bit
counter = 100 ;
counter = 32'h64 ; //16进制64 == 十进制100
负数表示:通常在表示位宽的数字前面加一个减号来表示负数,
-6'd15
-15
4'd-2 //非法说明,错误
其中负数使用补码形式,-15 在 5 位二进制中的形式为 5'b10001(补码:取反加一为实际值,最高位1表示符号位,0001取反加一等于1111), 在 6 位二进制中的形式为 6'b11_0001(最高位1为符号扩展位)。
注意:减号放在基数和数字之间是非法的,
实数:实数可以用两种形式定义:十进制计数法和科学计数法。
1.十进制计数法
2.0
3.1415926
2.科学计数法
235.12e2 实际值23512
5e-4 实际值0.0005
其中e与E相同,根据Verilog语法定义,实数通过四舍五入隐式的转换为最相近的整数
字符串:字符串是双引号内的字符序列,字符串不能分成多行书写,用8位ASCII值表示的字符可看作是无符号整数,一个ASCII码占8位。
reg [1:8*7] Char;
Char = "counter";
1.4 阻塞赋值与非阻塞赋值
1.5 运算符和表达式
1.6 描述语句
1.6.1 结构描述形式
1.6.2 数据流描述形式(assign)
数据流型描述一般都采用(assign)连续赋值语句来实现,主要用于实现组合功能。连续赋值语句右边所有的变量受持续监控,只要这些变量有一个发生变化,整个表达式将被重新赋值给左端。这种方法只能用于实现组合逻辑电路。
assign L_s = R_s;
//一个利用数据流描述的移位器
module mlshift2 (a, b)
input a;
output b;
assign b == a << 2;
endmodule
//只要a值发生变化,b就会被重新赋值,所赋的值为a左移两位后的值
1.6.3 行为描述形式
行为级描述主要包括过程结构、语句块、时序控制、流控制4个方面,主要用于时序逻辑功能的实现。(个人以为只需要了解关键字的意义即可)
过程结构(initial、always)
- initial 模块
initial模块从模拟0时刻开始执行,且在仿真过程中只执行一次,在执行完一次后,该initial就被挂起,不在执行。如果仿真中有多个initial模块,则同时从0时刻开始并行执行。(initial模块是面向仿真的,是不可增和的,通常被用来测试模块初始化、监视、波形生成等功能)
initial begin
//初始化输入
clk = 0;
ai = 0;
bi = 0;
//等待100ns,全局reset信号有效
# 100;
ai = 20;
bi = 10;
end
- always 模块
always模块是一只重复执行的,并且可被综合。敏感事件表的目的就是触发always模块的运行,而initial后面是不允许有敏感事件表的,always过程块的触发条件是:只要a、b、c信号的电平有任意一个发生变化。
always @(a or b or c) begin
...
end
always模块主要是对硬件功能的行为进行描述,可以实现锁存器和触发器,也可以用来实现组合逻辑。利用always实现组合逻辑时,要将所有的信号放进敏感列表,而实现时序逻辑时却不一定要将所有的结果放进敏感信息列表。(上例中只有a、b、c中存在变化时就会执行语句块,如果存在其他的信号,比如e发生变化则不会执行语句块)
语句块(begin...end/fork...join)
语句块就是initial模块或者always模块中位于begin...end/fork...join块定义语句之间的一组行为语句。
begin ... end 串行块,用来组合需要顺序执行的语句
fork ... join 并行块,用来组合需要并行执行的语句
//例:下面两段程序都是用来产生一系列延迟波形
parameter d = 50;
reg [7:0] r;
begin
# d r = 'h35; //语句1
# d r = 'h35; //语句2
# d r = 'h35; //语句3
# d r = 'h35; //语句4
# d -> end_wave; //语句5,触发事件end_wave
end
parameter d = 50;
reg [7:0] r;
fork
# d r = 'h35; //语句1
# 2d r = 'h35; //语句2
# 3d r = 'h35; //语句3
# 4d r = 'h35; //语句4
# 5d -> end_wave; //语句5,触发事件end_wave
join
//由于并行块中语句是同步进行的,所以为了产生间隔d的波形需要递增延时时间
时序控制
- 延时控制 always #10 Clk = ~Clk; 延时控制只能在仿真时使用,是不可综合的。在综合时所有的延时控制都会被忽略。
- 事件控制 边沿触发事件与电平触发事件
//边沿触发事件计数器(计算时钟变化次数)
reg [4:0] cnt;
always @(posedge Clk or negedge Reset_n)begin //posedge上升沿关键字 negedge下降沿关键字
if(!Reset_n)
cnt <= 0;
else
cnt <= cnt + 1;
end
//电平触发事件计数器(记录a,b,c变化的次数)
reg [4:0] cnt;
always @(a or b or c)begin
if(!Reset_n)
cnt <= 0;
else
cnt <= cnt + 1;
end
???如果a,b,c同时变化只记录一次???
流控制(if、case、for、while、forever、repeat)
流控制语句包含3类,即跳转、分支、循环语句。
//if语句
always @(a1 or b1)begin
if(a1) q <= d;
else q <= 0 //else分支也可以默认,但会产生一些不可预料的结果,生成成本不期望的锁存器。
end
//case语句
always @(a1[1:0] or b1)begin
case (a1)
2'b00 : q <= b1;
2'b01 : q <= b1 + 1;
default : q <= b1 + 2; //default分支虽然可以默认,但一般不要默认,可能会生成锁存器。
end
//for循环
for(i = 1; i <= size; i = i + 1)
result = result + (a << (i - 1));
//while循环
while (temp)begin
count = count + 1;
end
//forever循环
/*forever必须写在initial模块中,用于产生周期性波形,系统函数 $finish 可退出 forever。*/
initial forever begin
if (d) a = b + c;
else a = 0;
end
//repeat循环
repeat (size)begin //指定循环次数
c = b << 1;
end
二、 Testbench编写
2.1 模块例化
在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化(一个模块其实就是一个器件,将器件连接起来的操作就是例化)。模块例化建立了描述的层次。信号端口可以通过位置或名称关联,端口连接也必须遵循一些规则。如果某些输出端口并不需要在外部连接,例化时 可以悬空不连接,甚至删除。一般来说,input 端口在例化时不能删除,否则编译报错,output 端口在例化时可以删除。建议 input 端口不要做悬空处理,无其他外部连接时赋值其常量,
当例化端口与连续信号位宽不匹配时,端口会通过无符号数的右对齐或截断方式进行匹配。子模块的端口信号位宽大于例化对应端口,则截断;反之补位右对齐。