实例:点灯学习
一、Verilog
语法学习
1. 参考文章
刚接触
Verilog
,作为一个硬件小白,只能尝试着去理解,文章未完…持续更新。
参考博客文章:
- Verilog语言入门学习(1)
- Verilog语法
- 【
Verilog
】一文带你了解Verilog
基础语法 - 子墨祭的文章 - 知乎 - 关于Verilog中的几种赋值语句
这里抄点,那里扣点,整理了一下,感谢以上各位作者。具体的实例项目是同事给的,他让我学着编出个流水灯,这不要我的命嘛,慢慢学吧。
你还可以在哪里看到这篇文章:
- 知乎
- 简书
2. Verilog
模块
Verilog
的基本设计单元是“模块”。一个模块是由两部分组成的,一部分描述接口,另一部分描述逻辑功能,即定义输入是如何影响输出的。下面举例说明:
可以看到模块由关键字module....endmodule
确定。
module
模块名(接口信号);
//信号声明
//功能描述
endmodule
要求: 1. 模块名具有意义;2. 一个.v文件只有一个模块。
3. Verilog
语法
Verilog
是一种硬件描述语言,以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。
Verilog
和C
的区别:
Verilog
是硬件描述语言,编译下载到FPGA
之后,会生成电路,所以Verilog
全部是并行处理与运行的C
语言是软件语言,编译下载到单片机CPU
之后,还是软件指令,而不会根据代码生成相应的硬件电路,而单片机CPU
处理软件指令需要取址、译码、执行,是串行执行的。
**可综合描述:**综合tool
能够Verilog
描述转化(complie
)成基本的数字电路底层cell
(与或非gate
,寄存器等)的描述。
assign y = a & b;
**不可综合描述:**综合tool
不能把Verilog
描述转换为基本的数字电路底层cell
的描述。
$display("hello word.\n")
Verilog
设计仿真与实现:通过EDA TOOL
,可以在计算机上对Verilog
设计的功能进行仿真。
数字电路设计方法学:
Bottom-Up
:从底层cell
开始,逐渐往上加功能;top-Down
:从底层结构,协议算法开始,向下逐步划分功能模块;再细分各功能模块与IO
。
目前,基于Verilog
的数字电路通常使用TOP-Down
的设计方法。因为数字IP/IC
的规模很大,需要先抽象思维再细化;
Verilog
给数字电路的设计的抽象思维提供了一种设计语言,但是:数字设计和软件设计不同的,需要Think in Hardware
,写代码前先设计电路结构。Verilog
的功能描述:
Top-Dowan
描述数字电路功能:通过模块module的层次化设计,实现一个复杂的数字逻辑功能
Verilog
逻辑值:
逻辑电路中有四种值,即四种状态:
- 逻辑0:表示低电平,对应电路的
GND
; - 逻辑1:表示高电平,对应电路的
VCC
; - 逻辑X:表示未知,有可能是高电平,也有可能是低电平;
- 逻辑Z:表示高组态,外部没有激励信号是一个悬空状态;
Verilog
的数字进制
Verilog
数字进制格式包括二进制、八进制、十进制和十六进制,一般常用的为二进制、十进制和十六进制。
- 二进制表示如下:
4'b0101
表示4位二进制数字0101 - 十进制表示如下:
4'd2
表示4位十进制数字2 - 十六进制表示如下:
4'ha
表示4位十六进制数字a
当没有指定数字的位宽与进制时,默认为32位的十进制,比如100,实际上表示的值为
32'd100
4. Verilog
语法详细介绍
(1)标识符:
用于定义code
中的各种名字,比如:信号,module
,define
,parameter
;
- 标识符由:字符,数字,下划线组成;
- 首字母必须是字母或者下划线;
- 标识符是区分大小写的;
- 信号名字与信号功能相对应;
不建议大小写混合使用,普通内部信号建议全部小写,参数定义建议大写
- 用有意义的有效的名字如
sum
、cpu_addr
等- 用下划线区分词语组合,如
cpu_addr
- 采用一些前缀或后缀
- 比如:时钟采用
clk
前缀:clk_50m
,clk_cpu
;低电平采用_n
后缀:enable_n
- 统一缩写,如全局复位信号
rst
- 同一信号在不同层次保持一致,如同一时钟信号必须在各模块保持一致。
- 自定义的标识符不能与保留字(关键字)同名
- 参数统一采用大写,如定义参数使用
SIZE
(2)关键字
优先记录常用关键字:
关键字 | 含义 |
---|---|
module | 模块开始定义 |
input | 输入端口定义 |
output | 输出端口定义 |
inout | 双向端口定义 |
parameter | 信号的参数定义 |
wire | wire信号定义 |
reg | reg信号定义 |
always | 产生reg信号语句的关键字 |
assign | 产生wire信号语句的关键字 |
begin | 语句的起始标志 |
end | 语句的结束标志 |
posedge、negedge | 时序电路的标志 |
case | Case语句起始标记 |
default | Case语句的默认分支标志 |
endcase | Case语句结束标志 |
if | if/else语句标志 |
else | if/else语句标记 |
for | for语句标记 |
endmodule | 模块结束定义 |
(3)注释:
-
注释一行:以//开始,到行末;
-
注释多行:以
/*
开始,到*/
为止的所有内容;
(4)数据类型:
主要有三大类数据类型,
- 寄存器类型;
- 线网类型;
- 参数类型;
线性数据:用于连续赋值语句(assign
)描述组合逻辑或者module
间的信号连接线;
1) 寄存器类型
寄存器类型表示一个抽象的数据存储单元,它只能在always
语句和initial
语句中被赋值,并且它的值从一个赋值到另一个赋值过程中被保存下来。
如果语句描述的是时序逻辑,即always
语句带有时钟信号,则该寄存器变量对应为寄存器;如果该过程语句描述的是组合逻辑,即always
语句不带有时钟信号则该寄存器变量对应为硬件连线
寄存器类型的缺省值是x
(未知状态)。
寄存器数据类型有很多种,如reg
、integer
、real
等,其中最常用的就是reg
类型
reg [31:0] delay_cnt; // 延时计数器
reg key_flag; // 按键标志
wire [3:0] din;
reg [3:0] d0; //4bit
reg d1;//1bit
initial begin //过程赋值语句
d1 = 0;#10;
d1 = 1;
end
always(posedge clk)begin //clk端一般就是寄存器
if(d1) d0 <= din;
end
2)线网类型
线网表示Verilog
结构化元件间的物理连线。
值由驱动元件的值决定,例如连续赋值或门的输出。
如果没有驱动元件连接到线网,线网的缺省值为z
(高阻态)。
线网类型,如tri
和wire
等,其中最常用的就是wire
类型,它的使用方法如下:
wire data_en; //数据使能信号
wire [7:0] data;//数据
wire a;//1bit位宽
wire [3:0] b; //4bit
wire [7:0] c;//8bit
assign a = 1'b0;//连续赋值语句
assign c = 8'h5a;//8bit的十六进制数
//tri0、tri1:带下拉、上拉电阻的特性,没有驱动时,会由默认的值为0/1,一般综合代码不用
tri0 s0;
tri1 [20:0] s1;
3)参数类型
参数其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等
可以在编译时修改参数的值,因此又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。
在定义参数时,可以一次定义多个+参数,参数与参数之间需要用逗号隔开。
要注意的是参数的定义是局部的,只在当前模块中有效。
parameter DATA_WIDTH = 8;//数据位宽为8
(5)Verilog
运算符
1)算术运算符
+
-
*
/
%
Verilog
实现乘除比较浪费组合逻辑资源,尤其是除法。一般2的指数次幂的乘除法使用移位运算来完成运算.
非2的指数次幂的乘除法一般是调用现成的IP
,QUARTUS/ISE
等工具软件会有提供,不过这些工具软件提供的IP
也是由最底层的组合逻辑(与或非门等)搭建而成的。
2)关系运算符
>
<
>=
<=
==
!=
用来进行条件判断,在进行关系运算符时,如果声明的关系是假的,则返回值是0,如果声明的关系是真的,则返回值是1;
所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别。
3)逻辑运算符
!
&&
||
连接多个关系表达式,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整意思。
4)条件运算符
?
:
从两个输入中选择一个作为输出的条件选择结构,功能等同于always
中的if-else
语句。
5)位运算符
~
&
|
^
直接对应数字逻辑中的与、或、非门等逻辑门
位运算符一般用在信号赋值上。
6)移位运算符
<<
>>
移位运算符包括左移位运算符和右移位运算符,这两种移位运算符都用0来填补移出的空位。
一般使用左移位运算代替乘法,右移位运算代替除法,但是只能表示2的指数次幂的乘除法。
7)拼接运算符
{a,b}
可以把两个或多个信号的某些位拼接起来进行运算操作
5. 阻塞赋值(Blocking)和非阻塞赋值(Non-Blocking)
(1)阻塞赋值
在一个always
块中,后面的语句会受到前语句的影响,具体来说,在同一个always
中,一条阻塞赋值语句如果没有执行结束,那么该语句后面的语句就不能被执行,即被“阻塞”。
也就是说always块内的语句是一种顺序关系
符号“=”用于阻塞的赋值(如:b=a;),阻塞赋值“=”在begin和end之间的语句是顺序执行,属于串行语句。其后面的赋值语句从概念上来讲是在前面一条语句赋值完成之后才执行的。
(2)非阻塞赋值
符号"<="用于非阻塞赋值(如:b<=a;
),非阻塞赋值是由时钟节拍决定,在时钟上升到来时,执行赋值语句右边,然后将begin-end
之间的所有赋值语句同时赋值到赋值语句的左边。
begin-end
之间的所有语句,一起执行,且是个时钟只执行一次,属于并行执行语句。
非阻塞赋值的操作过程可以看作两个步骤:
- 赋值开始的时候,计算
RHS
(等号右边的表达式); - 赋值结束的时候,更新
LHS
(等号左边的表达式)。
非阻塞的概念是指,在计算非阻塞赋值的RHS
以及LHS
期间,允许其他的非阻塞赋值语句同时计算RHS
和更新LHS
。
6. assign和always的区别
assign
语句使用时不能带时钟;always
语句可以带时钟,也可以不带时钟;- 在
always
不带时钟是,逻辑功能和assign
完全一致,都是只产生组合逻辑。 - 比较简单的组合逻辑推荐使用
assign
语句,比较复杂的组合逻辑推荐使用always
语句。
(1)带时钟和不带时钟的always
always
语句可以带时钟,也可以不带时钟。
- 在
always
不带时钟时,逻辑功能和assign
完全一致,虽然产生的信号定义为reg
类型,但是该语句产生的还是组合逻辑; - 在
always
带时钟信号时,这个逻辑语句才能产生真正的寄存器。
(2)latch
latch
指锁存器,是一种对脉冲点平敏感的存储单元电路
锁存器和寄存器都是基本存储单元,锁存器是电平触发的存储器,寄存器是边沿触发的存储器。
两者的基本功能是一样的,都可以存储数据。
锁存器是组合逻辑产生的,而寄存器是在时序电路中使用,由时钟触发产生的。
latch
的主要危害是产生毛刺(glitch
),这种毛刺对一级电路是很危险的。并且其隐蔽性很强,不易查出。
在设计中,应尽量避免
latch
的使用。
代码里出现latch
的两个原因是在组合逻辑中,if
或者case
语句不完整的描述,比如if
缺少else
分支,case
缺少default
分支,导致代码在综合过程中出现了latch
。解决办法就是if
必须带else
分支,case
必须带default
分支。
只有不带时钟的always
语句if
语句或者case
语句不完整才会产生latch
,带时钟的语句if
或者case
语句不完整描述不会产生latch
。
7. 状态机
Verilog
是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个功能是,代码中通常会使用很多个if
嵌套语句来实现,这样就增加了代码的复杂度,以及降低了代码的可读性,这个时候就可以使用状态机来编写代码。
状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
状态机,全程是有限状态机(Finite State Machine
,缩写为FSM
),是一种在有限状态之间按一定规律转换的时序电路,可以认为是组合逻辑和时序逻辑的一种组合。状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如SDRAM
控制器等。
根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore
)状态机和米勒(Mealy
)型状态机
Mealy
状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态
Moore
状态机:组合逻辑的输出只取决于当前状态
(1)三段式状态机
根据状态机的实际写法,状态机还可以分为一段式、和二段式和三段式状态机。
- 一段式:整个状态机写到一个
always
模块里面,在该模块中即描述状态转移,又描述状态的输入和输出。
不推荐,一般都会要求把组合逻辑和时序逻辑分开,组合逻辑和时序逻辑混合在一起不利于代码维护和修改
- 二段式:用两个
always
模块来描述状态机,其中一个always
模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出,需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑。 - 三段式:在两个
always
模块描述方法基础上,使用三个always
模块,一个always
模块采用同步时序描述状态转移,一个always
采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always
模块描述状态输出(可以用组合电路输出,也可以时序电路输出)。- 三段式状态机的基本格式是:
- 第一个
always
语句实现同步状态跳转; - 第二个
always
语句采用组合逻辑判断状态转移条件; - 第三个
always
语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
- 第一个
- 三段式状态机的基本格式是:
8. 模块化设计
划分模块的基本原则是子模块功能相对独立、模块内部联系尽量紧密、模块间的连接尽量简单。
在进行模块化设计中,对于复杂的数字系统,我们一般采用自顶向下的设计方式。可以把系统划分成几个功能模块,每个功能模块再划分成下一层的子模块;每个模块的设计对应一个module
,一个module
设计成一个Verilog
程序文件。因此,对一个系统的顶层模块,我们采用结构化的设计,即顶层模块分别调用了各个功能模块。
FPGA
逻辑设计中通常是一个大的模块中包含了一个或多个功能子模块,Verilog
通过模块调用或称为模块实例化的方式来实现这些子模块与高层模块的连接,有利于简化每一个模块的代码,易于维护和修改。
如果子模块内部使用parameter
定义了一些参数,Verilog
也支持对参数的例化(也叫参数的传递),即顶层模块可以通过例化参数来修改子模块内定义的参数。
子模块名是指被例化模块的模块名,而例化模块名相当于标识,当例化多个相同模块时,可以通过例化名来识别哪一个例化,一般命名为“u_”+“子模块名”
参数的例化,参数的例化是在模块例化的基础上,增加了对参数的信号定义
// 例子
time_count #(
.MAX_NUM (TIME_SHOW) // 参数例化
)u_time_count(
.clk (sys_clk),
.rst_n (sys_rst_n), // 信号例化
.flag (add_flag)
);
9. Verilog
的编程规范
工程的组织形式一般包括如下几个部分,分别是doc、par、rtl
和sim
四个部分
doc
:一般存放工程相关的文档,包括该项目用到的datasheet(数据手册)、设计方案等。par
:主要存放工程文件和使用到的一些IP
文件rtl
:主要存放工程的rtl
代码,是工程的核心,文件名与module
名称应当一致,建议按照模块的层次分开存放sim
:主要存放工程的仿真代码,复杂的工程里面,仿真也是不可或缺的部分,可以极大减少调试的工作量。
(1)文件头声明
每一个Verilog
文件的开头,都必须有一段声明的文字。包含文件的版权、作者、创建日期,以及内容简介等等
//*************************************Copyright(c)*******************//
// FileName:
// Last modified Date:
// Last Version:
// Descriptions:
//*******************************************************************//
(2)输入输出定义
module led(
input sys_clk, // 系统时钟
input sys_rst_n , // 系统复位
output reg [3:0] led // 4位LED灯
);
- 一行只定义一个信号
- 信号全部对齐
- 同一组的信号放在一起
(3)parameter定义
module
中的parameter
声明,不建议随处乱放,将parameter
定义放在紧跟着module
的输入输出定义之后- parameter等常量命名全部使用大写
(4)wire/reg定义
一个module
中的wire/reg
变量声明需要集中放在一起,不建议随处乱放
- 将
reg
与wire
的定义放在紧跟着parameter
之后 - 建议具有相同功能的信号集中放在一起
- 信号需要对齐,
reg
和位宽需要空2
格,位宽和信号名字至少空四格 - 位宽使用降序描述,
[6:0]
- 时钟使用前缀
clk
,复位使用后缀rst
- 一行只定义一个信号
(5)信号命名
- 内部信号不要使用大写,也不要使用大小写混合,建议全部使用小写
- 模块名字使用小写
- 异步信号,使用
_a
作为信号后缀
(6)always块
- 一个
always
需要配一个begin
和end
always
前面需要有注释- 一个
always
和下一个always
空一行即可,不要空多行 - 时序逻辑使用非阻塞赋值
(7)assign块
assign
的逻辑不能太复杂,否则易读性不好assign
前面需要有注释- 组合逻辑使用阻塞赋值
(8)空格 和 TAB
- 由于不同的解释器对于
TAB
翻译不一致,所以建议不使用TAB
,全部使用空格
二、环境Lattice Dimond环境的搭建
-
从
Lattice
的官网下载Dimond
,下一步下一步安装; -
获取电脑的
MAC
地址,注意是有线网卡的MAC
地址,不能是无线的; -
在官网申请免费的证书,这里有问题,官网不会跳出来,所以是请同事帮忙生成的;
-
打开软件添加证书文件,即可。
-
Lattice官网链接
-
证书申请网址
(有的会跳出来什么联系管理员,我这里是把IP
改为马来西亚打开网址的)
填写物理网卡MAC地址质料:
新建工程项目:
选择芯片,我的板子上的芯片是:
生成项目文件夹:
添加Verilog
文件;
这样就可以编写Verilog
代码了。
三、点灯实例分析
同事给出的一段实例,能够点亮两个灯;
实例环境硬件如下:
- 电脑
- 可调电源
module clk_div
(
in_clk,
clk_8hz,
clk_1hz,
pg_rstn
);
input in_clk;
output clk_8hz;
output clk_1hz;
input pg_rstn;
reg [4:0] count0_r;
reg [8:0] count1_r;
reg [9:0] count2_r;
reg div0_clk;
reg div1_clk;
reg div2_clk;
wire clk_1hz;
wire clk_8hz;
assign clk_1hz = count2_r[9];
assign clk_8hz = count2_r[6];
always @(posedge in_clk or negedge pg_rstn) //25M--1M
begin
if (!pg_rstn)
begin
div0_clk<= 1'b1;
count0_r <= 5'b0;
end
else if (count0_r==5'b11001)
begin
div0_clk<=1'b1;
count0_r<=5'b0;
end
else
begin
count0_r<=count0_r+1'b1;
div0_clk<= 1'b0;
end
end
always @(posedge div0_clk or negedge pg_rstn) //1M--1k
begin
if (!pg_rstn)
begin
count1_r <= 9'b0;
div1_clk <= 1'b0;
end
else if (count1_r==9'h1ff)
begin
div1_clk<=~div1_clk;
count1_r<=9'h0;
end
else
count1_r<=count1_r+1'b1;
end
always @(posedge div1_clk or negedge pg_rstn) //1k--1Hz
begin
if (!pg_rstn)
begin
count2_r <= 10'b0;
end
else if (count2_r==10'h3ff)
begin
count2_r <= 10'b0;
end
else
begin
count2_r <= count2_r+1'b1;
end
end
OSCH #("10.23") osc_int (
.STDBY(1'b0),
.OSC(clk_i),
.SEDSTDBY());
endmodule
1. 实例代码分析
module clk_div
(
in_clk,
clk_8hz,
clk_1hz,
pg_rstn
);
input in_clk;
output clk_8hz;
output clk_1hz;
input pg_rstn;
....
endmodule
这一段是接口定义,定义了两个输出信号clk_8hz
、clk_1hz
,代表两个灯不同的频率。两个输入信号in_clk
、pg_rstn
指芯片的时钟和复位。
模块由关键字module....endmodule
确定。
clk_div
模块名
reg [4:0] count0_r;//5bit
reg [8:0] count1_r;//9bit
reg [9:0] count2_r;//10bit
reg
定义寄存器,寄存器数据类型有很多种,如reg、integer、real
等,其中最常用的就是reg
类型。
reg div0_clk;
reg div1_clk;
reg div2_clk;
默认位宽是1。
wire clk_1hz;
wire clk_8hz;
wire
定义的是线网,线网指的是Verilog
结构化元件间的物理连线
assign clk_1hz = count2_r[9];
assign clk_8hz = count2_r[6];
-
assign
产生wire
信号语句的关键字,连续赋值语句assign -
比较简单的组合逻辑推荐使用
assign
语句,比较复杂的组合逻辑推荐使用always
语句。 -
reg与wire的区别详细
always @(posedge in_clk or negedge pg_rstn) //25M--1M
begin
if (!pg_rstn)
begin
div0_clk<= 1'b1;//一位二进制数1
count0_r <= 5'b0;//五位二进制数0
end
else if (count0_r==5'b11001)//五位二进制数11001
begin
div0_clk<=1'b1;
count0_r<=5'b0;
end
-
时序逻辑 ,带异步复位 ;
-
posedge
是上升沿,电平从低到高跳变 -
negedge
是下降沿,电平从高到低跳变 -
参考文章
文章持续更新,由于刚接触,所以很多东西也分析不错来,之后慢慢补充。
2. 编译运行
- 编译代码
- 生成
JED
文件
- 引脚设置
- 找到项目中的
.jed
文件
- 设备上电,插上烧录器插上电脑。点击烧录
- 等待
- 完成
四、总结
这东西对于像我这样跨专业的初学者来说真难,慢慢学吧,之后会找有关书或者视频,更新Verilog
语言的基础学习笔记。