Verilog语法
一、Verilog基础知识
逻辑值
0、1、X(未知)、Z(高阻态)
数字进制
二进制:4’b0101 表示 4 位二进制数字 0101;
十进制:4’d2 表示 4 位十进制数字 2(二进制 0010);
十六进制:4’ha 表示 4 位十六进制数字 a(二进制 1010)
当代码中没有指定数字的位宽与进制时,默认为 32 位的十进制,比如 100,实际上表示的值为 32’d100。
标识符
用于定义模块名,端口名,信号名,由字母、数字、$和_(下划线)组成,第一个字符必须是字母或者下划线,区分大小写。
不建议大小写混合使用,普通内部信号建议全部小写,参数定义建议大写。
采用一些前缀或后缀,比如:时钟采用 clk 前缀:clk_50m,clk_cpu;低电平采用_n 缀:enable_n;
数据类型
寄存器类型、线网类型和参数类型。
寄存器类型
是一个数据存储单元,常用 reg 类型,默认初始值为X(未知)。
//reg define
reg [31:0] delay_cut; //32位寄存器
reg key_reg; //默认位宽为 1
只能在 always 语句和 initial 语句中被赋值。(不允许像C语言一样,在定义同时赋值)
如果该过程语句描述的是时序逻辑,即 always 语句带有时钟信号,则该寄存器变量对应为寄存器;
如果该过程语句描述的是组合逻辑,即 always 语句不带有时钟信号,则该寄存器变量对应为硬件连线。
线网类型
表示结构化实体(e.g.门)的物理连线。不能存储值,它的值由驱动它的元件决定。驱动元件有连续赋值、门、assign等。默认是Z高阻态。 有tri 和 wire型。
//wire define
wire data_en; //数据使能信号
wire [7:0] data ; //数据
参数类型
参数其实就是一个常量,用parameter定义(类似C语言中的define)我们可以一次定义多个参数,参数与参数之间需要用逗号隔开。参数的定义是局部的,只在当前模块中有效。
//parameter define
parameter DATA_WIDTH = 8; //数据位宽为8位
//可以[DATA_WIDTH : 0]只修改参数值来修改位宽
参数型数据用于定义状态机的状态、数据位宽和延迟大小等,在模块调用时可以通过参数传递来改变调用模块中已定义的参数。
运算符
算术运算符: Verilog 实现乘除比较浪费组合逻辑资源,尤其是除法。所以 2 的指数次幂的乘除习惯用移位,非 2 的指数次幂的乘除调用现成的 IP(ISE提供)
关系运算符
逻辑运算符: !、&& 、 ||
条件操作符: a ? b : c
位运算符: ~(取反)、&、 |、^(异或)
移位运算符: << a << b 将 a 左移 b 位
左移时位宽增加,右移时位宽不变。
4‘b1001 <<2 = 6’b100100
拼接运算符: {a,b} 将 a 和 b 拼接起来作为一个新信号。
二、Verilog程序框架
Verilog注释
//与/* */
Verilog关键字
关键字 | 含义 |
---|---|
module | 模块开始定义 |
input | 输入端口定义 |
output | 输出端口定义 |
inout | 双向端口定义 |
parameter | 信号的参数定义 |
wire | wire信号定义 |
reg | reg信号定义 |
always | 产生reg信号语句的关键字 |
assign | 产生wire信号语句的关键字 |
begin | 语句的起始标志 |
end | 语句的结束标志 |
posedge/negedge | 时序电路的标志 |
case | Case语句起始标记 |
default | Case语句的默认分支标志 |
endcase | Case语句结束标记 |
endmodule | 模块结束定义 |
Verilog程序框架
基本设计单元是模块(Block),类比C语言中的函数。
模块由两部分组成,一部分描述接口,一部分描述逻辑功能,
每个Veirlog程序包括端口定义、IO说明、内部信号说明、功能定义。下面以程序示例讲解知识点。
// 模块名(端口定义);
module block(a,b,c,d);
//IO说明
input a,b;
output c,d;
/************************************************
1.未说明类型就是wire类型,reg类型定义为:
output reg [3:0] led //在位宽之前加reg
2.端口定义和IO说明可以放在一起,例如
module led(
input sys_clk , //系统时钟
input sys_rst_n, //系统复位,低电平有效
output reg [3:0] led //4 位 LED 灯
);
************************************************/
//内部信号说明就是只在此模块内部使用的一些信号,不是输入输出
/************************************************
功能定义:3种方法:
assign描述组合逻辑
always描述组合/时序逻辑
例化示例元件 例如 and #2 u1(q,a,b)
3种逻辑功能是并行的
************************************************/
//assign 给wire类型赋值
assign c = a|b;
assign d = a&b;
endmodule
模块调用、例化——模块化设计
Verilog 语法中的模块例化。FPGA 逻辑设计中通常是一个大的模块中包含了一个或多个功能子模块,Verilog 通过模块调用或模块实例化来实现这些子模块与高层模块的连接。【把系统划分为几个模块,每个模块对应一个module,一个module一个Verilog程序文件。顶层模块只做例化(调用其它模块),不做逻辑。】
首先将被调用模块例化,例化模块名加u_
//顶层模块
module seg_led_static_top (
input sys_clk , // 系统时钟
input sys_rst_n, // 系统复位信号(低有效)
output [5:0] seg_sel , // 数码管位选
output [7:0] seg_led // 数码管段选
);
//parameter define
parameter TIME_SHOW = 25'd25000_000; // 数码管变化的时间间隔 0.5s
//wire define
wire flag; // 数码管变化的通知信号
//每隔 0.5s 产生一个时钟周期的脉冲信号
time_count #(
.MAX_NUM (TIME_SHOW) //参数TIME_SHOW控制数码管每隔多长时间改变显示的数值,底层代码给参数MAX_NUM了值,此处也是一种参数赋值方式,就是把TIME_SHOW的值给MAX_NUM
) u_time_count( //例化模块名加_u
.clk (sys_clk ), //由模块代码看以看到clk是模块的输入
.rst_n (sys_rst_n),//所以这就是把上层模块的信号输入到底层模块,这里()里可以是wire或者reg,而下面的output的连接必须是wire型的
.flag (flag ) //而底层模块的输出信号是连接到了上层模块中定义的其他变量
//信号传递的时候信号的位宽必须一致
);
//属于底层文件
//计时模块代码
module time_count(
input clk , // 时钟信号
input rst_n , // 复位信号
output reg flag // 一个时钟周期的脉冲信号
);
parameter MAX_NUM = 25000_000; // 计数器最大计数值
reg [24:0] cnt; // 时钟分频计数器
三、Verilog高级知识点
结构语句
initial与always
initial:
只执行一次,立即执行,用于tb文件编写,产生激励信号或对reg变量赋初值。
initial begin
sys_clk <= 1b'0;
touch_key <= 1b'0;
#10 touch_key <= 1b'1;
end
always:
一直执行,但需要结合一定的时间控制才有作用
always的时间控制可以是电平触发也可是边沿触发。可以是单个信号也可以是多个信号,多个信号用or连接
/**************边沿触发(时序逻辑行为)**************/
//用于产生 0.5 秒使能信号的计数器
//@()里的多个事件名或信号名组成的列表叫做敏感列表,只有满足敏感列表才能执行后面的语句
always @(posedge sys_clk or negedge sys_rst_n) begin
if (sys_rst_n == 1'b0)
counter <= 1'b0;
else if (counter_en)
counter <= 1'b0;
else
counter <= counter + 1'b1;
end
/**************电平触发(组合逻辑行为)**************/
always @(a or b or c or d or e) begin
out1 = a ? (b + c) : (d + e);
end
//如果输入变量很多,敏感列表就很长且易错
always @( * ) begin //表示后面语句块中所有输入变量都是敏感的
out1 = a ? (b + c) : (d + e);
end
赋值语句
分为阻塞赋值和非阻塞赋值;RHS(Right hand side)是赋值符号右侧信号, LHS是左侧
阻塞赋值(Blocking)
b = a;
计算 RHS 的值并更新 LHS。
在一个 always 块中,后面的赋值语句是在前一句赋值语句结束后才开始赋值的。即 always 块内的语句是一种顺序关系。
非阻塞赋值(Non-Blocking)
b <= a;
(1)赋值开始的时候,计算 RHS;
(2)赋值结束的时候,更新 LHS。
在计算非阻塞赋值的 RHS 以及 LHS 期间,允许其它的非阻塞赋值语句同时计算 RHS 和更新 LHS。
非阻塞赋值只能用于对寄存器类型的变量进行赋值,因此只能用在initial和always块等过程块中。
需要注意的是赋值语句是一直在always中执行,所以随着clk的上升沿一直来,bc最后也都变成a =0;
总结
1.描述组合逻辑电路,用阻塞赋值,比如 assign 赋值语句和不带时钟的 always 赋值语句,这种电路结构只与输入电平的变化有关系
2.描述时序逻辑电路,用非阻塞赋值,综合成时序逻辑的电路结构,比如 带时钟的 always 语句;这种电路结构往往与触发沿有关系,只有在触发沿时才可能发生赋值的变化
注意 :
- 同一个always中不要既用阻塞又用非阻塞
- 不允许在多个always中对同一个变量赋值(因为并行执行)
assign 和 always 区别
assign 不能带时钟。 always 可以带也可以不带。在 always 不带时钟时,逻辑功能和 assign 完全一致,都只产生组合逻辑。比较简单的组合逻辑推荐使用 assign 语句,比较复杂的组合逻辑推荐使用 always 语句。
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1 : 1'b0;
always @(*) begin
case (led_ctrl_cnt)
2'd0 : led = 4'b0001;51 2'd1 : led = 4'b0010;
2'd2 : led = 4'b0100;
2'd3 : led = 4'b1000;54 default : led = 4'b0000;
endcase
end
条件语句
if_else语句:
1.必须在过程块(有initial和always语句引导的)中使用
2.表达式值为X,Z时按假处理
3.if之后如果多句,要用begin end
4.不允许if嵌套
case语句:
与C中的case不同,没有switch关键字。
case (curr_st)
4'h0: next_st = S1;
4'h1: next_st = S2;
4'h2: next_st = S3;
4'h3: next_st = S4;
default: next_st = S0;
endcase
1.所有表达式的位宽必须相等,不能用’bx代替n’bx,
2.casez:比较时不考虑z
casex:比较时不考虑z和x
reg [7:0] sel; //1100_0011
casez(sel) //这种就不用考虑下面表达式中的高阻值
8'b1100_zzzz:语句1;
8'b1100_xxzz:语句2;//xx与00不相等,所以执行语句1
endcase
casex(sel)此时两个表达式相当于相同了,error!
latch锁存器
锁存器是电平触发的存储器,是组合逻辑产生的,寄存器是边沿触发的存储器,在时序电路中使用,由时钟触发产生的。
if 缺少 else 分支,case 缺少 default 分支会导致代码在综合过程中出现了 latch。latch只在组合逻辑电路中产生,也就是只有不带时钟的 always 语句中 if 或者 case 语句不完整才会产生 latch,带时钟的语句 if或者 case 语句不完整描述不会产生 latch。
四、Verilog状态机FSM
状态机FSM概念
我们一直在强调硬件描述语言是并行执行的,所以如果按顺序流程来完成一个操作的时候,就需要很多的if_else,降低了代码的可读性,也让代码编写难度增加(与单片机编写程序的区分)。所以就需要引入状态机,将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程,就像数电里学过的画状态转换图/表。
状态机,(Finite State Machine,FSM有限状态机)一种在有限个状态之间按一定规律转换的时序电路,是组合逻辑和时序逻辑的组合。
状态机模型
Mealy状态机(输出=输入+之前状态)
- F 是当前状态和输入信号的函数,状态是否改变、如何改变,取决于组合逻辑 F 的输出;
- 状态寄存器,其由一组触发器组成,用来记忆状态机当前所处的状态,状态的改变只发生在时钟的跳边沿;
- 状态机的输出是由输出组合逻辑 G 提供的,G 也是当前状态和输入信号的函数。
Moore 状态机(输出=之前状态)
Moore型不是没有输入,而是输出与输入无关。
状态机设计
四段论:
- 状态空间定义;
- 状态跳转;
- 下个状态判断;
- 各个状态下的动作;
1.状态空间定义
/************************方法1:************************/
//state space
parameter S0 = 2'b00;
parameter S1 = 2'b01;
parameter S2 = 2'b10;
parameter S3 = 2'b11;
// internal varialbe
reg [1:0] current_state;
reg [1:0] next_state;
/************************方法2:推荐************************/
//独热码:每个状态只有一个寄存器置位,译码逻辑简单
//state space
parameter S0 = 2'b1000;
parameter S1 = 2'b0100;
parameter S2 = 2'b0010;
parameter S3 = 2'b0001;
// internal varialbe
reg [3:0] current_state;
reg [3:0] next_state;
2.状态跳转(时序逻辑)
就是上面框图中的状态寄存器
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
current_state <= S0;
else
current_state <= S1;
3.下一个状态的判断(组合逻辑)
就是上面框图中的产生下一状态的组合逻辑F
always @(current_state or input signals) begin
case(current_state ) //强烈建议使用case 而不用if_else
S0: begin
if(a)
next_state = S1;
else
next_state = S0;
S1:.......;
S2:.......;
S3:.......;
default:.......;
endcase
end
4.各个状态下的动作(组合逻辑)
就是上面框图中的产生输出的组合逻辑G
//action
wire qook;
assign qook = (current_state == S1) ? 1'b1 : 1'b0;
//下面是用always写上面的语句
always @(current_state ) begin
if(current_state == S1)
qook = 1;
else
qook = 0;
end
总结
这里我们实际用了一个三段式状态机。基本格式是:
第一个 always 语句实现同步状态跳转;
第二个 always 语句采用组合逻辑判断状态转移条件;
第三个 always 语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
-
可以在组合逻辑之后再加一级寄存器实现时序逻辑输出
1.滤除组合逻辑输出的尖峰脉冲
2.进行时序计算和约束
3.对于总线形式的输出信号来说,容易使总线数据对齐,减小总线数据间的偏移,减小接收数据采样出错的频率。比如8位位宽信号,如果用组合逻辑,可能导致并行的8个数据从输入端到达输出端的时间不一致。