7. 层次化设计

7.1. 章节导读

我们在基础篇中涉及的例程功能都比较简单,其模块划分就相对较少,有些工程只需一个模块就可实现功能,所以在层次化设计上的体现并不是很明显,但是我们要尽早让大家知道这种思想方法,以便在长期的学习过程中能够慢慢体会。本章我们就通过全加器的例子来给大家讲解层次化设计的思想。

7.2. 理论知识

数字电路中根据模块层次不同有两种基本的结构设计方法:自底向上(Bottom-Up)的设计方法和自顶向下(Top- Down)的设计方法。这两种方法能够有助于我们对整个项目的系统和结构有一个宏观的把控,这也是我们在基础章节中每个简单的实例都分析模块是如何设计的原因,虽然简单的系统不需要划分结构,但是养成好的习惯后会在强化和提高篇中的大型多模块设计中有着及其重要意义。

自底向上的设计是一种传统的设计方法,对设计进行逐次划分的过程是从存在的基本单元出发的,设计树最末枝上的单元要么是已经构造出的单元,要么是其他项目开发好的单元或者是可外购得到的单元。在自底向上建模方法中,我们首先对现有的功能块进行分析,然后使用这些模块来搭建规模大一些的功能块,如此继续直至顶层模块。如 图 13‑1所示显示了这种方法的设计过程。

fullad002

图 13‑1 自下而上的设计方法

自上而下的设计是从系统级开始,把系统分为基本单元,然后再把每个单元划分为下一层次的基本单元,一直这样做下去,直到直接可以用EDA元件库中的原件来实现为止。在自顶向下设计方法中,我们首先定义顶层功能块,进而分析需要哪些构成顶层模块的必要子模块;然后进一步对各个子模块进行分解,直到到达无法进一步分解的底 层功能块。如图 13‑2所示显示了这种方法的设计过程。

fullad003

图 13‑2 自上而下的设计方法

在典型的设计中,这两种方法是混合使用的。设计人员首先根据电路的体系结构定义顶层模块。逻辑设计者确定如何根据功能将整个设计划分为子模块;与此同时电路设计者对底层功能块电路进行优化设计,并进一步使用这些底层模块来搭建其高层模块。两者的工作按相反的方向独立进行,直至在某一中间点会合。这时,电路设计者已经创 建了一个底层功能块库(具有独立完整的功能块、IP核或逻辑门),而逻辑设计者也通过使用自顶向下的方法将整个设计分解为由库单元构成的结构描述。如图 13‑3所示显示了这种方法的设计过程。

fullad004

图 13‑3 典型的设计方法

上面介绍的是相对抽象的理论总结,理论往往是晦涩难懂的,但是作为一种概括性的总结还是很有用的,或许大家在对FPGA的设计有了较为深刻的理解后再回过头来看这些理论的总结就会有感而发了。为了说明层次化设计的概念,下面我们以全加器的例子为载体,讲解一个简单的层次化设计在设计模块的思路和代码的编写上有何不同。

7.3. 实战演练

7.3.1. 实验目标

使用上一章节实现的半加器,结合层次化设计思想,设计并实现一个全加器。

7.3.2. 硬件资源

与半加器相同,我们使用开发板上的按键和LED灯进行全加器的验证,选取KEY1、KEY2、KEY3分别作为被加数in1、被加数in2和进位信号cin的信号输入;以LED灯D6作为和的输出sum,以LED灯D7作为进位的输出count;如图 13‑4所示。

fullad005

图 13‑4 硬件资源

7.3.3. 程序设计

7.3.3.1. 模块框图

我们先给设计的顶层模块取一个名字为full_adder,全加器和半加器唯一的不同就是输入除了有两个加数之外还有一个加数,第三个加数是上一级加法器的进位信号,这样子就相当于是三个1bit的加数相加求和。所以在整体结构框图的设计上我们依然可以采用半加器那样的设计,然后再在输入端加上一个1bit名为cin 的信号即可,如图 13‑5所示。

fullad006

图 13‑5 顶层模块框图

