FPGA Verilog开发实战指南:基于Intel Cyclone IV(基础篇)
上QQ阅读APP看书,第一时间看更新

2.6 FPGA内部硬件结构与代码的关系

注意:本节主要讲解FPGA内部硬件结构与代码的关系,为了能够深入理解本节相关知识,读者可在完成“基础入门篇”的学习后再阅读本节。

代码和硬件之间的映射是一个很奇妙的过程,也展现出人类的智慧。单片机内部的硬件结构都是固定的,无法改变,我们通过代码操作着内部寄存器的读写,进而执行各种复杂的任务。FPGA的硬件结构并不像单片机一样是固定不变的,而是由更加原始的基本逻辑单元构成的,我们需要用HDL语言来描述要实现的功能,而并不需要关心硬件的结构是如何构建的。我们通过使用FPGA厂商的综合器来将HDL所描述的功能代码映射到FPGA基本逻辑单元上,而这个映射的过程是综合器自动完成的,我们并没有直接用语言去操作这些基本逻辑单元,这样也就可以理解为什么HDL叫硬件描述语言,而不是硬件语言了。

我们使用Verilog语言来描述功能,用Altera CycloneIV系列的EP4CE10F17C8芯片来验证下面的例子,观察我们编写的Verilog代码综合后到底映射到了哪些硬件结构上。我们通过查看RTL Viewer、Technology Map Viewer(Post Mapping)、Chip Planner来得出验证分析结果。

▪ RTL Viewer:寄存器级的视图,包括原理图视图,也包括层次结构列表,列出整个设计网表的实例、基本单元、引脚和网络。主要体现的是逻辑连接关系和模块间的结构关系,和具体的FPGA器件无关。

▪ Technology Map Viewer(Post Mapping):将RTL所表达的结构进行优化,增加或减少一些模块,包括一个原理视图以及一个层次列表,列出整个设计网表的实例、基本单元、引脚和网络。更接近于最后底层硬件映射的结果,以便于映射到具体的FPGA器件上。

▪ Chip Planner:可以看作版图模型,用于查看编译后布局布线的详细信息,显示器件的所有资源,例如,互连和布线连线、逻辑阵列块(LAB)、RAM块、DSP块、I/O、行、列以及块与互连和其他布线连线之间的接口、真实地表达所使用的资源,以及在芯片中的相对位置信息。还可以实现对逻辑单元、I/O单元或PLL基元的属性和参数进行编译后编辑,而无须执行完整的重新编译。

2.6.1 I/O的映射

给一个输入信号,然后不进行任何逻辑运算直接输出,示例代码参见代码清单2-1。

代码清单2-1 I/O的映射示例代码(line.v)


 1 module  line
 2 (
 3     input   wire        in,
 4 
 5     output  wire        out
 6 );
 7 
 8 assign out = in;
 9 
10 endmodule

代码编写完后点击“Start Analysis & Synthesis”图标进行分析和综合,如图2-29所示。

图2-29 分析与综合

(1)查看RTL Viewer

双击“Netlist Viewers”下的“RTL Viewer”,查看RTL视图,如图2-30所示。

图2-30 查看RTL视图

因为代码就是对RTL级层次的描述,所以RTL视图只有一根连线,这和我们设计代码的思想是完全一致的,如图2-31所示。

图2-31 RTL视图

(2)查看Technology Map Viewer(Post Mapping)

双击“Netlist Viewers”下的“Technology Map Viewer(Post.Mapping)”,如图2-32所示。

图2-32 查看Technology Map视图

可以看到和RTL视图不同的是,输入端口和输出端口分别加上了buffer,这是我们代码中没有的,是综合器优化后自动加上的,如图2-33所示。

图2-33 综合器优化

双击图2-33中的IO_IBUF和IO_OBUF可以看到图2-34中三角形的缓冲器。

图2-34 显示三角形缓冲器

(3)查看Chip Planner

点击“Start Compilation”图标进行全编译,此过程中会进行布局布线,如图2-35所示。

图2-35 全编译

点击“Chip Planner”图标打开版图模型,在“Flow Summary”报告中也可以看到全编译后的详细信息,其中只使用了两个引脚资源,如图2-36所示。

