围绕Verilog基础知识和工作中常用到的关键知识点展开,可是笔者并不打算在这里过分叙述FPGA底层结构和穷举Verilog语法。大家不妨可以回忆下读书时候学习谭浩强版C语言和严蔚敏版数据结构,这两本书是计科、软工、通信、电控等专业的必修课,当时是不是只记得书很厚,然后似乎把C语言每个语法都好像讲了一遍,一到笔试和机试,虽然很辛辛苦苦地刷题突击,但是到头来还是感觉没有学会C语言,很多东西依然感觉是云里雾里,接着上完C语言的课,再看看数据结构又有很多生僻深奥的知识点,配上书中晦涩难懂的伪代码,这完全就是在无情劝退初学者,只不过那时候我们还年轻稚嫩一直在和书较劲,硬啃硬想但是到头来仍然收获甚微。
工作以后周围也有很多从事嵌入式软件开发的朋友,他们对FPGA也非常感兴趣,想找一个好点的教程学习提高,增加自己的社会竞争力,但是很多人却无奈地从入门到放弃,甚至还没有入门就被形形色色的Verilog语法书给劝退了,所以笔者想切实站在初学者角度把很多晦涩难懂的东西讲通俗也讲明白,多讲实用的地方帮助大家入门上手,当然随着阅历和实践上的丰富,周边一些知识点也会自然而然地慢慢清晰起来。
大家可以手上备两本比较经典的Verilog语法书和讲FPGA底层逻辑资源方面的书当做工具书使用,多动手实践在经历过项目研发后,回过头来再去关注FPGA周边的设计思想和相关技术等等,然后去深究时序分析和约束设置以及不同厂家如高云小蜜蜂、Altera飓风、Xilinx 7系列等等FPGA底层硬件架构上的异同,结合具体应用把技术做细做深做精,再去扩展自己的知识面,可能这才是真正的学习和成长之路。
D触发器是我们学习FPGA过程中无法绕过的一个模型,不少FPGA书籍会讲了很多很多基本门电路、触发器等等,甚至连带着卡诺图、真值表一起呈上,这就非常容易让初学者产生畏惧感,其实我们完全可以暂时先跳过这些概念,而去思考一个模型那就是经典的D触发器,那么这个D触发器是做什么的呢,图3-1就是D触发器的示意图。其中,clk代表了时钟,reset是复位,d是输入,q是输出。其功能也非常简单,就是在复位有效时,这个q的值是0,而在复位无效时,在时钟上升沿的时候,把d的值赋值给了q。
图1 D触发器的示意图
我们再去看下面一段Verilog代码,也是工作中经常遇到的,即对应上面这个D触发器的模型,代码的意思就是时钟上升沿posedge clk的时候把d的值赋值给q,然后在复位rst_n信号为1'b0的时候,把q的值拉回默认的0,这也就是一个经典的时序逻辑。
然后我们回过头把一些工作中经常用到的Verilog语法子集总结起来,这里大家不需要去硬记它们,后面实践多了自然而然就记得住了,现在去看一看,能有个大概印象就很好。
一、模块声明的语法:module … endmodule
在FPGA的设计当中,每个Verilog代码模块,都是以.v结尾的文件,类似于C语言当中的.c和.h文件,每个代码模块即每个.v文件,需要用module声明下,以endmodule作为结尾,这是一个固定的用法,举个了流水灯的例子:
二、端口声明的语法:input,output,inout
FPGA的每个module模块必然都会有输入输出的信号用于连接到其他module模块或者是外部器件,其实这些信号也只存在三种可能:输入信号、输出信号、输入输出双向信号,通常input、output这些信号定义既可以写到module的定义内,也可以写到module的定义外,也是流水灯的例子,下面两种写法在Verilog的语法上都是合法的,但是请记住module定义的最后一个信号不能打逗号,否则会因为语法错误编译不通过。
三、常量参数的语法:parameter
FPGA设计当中经常会用parameter关键字去声明一些常量,其实也就类似于C语言当中的#define,这样可以增加代码的可读性,也方便后期对代码的维护,比如时钟周期是50mhz,那么一个时钟即为20ns,我们想去数1ms,即5000个时钟周期,那么可以用关键字parameter写成如下形式。
四、信号类型定义的语法:wire、reg
这里不过多论述wire和reg型的差别,大家只要在写Verilog的时候,记得由assign产生的信号用wire型定义,由always产生的信号用reg型定义,也举个例子, cnt是由always产生的,所以把它定义成reg型,spi_mosi是由assign产生的,所以把它定义成wire型,在定义wire、reg时请注意位宽上一定能要对齐,其实一些稀奇古怪的问题检查到最后很多都是因为位宽定义错误导致的。
五、多语句定义的语法:begin … end
通俗的说begin … end就类似于C语言当中的花括号{ … },begin和end一定要匹配上,就好比写C语言“{”和“ }”一定要对得上,否则在编译的时候会报错。
六、比较判断的语法:if … else,case … default … endcase
判断语句if … else,case … default … endcase,可以类比C语言,一般少些的逻辑判断用if … else即可,case … default … endcase可以简化代码量,同时case判读语句多用于Verilog在状态机的设计上。
七、直接赋值语法:assign … ?
assign用于直接互联不同的信号或者直接去给wire型变量赋值,举个例子,FPGA和MCU之间通过spi总线连接,当MCU为主机,FPGA为从机时,那么检测到片选CS信号上升沿的时候,即spi总线的数据发送完成。
八、always模块语法:always …
always即可用在时序逻辑上也可以用到组合逻辑上,如果always @后有上升沿posedge和下降沿negedge的声明,则为时序逻辑,如果always @后有为*,则为组合逻辑,下面两个例子即为always模块做时序逻辑和组合逻辑。
九、运算操作符
Verilog中绝大多数的运算操作符都是可以被综合的,也是被语法所支持,如下是常用到的运算符,这里按照:1.算术运算符;2.关系运算符;3.逻辑运算符;4.条件运算符;5.位运算符;6.移位运算符;7.拼接运算符来逐一介绍给大家。
1. 算术运算符
算术运算符通常数学运算上的加减乘除,数字时序逻辑当中有时也需要做数字运算,因此需要算术运算符,常用的算术运算符除了加减乘除外还有模除,就是通常说的取余运算。但是也请大家注意,用Verilog通过直接写“*” 和“/”去实现乘除法运算是很耗费底层逻辑资源的,虽然这样写Vivado环境也会通过综合布局布线转换成最底层的组合逻辑。如果是乘除以2为指数次幂,一般是通过移位运算去实现,简单高效,如果乘除的是乘除非以2为指数次幂,一般是去调用IP实现。
符号 | 举例 | 说明 |
+ | a+b | a加上b |
- | a-b | a减去b |
* | a*b | a乘以b |
/ | a/b | a除以b |
% | a%b | a取余b |
表1 算术运算符
2. 关系运算符
关系运算符通常应用在做一些条件判断上面,如果这个声明的关系是真的即为1,如果这个声明的关系是假的即为0。
符号 | 举例 | 说明 |
> | a>b | a大于b |
< | a<b | a小于b |
、>= | a>=b | a大于等于b |
<= | a<=b | a小于等于b |
== | a==b | a等于b |
!= | a!=b | a不等于b |
表2 关系运算符
3. 逻辑运算符
逻辑运算符通常是用来连接多个关系表达式的时候使用,可以实现更加复杂的逻辑判断,一般不单独使用,配合具体的语句来实现完整的意思。
符号 | 举例 | 说明 |
! | !a | 非a,如果a是0,则非a是1;如果a是1,则非a是0 |
&& | a&&b | a与上b,如果a和b都是1,a&&b结果为真,即为1 |
|| | a||b | a或上b,如果a或b有一个是1,a||b结果为真,即为1 |
表3 逻辑运算符
4. 条件运算符
条件运算符一般用在assign条件判断作为赋值输出的时候,功能和always @组合逻辑一模一样。
符号 | 举例 | 说明 |
? : | a ? b : c | 如果a为真,就选择b,否则就选择c |
表4 条件运算符
5. 位运算符
位运算符是一类最基本的运算符,可以把它们当作是数字逻辑当中的与、或、非门等基本逻辑门运算,其中位运算符的“&”、“|”容易和逻辑运算符的“&&”、“||”搞混淆,大家只要记得逻辑运算符通常用在条件判断上面,而位运算符通常用在信号赋值上面。
符号 | 举例 | 说明 |
~ | ~a | 将a逐位进行取反 |
& | a&b | 将a逐位与b相同的位相与 |
| | a|b | 将a逐位与b相同的位相或 |
^ | a^b | 将a逐位与b相同的位异或 |
表5 位运算符
6. 移位运算符
移位运算符包括了向左移位和向右移位运算符,而这两类移位运算符都用0来填补移出的空位。
符号 | 举例 | 说明 |
<< | a<<b | 将a左移b位 |
>> | a>>b | 将a右移b位 |
表6 移位运算符
7. 拼接运算符
Verilog中的拼接运算符是C语言当中没有的,这个运算符可以灵活的把两个或者多个信号的某些位进行拼接组合起来。
符号 | 举例 | 说明 |
{} | {a,b} | 将a和b拼接起来,组成一个新的信号 |
表7 拼接运算符
十、赋值符号
赋值符号即:=和<=,分别为阻塞赋值和非阻塞赋值,一般而言,组合逻辑当中多用“=” 赋值,而时序逻辑当中多用“<=”赋值,当然阻塞和非阻塞赋值,在后面笔者会详细介绍给大家。