在之前的设计中分析到这里就结束了,可以进行该模块的代码编写了,实现三个1bit数的加和的功能,但是这里我们并不用这种简单的方法,而是采用层次化的结构方法将顶层模块进一步划分。之前我们之所以没有采用层次化的设计方法是因为实现的功能相对简单,一个模块就能够很好的实现完整的功能,而且顶层模块也不容易再往下 划分为更加独立的小模块了,所以也就不必再关心模块的内部是用怎样实现的,直接根据功能写代码即可。然而本章中的这个模块却有所不同,我们在学习数电的时候知道全加器并不是最基本的结构,它可以由两个半加器构成,也就是说我们可以根据之前设计的半加器通过一定的组合再加上适当的门电路来构成一个全加器。全加器由半加器 的推导方式有很多种,这里我们用一个方法推导一下:全加器有三个1bit的加数,我们可以先实现两个数的加和,再加上第三个数并不会影响最后的结果。我们知道两个数的加和就是半加器所实现的功能,所以先进行的两个数的加和运算需要用到一个半加器来实现,然后输出求和信号和进位信号,求和信号再和第三个加数加和需要再使 用一个半加器,然后输出进位信号和最后的总和号。但是进位信号有两个,这两个进位信号都是有用的,但又不会同时为存在,一个有效即有效,所以将两个半加器的进位信号用一个或门运算后作为最后的输出进位信号(也可以用逻辑表达式的方式推导)。本例我们将半加器作为本设计的一个基本单元,它既是顶层模块下的一个子模块,也 是一个独立的模块。

根据上面的分析设计出的Visio框图如图 13‑6所示,我们可以看到外部的信号端口和图 13‑5是一样的没有变化,而丰富的内容是其内部的结构,我们可以看到内部由两个半加器分别名为half_adder0和half_adder1,每个半加器的信号端口依然和上一章的一模一样,除了两个半加器还有一个或门电路 。其次我们需要关注的就是连线,即外部的输入输出信号线是如何与内部模块进行连接的,内部的模块之间的信号线又是如何互相连接的,这里的连接关系是我们根据数字电路推导出的,但是其中的信号命名虽然是同一根线,但是却不在同一个层,为了和后面编写的代码对应,也为了让大家更容易理解所以将同一信号线上的名字和颜色(红 色的为顶层的信号线,黑色的为底层模块的信号线)清晰的进行了划分。

fullad007

图 13‑6 模块整体框图

首先从in1和in2信号从外部模块的端口输入进来,然后连接到内部half_adder0的输入端口上,然后进入到half_adder0模块中进行运算(信号的颜色变化也代表层的变化),外部模块的输入信号in1和in2也可以取和half_adder0的输入端口不同的名字,但是为了清晰表达他们是一根线所以我 们用相同的名字,在代码编写的时候我们也遵循同样的原则。然后in1和in2经过半加器half_adder0的运算后得出cout和sum信号,此时将half_adder0的sum信号和half_adder1的in1信号连接,连接线还单独取了一个名字为h0_sum用于将sum信号的数据传到顶层(否则两个独 立模块是没有任何交集的),外部的进位信号cin和half_adder1的in2连接,经过half_adder1的加和运算后产生cout和sum信号(进入到模块内部运算的过程有点和C语言中的函数调用类似)。因为half_adder0的cout和half_adder0的cout还要再经过一个或门电路,如 果两个名字相同在顶层中出现会产生冲突,所以为了将half_adder0的cout和half_adder1的cout区别开,我们将half_adder0的cout和h0_cout连接,将half_adder1的cout和h1_cout连接后再经过或门进行运算,运算后的结果为系统的cout并输出,而ha lf_adder1输出的sum就是系统的输出了。至此,整个信号运算的加和过程就是这样进行的,以上的描述过程其实就是代码的编写过程,后面面可以结合代码再回过头来和框图进行对照分析。

在此提及一点,之前讲过数字电路中的每一个模块都相当于一个实体的“芯片”,而框图中的half_adder0和half_adder1就相当于两个不同的半加器芯片,再加上一个或门芯片就可以实现一个具有全加器功能的系统,这个“芯片”的概念结合下面这个框图我想表达是相当清晰的,我们做好的这个全加器也是一个模块 ,如有需要也可以把这个模块当成一个“芯片”用在其他的系统中。所以设计的时候我们可以把每个模块都做好,特别是具有通用性功能的模块,等用到的时候我们不必关心其内部结构是怎样的,只需知道其功能和端口信号,直接拿过来使用即可,是不是很方便。再如你设计实现了一个复杂的功能模块,而且通用性也很强,就可以做成加密 IP核(知识产权核)卖给需要的用户。所以在综合器的内部,官方也提供了很多通用的免费IP核,使我们不用再对一些通用的模块进行单独设计,后面会有单独的章节进行详细介绍。

端口列表与功能总结如表格 13‑1所示。

表格 13‑1 输入输出信号描述

信号

位宽

类型

功能描述

in1

1Bit

Input

加数1

in2

1Bit

Input

加数2

cin

1Bit

Input

前一级的进位信号

cout

1Bit

Output

三个加数求和后的进位信号

sum

1Bit

Output

三个加数的求和结果

7.3.3.2. 波形图绘制

