注:本文由不愿透露姓名的
同学撰写。以下为正文。
在上一篇文章中已经介绍了简单组合逻辑在FPGA开发板上的实现,包括大家熟悉的优先编码器、多路复用器和全加器等,文章链接如下:
另外,本文的所有实例都使用硬木课堂Xilinx Aritx 7 FPGA板实现,且附有上板演示视频,该开发板的链接如下:
在设计复杂数字系统时,根据整个系统也就是顶层的功能需求进行分析,将复杂的系统功能分解为多个必要的子功能,依据这些子功能分别对各个功能模块进行设计,这些功能模块同样可以再继续分解为多个更底层的模块,这就是自顶向下的设计思想,也是目前主流的数字系统设计思想,而模块化设计就是遵循这一设计思想的重要设计方法。事实上,无论多么复杂的系统总能够逐步分解为多个小的功能模块,模块化设计可以令整个设计的思路更清晰,便于大型设计的分工合作和仿真测试,而且有助于设计文件的维护和复用。在这篇文章中,将会分享两个组合逻辑的模块化设计实例以及仿真和上板演示过程。
自顶向下的设计思想
数码管显示原理
由于本次介绍的两个实例都涉及数码管的显示问题,因此在进行实例分析之前,我们先了解一下数码管的显示原理。开发板上八段数码管的实物图如下所示,所谓八段数码管是指通过八个发光二极管按指定图形排列并封装在一起的显示器件,单个八段数码管可以显示数字0至9、字母A至F以及一个小数点。
数码管
下图是开发板上的数码管原理图,数码管内八个发光二极管的阴极连接在一起故被称为共阴数码管,六个数码管的显示由8bit的段码以及6bit的位码控制,8bit的段码由八个段选信号从低位到高位以LED A、LEDB、LED C、LED D、LED E、LED F、LEDG、 LED DP的顺序组成,6bit的位码由六个位选信号从低位到高位以DIG1、DIG2、DIG3、DIG 4、DIG5、DIG6的顺序组成。段码决定数码管显示的数字或字母,而位码决定点亮哪几位数码管。对于共阴极数码管而言,段选信号是高电平有效而位选信号是低电平有效的。一般来说,多位数码管的段选信号是连接在一起的,这可以节省数码管占用的I/O,本文演示所使用的FPGA开发板上的数码管也是如此,也就是说数码管的段码是共享的,所有点亮的数码管在同一时刻会显示相同的数字或字母。
开发板上数码管的原理图
下面举例说明一下怎么让数码管显示指定的数字或字母。例如,对于开发板上的共阴极数码管,要使从左往右第一个数码管显示数字7,就需要通过位选信号选中DIG1,使第一个数码管点亮,而为了显示数字7,根据上方的原理图,需要点亮LED A、LED B以及LED C,所以数码管的位码(低电平有效)应为6’b111110,段码(高电平有效)为8’b00000111,若用十六进制表示,段码为8’h07。类似地,如果需要使左边三个数码管同时显示字母F(十六进制的15),那么就需要通过位选信号选中DIG1、DIG2和DIG3,而为了显示字母F,就需要点亮LED A、LED E、LED F以及LED G,所以数码管的位码应为6’b111000,段码为8’01110001即十六进制的8’h71。
组合逻辑模块化设计实例1
本文的第一个实例如下:在FPGA开发板上实现一个组合逻辑电路,拨码开关SW0至SW3作为4bit的数据输入并通过数码管以十六进制显示,拨码开关SW4至SW6作为控制输入,控制输入001至110分别对应点亮数码管的DIG1至 DIG6,若控制输入为000和111,则不点亮数码管。
实例1框图
分析如上的电路功能需求,不难发现,该电路的功能可以很清晰地分为两个部分,一是根据数据输入决定数码管显示什么样的数字/字母,即对数据输入进行数码管的段码译码;二是根据控制输入决定点亮哪一位的数码管,即对控制输入进行数码管的位选译码。那么依据模块化设计的思路和上文所介绍的数码管显示原理,我们可以针对分解得到的两个功能分别设计对应的功能模块。对于底层功能模块的设计,就和上一篇文章介绍的组合逻辑基础设计一样,首先弄清楚该模块的输入输出关系,再通过硬件描述语言对电路进行描述。本例中,段码译码模块的输入为4bit的数据输入,输出为8bit的数码管段码输出,位选译码模块的输入为3bit的控制输入,输出为6bit的数码管位选输出,两者都可采用译码器进行设计。
如下是数码管段码译码模块的RTL代码,根据上文介绍的数码管显示原理,通过case语句分别给出各输入情况下对应的数码管段码输出即可。
module seg_decoder(data_disp,seg);
input [3:0] data_disp;
output reg [7:0] seg;
always@(data_disp)
case(data_disp)
4'h0 : seg = 8'h3f;
4'h1 : seg = 8'h06;
4'h2 : seg = 8'h5b;
4'h3 : seg = 8'h4f;
4'h4 : seg = 8'h66;
4'h5 : seg = 8'h6d;
4'h6 : seg = 8'h7d;
4'h7 : seg = 8'h07;
4'h8 : seg = 8'h7f;
4'h9 : seg = 8'h6f;
4'ha : seg = 8'h77;
4'hb : seg = 8'h7c;
4'hc : seg = 8'h39;
4'hd : seg = 8'h5e;
4'he : seg = 8'h79;
4'hf : seg = 8'h71;
endcase
endmodule
接下来是数码管位选译码模块的参考代码,要注意的是由于开发板上的数码管为共阴极连接,因此位选是低电平有效,需要点亮的数码管对应的位选信号应该给低电平。
module DIG_decoder(ctrl,sel);
input [2:0] ctrl;
output reg [5:0] sel;
always @ (ctrl)
case (ctrl)
3'd1 : sel = 6'b111110;
3'd2 : sel = 6'b111101;
3'd3 : sel = 6'b111011;
3'd4 : sel = 6'b110111;
3'd5 : sel = 6'b101111;
3'd6 : sel = 6'b011111;
default : sel = 6'b111111;
endcase
endmodule
子功能模块设计好后,便可以对它们进行连接以构建功能完整的系统。下面将进行顶层文件的编写,通常只需要将各个功能模块例化并进行正确的连接即可。各个模块的例化其实就和画电路图一样,要分清楚各个模块的输入输出以及它们之间的连接关系。这里提一个初学者在进行模块例化时容易犯的一个错误,就是在例化进行最后一个端口映射时括号后多带了一个逗号。Vivado给这个错误的报错信息是不能混用顺序端口连接和命名端口连接,未明确指向这个逗号,萌新可能会去检查好几遍端口名,卡好一会儿才发现原来是错在这里。
module TOP(data_disp,ctrl,seg,sel);
input [3:0] data_disp;
input [2:0] ctrl;
output [7:0] seg;
output [5:0] sel;
DIG_decoder DIG_decoder(
.ctrl(ctrl),
.sel(sel)
);
seg_decoder seg_decoder(
.data_disp(data_disp),
.seg(seg)
);
endmodule
RTL代码文件都编写好后,不妨将这些文件都加入工程中,如果代码编写正确,在默认设置下就可以在Vivado的Sources窗口的Design Sources栏下已经自动地进行了分层显示,如下图所示,可以清楚地看到设计文件的层次。
Vivado中Design Sources的分层显示
添加好设计文件后还可以在Vivado侧边栏依次点击RTL ANALYSIS→Open Elaborated Design→Schematic,得到RTL分析的电路原理图,这与我们进行功能分解时画的框图很相似,在该图中可以清晰地看到各个模块是如何进行连接的,双击这些功能模块还可以看到每个模块内的电路细节。
实例1的RTL分析电路原理图
在进行复杂数字系统设计时,应该对各个子功能模块分别进行独立的仿真测试,最后再对由这些模块构建的系统进行整体的仿真,本文的实例由于相对简单,将略去对子功能模块仿真的步骤。如下是本例的testbench参考代码:
`timescale 1ns / 1ps
module digitron_tb;
reg [3:0] data_disp;
reg [2:0] ctrl;
wire [7:0] seg;
wire [5:0] sel;
TOP uut(
.data_disp(data_disp),
.ctrl(ctrl),
.seg(seg),
.sel(sel)
);
initial begin
data_disp = 4'b0000;
ctrl = 3'b000;
end
always #20 begin
data_disp = data_disp + 1;
ctrl = ctrl + 1;
end
endmodule
下图是本例在Modelsim中的部分仿真波形,testbench中已测试了所有的输入组合,比对输入和输出结果,可见该电路实现了正确的功能。
实例1的仿真波形
完成仿真后,就可进行后续的综合、管脚约束、布局布线以及生成比特流文件,根据实例1的功能需求,本例具体的管脚约束如下表所示。
实例1管脚约束
下面为一段该实例的上板演示视频。
实例1演示视频
组合逻辑模块化设计实例2
本文的第二个实例如下:在FPGA开发板上实现一个组合逻辑电路,拨码开关SW0至SW3为第一个数据输入ina,拨码开关SW4至SW7为第二个数据输入inb,由数码管以十六进制形式显示ina与inb中较大的那个数,且ina较大时由右边三位数码管一起显示,inb较大时由左边三位数码管一起显示,两个输入相等时由六位数码管共同显示该数据。
实例2框图
这里同样采用模块化设计的思路,首先对电路进行功能分解,可以看到整个电路的输入是两个数据输入ina和inb,输出仍然是数码管的段码和位选信号。不难分析出,该例从输入到输出有一个比较、选择和显示译码的过程。有了实例一的经验,既然涉及多位数码管的显示,那么必然需要由数码管段码译码模块和位选译码模块来负责显示译码的功能,两者都可以通过设计译码器来解决。选择模块也很简单,其功能是从两个数据输入中选择其一进行后续的显示译码,通过上一篇文章也介绍过的多路复用器即可实现。最后再分析比较模块,它的功能应该是比较两个数据输入的大小,并输出比较的结果,这很容易联想到数字电路中学习过的比较器。
下面是比较器的参考代码,实现对两个4bit的输入数据进行比较,并通过高电平有效的LT(表示ina<inb)、GT(表示ina>inb)、EQ(ina = inb)三个信号输出比较的结果。
module comparator(ina,inb,LT,GT,EQ);
input [3:0] ina;
input [3:0] inb;
output LT;
output GT;
output EQ;
assign LT = (ina < inb) ? 1'b1 : 1'b0;
assign GT = (ina > inb) ? 1'b1 : 1'b0;
assign EQ = (ina == inb) ? 1'b1 : 1'b0;
endmodule
如下是选择模块的参考代码,通过多路复用器从两路4bit的输入数据中选择一路进行输出。根据这种写法,选择信号为0时输出ina,选择信号为1时输出inb,因此根据本例的功能需求,在连接时需要将比较器的小于输出信号LT连到该模块的选择信号输入端口。
module Mux(ina,inb,sel,data_out);
input [3:0] ina;
input [3:0] inb;
input sel;
output [3:0] data_out;
assign data_out = sel ? inb : ina;
endmodule
数码管的段码译码模块与实例1完全一致,这里仅给出数码管位选译码的参考代码。根据本例的功能需求,数码管的位选根据ina与inb的三种大小关系(ina>inb, ina<inb,ina=inb)有三种不同的数码管位选结果(右边三位点亮,左边三位点亮,六位全部点亮),那么该译码模块至少需要两位的输入编码,下面的代码编写时已提前设想好通过将比较器的等于输出信号EQ和大于输出信号GT作为译码器的2bit编码输入。
module DIG_decoder(ctrl,sel);
input [1:0] ctrl;
output reg [5:0] sel;
always@(ctrl)
case(ctrl)
2'b00 : sel = 6'b111000;
2'b01 : sel = 6'b000111;
2'b10 : sel = 6'b000000;
default : sel = 6'b111111;
endcase
endmodule
最后是顶层文件的编写,需要注意各模块间的连接,尤其是比较器输出的连接,在上文已进行过详细的说明。
module TOP(ina,inb,seg,sel);
input [3:0] ina;
input [3:0] inb;
output [7:0] seg;
output [5:0] sel;
wire LT;
wire GT;
wire EQ;
wire [3:0] data_disp;
comparator comparator(
.ina(ina),
.inb(inb),
.LT(LT),
.GT(GT),
.EQ(EQ)
);
Mux Mux(
.ina(ina),
.inb(inb),
.sel(LT),
.data_out(data_disp)
);
DIG_decoder DIG_decoder(
.ctrl({EQ,GT}),
.sel(sel)
);
seg_decoder seg_decoder(
.data_disp(data_disp),
.seg(seg)
);
endmodule
这里我们同样可以借助Vivado的RTL分析功能得到对应RTL代码的电路原理图,如下图所示,能直观地看到四个子模块及其连接方式。在仿真之前,可以使用这个功能检查一下子模块的连接。
实例2的RTL分析电路原理图
接下来是对设计进行功能仿真,下面给出了实例2的testbench参考文件,设置了几组不同大小关系的输入组合。
`timescale 1ns / 1ps
module digitron_tb;
reg [3:0] ina;
reg [3:0] inb;
wire [7:0] seg;
wire [5:0] sel;
TOP uut(
.ina(ina),
.inb(inb),
.seg(seg),
.sel(sel)
);
initial begin
ina = 4'h0; inb = 4'h0; //ina = inb
#20
ina = 4'h5; inb = 4'h2; //ina > inb
#20
ina = 4'h7; inb = 4'h9; //ina < inb
#20
ina = 4'h6; inb = 4'h6; //ina = inb
#20
ina = 4'he; inb = 4'ha; //ina > inb
#20
ina = 4'hb; inb = 4'hf; //ina < inb
#20
$stop;
end
endmodule
使用上述testbench得到的部分仿真波形如下,这里除了顶层的输入输出信号外还添加了中间信号比较器的输出波形进行观察,可以看到比较器输出正常,数码管的段码与位选也译码正确。
实例2的仿真波形
之后进行综合、管脚约束、布局布线以及生成比特流文件。这里同样给出了本例具体的管脚约束,根据要求,将两个4bit的数据输入分别约束至了拨码开关SW0至SW3以及SW4至SW7,输出约束至数码管的段选和位选即可。
实例2管脚约束
最后给出实例2的上板演示视频如下。
实例2演示视频
本文小结
至此,两个组合逻辑的模块化设计实例就分享完毕了,这里简单总结一下,模块化设计是设计复杂数字系统的重要方法,对复杂设计的分工、仿真测试以及代码的维护和复用都有好处。进行模块化设计时,首先要明确电路的功能需求,将复杂的整体功能逐步分解为相对简单的子功能,然后根据分解得到的子功能来设计各个功能模块,最后将各个功能模块进行例化连接,构建功能完整的系统。
这里引用笔者导师的一段话来结尾:如何将复杂的功能划分成若干小的功能模块和子系统是一种能力,是FPGA/逻辑设计工程师真正的核心竞争力。这种能力的培养一方面依赖于对于逻辑设计方法的掌握,另外一方面来自于对于所面向的行业应用的理解。希望大家在后续的学习中要注意训练自己的这种能力,我们也将在后续的文章中不断地强调这一点。
END
知乎:https://zhuanlan.zhihu.com/p/165628931
推荐阅读
更多内容请关注其实我是老莫的网络书场专栏