图2-36 查看芯片版图模型

Chip Planner打开后的界面如图2-37所示,我们要找到代码最后映射到版图模型中的具体位置,可以在右上角的“Find what”处搜索定位,如果没有找到“Find what”,那么按住键盘上的“Ctrl + F”快捷键就会自动出现。

图2-37 芯片版图模型

在“Find what”中搜索RTL代码中的信号名“in”,然后点击“List”按钮,如图2-38所示。

图2-38 搜索端口(一)

继续点击“Go Next”按钮,如图2-39所示。

图2-39 搜索端口(二)

方框区域中深色的小矩形块就是输入信号“in”映射到版图模型中的位置,如图2-40所示。

图2-40 输入信号对应版图模型

在选中“in”所映射的模块的前提下点击图中左侧“Generate Fan-Out Connections”图标可以将从该位置扇出的连线显示出来,一直点击此扇出线就会一直追踪下去,如图2-41所示。

图2-41 映射端口连线(一)

我们看到连线从“in”处开始,到“out”结束,并将“in”和“out”连到一起,这也就说明了代码中的“in”和“out”之间确实是用一根导线连接的。图2-42中的①相当于外部引脚输入的信号经过内部连线,从引脚②输出到外部。分别双击①和②打开模块内部观察其映射的结构。

图2-42 映射端口连线(二)

我们看到了一个完整的IOE内部结构,其中粗实线部分显示的是真实使用到的结构,灰色的是未使用到的结构。图2-43是IOE输入的内部结构,图2-44是IOE输出的内部结构。

图2-43 IOE输入内部结构

图2-44 IOE输出内部结构

我们根据信号的流向进行标号,如图2-45和图2-46所示。①和⑦是PAD,为硅片的引脚,封装在芯片内部,用户看不到,PAD的输入输出往往和外部的引脚有一段连线。②和⑥分别是输入缓冲器和输出缓冲器,我们在Technology Map Viewer(Post-Mapping)视图中就已经看到过,这里的功能和之前的是一样的,只是具体的实现结构不同。③是一个输入延时模块,用来调节输入信号的相位延时(在静态时序分析中会详细讲解其使用方法),右击该模块可以设置延时的时间,这是综合布局布线工具自动添加的(当输入信号绑定到时钟引脚上时就不会自动连接到输入延时模块上),并不是我们在RTL代码中设计的。④⑤⑧是和外部引脚以及其他层连接的接触点,可以理解为PCB中的过孔,⑧用于连接到和外部信号输入的引脚上,④和⑤在内部通过导线连接到一起。

图2-45 信号流向(一)

图2-46 信号流向(二)

上面的操作我们并没有进行引脚的绑定约束,是开发工具自动给分配了到一个任意位置的引脚,如果约束了具体的引脚,那么其在Chip Planner中映射的位置还会变化,但结构基本相同。

综合器在帮我们自动完成综合和布局布线的过程中会根据我们的HDL代码与实际的功能来做一些适当的优化,这些优化是为了让整个映射后的硬件更加适配具体的FPGA器件,所以有些时候我们用HDL描述的功能并不是我们所认为的会使用基本逻辑单元,而是进行了优化后的结果,这些优化包括面积的优化、速度的优化、功耗的优化、布局布线的优化、时序的优化等。FPGA开发工具同样也给用户预留了一些可供用户优化的选项设置,但这都要在用户能够熟练掌握开发工具和内部结构的前提下才能够实现。

2.6.2 组合逻辑映射

大家可能会有这样的疑问,我们编写的Verilog代码最终会在FPGA上以怎样的映射关系来实现功能呢?我们以一个最简单的组合逻辑与门为例来向大家说明,参见代码清单2-2。

代码清单2-2 组合逻辑映射示例代码(and_logic.v)


 1 module  and_logic(
 2   input   wire  in1 , 
 3   input   wire  in2 , 
 4 
 5   output  wire  out   
 6 );
 7 
 8 //out:输出in1与in2相与的结果
 9 assign  out = in1 & in2;
10 
11 
12 endmodule

