【第一季】CH04_FPGA设计Verilog基础(一)
4.1 Verilog HDL 代码规范
u 项目构架设计
项目的构架用于团队的沟通,以及项目设计的全局把控
u 接口时序设计规范
模块和模块之间的通过模块的接口实现关联,因此规范的时序设计,对于程序设计的过程,以及程序的维护,团队之间的沟通都是非常必要的。
u 命名规则
1、顶层文件
对象+功能+top
比如:video_oneline_top
2、逻辑控制文件
介于顶层和驱动层文件之间
对象+ctr
比如:ddr_ctr.v
3、驱动程序命名
对象+功能+dri
比如:lcd_dri.v、uart_rxd_dri.v
4、参数文件命名
对象+para
比如:lcd_para.v
5、模块接口命名:文件名+u
比如lcd_dir lcd_dir_u(........)
6、模块接口命名:特征名+文件名+u
比如 mcb_read c3_mcb_read_u
7、程序注释说明
//
// Company: milinker corperation
// Engineer:jinry tang
// WEB:www.milinker.com
// BBS:www.osrc.cn
// Create Date: 07:28:50 07/31/2015
// Design Name: FPGA STREAM
// Module Name: FPGA_USB
// Project Name: FPGA STREAM
// Target Devices: XC6SLX16-FTG256/XC6SLX25-FTG256 Mis603
// Tool versions: ISE14.7
// Description: CY7C68013A SLAVE FIFO comunication with fpga
// Revision: V1.0
// Additional Comments:
//1) _i input
//2) _o output
//3) _n activ low
//4) _dg debug signal
//5) _r delay or register
//6) _s state mechine
//
8、端口注释
input Video_vs_i,//输入场同步入
9、信号命名
命名总体规则:对象+功能(+极性)+特性
10、时钟信号
对象+功能+特性
比如:phy_txclk_i、sys_50mhz_i
11、复位信号
对象+功能+极性+特性
比如:phy_rst_n_i、sys_rst_n_i
12、延迟信号
对象+功能+特性1+特征2
比如:frame_sync_i_r0、frame_sync_i_r1
13、特定功能计数器
对象+cnt
比如:line_cnt、div_cnt0、div_cnt1
功能+cnt
比如:wr_cnt、rd_cnt
对象+功能+cnt
比如:fifo_wr_cnt、mcb_wr_cnt、mem_wr_cnt
对象+对象+cnt
比如:video_line_cnt、video_frame_cnt
14、一般计数器
cnt+序号
用于不容易混淆的计数
比如:cnt0、cnt1、cnt2
15、 时序同步信号
对象+功能+特性
比如:line_sync_i、frame_sysc_i
16、 使能信号
功能+en
比如:wr_en、rd_en
对象+功能+en
比如:fifo_wr_en、mcb_wr_en
4.2技术背景
大规模集成电路设计制造技术和数字信号处理技术,近三十年来,各自得到了迅速的发展。这两个表面上看来没有什么关系的技术领域实质上是紧密相关的。因为数字信号处理系统往往要进行一些复杂的数学运算和数据的处理,并且又有实时响应的要求,它们通常是由
高速专用数字逻辑系统或专用数字信号处理器所构成,电路是相当复杂的。因此只有在高速大规模集成电路设计制造技术进步的基础上,才有可能实现真正有意义的实时数字信号处理系统。对实时数字信号处理系统的要求不断提高,也推动了高速大规模集成电路设计制造技
术的进步。现代专用集成电路的设计是借助于电子电路设计自动化(EDA)工具完成的。学习和掌握硬件描述语言(HDL)是使用电子电路设计自动化(EDA)工具的基础。
笔者建议Verilog,虽然很多学校古董级的老师还在教VHDL.当然VHDL也是要了解的,因为这门古老的语言的历史遗留问题,现在还有很多VHDL的模块,有的时候我们要拿来主义,所以还有必要了解下的。但是历史的车轮总是在前进,优胜劣汰。也许不久的将来Verilog也会被C,C++这种高级语言代替。
为了更方面地切入主题,笔者假设,你已经学过单片机,并且掌握C语言。因为单片机,和C语言,可以说是当代大学生的一项基本能力。有了这个基础,再学习其他现代计算机编程,算法,才能达到事半功倍的效果。如果你还不会单片机和C语言,建议你首先学会单片机,或者C语言。当然,这只是笔者的建议,不会单片机,或者C语言,并不代表学不好Verilog语言。
学过单片机的都知道,我们的程序代码是一条指令一条指令来执行的。CPU首先通过总线,读取一条指令,然后解析这条指令,再然后执行这条指令。我们写的C代码总是一条一条地执行。如果我们同时要处理10个子程序,那么CPU必须一个个子程序来执行。如果有些实时性较高的,如扫描下矩阵键盘,VGA刷个屏,都需要中断来实现。如果刷屏时间比较长,就会影响到你按键的灵敏度。另外比如,我们的单片机在用串口接收数据,并且也要发送数据,同时我们的单片机要处理外部的IO信号,如果我们的IO信号非常快,并且有几百个信号,可能同一个时刻触发,很显然,如果这些信号比较快,那么我们的单片机,就没法实现了。
这是笔者简单举了两种情况,那么如果使用FPGA就可以很方便地解决以上问题。由于FPGA的并行性,不管是扫描键盘,还是扫描VGA,都可以把它们做成独立的模块,时间上没有冲突,每个模块可以同时执行。
再比如用一个FPGA,就可以同时完成串口的收发,以及IO的监控,因为FPGA的程序实际上就是电路,是瞬间就完成了,我们只要用Verilog写出来相应功能的程序模块,这些模块是同时运行的。
这样看来FPGA真是太强大了,太完美了。但不要高兴地太早,由于FPGA可以在一个时钟内,完成多条语句的赋值,但是如果赋值必须有个前后顺序呢?也就是需要一步步的完成,怎么办?如果说并行控制是FPGA的优点,那么顺序控制就是他的不足之处。世界上永远没有完美的东西,我们在获得一种优势的时候,往往也获得了一种劣势。但是,办法总比问题多。
n 顺序控制的第一种办法——状态机设计
可以说,我们用Verilog来写程序,状态机无处不在。顾名思义,通过设计状态机,我们可可以控制Verilog让他该快的时候快,该慢的时候慢,该做什么的时候就做什么。这才是我们想要的。状态机是很不错的东西,初学者对他望而生畏,而熟悉Verilog语言的人都对其会爱不释手。
n 顺序控制的第二种方法——FPGA中运行CPU
FPGA也可以运行CPU?是的,没错,FPGA也可以像单片机一样使用,这样我们就可以用C代码来一条条指令来执行了,这不是太强大了?是的,没错。关键的问题是,我们是可以把一些逻辑控制顺序复杂的事情用C代码来实现,而实时处理的部分仍然用Verilog来实现。并且那部分Verilog可以被C代码控制。Xilinx FPGA目前支持的CPU有Microblaze,ARM9,POWERPC,CortexA9(zynq就Xilinx比较新的一款片子,完美的将CortexA9和FPGA整合到一起,有兴趣的可以淘宝搜索MiZ702)其中Microblaze是一款软CPU,是软核。ARM9,CortexA9和POWERPC是硬核。这里有两个概念:
1)软核就是用代码就是能现的CPU核,这种核配置灵活,成本较低。但是要占用FPGA宝贵的资源。
2)硬核就是一块电路,做到FPGA内部,方便使用,性能更高。比如Xilinx的DDR内存控制器,就是一种硬核,其运行速度非常高,我们只要做些配置,就可以方便使用。
两种核可谓各有所长。
n FPGA还是ASIC
根据具体看情况而定,从我们上面的一些介绍,笔者相信你已经有一定的判断能力了。笔者的建议是,低速场合,实时性要求的低的地方用ASIC,有些功能用ASIC方便的用ASIC,成本低的用ASIC。排除那些可以不用FPGA地方,那么剩余的就要考虑是不是用FPGA来实现更加方便。一般来说,FPGA程序开发相对来说要难度大一些,并且成本要高一些。
讲了这么多的背景知识,我们来看一小段代码:
u32sum(a,b) { a=a+1; b=b+1; c=a+b; return c; } always@(posedge clk) begin a = a + 1; b = b + 1; c = a + b; end |
同样是实现了求和,但是,C代码需要N多个(很多)CPU周期才得出结果,而用Verilog一个clk周期就计算出来了。
或许现在你还不知道为什么。没关系,下面的内存笔者讲解Verilog语言基础。
4.3 Verilog最最基础语法
Verilog和C在外形上有很大相识的地方,有了C基础背景,Verilog看起来就并不陌生。
C语言和Verilog的关键词和结构对比:
C语言和Verilog运算符对比:
真是太振奋人心了,一切都是这么熟悉。学好FPGA已经没有心理障碍了。
4.4关键字
信号部分:
input关键词,模块的输入信号,比如input Clk,Clk是外面关键输入的时钟信号;
output关键词,模块的输出信号,比如output[3:0]Led; 这个地方正好是一组输出信号。其中[3:0]表示0~3共4路信号。
inout模块输入输出双向信号。这种类型,我们的例子24LC02中有使用。数总线的通信中,这种信号被广泛应用;
wire关键词,线信号。例如:wire C1_Clk; 其中C1_Clk就是wire类型的信号;
线信号,三态类型,我们一般常用的线信号类型有input,output,inout,wire;
reg关键词,寄存器。和线信号不同,它可以在always中被赋值,经常用于时序逻辑中。比如reg[3:0]Led;表示了一组寄存器。
结构部分:
module() … endmodule |
代表一个模块,我们的代码写在这个两个关键字中间
always@()括号里面是敏感信号。这里的always@(posedge Clk)敏感信号是posedge Clk含义是在上升沿的时候有效,敏感信号还可以negedge Clk含义是下降沿的时候有效,这种形式一般时序逻辑都会用到。还可以是*这个一符号,如果是一个*则表示一直是敏感的,一般用于组合逻辑。
assign用来给output,inout以及wire这些类型进行连线。assign相当于一条连线,将表达式右边的电路直接通过wire(线)连接到左边,左边信号必须是wire型(output和inout属于wire型)。当右边变化了左边立马变化,方便用来描述简单的组合逻辑。示例:
wire a, b, y; assign y = a & b;、 |
这些语句含义上都和高级语言一样:
if(...)begin ............ End if(...)begin ............ end else begin ............ End if(...)begin ............ end else if(...)begin ............ end case(...) ............ endcase begin..... end作用域范围,类似于C的大括号。用法举例: always@(posedge clk)begin ............ end |
符号部分:(我们这里FALSH为0,TRUE为1)
“;”分号用于每一句代码的结束,以表示结束,和C语言一样。
“:”冒号,用在数组,和条件运算符以及case语句结构中。case结构会在后面讲解。
“<=”赋值符号,非阻塞赋值,在一个always模块中,所有语句一起更新。它也可以表示小于等于,具体是什么含义编译环境根据当前编程环境判断,如果“<=”是用在一个if判断里如:if(a <= 10);当然就表示小于等于了。
“=”阻塞赋值,或者给信号赋值,如果在always模块中,这条语句被立刻执行。阻塞赋值和非阻塞赋值将再后面详细举例说明。
“+,-,*,/,% ”是加、减、乘、除运算符号,这些使用和C语言基本是一样的,当你用到这些符号时,编译后会自动生成或者消耗FPGA原有的加法器或是乘法器等。其中符号/,%会消耗大量的逻辑,谨慎使用。
“<”小于,比如A
“<=”小于等于,比如A<=B含义就是A和B比较,如果A小于等于B就是TURE,否则为FALSE。
“>”大于,比如A>B含义就是A和B比较,如果A大于B就是TURE,否则为FALSE。
“>=”大于等于,比如A>=B含义就是A和B比较,如果大于等于B就是TURE,否则为FALSE。
“==”等于等于,比如A==B含义就是A和B比较,如果A等于B就是TURE,否则为FALSE。
“!=”不等于,A!=B含义是A和B比较,如果A不等于B就是TURE,否则为FALSE.
“>>”右移运算符,比如A>>2表示把A右移2位。
“<<”左移运算符,比如A<<2表示把A左移2位。
“~”按位取反运算符,比如A=8’b1111_0000;则~A的值为8’b0000_1111;
“&”按位于与,比如A=8’b1111_0000;B=8’b1010_1111;则A&B结果为8’b1010_0000;
“^”异或运算符,比如A=8’b1111_0000;B=8’b1010_1111;则A^B结果为8’b0101_1111;
“&&”逻辑与,比如A==1,B==2;则A&&B结果为TRUE;如果A==1,B==0,则A&&B结果为FALSE,一般用于条件判断。
A = B ? C : D是一个条件运算符,含义是如果B为TRUE则把C连线A,否则把D连线A。B通常是个条件判断,用小括弧括起:
assign C1_Clk = (C1==25'd24999999) ? 1 : 0 ;
C1_Clk,是一个wire类型的信号,当C1==25'd24999999时候,连线到1,否则连线到0.
“{}”在Verilog中表示拼接符,{a,b}这个的含义是将括号内的数按位并在一起,比如:{1001,1110}表示的是10011110。拼接是Verilog相对于其他语言的一大优势,在以后的编程中请慢慢体会。
参数部分:
parameter定义一个符号a为常数(十进制180找个常量的定义等效方式):
parameter a = 180;//十进制,默认分配长度32bit(编译器默认) parameter a = 8’d180;//十进制 parameter a = 8’hB4; //十六进制 parameter a = 8’b1011_0100; //二进制 |
预处理命令
//-------------------------------- `include file1.v //-------------------------------- `define X = 1; //-------------------------------- `deine Y; `ifdef Y Z=1; `else Z=0; `endif //-------------------------------- |
有的时候我们一些公共的宏参数,我们可以放在一个文件中,比如这个文件名字为xx.v那么我们可以`include xx.v就可以包含找个文件中定义的一些宏参数。我还是来详细说明下吧!
话说Verilog 的`include和C语言的include用法是一样一样的,要说区别可能就在于那个点吧。
include一般就是包含一个文件,对于Verilog这个文件里的内容无非是一些参数定义,所以这里再提几个关键字:`ifdef `define `endif(他们都带个点,呵呵)。
他们联合起来使用,确实能让你的程序多样化,就拿VGA程序说事吧。
首先,你可以新建一个.v文件(可以直接新建一个TXT,让后将后缀换成.v)其实这个后缀没所谓,.v也是可以的,我觉得,写成.v更能体现出这个文件的意义。
假设有个lcd_para.v文件,内容如下:
// 640 * 480 //--------------------------------- `define H_Start (`H_SYNC + `H_BACK) `define V_Start (`V_SYNC + `V_BACK) |
这里为VGA定义了两种分辨率,通过`define VGA_800_600_60MHz或 VGA_640_480_60FPS_25MHz 或`define VGA_800_600_72FPS_50MHz来决定使用哪种分辨率。
比如,我的xxx.v文件想调用lcd_para.h,那么xxx.v我可以写到:
`define VGA_800_600_60MHz //这句要放在"lcd_para.h"的上面,不然编译不通过 `include "lcd_para.h" |
那么xxx.v文件中就可以用lcd_para.v中的参数了,且对应是VGA_800_600_60MHz下的参数。
其次`include "lcd_para.v" 这个路径也有一点讲究,xxx.v作为引用lcd_para.v的文件它和lcd_para.v在同一文件夹下才能怎么写,就是相对路径一说了。也就是以xxx.v为当前路径去引索lcd_para.v文件的位子。所以如果他们不再一个文件夹那么请写出更详细(正确)的路径。顺便说一句,lcd_para.v添不添加到工程是无所谓的,只要路径
对了即可,当然我还是建议添加到工程,且和.v文件放在同一文件夹下,以方便观察和管理。
4.5 Verilog中数值表示的方式
如果我们要表示一个十进制是180的数值,在Verilog中的表示方法如下:
二进制:8’b1010_1010; //其中“_”是为了容易观察位数,可有可无。
十进制:8’d170;
16进制:8’hAA;
4.6阻塞赋值和非阻塞赋值详解
说到阻塞赋值和非阻塞赋值,是很多初学者很迷惑的地方。原因是C语言没有可以类比的东西。
学习FPGA和单片机最大的区别在于,学FPGA时,你必须时刻都有着时钟的概念。不像单片机时钟相关性比较差,FPGA你必须却把握每一个时钟。
首先来说说非阻塞赋值,这个在时序逻辑中随处可见:
reg A; reg B; always @(posedge clk) begin A <= 1'b1; B <= 1'b1; /***或者** B <= 1'b1; A <= 1'b1; *********/ end |
这段程序里,A和B是同时被赋值的,具体是说在时钟的上升沿来的时刻,A和B同时被置1。调换A和B的上下顺序,将得到相同的结果。
接着看另外一段程序:
reg A; reg B; always @(posedge clk) begin A <= 1'b1; end always @(posedge clk) begin B <= 1'b1; end |
这段程序,与第一段程序也是完全等价的,A和B在同一时刻被赋值。两段程序综合出的逻辑也是完全相同的。这就是非阻塞赋值的特点,体现了FPGA的并行性!
接着来看阻塞赋值,它少了一个非,表示会阻塞住,那么体会下这个阻塞:
always @(posedge clk) begin A = 1'b1; B = 1'b1; end |
看到,上面这个程序是阻塞和非阻塞的混合使用,一般教材是极力反对这种写法的。其实只要你理解了,有的时候这种用法还能帮上大忙。只不过,不理解的话乱用会导致时序违规。
回到正题,我们这么写是为了更好的理解阻塞赋值:当时钟上升沿来临的时刻,首先A会被置1,然后B寄存器再置1。区别就是A和B不再同时置1。A要比B提前零点几纳秒。这样就出现了先后顺序。这个过程还是在一个时钟内完成的,但是数据到达B寄存器相比上面两段程序晚了那么零点几纳秒!
当我们的时钟跑的比较慢的时候,比如50M,一个周期有20ns,那么这么短暂的延时基本可以忽略不计,但是随着设计的复杂,以及时钟速度的提高,这样的语句就要小心。
假设,我们要计算AB求和再除以2的结果。先用非阻塞方法去实现,由于AB求和再除以2是两个步骤,而非阻塞所以的事情都在一个时钟完成,所以这里我们用状态机,将两个步骤分配到两个时钟里去完成:
module unblock ( input clk_i, input rst_n_i, output reg [4:0]result_o ); reg [3:0]A; reg [3:0]B; reg [4:0]C; reg i; always @(posedge clk_i ) if(!rst_n_i) begin #2 A <= 4'd4; B <= 4'd12; C <= 5'd0; result_o = 5'd0; end else begin #2 C <= A + B; result_o <= (C >> 1); end endmodule |
第一个时钟上升沿来临时,完成C <= A + B;
第二个时钟来临时完成result <= (C >> 1);
求出结果,这个过程耗费两个时钟。(不考虑复位消耗的时钟)
再来,用添加阻塞的方式实现:
module block( input clk_i, input rst_n_i, output reg [4:0]result_o ); reg [3:0]A; reg [3:0]B; reg [4:0]C; always @(posedge clk_i) if(!rst_n_i) begin #2 A = 4'd4; #0.2 B = 4'd12; #0.2 C = 5'd0; #0.2 result_o = 5'd0; end else begin #2 C = A + B; #0.2 result_o = (C >> 1); end endmodule |
仿真结果:
先通过阻塞的方法提前得到C的值,再将C右移1位,达到除以2的效果。整个过程耗时一个时钟。
以上的程序并没有什么实际的参考价值,但是解释清楚阻塞和非阻塞赋值,它已经做到了~~
讲到这里,笔者以最快的速度,最简单的方式,让读者学习了Verilog语言的语法部分。具备这些基础知识,下面笔者将带你通过代码来学习Veriog语言。最后,笔者提一点建议,学习Verilog多看别人写的优秀的代码,多看官方提供的代码和文档。其中官方提供的代码,很多时候代表了最新的用法,或者推荐的用法。读者学习,首先把最最基础的掌握好,这样,在项目中遇到了问题,也能快速学习,快速解决。
对于理论知识的学习,没必要一开始就研究得那么深刻,只是搞理论学习,对于学习Verilog语言,或者FPGA开发是不实际的,要联系理论和实践结合。多仿真,多验证,多问题,多学习,多改进。