逻辑值规则
Verilog中有四种逻辑状态:
- 逻辑
0
:表示低电平GND。 - 逻辑
1
:表示高电平VCC。 - 逻辑
x
:未定义状态,有可能是高电平,也可能是低电平。 - 逻辑
z
:高阻态状态,没有外部激励信号的悬空状态。
标识符规则
- 标识符用于定义模块名、端口名等。
- 标识符可以是包括字母、数字、$、下划线的任意字符串。
- 标识符的第一个字符只能是字母或下划线。
- 标识符区分大小写。
-
数字进制格式
Verilog支持二进制、八进制、十进制、十六进制这几种数字进制格式。
4'b1010
二进制四位宽的1010(十进制10)4'd2
十进制四位宽的数字25'h0b
十六进制五位宽数字0x0b(十进制11,二进制01011)
- 当没有指定数字位宽和进制时,默认使用32位宽的十进制数字。
-
变量数据类型
数据类型可以分为三大类,包括寄存器类型、线网类型、参数类型。
reg - 寄存器类型
寄存器类型标识一个抽象的存储单元,它只能在always语句和initial语句中被赋值。它的值在赋值过程中被保存下来。
- 如果赋值语句在时序逻辑电路中,即在always语句中带有时钟信号,则该寄存器被综合为触发器。
- 如果赋值语句在组合逻辑电路中,即在always语句中没有时钟信号,则该寄存器被综合为硬连线。
- 寄存器数据类型有很多种,如
reg
、integer
、real
等,常用的是reg
类型。 寄存器类型的缺省值为x,未知状态。
reg [15:0] counter; // 16位宽寄存器类型
wire - 线网类型
线网类型表示元器件之间的物理连线。
- 线网类型的变量的指由驱动元件的值决定,如果没有驱动输入,缺省值为z高阻态。
线网类型有很多种,如
tri
和wire
等,常用的是wire
。wire data_en; // 数据使能信号
parameter - 参数类型
参数类型其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等。
- 参数类型常被用于一些参数可调的模块中。
- 在定义参数时,可以一次定义多个参数,参数与参数之间需要用逗号隔开。
- 参数的定义是局部的,只在当前模块
module
中可用。//parameter define
parameter DATA_WIDTH = 8; //数据位宽为8位
运算符规则
Verilog的运算符包括:算术运算符、关系运算符、逻辑运算符、条件运算符、位运算符、移位运算符、拼接运算符这7类。1. 算术运算符
| 符号 | 使用方法 | 说明 | | —- | —- | —- | | + | a + b | a 加上 b | | - | a - b | a 减去 b | | | a b | a 乘以 b | | / | a / b | a 除以 b | | % | a % b | a 取模(取余) b |
需要说明的是,Verilog的乘除法实现起来比较耗费资源,所以在以2的指数次幂的乘除法时,一般使用移位方式
运算;非2的指数次幂的乘除法一般调用现成的IP
来实现。
2. 关系运算符
符号 | 使用方法 | 说明 |
---|---|---|
> | a > b | a 大于 b |
< | a < b | a 小于 b |
>= | a >= b | a 大于等于 b |
<= | a <= b | a 小于等于 b |
== | a == b | a 等于 b |
!= | a != b | a 不等于 b |
3. 逻辑运算符
符号 | 使用方法 | 说明 |
---|---|---|
! | !a | 非a,a的值为0则结果为1 |
&& | a && b | a 与 b,如果a和b都是1,结果才是1,否则结果为0 |
|| | a || b | a 或 b,如果a和b之中有一个1,结果就是1;否则a和b全是0时,结果为0 |
4. 条件运算符
符号 | 使用方法 | 说明 |
---|---|---|
? : | a ? b : c | 如果a是真,结果就是b,否则结果是c。 |
5. 位运算符
符号 | 使用方法 | 说明 |
---|---|---|
~ | ~a | 将a的每一位取反 |
& | a & b | 按位与,将a的每一位和b对应的位相与 |
| | a | b | 按位或,将a的每一位和b对应的位相或 |
^ | a ^ b | 按位异或,将a的每一位和b对应的位相异或。异或是指如果两个操作数不同,异或结果是1;如果两个操作数相同,异或结果就是0。 |
6. 移位运算符
符号 | 使用方法 | 说明 |
---|---|---|
<< | a << b | 将a的值左移b位 |
>> | a >> b | 将a的值右移b位 |
7. 拼接运算符
符号 | 使用方法 | 说明 |
---|---|---|
{} | c = {a, b} | 将a与b拼接起来的数值赋值到c中 |
8. 运算符优先级
程序编写框架
注释
// 这是单行注释
/*
这是多行注释
这是注释块里的内容
*/
关键字(共103个)
and | always | assign | begin | buf | bufif0 | bufif1 | case |
---|---|---|---|---|---|---|---|
casex | casez | cmos | deassign | default | defparam | disable | edge |
else | end | endcase | endfunction | endprimitive | endmodule | endspecify | endtable |
endtask | event | for | force | forever | fork | function | highz0 |
highz1 | if | ifnone | initial | inout | input | integer | join |
large | macromodule | medium | module | nand | negedge | nor | not |
notif0 | notif1 | nmos | or | output | parameter | pmos | posedge |
primitive | pulldown | pullup | pull0 | pull1 | rcmos | real | realtime |
reg | release | repeat | rnmos | rpmos | rtran | rtranif0 | rtranif1 |
scalared | small | specify | specparam | strength | strong0 | strong1 | supply0 |
supply1 | table | task | tran | tranif0 | tranif1 | time | tri |
triand | trior | trireg | tri0 | tri1 | vectoted | wait | wand |
weak0 | weak1 | while | wire | wor | xnor | xor |
常用的一些关键字及其含义如下:
关键字 | 含义 |
---|---|
module / endmodule | 模块开始定义、结束定义 |
input / output / inout | 输入端口定义、输出端口定义、双向端口定义 |
parameter | 信号参数定义 |
wire | 线束信号定义 |
reg | 寄存器信号定义 |
always | 产生寄存器信号的语句关键字 |
assign | 产生线束信号语句的关键字 |
begin / end | 语句的开始标志、结束标志 |
posedge / negedge | 时序电路上升沿、下降沿 |
case / default / endcase | case语句的其实标志、默认分支、结束标志 |
if / else | 条件判断语句标志 |
for | 循环语句标志 |
高级知识点
阻塞赋值和非阻塞赋值
1. 阻塞赋值
阻塞赋值是指在同一个always语句块中,前面的语句的执行会影响右面的语句。只有当前面的语句执行结束,后面的各条语句才能执行,相当于是逐条语句执行的,是一种顺序执行关系
。
符号=
用于阻塞赋值,在同一个always块中,在begin和end之间的语句顺序执行。
我们进行一个测试用来验证阻塞赋值过程:
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
a = 1;
b = 2;
c = 3;
end
else begin
a = 0;
b = a;
c = b;
end
end
仿真结果如下图(图来自正点原子FPGA手册):
上图看出:
- 在第0个位置,复位信号拉高
- 在第2个位置,遇到第一次时钟信号上升沿,进入else语句段
-
2. 非阻塞赋值
非阻塞赋值是指由时钟节拍决定的并行赋值过程,在同一个always语句块中,前面语句的执行不会影响后面的语句。在一个时钟的上升沿,同时执行所有语句右边的代码,并同时将赋值语句右边的表达式赋值给语句左边的变量。相当于多条语句同时执行,是一种
并行执行关系
。
符号<=
用于非阻塞赋值,在同一个always块中,在begin和end之间的语句同时执行。always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
a <= 1;
b <= 2;
c <= 3;
end
else begin
a <= 0;
b <= a;
c <= b;
end
end
仿真结果如下图(图来自正点原子FPGA手册):
上图看出: 在第0个位置,复位信号拉高
- 在第2个位置,遇到第1次时钟信号上升沿,第1次进入else语句段。由于是非阻塞赋值,赋值语句同时执行,导致
a=0,b=a=1,c=b=2
。 - 在第4个位置,遇到第2次时钟信号上升沿,第2次进入else语句段。由于是非阻塞赋值,赋值语句同时执行,导致
a=0,b=a=0,c=b=1
。 - 在第6个位置,遇到第3次时钟信号上升沿,第3次进入else语句段。由于是非阻塞赋值,赋值语句同时执行,导致
a=0,b=a=0,c=b=0
。
在描述组合逻辑电路时,使用阻塞赋值。(如assign语句和组合逻辑时的always语句)
在描述时许逻辑电路时,使用非阻塞赋值。(如时序逻辑时的always语句)
assign和always语句
- assign语句和always语句都是用来产生组合逻辑或者时序逻辑的语句。
- assign语句只能用来产生组合逻辑,always在不带时钟时,功能与assign一样,都只能产生组合逻辑。
- 比较简单的组合逻辑常用assign,逻辑复杂的话常用always。
always带时钟时,产生时许逻辑电路,reg类型被综合成真正的寄存器。
latch锁存器注意事项
锁存器和存储器一样都是存储单元。锁存器是电平触发,寄存器是边沿触发。
- 锁存器在组合逻辑中产生,寄存器在时序逻辑中使用。
- 锁存器的坏处是会产生毛刺(
glitch
),毛刺对下级电路是很危险的,并且不容易被察觉,所以尽量避免使用锁存器。 - 在不带时钟的always语句中,如果if/else或case/default语句不完整(即缺少else或default分支),就会导致产生锁存器。因此,在不带时钟的always语句(即组合逻辑)中,必须补全else或default分支。
- 在带有时钟的always语句(即时序逻辑)中,语句分支不完整也不会产生锁存器,因为锁存器只会在组合逻辑中产生。
状态机分类
状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂。
在控制复杂流程时,状态机的优势就非常明显。
根据状态机的输出是否与输入条件相关,可将状态机分为两大类:Moore状态机和Mealy状态机。
- 摩尔(Moore)型状态机:组合逻辑的输出只取决于当前状态。
- 米勒(Mealy)型状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态。
三段式状态机
三段式状态机的基本格式是:
- 第一个always语句实现同步状态跳转;
- 第二个always语句采用组合逻辑判断状态转移条件;
- 第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。 ```verilog module divider7_fsm ( //系统时钟与复位 input sys_clk , input sys_rst_n , //输出时钟 output reg clk_divide_7 );
//parameter define parameter S0 = 7’b0000001; //独热码定义方式 parameter S1 = 7’b0000010; parameter S2 = 7’b0000100; parameter S3 = 7’b0001000; parameter S4 = 7’b0010000; parameter S5 = 7’b0100000; parameter S6 = 7’b1000000;
//reg define reg [6:0] curr_st ; //当前状态 reg [6:0] next_st ; //下一个状态
//* // main code //*
//状态机的第一段采用同步时序描述状态转移 always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) curr_st <= S0; else curr_st <= next_st; end
//状态机的第二段采用组合逻辑判断状态转移条件 always @(*) begin case (curr_st) S0: next_st = S1; S1: next_st = S2; S2: next_st = S3; S3: next_st = S4; S4: next_st = S5; S5: next_st = S6; S6: next_st = S0; default: next_st = S0; endcase end
//状态机的第三段描述状态输出(这里采用时序电路输出) always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) clk_divide_7 <= 1’b0; else if ((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3)) clk_divide_7 <= 1’b0; else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6)) clk_divide_7 <= 1’b1; else ; end
endmodule
<a name="OrGGl"></a>
### 模块化设计
模块化设计能够使一个大型设计的分工协作、仿真测试更加容易,代码维护或升级更加便利,当更改某个子模块时,不会影响其它模块的实现结果。 <br />。。。。
<a name="IH7Qq"></a>
## 编程规范
<a name="SCiPF"></a>
### 代码组织结构
正点原子的代码组织方式为:
```verilog
项目工程名
├── doc
├── par
├── rtl
└── sim
- doc目录:存放工程相关的文档,包括数据手册、设计方案等。
- par目录:主要存放工程文件和使用到的IP核文件。
- rtl目录:主要存放rtl代码,这是工程的核心,文件名与模块名一致,建议按照模块的层次分开存放。
- sim目录:主要存放工程的仿真代码。
头文件声明
参照正点原子开发文档改写的头文件声明如下://---------------------- Copyright (c) ----------------------
// Copyright(C) ZHJ0125 2022
// All rights reserved
//-----------------------------------------------------------
// File Name : touch_led.v
// Created Date : 2022-04-15 17:54
// Modified Date : 2022-04-15 17:54
// Created By : ZHJ0125
// Last Version : V0.1
// Descriptions : 使用触摸按键控制LED。
// 开发板上电后LED为熄灭状态,手指触摸后LED点亮;
// 再次触摸,LED熄灭,以此循环。
//-----------------------------------------------------------
输入输出定义
模块的输入输出定义需要注意以下几点:
- 每行只定义一个信号。
- 信号名要全部对齐。
同一组的信号放在一起。
module led (
input sys_clk, // 系统时钟
input sys_rst_n, // 系统复位
output reg [3:0] led // 4位LED灯
);
参数类型定义
将参数类型parameter声明,紧跟着module的输入输出定义之后。
参数类型parameter声明的常量全部使用大写。
// parameter define
parameter WIDTH = 25;
parameter COUNT_MAX = 25_000_000;
线束类型/寄存器类型定义
每行只定义一个信号。
- 在一个module中,wire和reg变量要集中放在一起。
- wire/reg变量声明要紧跟在parameter参数变量声明之后。
- 信号名需要对齐,reg和位宽需要空2格,位宽和信号名至少空4格。
- 位宽使用降序描述方式[15:0]。
时钟信号缩写
clk
,复位信号缩写rst
,低电平有效加后缀n
// reg define
reg [WIDTH-1:0] counter;
reg [1:0] led_ctrl_cnt;
// wire define
wire counter_en;
信号命名规则
信号命名要体现实际意义。
- 可以使用
_
下划线隔开。 - 内部信号全部使用小写,不要大小写混用。
- 模块名字也要使用小写。
- 低电平有效信号使用
_n
后缀。 - 一部信号使用
_a
后缀。 -
always块描述规则
if需要4个空格。
- 每个always都要有begin和end,并且always和begin放在同一行。
- 每个always前面都要有一行注释。
- 一个always块只包含一个时钟和复位。
-
assign块描述规则
assign描述的逻辑不要太复杂,否则不宜阅读。
-
空格和TAB规则
注释规则
简洁清晰,不说废话
-
模块例化规则
模块例化使用
u_xxxxx
表示,即使用u_
前缀。 ```verilog //例化计时模块 time_count #( .MAX_NUM (TIME_SHOW) ) u_time_count( .clk (sys_clk ), .rst_n (sys_rst_n),.flag (add_flag ) );
//例化数码管静态显示模块 seg_led_static u_seg_led_static ( .clk (sys_clk ), .rst_n (sys_rst_n),
.add_flag (add_flag ),
.sel (sel ),
.seg_led (seg_led )
其他注意事项
- 代码不要过分简介,要方便他人阅读理解。
- 不要使用
repeat
等循环语句。 - RTL级别里不使用initial语句,但在代码仿真的激励文件里可以使用。
- 避免产生锁存器Latch,在组合逻辑里要补全if/else、case/default分支。
- 避免使用罕见的语法,不利于语法综合时的优化。