编写完代码后依然需要点击“Start Analysis & Synthesis”图标进行分析和综合。然后双击“Netlist Viewers”下的“RTL Viewer”查看RTL视图,可以看到两个输入信号经过一个与门后输出,如图2-47所示,和我们代码设计的结果是完全一致的。

图2-47 RTL视图

点击“Start Compilation”全编译图标进行布局布线,然后打开Chip Planner视图,界面如图2-48所示,可以看到在版图模型中有一块区域的颜色变深,说明该区域的资源被占用,由2.5节的内容我们知道这是一个逻辑阵列块,将该区域放大,放大后如图2-49所示,可以看到颜色变深的区域中有16个小块,这16个小块就是LE,其中只有一个LE的颜色改变了,说明该处的资源被使用了,双击这个LE即可观察其内部的结构。

图2-48 芯片版图模型

图2-49 LE版图模型

打开LE后,其内部的结构如图2-50所示,其中黑色实线显示的是真实使用到的结构,灰色的是未使用到的结构,可以看到有两个输入和一个输出,与RTL代码的描述是对应的,粗实线方框处就是查找表(LUT)。

图2-50 LE内部结构图

大家可能还是不理解LUT是如何实现与逻辑的。我们先来看一下与逻辑的真值表,如表2-1所示。

表2-1 与逻辑真值表

根据真值表可以看出输入有2个,2个输入对应的输出共有4种情况,LUT需要做的工作就是根据输入的变化对应输出正确的值。我们可以在LUT中预先存储所有输出的4种情况,然后判断输入,对应输出就可以了。LUT的内部结构在Chip Planner中并没有表达出来,但是我们可以推断出来,如图2-51所示为与门所对应的LUT内部结构图,其中LUT中存储的是4种输出情况,输入信号in1和in2通过多路器选择存储在LUT中的值输出。图2-51中展示的是当in1和in2输入的值都为1时,存储在LUT中的“1”从标注的加粗路径中输出到out,LUT中存储的值会在综合工具综合时进行映射。这里不难看出LUT所充当的角色其实就是RAM,所以我们也可以用LUT来构成小规模的RAM用于存储数据。LUT所构成的RAM就是我们常说的Distribute RAM,简称DRAM。

图2-51 LUT内部结构图

2.6.3 时序逻辑映射

了解了组合逻辑和FPGA之间的映射关系,那么时序逻辑和FPGA之间又是一种怎样的映射关系呢?先来看一下同步复位D触发器的RTL代码,具体参见代码清单2-3。