上面的分析已经很详细了,我想大家也应该知道这里应该如何绘制波形图了。在绘制波形图前,我们还是先把如表格 13‑2所示的真值表列出,然后再根据真值表的输入与输出的对应关系画出波形图。首先输入是有三个加数,我们要表达出三种加数的任意8种组合就能够进行完全列举了,然后根据三个输入的相加关系,画出对应的进位cout信号和求和sum信号,其波形如图 13‑7所示,与真值表一一对应。

表格 13‑2 真值表

输入(input)

输出(output)

in1

in2

cin

sum

cout

0

0

0

0

0

0

0

1

1

0

0

1

0

1

0

0

1

1

0

1

1

0

0

1

0

1

0

1

0

1

1

1

0

0

1

1

1

1

1

1

fullad008

图 13‑7 信号关系波形图

7.3.3.3. 代码编写

顶层模块代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
module full_adder
(
input wire in1 , //加数1
input wire in2 , //加数2
input wire cin , //上一级的进位

output wire sum , //两个数的加和
output wire cout //加和后的进位
);

 //wire define
 //在顶层中作为half_adder_inst0的sum信号和half_adder_inst1的in1信号的中间连线
 wire h0_sum ;

 //在顶层中作为half_adder_inst0的cout信号和或门的中间连线
 wire h0_cout;

 //在顶层中作为half_adder_inst1的cout信号和或门的中间连线
 wire h1_cout;

 //------------------------half_adder_inst0------------------------
 half_adder half_adder_inst0//前面是实例化(调用)的模块的名字相当于是
 //告诉顶层我要使用来自half_adder这个模块的功能
 //后面的是在顶层中重新起的在本模块中的名字相当
 //于是这个模块具体到顶层中
 (
 .in1 (in1 ), //input in1 前面in1是相当于half_adder模块中的信号
 //(in1)顶层中的信号,然后最前面加上“.”,可以形象的
 //理解为把这两个信号线连接到一起(rtl中的实例化过程和
 //Testbench中的实例化过程是一样的,可以对比理解学习)

 .in2 (in2 ), //input in2

 .sum (h0_sum ), //ouptut sum
 .cout (h0_cout) //output cout
 );

 //------------------------half_adder_inst1------------------------
 half_adder half_adder_inst1//同一个模块可以被实例化多次(所以相同功能只设计
 //一个通用模块即可),但是在顶层的名字一定要区别开,
 //这样子才能表达出是实例化的两个相同功能的模块
 (
 .in1 (h0_sum ), //input in1
 .in2 (cin ), //input in2

 .sum (sum ), //ouptut sum
 .cout (h1_cout) //output cout
 );

 //cout:总的进位信号
 assign cout = h0_cout \| h1_cout;

 endmodule

顶层代码编写完成后还不能直接综合,否则会有如图 13‑8所示的报错提示,这是提示你找不到half_adder这个模块,也就是说你虽然实例化,但是还没有把half_adder这个模块添加到工程中,所以识别不了。所以我们还要将半加器的.v文件拷贝到全加器的rtl文件夹中,如图 13‑9所示,然后再将该文件添加到工程中。把文件添加到工程中的过程和添加Testbench文件的方式是一样的,找准文件夹的位置,按照相同的步骤添加即可。

fullad009

图 13‑8 报错提示界面

fullad010

图 13‑9 rtl文件夹

添加完half_adder后再进行综合就不会再产生报错信息了,然后我们选择“Hierarchy”标签看到顶层文件名下会有两个模块,如图 13‑10所示分别名为half_adder:half_adder_inst0和half_adder:half_adder_inst1,这种层此化关系一目了然。

fullad011

图 13‑10 顶层下的子模块

接下来我们看一下RTL代码综合出的RTL视图如图 13‑11所示,可以看到和我们最初分析设计的结构一模一样。

fullad012

图 13‑11 RTL视图(一)

我们可以双击任意一个模块,如图 13‑12所示,其内部结构就是上一章中设计的半加器。

fullad013

图 13‑12 RTL视图(二)

至此,我想学习者应该渐渐明白了这种层次化设计的思想到底是怎么一回事了,而且可以发现这种基于层次化的设计方案结构清晰明了,而且还能够实现模块的复用,甚至可以实现协同分工,十分方便高效,在大型项目中用这种方法可以极大地加快开发进程。

在以后的设计中会有很多情况下具有特定功能的模块需要再次被使用,为了方便调用我们往往把这种具有独立功能的模块做成通用的模块,日积月累,当我们的积累越来越多的时候,开发也会变得更容易了。

7.3.3.4. 仿真验证

仿真文件编写

仿真文件参考代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
module tb_full_adder();

reg in1;
reg in2;
reg cin;

