前言
Verilog 是一种硬件描述语言,用于描述数字电路的行为和结构。它通过模块化的方式来组织设计,每个模块包含输入、输出端口和内部逻辑。Verilog 提供了丰富的语法结构,可以描述从简单的逻辑门到复杂的处理器架构的各种数字电路设计。对于其格式也有一定的规范(如下图,该实例为一简易分频器),下面对其语法进行具体介绍。
//计数器分频:偶数分频
module cnt_freq(
input clk,//基础时钟为50MHz,即周期为20ps
input rst_n,
output reg signal//周期为2us,占空比为50%
);
reg [6:0] cnt;
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)//复位有效
cnt <= 7'd0;
else
if(cnt < 49)//记50次20ps的clk,即1us
cnt <= cnt + 7'd1;
else
cnt <= 7'd0;
end
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
signal <= 1'b1;
else
if(cnt == 49)//1us后使输出取反,再过1us后又取反,即signal一个周期为2us
signal <= ~signal;
else
signal <= signal;
end
endmodule
正文
一、Verilog 基本格式
1.写作习惯
Verilog 是区分大小写的。格式自由,可以在一行内编写(不推荐),也可跨多行编写(推荐)。每个语句必须以分号为结束符。空白符(换行、制表、空格)都没有实际的意义,在编译阶段可忽略。例如下面两中编程方式都是等效的。
always @(posedge clk,negedge rst_n) begin if(rst_n == 0) signal <= 1'b1; else if(cnt == 49) signal <= ~signal ; else signal <= signal; end
//将所有代码写一行,编辑器不会报错,但可读度下降。
always @(posedge clk,negedge rst_n)
begin
if(rst_n == 0)
signal <= 1'b1;
else
if(cnt == 49)
signal <= ~signal;
else
signal <= signal;
end
//换行,代码逻辑明了,方便查错和修改。
2.注释
代码注释在编程中起着非常重要的作用,可以解释代码的意图,帮助他人和自己理解代码的逻辑思路。便于项目的维护和交接。良好的注释是一个程序员编写优质代码的重要组成部分。因此,在编程过程中,应该养成注释的良好习惯,注释要清晰、简洁、具有针对性,避免过多或冗长的注释,确保注释与代码保持同步更新。Verilog 中有 2 种注释方式:(在Quartus II 中对应注释的内容会变色)
用 // 进行单行注释:
用 /* 与 */ 进行跨行注释:
3.关键字
由于Verilog 语法参照C语言设计,其标识符也具有一定C语言特点,标识符(identifier)可以是任意一组字母、数字、$ 符号和 _(下划线)符号的合,但标识符的第一个字符必须是字母或者下划线,不能以数字或者美元符开始。另外,标识符是区分大小写的。关键字是 Verilog 中预留的用于定义语言结构的特殊标识符。Verilog 中关键字全部为小写。下面介绍一些常用关键字(后续会补充):
module 模块名称 | 模块开始定义 |
input 端口名称 | 输入端口定义 |
output 端口名称 | 输出端口定义 |
inout 端口名称 | 双向端口定义 |
parameter 信号名称 | 信号的参数定义 |
wire 信号名称 | 信号定义 |
reg 信号名称 | 信号定义 |
always @(敏感信号) | 产生reg信号语句的关键字 |
assign 逻辑式 | 产生wire信号语句的关键字 |
begin | 语句的起始标志 |
end | 语句的结束标志 |
posedge 上升沿检查信号 /negedge 下降沿检测信号 | 时序电路的标志 |
case(多分枝判断目标) | CASE语句起始标记 |
default :逻辑式 | 语句的默认分支标志 |
endcase | 语句结束标记 |
if(判断式) if/else end | 判断语句标记 |
else if(判断式)/else | 选择语句标记 |
fo(初始条件;终止条件;增减计数) | 循环语句标记 |
endmodule | 模块结束定义 |
initial | 模拟器开始执行 |
4.结构
模块定义必须以关键字 module 开始,以关键字 endmodule 结束。模块名,端口信号,端口声明和可选的参数声明等,出现在设计使用的 Verilog 语句(图中Declarations_and_Statements)之前。模块内部有可选的 5 部分组成,分别是变量声明,数据流语句,行为级语句,低层模块例化及任务和函数,如下图表示。这 5 部分出现顺序、出现位置都是任意的。但是,各种变量都应在使用之前声明。变量具体声明的位置不要求,但必须保证在使用之前的位置。
二、数据类型与数值表示
1.数值表示
Verilog HDL 有下列四种基本的值来表示硬件电路中的电平逻辑:
0 | 逻辑”0“或假 |
1 | 逻辑”1“或真 |
X或x | 未知 |
Z或z | 高祖 |
x 意味着信号数值的不确定,即在实际电路里,信号可能为 1,也可能为 0。
z 意味着信号处于高阻状态,常见于信号(input, reg)没有驱动时的逻辑结果。例如一个 pad 的 input 呈现高阻状态时,其逻辑值和上下拉的状态有关系。上拉则逻辑值为 1,下拉则为 0 。
合法的基数格式有 4 中,包括:十进制('d 或 'D),十六进制('h 或 'H),二进制('b 或 'B),八进制('o 或 'O)。数值可指明位宽,也可不指明位宽(默认为十进制表示)。
//整数表示
4'b1011 // 4bit 数值,表示二进制1011
32'h3022_c0de // 32bit 的数值 表示十六进制是的3022_c0de
//下划线 _ 是为了增强代码的可读性。
'd100 //会根据编译器自动分频位宽,常见的为32bit
-6'd15 //负数表示
//-15 在 5 位二进制中的形式为 5'b10001,
//在 6 位二进制中的形式为 6'b11_0001。需要注意的是,减号放在基数和数字之间是非法的。
//实数表示
30.123
1.2e4 //大小为12000
1E-3 //大小为0.001
//字符串表示
reg [0: 14*8-1] shiyan;
initial
begin
shiyan= "asdfghjklqwert";
end
2.数据类型
2.1线网(wire)
wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。
wire interrt ;
wire gccc = 1'b0 ;
2.2寄存器(reg)
寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。声明举例如下:
reg clk;
reg flag;
2.3向量
当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。例如:
reg [3:0] counter ; //声明4bit位宽的寄存器counter
wire [32-1:0] gpio; //声明32bit位宽的线型变量gpio
wire [8:2] addr ; //声明7bit位宽的线型变量addr,位宽范围为8:2
reg [0:31] data ; //声明32bit位宽的寄存器变量data, 最高有效位为0
对于上面的向量,我们可以指定某一位或若干相邻位,作为其他逻辑使用。Verilog 支持可变的向量域选择,Verillog 还支持指定 bit 位后固定位宽的向量域选择访问。
[bit+: width] : 从起始 bit 位开始递增,位宽为 width。
[bit-: width] : 从起始 bit 位开始递减,位宽为 width。
wire [9:0] data_low = data[0:9] ;//data_low[9]=data[0]......
addr_temp[3:2] = addr[8:7] + 1'b1 ;//把addr的第7、8位数据加一后给到addr_temp3、4位
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
2.4整数,实数,时间寄存器变量
整数,实数,时间等数据类型实际也属于寄存器类型。整数(integer)整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数。实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。
reg [31:0] data1 ;
reg [3:0] byte1 [7: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
real data1 ;
integer temp ;
initial begin
data1 = 2e3 ;
data1 = 3.75 ;
end
initial begin
temp = data1 ; //temp 值的大小为3
end
time current_time ;
initial begin
#100 ;
current_time = $time ; //current_time 的大小为 100
end
2.5数组
在 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数据变量数组
flag [1] = 32'd0 ; //将flag数组中第二个元素赋值为32bit的0值
counter[3] = 4'hF ; //将数组counter中第4个元素的值赋值为4bit 十六进制数F,等效于counter[3][3:0] = 4'hF,即可省略宽度;
assign addr_bus[0] = 8'b0 ; //将数组addr_bus中第一个元素的值赋值为0
assign data_bit[0][1] = 1'b1; //将数组data_bit的第1行第2列的元素赋值为1,这里不能省略第二个访问标号,即 assign data_bit[0] = 1'b1; 是非法的。
data_4d[0][0][0][0][15:0] = 15'd3 ; //将数组data_4d中标号为[0][0][0][0]的寄存器单元的15~0bit赋值为3
2.6存储器
存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为。
reg membit[0:255] ; //256bit的1bit存储器
reg [7:0] mem[0:1023] ; //1Kbyte存储器,位宽8bit
mem[511] = 8'b0 ; //令第512个8bit的存储单元值为0
2.7参数
参数用来表示常量,用关键字 parameter 声明,只能赋值一次。
parameter data_width = 10'd32 ;
parameter i=1, j=2, k=3 ;
parameter mem_size = data_width * 10 ;