代码清单2-3 同步复位D触发器示例代码(flip_flop.v


 1 module  flip_flop(
 2   input   wire  sys_clk , 
 3   input   wire  sys_rst_n , 
 4   input   wire  key_in  , 
 5     
 6   output  reg led_out     
 7 );
 8 
 9 //led_out:LED灯输出的结果为key_in按键的输入值
10 always@(posedge sys_clk) 
11   if(sys_rst_n == 1'b0)                     
12     led_out <= 1'b0;
13   else
14     led_out <= key_in;
15  
16 endmodule

编写完代码后依然需要点击“Start Analysis & Synthesis”图标进行分析和综合,然后双击“Netlist Viewers”下的“RTL Viewer”查看RTL视图。如图2-52所示,可以看到一个D触发器的结构,也可以称为寄存器,还附加了一个选择器,用于同步复位的控制,和代码设计的结果是完全一致的。

图2-52 RTL视图

点击“Start Compilation”全编译图标进行布局布线,完成后可以看到“Flow Summary”资源使用量,如图2-53所示,使用了LE中的一个组合逻辑资源和一个时序逻辑资源。

图2-53 资源使用量

然后打开Chip Planner视图,如图2-54所示,可以看到在版图模型中有一个区域的颜色变深,说明该区域的资源被占用,我们将该区域放大。

图2-54 芯片版图模型

放大后如图2-55所示,可以看到变深的区域中有16个小块,这16个小块就是LE,其中只有一个LE的颜色发生了变化,这次不仅有蓝色,还有红色,说明该处的资源被使用了,双击这个LE即可观察其内部的结构。

图2-55 LE版图模型局部放大图

打开LE后内部的结构如图2-56所示,其中黑色细实线部分显示的是真实使用到的结构,灰色的是未使用到的结构,我们可以看到①、②、③为三个输入,其中③为时钟的输入端,然后有一个输出,和RTL代码的描述是对应的。因为我们设计的是时序逻辑,所以这次我们可以发现比组合逻辑多出来的结构主要是粗实线方框所表示的寄存器。

图2-56 LE内部结构图

在该视图中点击下面复位信号的名称后,会看到在LE的内部结构图中用粗实线标注路径,如图2-57中粗实线部分所示,可以知道①为复位信号的输入端,②为key_in信号的输入端。

图2-57 LE内部结构图输入输出端

我们再来看一下异步复位D触发器的RTL代码,如代码清单2-4所示。

代码清单2-4 异步复位D触发器示例代码(flip_flop.v


 1 module  flip_flop(
 2   input   wire  sys_clk , 
 3   input   wire  sys_rst_n , 
 4   input   wire  key_in  , 
 5     
 6   output  reg led_out     
 7 );
 8 
 9 //led_out:LED灯输出的结果为key_in按键的输入值
10 always@(posedge sys_clk or negedge sys_rst_n) 
11   if(sys_rst_n == 1'b0)                    
12     led_out <= 1'b0;
13   else
14     led_out <= key_in;
15  
16 endmodule

编写完代码后依然需要点击“Start Analysis & Synthesis”图标进行分析和综合。然后双击“Netlist Viewers”下的“RTL Viewer”查看RTL视图。如图2-58所示,可以看到一个D触发器的结构,和代码设计的结果是完全一致的。

图2-58 RTL视图

点击“Start Compilation”全编译图标进行布局布线,完成后可以看到“Flow Summary”资源使用量报告,如图2-59所示,只使用了LE中的一个时序逻辑资源。

图2-59 资源使用量

打开Chip Planner视图,如图2-60所示,可以看到在版图模型中同样的位置也有一个区域的颜色变深,说明该区域的资源被占用,将该区域放大。

图2-60 芯片版图模型

放大后如图2-61所示,可以看到变深的区域中有16个LE,其中只有一个LE的颜色发生了变化,也有蓝色和红色,说明该处的资源被使用了,双击这个LE即可观察其内部的结构。

图2-61 LE版图模型放大图

打开LE后,其内部结构如图2-62所示,其中黑色实线部分显示的是真正用到的结构,灰色的是未使用到的结构,我们可以看到①、②、③为三个输入,其中③为时钟的输入端,②仍为key_in的输入端,而复位信号①的位置则发生了变化,直接连到了寄存器上。

图2-62 LE内部结构图

看到这里我们不禁会有两个疑问:

▪ 异步复位D触发器LE内部结构图明明显示使用了查找表,为什么在“Flow Summary”资源使用量报告中却显示没有使用该部分资源?

▪ 为什么同步复位D触发器比异步复位D触发器多使用了一部分资源呢?

首先看第一个问题,虽然异步复位D触发器LE内部结构图明明显示使用了LUT,但是几乎没有任何逻辑需要使用LUT,相当于通过查找表将key_in信号连接到寄存器的输入端,所以在“Flow Summary”资源使用量报告中显示没有使用该部分资源。

再来看第二个问题,如图2-63所示,我们将寄存器部分的视图放大,可以发现该寄存器本身就包含一个异步清零信号“aclr”,且该清零信号还标识为“!ACLR”,也就是低电平有效,这下我们终于明白为什么代码中使用异步低复位了,因为这部分资源本来就有,不需要额外创造,而如果我们使用同步高复位,就会增加额外的逻辑,需要使用LUT资源,所以同步复位D触发器比异步复位D触发器多使用了一个LUT。如果使用很多同步复位D触发器,就会占用很多不必要的LUT资源,从而造成资源的浪费,大家在编写代码时要注意这一点。

图2-63 D触发器

在FPGA的开发中,理想情况下FPGA之间的数据要通过寄存器输入、输出,这样才能使延时最小,从而更容易满足建立时间方面的要求。我们由FPGA内部硬件结构得知,IOB内是有寄存器的,且IOB内的寄存器比FPGA内部的寄存器更靠近外部的输出引脚,这样就能够得到更小的延时,从而使时序更好。在没有进行指定的情况下寄存器的映射都是随机的,那么问题来了,如何才能指定寄存器映射到IOB中呢?我们依然用异步复位D触发器的例子来演示。

如图2-64所示,回到工程界面点击“Assignment Editor”图标来约束寄存器映射的位置。

图2-64 约束寄存器映射位置

如图2-65所示,在打开的“Assignment Editor”界面中点击“To”下面的“<<new>>”添加要约束的项。

图2-65 添加约束项

点击如图2-66所示的望远镜图标,打开“Node Finder”。

图2-66 打开“Node Finder”

在打开的“Node Finder”对话框中找到信号的输入key_in,如图2-67所示,根据序号顺序,在“Named :”选项框中输入“*”,点击“List”按钮,在“Nodes Found :”列表中就会列出名为key_in的信号,双击③处的key_in信号或点击按钮,key_in信号就被添加到“Selected Nodes:”中了。如果我们想取消⑤处选择的信号,则在“Selected Nodes:”选中该信号后点击按钮即可。设置完毕后点击“OK”按钮退出。

图2-67 设置Node Finder选项

如图2-68所示,设置“Assignment Name”,在下拉列表中找到“Fast Input Register (Accepts wildcards/groups)”,这是设置将寄存器映射在输入IOB中的约束。如果设置将寄存器映射在输出IOB中,则选择“Fast Output Enable Register(Accepts wildcards/groups)”。

图2-68 将寄存器映射在输出IOB中

如图2-69所示,设置“Value”的值为“On”。

图2-69 设置“Value”

全部设置完成后的结果如图2-70所示。

图2-70 完成设置

点击“Start Compilation”全编译图标进行布局布线,否则无法重新映射资源。此时会弹出如图2-71所示的对话框,提示是否要保存更改,选择“Yes”后会执行布局布线操作。

图2-71 保存更改项

当布局布线重新完成映射后,我们再来看Chip Planner视图,如图2-72所示,我们发现整个视图没有明显的变化,难道是映射失败了?

图2-72 芯片版图模型

如图2-73所示,既然不能用肉眼直接看到,那我们可以在Chip Planner界面右上角的“Find what:”处搜索定位信号在版图模型中的位置,如果没有找到“Find what”搜索框,按“Ctrl + F”快捷键就会自动出现。

图2-73 使用“Find what:”搜索框

在图2-74所示的“Find what:”中搜索RTL代码中的信号名“key_in”,然后点击“List”按钮。

图2-74 搜索“key_in”

点击图2-75所示的“key_in”,可以看到在版图模型的对应位置高亮显示,这个位置就是FPGA的IOB区域。

图2-75 定位信号位置

如图2-76所示,将映射的IOB区域放大,其中①为key_in的输入端,②则是寄存器所映射的新位置。

图2-76 IOB区域放大图

双击图2-76中②处的寄存器,观察其内部结构,如图2-77所示,发现IOB中的寄存器已经高亮显示了,说明映射成功,达到了我们的要求。

图2-77 IOB内部结构图

2.6.4 指定PLL的映射位置

既然可以指定寄存器放在IOB内,那同样也可以指定PLL的位置。首先要确保有多个PLL。如图2-78所示,我们所使用的EP4CE10F17C8芯片刚好有两个。

图2-78 器件资源

为了演示这个例子,我们使用pll工程,RTL代码具体参见代码清单2-5。

代码清单2-5 pll示例代码(pll.v)


 1 module  pll
 2 (
 3     input   wire    sys_clk      , //系统时钟(50MHz)
 4 
 5     output  wire    clk_mul_2    , //系统时钟经过2倍频后的时钟
 6     output  wire    clk_div_2    , //系统时钟经过2分频后的时钟
 7     output  wire    clk_phase_90 , //系统时钟经过相移90°后的时钟
 8     output  wire    clk_ducle_20 , //系统时钟变为占空比为20%的时钟
 9     output  wire    locked         //检测锁相环是否已经锁定
10                                    //只有该信号为高时,输出的时钟才是稳定的
11 );
12 
13 //********************************************************************//
14 //*************************** Instantiation **************************//
15 //********************************************************************//
16 
17 //------------------------pll_ip_inst------------------------
18 pll_ip  pll_ip_inst
19 (
20     .inclk0     (sys_clk        ),      //input     inclk0
21 
22     .c0         (clk_mul_2      ),      //output    c0
23     .c1         (clk_div_2      ),      //output    c1
24     .c2         (clk_phase_90   ),      //output    c2
25     .c3         (clk_ducle_20   ),      //output    c3
26     .locked     (locked         )       //output    locked
27 );
28 
29 endmodule

代码编写完后依然需要点击“Start Analysis & Synthesis”图标进行分析和综合。然后双击“Netlist Viewers”下的“RTL Viewer”查看RTL视图,如图2-79所示。

图2-79 RTL视图

点击“Start Compilation”全编译图标进行布局布线,然后打开Chip Planner视图,界面如图2-80所示,可以看到在版图模型中左下角有一块颜色变深的区域,与之形成鲜明对比的是右上角颜色没有变深的位置,这就是我们FPGA芯片中两个PLL的位置,颜色变深的区域说明资源被占用。

图2-80 芯片版图模型

放大并点击该PLL,如图2-81所示,可以在右侧看到该PLL的结构图中显示的部分高亮信号,下面的“Location”则显示了该PLL的名字为“PLL_1”。

图2-81 PLL版图模型放大图

如图2-82所示,选中该PLL后点击左侧的图标,显示扇入/扇出线路径,可以看到PLL在芯片内的连接关系。

图2-82 扇出时钟线

如图2-83所示,我们回到工程界面点击“Assignment Editor”图标来约束PLL的位置。

图2-83 约束PLL位置

如图2-84所示,在打开的“Assignment Editor”界面中点击“To”下面的“<<new>>”添加要约束的项。

图2-84 添加约束项

在打开的“Node Finder”对话框中找到信号的输入key_in,如图2-85所示,根据序号顺序,在“Named :”选项框中输入“*pll*”,点击②处的“List”按钮,在“Node Found :”列表中就会列出名为altpll:altpll_component的信号,双击③处的altpll:altpll_component信号或点击按钮,altpll:altpll_component信号就被添加到“Selected Nodes:”中了。如果我们想取消⑤处选择的信号,则在“Selected Nodes:”选中该信号后点击按钮即可。设置完毕后点击“OK”按钮退出。

图2-85 设置“Node Finder”对话框

如图2-86所示,设置“Assignment Name”,在下拉列表中找到“Location(Accepts wildcards/ groups)”,这是设置位置的约束。

图2-86 设置位置约束

如图2-87所示,点击“Value”下的“...”按钮。

图2-87 点击“...”按钮开始设置Value

如图2-88所示,在弹出的“Location”对话框中的“Element:”中选择“PLL”。可以看到在这里还可以设置其他元素的位置。

图2-88 在“Element:”中选择“PLL”

如图2-89所示,在“Location:”中选择“PLL_2”。

图2-89 在“Location:”中选择“PLL_2”

如图2-90所示,“Location”对话框设置完毕后点击“OK”按钮。

图2-90 完成“Location”对话框的设置

全部设置完成后的结果如图2-91所示。

图2-91 完成设置

点击“Start Compilation”全编译图标进行布局布线,否则无法重新映射资源。此时会弹出如图2-92所示的对话框,提示是否要保存更改,点击“Yes”按钮后会执行布局布线操作。

图2-92 保存更改项

当布局布线重新完成映射后我们再来看一看Chip Planner视图,如图2-93所示,可以发现在版图模型的右上角有一块颜色变深的区域,与左下角颜色没有变深的位置形成鲜明对比,颜色变深的区域说明资源被占用。

图2-93 芯片版图模型

放大并点击该PLL,如图2-94所示,可以在右侧看到该PLL的结构图中显示的部分高亮信号,下面的“Location”则显示了该PLL的名字为“PLL_2”,说明映射成功。

图2-94 映射成功

如图2-95所示,选中该PLL后点击左侧的图标显示扇入/扇出线路径,可以看到PLL在芯片内的连接关系。

图2-95 扇出时钟线

修改PLL映射位置的意义何在?当某些情况下时序不好时,就可以通过修改PLL的映射位置来调整时序,以实现时序的收敛。