wire sum ;
wire cout;

 //初始化输入信号
 initial begin
 in1 <= 1'b0;
 in2 <= 1'b0;
 cin <= 1'b0;
 end

 //in1:产生输入随机数,模拟加数1的输入情况
 //取模求余数,产生随机数1'b0、1'b1,每隔10ns产生一次随机数
 always #10 in1 <= {$random} % 2;

 //in2:产生输入随机数,模拟加数2的输入情况
 always #10 in2 <= {$random} % 2;

 //cin:产生输入随机数,模拟前级进位的输入情况
 always #10 cin <= {$random} % 2;

 //------------------------------------------------------------
 initial begin
 $timeformat(-9, 0, "ns", 6);
 $monitor("@time %t:in1=%b in2=%b cin=%b sum=%b cout=%b",
 $time,in1,in2,cin,sum,cout);
 end
 //------------------------------------------------------------

 //---------------full_adder_inst------------------
 full_adder full_adder_inst(
 .in1 (in1 ), //input in1
 .in2 (in2 ), //input in2
 .cin (cin ), //input cin

 .sum (sum ), //output sum
 .cout (cout ) //output cout
 );

 endmodule

层次化设计的仿真需要注意一些问题,这里因为半加器我们在上一章已经验证过了,是好用的,所以本章直接对顶层模块进行验证,对顶层的验证和之前对某一个模块的验证过程没有任何区别。如果之前我们没有对顶层的子模块进行验证过的话,而在这里直接验证顶层模块,子模块有可能会是错误的,这样我们在分析系统错误原因的时候往 往会变得复杂,所以推荐大家一定要先对子模块进行单独验证,这样不至于当整个设计太大的时候直接验证最顶层模块而导致错误很难找,养成好的习惯,会让你的设计越来越轻松,越来越顺利。

仿真波形分析

Testbench编写完毕后,我们开始启动ModelSim进行功能验证,同样我们也让波形跑500ns即可验证结果,通过图 13‑13所示的波形我们可以观察到3个1bit加数通过随机数函数组成的任意组合与对应的sum和cout,一个一个对应查看,发现波形中所有的输入与输出之间的对应关系和编写的代码中的逻辑关系是完全一致的。

fullad014

图 13‑13 仿真波形图

我们又通过观察“Transcript”界面(如图 13‑14所示)中打印的结果比对真值表进一步验证发现输入与输出的关系都符合全加器运算结果。

fullad015

图 13‑14 Transcript界面图

7.3.4. 上板验证

7.3.4.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 12‑3所示。引脚配置如图 13‑15所示。

表格 13‑3 引脚分配表

信号名

信号类型

对应引脚

备注

in1

Input

M2

按键

in2

Input

M1

按键

cin

Input

E15

按键

sum

Output

L7

LED灯

count

Output

M6

LED灯

fullad016

图 13‑15 引脚配置图

7.3.4.2. 结果验证

如图 13‑16所示,开发板连接12V直流电源和USB-Blaster下载器JTAG端口。线路正确连接后,打开开关为板卡上电。

fullad017

图 13‑16 程序下载连线图

如图 13‑17所示,使用“Programmer”为开发板下载程序。

fullad018

图 13‑17 程序下载窗口

程序下载完毕后,开始进行结果验证。如图 13‑18所示,当按键KEY1、KEY2同时按下,in1、in2输入均为低电平,进位cin输入为高电平,得到和sum为1,进位count为0, D7被点亮。

fullad019

图 13‑18 上板验证(一)

如图 13‑19所示,只按下按键KEY3,in1、in2输入高电平,进位cin输入低电平,得到和sum为0,进位count为1, D6被点亮。

fullad020

图 13‑19 上板验证(二)

如图 13‑20所示,同时按下三个按键,in1、in2和cin输出均为低电平,得到和sum为0,进位count为0,D6、D7被点亮。

fullad021

图 13‑20 上板验证(三)

由于篇幅原因,在本章节的结果验证中,我们进上板验证了三种情况,读者可参照真值表和波形图对剩余几种情况进行验证。

7.4. 章末总结

本章通过全加器的例子介绍了如何利用层次化的设计方法来进行设计一个项目,全加器的项目例子虽然简单,但是已经足够有代表性,在基础部分,我们对层次化设计的要求并不高,因为设计的例子都很简单,大多一个或几个模块就可以实现整个的设计,但是在强化和高级部分,会有数十个以上的模块,那时就会体会到这种层次化设计的方 法好处,也会有更多的体会,希望大家能够通过不断的学习熟练掌握这种方法。

知识点总结

1、理解层次化的设计方法,学会通过实例化RTL代码调用底层的.v文件。

7.5. 拓展训练

用一个模块实现全加器的功能,和层次化的设计方法进行对比,观察所消耗的逻辑资源是否相同。