书名:FPGA设计简明教程
ISBN:978-7-115-67100-4
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
编 著 赵延宾
责任编辑 李永涛
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
现场可编程门阵列(Field Programmable Gate Array,FPGA)是一种以数字电路为核心的集成芯片,它属于可编程逻辑器件(Programmable Logic Device,PLD)的范畴。FPGA凭借卓越的灵活性、可重构性以及在众多应用领域的广泛应用,在现代电子系统中日益重要。
本书共8章,内容包括Verilog HDL基础语法、FPGA在驱动LED显示效果中的应用、PWM信号发生器的设计、蜂鸣器的驱动技术、七段数码管的显示技术、温度传感器数据的读取、串口调试系统的构建、LCD/OLED显示模组的驱动以及电压计的实现等实用技能。书中特别强调模块化设计方法和功能仿真在FPGA设计过程中的关键作用,并以小脚丫MAX10核心板为例,对所有程序进行验证。
本书内容充实且实用价值高,包含多种案例分析,既适合作为高等院校FPGA设计课程的教材,也适用于与集成电路和FPGA相关的培训课程。对FPGA领域的专业人士来说,本书也极具参考价值。
几年前,因业务缘由结识了赵延宾老师。数次接触,皆相谈甚欢,颇有相见恨晚之感。赵延宾老师是在FPGA领域深耕多年的专家,曾在FPGA原厂、供应商以及设计公司任职,成绩斐然,对FPGA行业有着深刻的认识。
FPGA在半导体领域一直独具特色。虽产业规模并不庞大,但在各个行业中不可或缺。FPGA的诞生旨在提高数字电路设计的效率,在整个半导体行业中属于较为高端的环节。在现代电子设计,尤其是硬件设计中,FPGA更是不可或缺的工具。如今,随着数字电路设计难度逐渐降低,一些厂家和第三方平台提供了丰富的系统级方案,而FPGA的逻辑设计能力正是体现设计水平之处。掌握设计FPGA的能力,能为自身产品设计带来更多可能。
在初学者眼中,FPGA设计难度较大、门槛较高。得知赵延宾老师计划编写一本面向入门读者的FPGA图书,我深感欣喜,同时由衷钦佩。为推广FPGA的应用,我们也进行了诸多尝试,“小脚丫”便是成果之一。小脚丫FPGA的设计初衷是降低FPGA的学习门槛,秉持极简设计理念,让初学者专注于逻辑设计方面,以最简单的方式踏入FPGA的世界。为此,我们在小脚丫官网开源了多种FPGA学习板的硬件设计,并针对这些学习板提供了众多简单教程,如LED流程灯、呼吸灯、数码管显示驱动、交通灯演示等。每个教程皆介绍了学习板对应的硬件资源情况,并提供了FPGA源代码,用户可直接用这些代码进行编译,并在对应的学习板上使用。此外,我们还提供了在线编译工具,让用户在浏览器端即可进行FPGA设计,极大地简化了FPGA设计流程。
本书基于小脚丫的MAX10核心板,是对小脚丫官网教程的有效补充与扩展。为便于读者更好地理解,赵延宾老师将全部代码重新编写,并把全部工程共享至小脚丫官网。对于有兴趣或有志进入FPGA领域的初学者,此书是极佳选择。通过阅读本书,读者会发现FPGA并非高不可攀的技术。归根结底,它只是一个工具。而对于硬件工程师,FPGA犹如一个宝藏,能够神奇地设计出你想构建的硬件。FPGA涉及诸多方面的知识,可应用于几乎所有行业,读者跨过门槛之后,将进入一个更为丰富多彩的世界。
把一个技术问题用简单的语言讲述出来并非易事,创作一本书更需耗费诸多心血。相信赵延宾老师基于多年产业经验编写的这本书能为读者带来丰富的收获。
吴志军
苏州思得普信息科技有限公司
由于工作的需要,笔者自己也偶尔会用MCU(Microcontroller Unit,微控制单元)进行项目开发。即使在FPGA方面已有很多年的应用和设计经验,但在面对MCU开发时,笔者依然有种无从下手的感觉。有些项目是硬件设计人员已经设计好了硬件,只需要进行软件代码的设计开发;而有些项目则需要从芯片选型开始,此时,第一个问题就是,在众多的MCU芯片中,应该选择哪一个器件呢?
MCU的发展,可以追溯到英特尔推出的4004。经过几十年的发展,MCU架构经历了多次飞跃,除了传统的8051、MIPS架构,ARM、RISV-V以及一些厂家自研的架构也得到了广泛应用。在MCU的发展过程中,参与这一行业的企业数不胜数,现在可以使用的MCU器件用浩如烟海来形容也不为过。选择一个适合自己应用的MCU,并不是一件简单的事情。
熟悉FPGA的人都知道,不同厂家的FPGA必须在各自指定的集成开发环境(Integrated Development Environment,IDE)中进行设计开发。MCU的开发情况类似,后来才出现了Keil这样的第三方集成开发环境。复杂的开发环境让初学者对MCU开发过程难以理解,不像在课堂上学习C语言那样,通过计算机的编译环境就能看到“Hello World”的直观输出结果。不管是MCU厂家提供的专用集成开发环境,还是像Keil这样的第三方集成开发环境,对初学者来说都是一个不小的挑战。
近年来,MCU的发展似乎比FPGA更快。许多人都有一个共识,就是FPGA虽功能强大,但价格昂贵;MCU亲民,价格便宜。FPGA的高成本是FPGA的器件架构所决定的。FPGA最初是为了满足不同场景的应用需要,把各种功能以查找表为基本功能单元放在芯片内部,在不同的应用中改变各个查找表的功能以及全部查找表之间的电气连接。这种灵活性必然会带来底层硬件单元的冗余,从而造成成本的提高。某一个行业使用FPGA的器件数量达到一定程度后,将被ASIC(Application Specific Integrated Circuit,专用集成电路)所取代,其本质就是去掉器件中被浪费掉的底层冗余硬件。目前,构建ASIC最快的方式是采用SoC(System on Chip,单片系统)架构,用一个MCU作为主控器件,控制一系列的硬件功能单元。我国台湾省的电子产业发展表明这种方式十分成功。现在台式计算机、笔记本电脑中几乎都少不了这样一类器件:SuperIO。其本质就是用一个MCU控制CPU(Central Processing Unit,中央处理器)的一些外设,例如串口、并口、键盘、鼠标、风扇等,还可以检测芯片、主板的电压、工作温度等。还有其他很多行业使用与SuperIO类似的方案,进一步推动了MCU的快速发展。
在集成硬核方面,FPGA与MCU都向着集成越来越多硬核的方向发展。FPGA内集成的几乎都是针对接口需要很高速度的硬核,比如几乎成为中高端FPGA标配的SERDES以及GE、PCIe、HDMI等硬核。而在MCU中,常见的是I2C、SPI、串口等这些低速应用的硬核。尽管这些硬核看似简单,但它们都是各种应用中更常用的功能模块。对于这些硬核,MCU厂家通常做好了相应的底层驱动设计。使用这些硬核,就像调用系统函数一样简单。
那么,能不能借鉴MCU的发展方式,把FPGA能实现的一些基本功能固化,从而让FPGA的入门变得更加容易?当笔者与小脚丫的团队进行交流时,发现他们也有类似的想法,并且已经进行了实践,比如他们的官方网站提供了很多开源的FPGA项目。一些项目不仅将设计源代码进行开源,还把对应的硬件平台进行了开源。
因此,我们希望结合小脚丫的FPGA核心板,向初学者提供一些FPGA设计中常用的功能模块设计。通过对这些简单功能的设计进行介绍,让初学者得到“FPGA到底能帮我做什么”这个问题的答案:用FPGA能够驱动LED实现各种显示效果,能够驱动喇叭播放一段乐曲,能从一个温度传感器读取温度值并把相关信息显示到LCD显示模组上等。这些底层模块与具体的FPGA器件无关,可以方便地移植到各种平台,便于在后续实际的项目开发中直接使用。
随着技术的发展和集成度的提高,FPGA在通信系统、视频图像处理、高速接口信号处理、人工智能等领域都得到了很广泛的应用。越来越多的人进入FPGA开发、应用领域,现在已经有很多对FPGA进行系统介绍的图书,不仅对FPGA的发展历史进行介绍,还对FPGA中的一些关键技术进行深入说明,并对特定FPGA器件的结构、开发环境、开发流程、应用场景等进行探讨。然而,对初学者来说,其中很多不一定适用。
本书旨在让FPGA初学者用尽量少的时间对FPGA的应用场景产生直观的理解,并用尽量简洁的语言说明如何使用FPGA满足设计需求。希望读者在读完本书后的想法是“原来FPGA这么简单”。
本书以小脚丫的MAX10核心板为基础,向FPGA初学者介绍FPGA设计的基本概念。本书共8章,各章内容简要介绍如下。
● 第1章介绍Verilog HDL的基础语法,并对一些常用的功能模块进行建模,这些模块可以用在实际的项目开发中。
● 第2章介绍用MAX10核心板上的LED实现各种显示效果,包括点亮LED,让LED闪烁,实现流水灯、呼吸灯效果等。LED灯的驱动可以归为PWM信号的产生,因此本章提供一个简单的任意占空比PWM信号发生器的设计。
● 第3章介绍如何用MAX10核心板驱动底板上的蜂鸣器,并通过解决蜂鸣器循环播放过程中的故障来说明FPGA设计过程中功能仿真的重要性。蜂鸣器的驱动也可归为PWM信号的产生,本章将用状态机的方式设计实现一个应用更广泛的PWM信号发生器,并说明模块规格的定义对模块设计的重要性。
● 第4章介绍如何让MAX10核心板上的2位七段数码管显示指定内容,并介绍字库、BCD码的基本概念,以及如何通过左移加3法将二进制数转换为8421BCD码。
● 第5章介绍如何利用FPGA从温度传感器DS18B20读取温度值,并用七段数码管显示,让FPGA初学者掌握层次化的设计思想,并了解把芯片手册的内容转化为模块设计规格的基本技巧。
● 第6章介绍串口的概念与UART,以及如何利用MAX10核心板实现与PC(Personal Computer,个人计算机)端串口调试软件的数据传输,让读者更加了解如何在实际项目中进行层次化设计和模块化设计。
● 第7章介绍如何使用MAX10核心板点亮一个OLED显示模组以及一个SPI发送模块的设计。
● 第8章简要介绍FPGA与ADC、DAC相关的应用,用FPGA从ADS7868读取电位计的电压,并用七段数码管、OLED屏显示,实现一个简单的电压计的应用。此外,本章还介绍如何设计SPI的接收模块,与第7章共同完成一个SPI收发模块的设计。
当然,FPGA的应用远不止这些。比如可以用MAX10核心板实现一个任意波形发生器,并用OLED显示模组显示获得的波形,从而实现一个简易示波器的设计。在FPGA的应用中经常会遇到一些复杂的问题,比如需要考虑严格的时序约束、需要进行面积优化(资源利用率优化)、需要考虑异步系统间的同步等,由于本书是针对初学者的,因此并没有对这些问题进行介绍。FPGA初学者可以把本书当作入门FPGA的教程,如果把书中的各个案例在对应的FPGA开发板上都操作一遍,一定能够掌握FPGA入门所必需的知识与设计技巧。已经有一定FPGA基础的读者可以把本书当作FPGA项目的参考资料,本书使用的层次化、模块化的设计方法,也是笔者多年在FPGA应用和项目开发中坚持使用的。
希望本书能对希望学习FPGA开发的人有所帮助。由于笔者水平有限,书中难免出现疏漏,希望读者批评斧正。
赵延宾
2025年1月
本书提供如下资源。
● 本书思维导图。
● 本书实例的素材文件、结果文件。
● 异步社区7天VIP会员。
要获得以上资源,您可以扫描下方二维码,根据指引领取。
作者和编辑尽最大努力来确保书中内容的准确性,但难免存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区(https://www.epubit.com),按书名搜索,进入本书页面,单击“发表勘误”,输入勘误信息,单击“提交勘误”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是liyongtao@ptpress.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发送给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”(www.epubit.com)是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作译者与读者的在线交流互动,以及传统出版与数字出版的融合发展。
“异步图书”是异步社区策划出版的精品IT图书的品牌,依托于人民邮电出版社在计算机图书领域40多年的发展与积淀。异步图书面向IT行业以及各行业使用IT的用户。
1995年,IEEE(Institute of Electrical and Electronics Engineers,电气电子工程师学会)正式颁布IEEE 1364-1995标准,标志着Verilog HDL首个国际标准(即Verilog-1995)的诞生。随后,该标准在2001年和2005年分别经历了修订,形成了Verilog-2001标准(IEEE 1364-2001)和Verilog-2005标准(IEEE 1364-2005)。尽管版本更迭,但各个版本的基本语法保持着高度的一致性。代码1-1展示了Verilog HDL中基本的模块结构。
代码1-1:Verilog HDL中基本的模块结构示例
module led_on ( output wire led_out ); assign led_out = 1'b0 ; endmodule
一个Verilog HDL模块包含以下基本要素。
(1)以关键字module开始,以关键字endmodule结束。
(2)关键字module后面紧跟模块的名称。代码1-1中,模块名为led_on。
(3)模块名称之后是模块端口列表,用半角圆括号“()”括起,并在括号外以半角分号“;”结束。有些模块不需要端口列表,如测试平台模块,这时在模块名称后直接以半角分号结束。
在模块端口列表中,若存在多个信号,它们之间以半角逗号“,”进行分隔,最后一个信号后不再添加半角逗号。在代码1-1中,由于只有一个端口信号,因此没有添加半角逗号。
(4)端口列表后是模块的主体部分,在这里使用Verilog HDL规定的语句完成逻辑功能的建模。
Verilog HDL是一种具有层次性的硬件描述语言,它能够用于描述从基础的逻辑门到复杂的数字系统。在传统的高级程序设计语言中,层次结构通常是通过模块调用来构建的。在Verilog HDL中,这一过程被称为例化(Instantiation)。由于Verilog HDL专注于硬件描述,因此例化一词不仅表示调用模块,还蕴含了将特定功能模块具体化或实例化的概念。
为了更清晰地说明问题,我们将代码1-1设计得复杂一些,参考代码1-2,并在代码1-3中例化它。代码1-3是代码1-2中描述的硬件模块led_on的测试平台模块。
代码1-2:将led_on加入输入控制信号
module led_on ( output wire led1_out, output wire led2_out, input wire SW1_2 ); assign led1_out = 1'0 ; assign led2_out = SW1_2 ; endmodule
代码1-3:硬件模块led_on的测试平台模块
module tb_led ; wire led1 ; wire led2 ; reg sw_in = 0 ; // 例化led_on模块 led_on led_en_inst0 ( /*output wire */.led1_out ( led1 ) , /*output wire */.led2_out ( led2 ) , /*input wire */.SW1_2 ( sw_in ) ); initial begin #5 sw_in = 1 ; #25 sw_in = 0 ; #65 sw_in = 1 ; end endmodule
图1-1更直观地说明了这种例化关系。
图1-1
在tb_led模块中例化led_on模块,相当于在对应位置使用led_on模块中的设计内容。
作为一门高级程序设计语言,Verilog HDL包含的内容也很丰富。本书不深入探讨其内容,仅为了方便后续介绍,对Verilog HDL基础语法进行概述。
在Verilog HDL中,注释分为两种类型:单行注释和多行注释。
● 单行注释:从“//”符号开始直到该行末尾的所有内容均为注释内容。
● 多行注释:被“/*”和“*/”符号所包围的内容均为注释内容。
变量(包括常量)和数据类型是程序设计语言的两个基本要素。尽管wire和reg是Verilog HDL设计者使用最频繁的变量声明方式,但Verilog HDL实际上还规定了多种其他类型的变量和常量,例如整数常量、实数常量、字符串、时间变量(time)以及参数(parameter)等。
在Verilog-1995标准中,描述变量reg时使用了register一词,这导致许多初学者对reg变量产生了误解,认为reg对应于硬件中的寄存器;他们还形成了“wire用于编写组合逻辑,而reg用于编写时序逻辑”的理解,这其实比较片面。
首先,wire变量确实仅限于组合逻辑建模,无法用于时序逻辑建模。然而,reg变量不仅适用于时序逻辑建模,还可以用于组合逻辑建模。一些设计者倾向于将wire类型称为线网类型,实际上,当使用reg变量进行组合逻辑建模时,reg变量本质上也充当了线网的角色。在Verilog HDL的后续标准中,术语variable取代了register,这在一定程度上减少了初学者对reg变量可能产生的误解。
其次,wire和reg是Verilog HDL中仅有的两种变量类型。在声明wire或reg变量时,它们可以是一位的,也可以是多位的。一位的wire或reg变量亦被称为标量,而多位的wire或reg变量则被称作矢量。此外,与其他程序设计语言类似,Verilog HDL也可以声明数组(Array)类型的变量。
在使用方法上,对wire和reg这两种类型的变量赋值存在显著差异:对于wire类型的变量,赋值必须通过assign语句(即连续赋值语句)来实现;而对于reg类型的变量,赋值则应在过程赋值语句中完成,例如在always语句块内进行。
将Verilog HDL标准中的Procedure译作“进程”并不准确。Procedure一词,实际上是指代码段,即由一个或多个语句构成的代码段。
Verilog HDL中的进程有以下4种形式。
● always语句。
● initial语句。
● function(函数)。
● task(任务)。
在这些形式中,使用频率最高的当数always语句。在构建测试平台时,initial语句也经常被采用。一些设计者会在可综合的设计代码中加入initial语句,以初始化特定变量在复位后的电平。然而,这种做法并非最佳解决方案,因为没有逻辑硬件电路与之对应。实际上,使用initial语句反映了软件设计人员的思维习惯,而Verilog HDL本质上是用来描述硬件的。对于硬件电路,我们期望它在上电或复位时能够达到一个确定的电平状态,无论是高电平还是低电平。实现这一点的最佳方式是通过不同的电路单元结构来完成。例如,若希望复位后电路处于高电平状态,应选择使用同步置位寄存器或异步置位寄存器。
在软件工程领域,进程和赋值等术语通常与程序设计语言紧密相关。Verilog HDL的特殊性就在于,虽然它也属于程序设计语言的范畴,但是其主要功能是对硬件进行详细描述。那么,在实体硬件电路中,赋值这一概念是如何实现和体现的呢?
在代码1-1中,为了使管脚led_out输出低电平,使用了assign关键字进行赋值操作:
assign led_out = 1'b0 ;
因此,在Verilog HDL中,对变量进行赋值实际上等同于在硬件层面设置信号的驱动源。
在Verilog HDL中运用预编译指令,可以有效应对特定的设计场景。例如,某个功能模块被编写并验证无误后,设计者可能不希望再对模块内部的代码进行修改,然而该模块可能需要适应两种不同的应用场景,一种用于实现功能A,另一种用于实现功能B。当然,设计者必须确保功能A和功能B都是正确的。
要实现这样的设计需求场景,除了使用预编译指令,也可以为功能模块设置一个控制信号。比如可以为一个模块设置一个控制信号bist_en,当该信号为低电平时,模块对输入管脚的其他信号进行处理;而当该信号为高电平时,模块内部切换到内置的测试激励来驱动对应的输入管脚,这样就可以通过输出管脚的响应来判断模块的输出是否符合预期。
下面用代码1-4提供的部分编码来详细说明这两种方式的差别。
代码1-4:预编译指令示例
// `define BIST_MODE assign fifo_wen = pwm_en ; `ifdef BIST_MODE assign fifo_din = cnt; `else assign fifo_din = pwm_gen_result; `endif |
assign fifo_wen = pwm_en ; assign fifo_din = bist_en ? cnt : pwm_gen_result ; |
代码1-4中,右侧的代码使用控制信号bist_en的方式,这相当于用bist_en作为输入的选择控制信号:当bist_en为高电平时,用cnt作为输入,这时可以设计cnt为规则变化的数据(每次累加1),以便分析输出数据;当bist_en为低电平时,使用pwm_gen_result作为输入。使用这种方式设计的硬件结构是一个两输入的数据选择器,由于两个数据源都会被用到,因此产生cnt、pwm_gen_result的逻辑资源都不可少。
代码1-4中,左侧的代码使用预编译指令的方式。当设计工程中定义了BIST_MODE时,只有`ifdef到`else之间的逻辑功能生效,即使用cnt作为输入。当没有定义BIST_MODE时,使用pwm_gen_result作为输入。从描述上看,好像和右侧代码一样,也是一个数据二选一的功能。但是,它实际综合的结果会根据是否定义了BIST_MODE而定:定义了BIST_MODE时,最后结果并不包含产生pwm_gen_result相关的功能;而没有定义BIST_MODE时,产生cnt的相关逻辑也会被优化掉。所以,可以简单地认为使用预编译指令的方式进行设计时,使用的逻辑资源比设置控制信号的方式要少。
虽然用于描述硬件,但是Verilog HDL本质上还是一种高级程序设计语言,所以每次标准的更新都借鉴了当时一些优秀的编程思想。相比Verilog-1995,Verilog-2001有了很大改善,让Verilog HDL更加精简、高效。Verilog-2005发布时,由于已经发布了System Verilog,所以Verilog-2005本身并没有引入太多的优化。本节描述的特性有很多是在Verilog-2001中就已提出的,但为了统一,本节将它们描述为Verilog-2005的特性。
端口声明方式的改进见表1-1。
表1-1
Verilog-2005语法 |
Verilog-1995语法 |
---|---|
module XX ( input wire [3:0] sig_in , output wire [7:0] sig_out1, output reg [3:0] sig_out2 ); // 模块功能描述:略 endmodule |
module XX ( sig_in , sig_out1, sig_out2 ); input sig_in ; output sig_out1; output sig_out2;
wire [3:0] sig_in ; wire [7:0] sig_out1; reg [3:0] sig_out2 ;
// 模块功能描述:略 endmodule |
在Verilog-1995中,同一个变量要在3个地方分别说明!Verilog-2005把这3个部分合而为一,不仅让代码更加简洁,还让代码编写过程中的误输入可能性大幅度降低。
早期从事FPGA设计的人员对编码风格中要求的“敏感变量列表要描述齐全”可能印象深刻,因为在用always语句描述组合逻辑时,如果不小心遗漏了一个敏感变量列表,就会导致所描述的硬件结构成为一个锁存(Latch)结构,使设计结果与预期大相径庭。Verilog-2005中,在描述时序逻辑的敏感变量列表时,用逗号来代替关键字or;而在描述组合逻辑时,直接用星号来替代全部敏感变量列表,参考表1-2、表1-3的对比情况。
表1-2
Verilog-2005语法 |
Verilog-1995语法 |
---|---|
always @ ( posedge clk , negedge rstn ) Y <= a | b ; |
always @ ( posedge clk or negedge rstn ) Y <= a | b ; |
表1-3
Verilog-2005语法 |
Verilog-1995语法 |
---|---|
always @ ( * ) Y = a | b & c | d; |
always @ ( a or b or c or d ) Y = a | b & c | d; |
在Verilog HDL中声明一个变量时,如果没有指定变量的位宽,就把该变量当作只有一位的变量,也称为标量;而如果指定了该变量的位宽为多位,该变量被称为矢量。要从一个矢量中选择其部分连续数据位时,Verilog-2005在Verilog-1995的基础上也有许多改进,参考表1-4的对比情况。
表1-4
Verilog-2005语法 |
Verilog-1995语法 |
---|---|
wire [7:0] sig_x1 ; wire [7:0] sig_x2 ; |
wire [7:0] sig_x1 ; wire [7:0] sig_x2 ; |
reg [31:0] sig_y ;
assign sig_x1 = sig_y[10+:8] ; assign sig_x2 = sig_y[17-:8] ; |
reg [31:0] sig_y ;
assign sig_x1 = sig_y[17:10] ; assign sig_x2 = sig_y[17:10] ; |
Verilog-2005引入了C语言的一些语法,10+:8表示从第10位开始往高位连续选择8位;17-:8表示从第17位开始往低位连续选择8位。用这两种方式都可选择信号sig_y的[17:10]这8位。
参数化设计是程序设计语言的重要思想。在Verilog-2005中,模块的parameter声明和值传递方式均比Verilog-1995简洁,参考表1-5的对比情况。
表1-5
Verilog-2005语法 |
Verilog-1995语法 |
---|---|
module param_def # ( parameter L_WIDTH = 8, parameter L_DEEPTH = 256 ) ( // 其他信号:略 input [L_WIDTH-1:0] sig_in , output [L_DEEPTH-1:0] sig_out ); // 内部功能描述:略
endmodule |
module param_def( // 其他信号:略 sig_in , sig_out ); parameter L_WIDTH = 8 ; parameter L_DEEPTH = 256 ;
input [L_WIDTH-1:0] sig_in ; output [L_DEEPTH-1:0] sig_out;
// 内部功能描述:略
endmodule |
Verilog-2005用#()的方式在端口列表声明前声明模块的parameter列表。
对于模块parameter的值传递方式,各个版本的Verilog标准都支持使用关键字defparam来传递parameter的值到例化模块中。除此之外,也可以在例化模块时直接用端口连接的方式传递参数,但是Verilog-1995只支持隐式参数传递,即各个parameter的值按照它们在模块声明时的顺序一一对应地传递。在Verilog-2005中,可以像端口信号一样只对指定parameter的值进行传递,即增加了显式参数传递机制,参考表1-6的对比说明。
表1-6
Verilog-2005语法 |
Verilog-1995语法 |
---|---|
// 例化param_def, //但是没有更新L_WIDTH param_def # ( .L_DEEPTH ( 200 ) ) inst_param_def ( .sig_in ( sig_in ), .sig_out ( sig_out ) ); |
param_def # ( 8, 200 ) inst_param_def ( .sig_in ( sig_in ), .sig_out ( sig_out ) ); |
Verilog-2005改进后采用显式参数传递机制,不仅参数列表的顺序可以任意调换,还可以根据需要只更新部分参数的值,其他未传递的参数则采用模块声明时的默认值。
在Verilog-2005中,与generate同步引入的还有endgenerate、genvar等关键字。generate语句可以用在多种场景中。
一种场景是替换`define,即前面介绍的预编译指令。预编译指令的使用方式是用关键字`define定义预编译变量,然后用`ifdef…`else语句实现功能。使用generate语句也可以实现这样的功能:用genvar定义一个变量,根据该变量的实际值用generate if、generate case语句生成不同的硬件。
另一种场景是使用generate for的结构描述功能相同的多个硬件结构,这将极大简化代码量,参考表1-7的对比说明。
表1-7
Verilog-2005语法 |
Verilog-1995语法 |
---|---|
// 模块其他功能:略 // 例化 param_def 模块4 次 wire [7:0] sig_in0 ; wire [255:0]sig_out0 ; wire [7:0] sig_in1 ; wire [255:0]sig_out1 ; wire [7:0] sig_in2 ; wire [255:0]sig_out2 ; wire [7:0] sig_in3 ; wire [255:0]sig_out3 ; genvar j ; wire [4*8-1:0] sig_in = { sig_in3, sig_in2, sig_in1, sig_in0}; wire [4*256-1:0]sig_out ; generate begin: INST_exam for (i=0;i<4;i=i+1) param_def param_def ( // 其他信号连接:略 .clk ( clk ) , .sig_in ( sig_in[i*8+7:i*8] ) , .sig_out (sig_out[i*256+255:i*256] ) );
end endgenerate assign sig_out0 = sig_out[255:0] ; assign sig_out0 = sig_out[511:256] ; assign sig_out0 = sig_out[767:512] ; assign sig_out0 = sig_out[1023:768] ; |
// 模块其他功能:略 // 例化 param_def 模块4 次
wire [7:0] sig_in0 ; wire [255:0]sig_out0 ; wire [7:0] sig_in1 ; wire [255:0]sig_out1 ; wire [7:0] sig_in2 ; wire [255:0]sig_out2 ; wire [7:0] sig_in3 ; wire [255:0]sig_out3 ;
param_def param_def_inst0 ( // 其他信号连接:略 .clk ( clk ) , .sig_in ( sig_in0 ) , .sig_out (sig_out0 ) );
param_def param_def_inst1 ( // 其他信号连接:略 .clk ( clk ) , .sig_in ( sig_in1 ) , .sig_out (sig_out1 ) );
param_def param_def_inst2 ( // 其他信号连接:略 .clk ( clk ) , .sig_in ( sig_in2 ) , .sig_out (sig_out2 ) );
param_def param_def_inst3 ( // 其他信号连接:略 .clk ( clk ) , .sig_in ( sig_in3 ) , .sig_out (sig_out3 ) ); |
在需要多次例化同一个模块时,如前所述,可以使用generate语句。Verilog-2005还提供一种类似于矢量声明的方式,比使用generate语句更简洁,参考代码1-5。
代码1-5:矢量化方式例化参考代码
// 模块其他功能:略 // 例化 param_def 模块4 次 wire [7:0] sig_in0 ; wire [255:0]sig_out0 ; wire [7:0] sig_in1 ; wire [255:0]sig_out1 ; wire [7:0] sig_in2 ; wire [255:0]sig_out2 ; wire [7:0] sig_in3 ; wire [255:0]sig_out3 ; param_def param_def_inst[3:0] ( // 其他信号连接:略 .clk ( {4{clk}} ) , // clk ( clk ) .sig_in ({ sig_in3, sig_in2, sig_in1, sig_in0}) , .sig_out ({ sig_out3, sig_out2, sig_out1, sig_out0} ) );
使用param_def_inst[3:0]的方式相当于例化了4个param_def模块,各个端口信号也相应地进行了扩展。如果所有例化模块的某个端口使用的是同一个信号,那么还可以简化。
比如在代码1-5中使用的是:
.clk ( {4{clk}} ) ,
这表明4个例化模块的clk端口连接的是同一个信号,此时可以替换为如下形式:
.clk ( clk ) ,
利用Verilog HDL的逻辑运算符,可以轻松对电子系统中的非门、与门、与非门、或门、或非门甚至异或门等功能单元进行建模。本节简单介绍如何对电子系统中的一些常用功能进行建模。
异步信号处理是电子系统中必不可少的功能单元。同步电子系统工作的基本要求是每个寄存器的建立时间、保持时间都必须满足,否则容易造成亚稳态的传播,从而导致系统功能失效。
当需要使用从另外一个时钟域产生的信号时,同步器是该信号进入新的时钟域时首先要使用的功能部件。最基本的同步器是两个挨得很近的寄存器,这两个寄存器在物理布局上应尽量靠近,并且让第一级寄存器的输出到第二级寄存器输入的布线延迟尽量短。用Verilog HDL描述同步器时,无法体现两个寄存器物理位置的关系,所以从代码上看同步器就是两个级联的寄存器,可参考代码1-6,图1-2所示为其描述的硬件结构。
代码1-6:同步器参考代码
module sig_sync ( output wire sync_out , input wire sig , input wire clk ); reg sig_reg1; reg sig_reg2; always @ ( posedge clk ) begin sig_reg1 <= sig ; sig_reg2 <= sig_reg1 ; end assign sync_out = sig_reg2 ; endmodule
图1-2
沿检测器用于检测输入信号的上升沿或者下降沿,参考图1-3,sig_pedge表示检测到sig信号从低到高的跳变后输出的高电平指示信号,sig_nedge表示检测到sig信号从高到低的跳变后输出的高电平指示信号。代码1-7是实现这两个功能的沿检测器参考代码。
图1-3
代码1-7:沿检测器参考代码
module pulse_det ( output wire sig_pedge , output wire sig_nedge , input wire sig_in , input wire clk , input wire rstn ); reg sig_in_p_dly ; reg sig_in_n_dly ; reg p_edge ; reg n_edge ; always @ ( posedge clk , negedge rstn) if ( !rstn ) begin sig_in_p_dly <= 1 ; sig_in_n_dly <= 0 ; p_edge <= 0 ; n_edge <= 0 ; end else begin sig_in_p_dly <= sig_in ; sig_in_n_dly <= sig_in ; p_edge <= {sig_in_p_dly,sig_in} == 2'b01 ; // 1 comes n_edge <= {sig_in_n_dly,sig_in} == 2'b10 ; // 0 comes end ////// Output Drivers assign sig_pedge = p_edge ; assign sig_nedge = n_edge ; endmodule
可以看到,沿检测器的基本原理是将输入信号通过寄存器延迟一个节拍,通过前后两级寄存器输出信号的高电平、低电平状态判断输入信号是出现了上升沿还是下降沿。上述代码中对输入信号sig进行延迟一个节拍的处理时,判断上升沿与判断下降沿使用了不同的寄存器类型:在复位信号rstn有效时,判断下降沿用的寄存器是异步复位寄存器,即sig_in_n_dly在复位时输出为低电平;而判断上升沿用的寄存器为异步置位寄存器,即sig_in_p_dly在复位时输出为高电平。为什么需要这样处理呢?
这是为了避免在复位释放时出现上升沿的误判,如图1-4所示。如果上升沿判断也使用异步复位寄存器,当输入信号sig在复位释放前后均为高电平时,复位释放后的第一个时钟节拍里sig_in_p_dly仍然为低电平,所以sig_pedge会输出一个周期的有效高电平,即判断这里也是输入信号sig的一个上升沿,这显然是一个误判。
图1-4
有时需要把某个宽度较窄的信号进行扩展。例如这样的设计场景:需要检测A、B、C、D 4个事件是否按顺序发生,并在检测到事件B发生后等待10个时钟周期再开始检测事件C。要实现“等待10个时钟周期”,就需要使用扩展器。
另一个常见的场景是异步信号处理。代码1-7输出的sig_pedge的有效高电平只有一个时钟周期宽度。当另一个频率更低的时钟域需要使用sig_pedge时,其时钟周期大于sig_pedge的宽度,如图1-5所示,t_clk的时钟周期明显大于sig_pedge的宽度,t_clk的上升沿并不能采样到sig_pedge信号的电平变化。所以,用同步器同步一位信号时,有一个基本条件需要满足:源时钟域内的信号宽度不能小于目标时钟域(t_clk)的两个时钟周期宽度。所以在该场景中,需要把sig_pedge信号在源时钟域内进行扩展。
图1-5
扩展器参考代码见代码1-8。
代码1-8:扩展器参考代码
module sig_ext_gen # ( parameter DELAY_CNT = 8 , // ext cycles : parameter D_WIDTH = 4 // bit size ) ( output sig_ext , // sig_in cycles + DELAY_CNT cycles input sig_in , input clk , input rstn ); reg busy ; reg [D_WIDTH-1:0] dly_cnt; wire busy_done = dly_cnt >= (DELAY_CNT-1) ; always @ ( posedge clk , negedge rstn) if ( !rstn ) begin busy <= 0 ; dly_cnt <= 0 ; end else begin if ( sig_in ) busy <= 1 ; else if ( busy_done ) busy <= 0 ; if ( sig_in ) dly_cnt <= 0 ; else if ( !busy ) dly_cnt <= 0 ; else dly_cnt <= dly_cnt + 1'b1 ; end ////// Output Drivers assign sig_ext = busy ; endmodule
沿同步器用于把一个信号的沿检测结果同步到另外一个时钟域。如图1-3所示,沿检测结果只有一个时钟周期宽度,当需要把它同步到低速目标时钟域时,需要先在源时钟域内对信号进行扩展,最好扩展到超过低速时钟域的3个时钟周期宽度。所以,能在任意两个时钟域之间进行同步的沿同步器应该包含以下3个部分。
● 源时钟域内信号的扩展器。
● 同步器。
● 目标时钟域内的沿检测器。
图1-6所示为信号变化,代码1-9是任意时钟域沿同步器参考代码。
图1-6
代码1-9:任意时钟域沿同步器参考代码
module sig_ext_sync_edge # ( parameter DELAY_CNT = 8, parameter D_WIDTH = 4 ) ( output sig_ext_syn , output sig_ext_pedge , output sig_ext_nedge , input sig_in , input src_clk , input tgt_clk , input rstn ) ; wire sig_ext ; sig_ext_gen # ( .DELAY_CNT ( DELAY_CNT ) , // ext cycles : .D_WIDTH ( D_WIDTH ) // bit size ) sig_exter ( /*output */.sig_ext ( sig_ext ) , // sig_in cycles + DELAY_CNT /*input */.sig_in ( sig_in ) , /*input */.clk ( src_clk ) , /*input */.rstn ( rstn ) ); sig_sync sig_ext_syner ( /*input wire */.clk ( tgt_clk ) , /*input wire */.sig ( sig_ext ) , /*output wire */.sync_out ( sig_ext_syn ) ); pulse_det sig_ext_edger ( /*output wire */.sig_pedge ( sig_ext_pedge ) , /*output wire */.sig_nedge ( sig_ext_nedge ) , /*input wire */.sig_in ( sig_ext_syn ) , /*input wire */.clk ( tgt_clk ) , /*input wire */.rstn ( rstn ) ); endmodule
为了扩大模块的适用范围,将信号扩展的宽度DELAY_CNT设计为模块的parameter。
序列检测器的功能是检测一系列事件是否按特定顺序发生过。比如一个8位宽的信号,其值可以从0x00到0xFF,需要检测该信号上是否先后出现过0x01、0x02、0x03、0x04这4个值,至少有两种检测方式。
第一种方式是“存在性”检测,即从该信号的值序列中出现值0x01开始,先后出现了0x02、0x03,又出现了0x04,就算序列检测成功,在值0x04处输出检测成功的指示,然后再次从0x01开始进行第二次序列检测操作。
第二种方式是连续检测,即该信号4个连续的值必须依次是0x01、0x02、0x03、0x04才算检测成功,中间隔了任何其他值都必须重新检测。
图1-7所示为两种检测方式的结果,可以看出第二种检测方式是第一种检测方式的子集。
图1-7
如果用状态机来描述这两种检测方式,可以更清楚地看到两者的差异,图1-8(a)表示第一种检测方式,图1-8(b)表示第二种检测方式。两种检测方式都用EVT_0、EVT_1、EVT_2、EVT_3分别表示检测到了字节0x01、0x02、0x03、0x04。
使用第一种检测方式检测到字节0x01后就一直停留在EVT_0状态,直到检测到0x02后进入EVT_1状态。而对于第二种检测方式,只有在检测到0x02后才会从EVT_0状态进入EVT_1状态,如果检测到出现的值不是0x02则进入IDLE状态,重新从0x01开始检测。
图1-8
对于第二种检测方式,还有一点需要注意,就是任何情况下检测到0x01,都不需要进入IDLE状态并再次检测0x01,因为已经检测到了0x01,所以应该进入EVT_0状态。图1-8(b)中用虚线的状态跃迁来表示这种情况。
两种检测方式的设计代码可以参考代码1-10、代码1-11。需要注意,这两段代码并不是完全按照图1-8中的状态机来设计的。
代码1-10:第一种检测方式的参考代码
module seq_chk_none_cont # ( parameter STATE = 8 ) ( output wire seq_cfm , input wire [STATE-1:0] trigger_in , input wire clk , input wire rstn ); genvar i ; reg [STATE-1:0] event_en ; reg seq_chk_done ; wire seq_chk_done_comb = event_en[STATE-2:0] == {(STATE-1){1'b1}} & trigger_in[STATE-1] ; always @ (posedge clk, negedge rstn) if (!rstn) seq_chk_done <= 0 ; else seq_chk_done <= seq_chk_done_comb ; always @ (posedge clk, negedge rstn) if (!rstn) event_en[0] <= 0 ; else if ( seq_chk_done_comb ) event_en[0] <= 0 ; else if ( trigger_in[0] ) event_en[0] <= 1 ; generate for (i = 1; i < STATE ; i=i+1) begin: seq_detectors always @ (posedge clk, negedge rstn) if (!rstn) event_en[i] <= 0 ; else if ( seq_chk_done_comb ) event_en[i] <= 0 ; else if ( !event_en[i] & event_en[i-1] & trigger_in[i] ) event_en[i] <= 1 ; end endgenerate /// output Drivers assign seq_cfm = seq_chk_done ; endmodule
代码1-11:第二种检测方式的参考代码
module seq_chk_cont # ( parameter STATE = 8 ) ( output wire seq_cfm , input wire [STATE-1:0] trigger_in , input wire clk , input wire rstn ); genvar i ; reg [STATE-1:0] event_en ; wire [STATE-1:0] event_en_comb ; reg seq_chk_done ; wire seq_chk_done_comb = event_en[STATE-1] & trigger_in[STATE-1] ; wire has_input = trigger_in != 0 ; always @ (posedge clk, negedge rstn) if (!rstn) seq_chk_done <= 0 ; else seq_chk_done <= seq_chk_done_comb ; always @ (posedge clk, negedge rstn) if (!rstn) event_en <= 1 ; else if ( seq_chk_done_comb ) event_en <= 1 ; else if ( has_input ) event_en[STATE-1:1] <= event_en_comb[STATE-1:1] ; generate for (i = 1; i < STATE ; i=i+1) begin: EVT_GEN assign event_en_comb[i] = trigger_in[i-1] ? event_en[i-1] : 0 ; end endgenerate /// output Drivers assign seq_cfm = seq_chk_done ; endmodule
代码1-10、代码1-11均采用参数化设计方式,并且都使用了generate语句。两个模块设计的巧妙之处在于,将event_en信号的最低位固定为高电平,其他位用来分别表示检测到了一个事件的发生。但是在两个模块中,由于检测方式不同,其他位的高电平时间是不相同的:在代码1-11中,其他位最多只能有一个为高电平;而在代码1-10中,其他位会按照从低到高的顺序,在分别检测到trigger_in [0]、trigger_in [1]……后依次变为高电平,并且保持到seq_chk_done_comb有效后才被清零,可以参考图1-9所示的仿真结果。
图1-9
|
注意代码1-10、代码1-11中generate的用法。如果把代码1-11中的generate写成代码1-12所示的样子,是否可以呢? |
代码1-12是generate的一种错误写法。
代码1-12:generate的一种错误写法
generate for (i = 1; i < STATE ; i=i+1) begin: seq_detectors always @ (posedge clk, negedge rstn) if (!rstn) event_en <= 1 ; else if ( seq_chk_done_comb ) event_en <= 1 ; else if ( has_input ) event_en[STATE-1:1] <= trigger_in[i-1] ? event_en[i-1] : 0 ; end endgenerate
对按键信号进行去抖处理是电子系统中很常用的功能。因为通常情况下,按键在接触点断开、闭合时,并不会立即稳定地断开和接通,图1-10所示的第一个信号描述了这种不稳定性,因此不能直接用该信号的下降沿表示信号已经变为稳定的低电平,也不能用该信号的上升沿表示信号已经变为稳定的高电平。
图1-10
为了判断信号是否已经变为稳定的低电平,稳妥的做法是启动一个计数器,在检测到信号的下降沿和信号为高电平时将计数器清零,只在检测到信号为低电平的周期将计数器累加1,如果计数器能计数到指定的宽度(比如Tf个时钟周期),表明对应的Tf个时钟周期内的输入信号都是低电平。因此,可以用计数器计数到特定值,表示信号已经变为稳定的低电平。如图1-10所示,当有多个下降沿并且它们的间隔小于Tf个时钟周期时,只有检测到最后一个下降沿后计数器才能计数到Tf,所以只有最后这一个下降沿生效,前面的下降沿都被滤除掉了。
对上升沿也可以采用相同的方式实现输入信号的去抖处理。在计数器的值为Tf时输入信号的采样值就可以作为去抖后的信号,图1-10所示最底部的信号就是输入信号采样触发信号,即计数器计数到Tf的位置。
设计代码可以参考代码1-13。
代码1-13:输入信号去抖处理参考代码
module debounce # (parameter TFILTER_SIZE = 32 , // bit size parameter TFILTER = 100 // De-Bounce Cycles ) ( output wire sig_out , input wire sig_in , input wire clk , input wire rstn ); reg enable ; reg busy ; reg sig_in_dly ; reg [TFILTER_SIZE-1:0] t_cnt ; reg sig_out_reg ; wire sig_in_edge = sig_in_dly ^ sig_in ; wire dly_done = busy & t_cnt >= (TFILTER-1) ; always @ ( posedge clk , negedge rstn ) if ( !rstn ) begin enable <= 0 ; sig_in_dly <= 0 ; busy <= 0 ; t_cnt <= 0 ; sig_out_reg <= 0 ; end else begin if ( sig_in_edge ) enable <= 1 ; sig_in_dly <= sig_in ; if ( sig_in_edge ) busy <= 1 ; else if ( dly_done ) busy <= 0 ; if ( sig_in_edge | (!busy) | dly_done ) t_cnt <= 0 ; else t_cnt <= t_cnt + 1 ; `ifdef DE_CASE1 if ( (!enable) | dly_done ) `else if ( (!enable) | ( dly_done&(!sig_in_edge) ) ) `endif sig_out_reg <= sig_in_dly ; end assign sig_out = sig_out_reg ; endmodule
代码1-13中使用了如下预编译指令:
`ifdef DE_CASE1 if ( (!enable) | dly_done ) `else if ( (!enable) | ( dly_done&(!sig_in_edge) ) ) `endif
这是为了解决如下问题:输入信号跳变沿处计数器被清零。但是在计数器正好计数到Tf时,又检测到一个跳变沿,dly_done与sig_in_edge在同一个时钟周期内生效了,参考图1-11,这时如何处理这个新的信号跳变?也就是说,在这种情况下,dly_done与sig_in_edge谁的优先级更高?
图1-11
第一种处理方式(DE_CASE1)是dly_done的优先级更高。其思路是既然计数器能够到达Tf−1,表示信号已经足够稳定,所以对应的信号沿有效。对应信号沿如果是下降沿则输出低电平,如果是上升沿则输出高电平。
第二种处理方式是sig_in_edge的优先级更高,其思路是既然最后一个周期检测到了新的信号沿,则表明信号又发生了跳变,所以前一个信号沿要忽略。
所以在设计代码中添加预编译指令,若在某种场景下需要使用第一种处理方式,再在模块中添加如下代码即可:
`define DE_CASE1
本章对Verilog HDL一些比较常用的语法进行了简单说明,并给出了一些基本功能单元的参考代码。