注:本文由不愿透露姓名的
同学撰写。以下为正文。
在之前的文章中已经介绍过了FPGA开发板以及EDA工具的使用,文章链接如下:
从这篇文章开始,我们将介绍和分享一系列的基础实例,期望能帮助一些读者逐步走近FPGA。本篇文章的演示视频均使用硬木课堂Xilinx Aritx 7 FPGA板实现,链接如下:
数字逻辑电路分为组合逻辑电路和时序逻辑电路,组合逻辑电路的输出仅取决于当前的输入,其逻辑功能的实现不需要时钟的参与,因此弄清楚组合逻辑电路的输入输出关系尤为重要。这次的文章将通过几个基础的实例介绍FPGA开发板上组合逻辑电路的实现,这些实例包括在数字逻辑设计课程中所熟知的部分中规模集成电路:优先编码器、多路复用器以及加法器,最后还将介绍算术逻辑单元ALU的实现。
优先编码器
在数字系统中,有时候需要将输入的数据信息变换为某种特定的编码输出,编码器便是实现这一编码功能的逻辑电路。编码器的逻辑功能是将输入的高低电平信号转换为二进制编码输出,通常是将多比特的输入数据转换为少比特的二进制编码输出。而另一个常用的组合逻辑电路有译码的功能,即译码器,其逻辑功能是编码的逆过程,通常是将少比特的输入编码翻译为多比特的数据信息输出。由于两者的实现方式非常类似,这里仅以编码器中的优先编码器为例介绍一下其在FPGA开发板上的实现过程。
编码器与译码器
如之前的文章已经提到,FPGA开发流程中的第一步是编写RTL代码进行电路描述,对于简单的组合逻辑电路来说,只要弄清楚了输入输出关系,RTL代码的编写就变得相对轻松。这里将通过一个4-2优先编码器的真值表来回顾一下该组合逻辑电路的输入输出关系。该优先编码器有4个比特的高电平有效的数据输入,2个比特的编码输出以及一个用于标识输入是否有效的输出信号GS。该优先编码器的功能是根据优先级最高的有效输入位,将输入转换为对应的二进制编码输出,而当所有输入位都无效时,GS信号会输出为0,表示无有效输入。
在硬件描述语言Verilog HDL中,多路分支语句case可以很好的对应真值表来对组合逻辑电路进行描述。如下的参考代码为对应该真值表的优先编码器的一种描述方式,这里使用了casex语句,在该语句中,若分支表达式的某些值为z或不定值x,将忽略对这些位的比较。
module priority_encoder(Data,Code,GS);
input [3:0] Data;
output reg [1:0] Code;
output GS;
assign GS = Data ? 1'b1 : 1'b0;
always@(Data)
casex(Data)
4'b1xxx : Code = 2'b11;
4'b01xx : Code = 2'b10;
4'b001x : Code = 2'b01;
4'b0001 : Code = 2'b00;
default : Code = 2'b00;
endcase
endmodule
当然,也可以使用嵌套的条件语句if-else来对优先编码器进行描述,其参考代码如下。
module priority_encoder(Data,Code,GS);
input [3:0] Data;
output reg [1:0] Code;
output GS;
assign GS = Data ? 1'b1 : 1'b0;
always@(Data) begin
if(Data[3]) Code = 2'b11;
else if(Data[2]) Code = 2'b10;
else if(Data[1]) Code = 2'b01;
else if(Data[0]) Code = 2'b00;
else Code = 2'b00;
end
endmodule
在编写好RTL代码后就需要进行RTL级的功能验证,即编写好testbench并使用仿真工具进行仿真。对于简单的组合逻辑电路,编写testbench时只需要将输入的所有组合方式枚举一遍,然后观察各种输入组合下的输出情况即可。以下是testbench的参考代码。通过每20ns对输入Data加1(输入数据Data将周期性地按照 0000 0001 0010 … 1111序列进行变化),就可以观察仿真波形得到所有输入组合方式下的输出情况。
`timescale 1ns / 1ps
module priority_encoder_tb;
reg [3:0] Data;
wire [1:0] Code;
wire GS;
priority_encoder uut(
.Data(Data),
.Code(Code),
.GS(GS)
);
initial Data = 0;
always #20 Data = Data + 1;
endmodule
下图是在Modelsim软件中的得到的部分仿真波形,可以观察到不同输入组合方式下的输出与真值表完全一致。
优先编码器波形
仿真完成后,可参考下表进行管脚约束,具体的两种约束方法都在之前的文章详细讲述过,这里不再赘述。
按照流程,之后将通过Vivado进行综合、布局布线并最后生成可下载到FPGA上的比特流,具体的流程已在走进FPGA的第二篇文章中详细介绍过,上面提到的管脚约束的具体方法也在这篇文章中进行过介绍,以下是该文章的链接:
将比特流下载到FPGA开发板上以后,便可以观察上板的现象,这里将通过一个短视频给出优先编码器的上板演示。上板时已按照上文中的表格进行了管脚约束,将数据输入约束到了拨码开关的SW0至SW3,编码输出由LED0和LED1显示,GS信号由LED11显示。
优先编码器
多路复用器
多路复用器也叫数据选择器,如下图所示,是根据选择信号从多个数据输入中选择其中一个进行输出,是数字系统中应用非常广泛的一种逻辑电路。
多路复用器
如下是一个典型的四选一多路复用器的真值表,其逻辑功能是从四个输入中选择一个输出,因此选择信号需要两个比特。
和之前的优先编码器类似,有了真值表便清楚了多路复用器的输入输出关系,编写RTL代码进行电路描述时只需要用硬件描述语言翻译该真值表即可,如下为四选一多路复用器的参考代码,使用了case语句在过程块中描述多路复用器的逻辑功能,代码中的过程块使用了always@(*)这种写法,该语句的always块中任一输入信号发生改变都将触发该过程块的执行,当敏感信号较多的时候推荐使用这种方便的写法。
module mux4(data_in,sel,data_out);
input [3:0] data_in;
input [1:0] sel;
output reg data_out;
always@(*)
casex(sel)
2'b11 : data_out = data_in[3];
2'b10 : data_out = data_in[2];
2'b01 : data_out = data_in[1];
2'b00 : data_out = data_in[0];
endcase
endmodule
这里还提供了另一种RTL代码的参考写法,没有使用多路分支语句,而是使用连续赋值语句assign和条件操作符?:来描述多路复用器的功能,虽然看似有点花里胡哨但也是可行的。
module mux4(data_in,sel,data_out);
input [3:0] data_in;
input [1:0] sel;
output data_out;
assign data_out = sel[1]?(sel[0]?data_in[3]:data_in[2]):(sel[0]?data_in[1]:data_in[0]);
endmodule
testbench的写法也和前文所说的类似,列举出所有的输入组合即可,这里将四路输入data\_in和选择信号sel拼接起来并周期性地加1便可得到所有的输入组合。
`timescale 1ns / 1ps
module mux4_tb;
reg [3:0] data_in;
reg [1:0] sel;
wire data_out;
mux4 uut(
.data_in(data_in),
.sel(sel),
.data_out(data_out)
);
initial begin
data_in = 4'b0000;
sel = 2'b00;
end
always #20 {data_in,sel} = {data_in,sel} + 1;
endmodule
下图是在modelsim中得到的四选一多路复用器的部分仿真波形,观察波形时可以右键sel信号,点击radix,之后选择unsigned类型便于更直观的观察。可以看到输出信号的无符号按照选择信号sel正确地选择了对应的输入数据进行输出。
MUX波形
之后使用Vivado软件进行综合、布局布线以及生成比特流文件并下载至FPGA开发板,便可观察上板现象。下表给出了该实例具体的管脚约束。
如下是四选一多路复用器上板演示的视频,上板时已按照上表将数据输入约束到了拨码开关的SW0至SW3,选择信号sel约束到了拨码开关的SW10至SW11,数据输出由LED0显示。
多路复用器
加法器
加法器是数字系统中常用的算术逻辑部件,用于实现加法运算。若加法器的输入为加数,输出为和,则该加法器为半加器;若输入为加数和低位的进位输入,输出为和以及进位输出,则该加法器为全加器,全加器相比半加器新增的进位输入和进位输出可用于加法器的级联。
全加器
由于Verilog HDL有现成的算术运算符,因此编写加法器的RTL代码也很简单。如下是描述四位全加器的参考代码,代码中使用了位拼接运算符{}将进位输出cout以及和输出sum拼接至一起,再使用连续赋值语句assign将加法表达式赋值即可,两个加数的位宽为4bit,得到的和可扩展至5bit,且最高位正好符合进位输出的含义。
module adder_4bit(ina,inb,sum,cout,cin);
input [3:0] ina,inb;
input cin;
output [3:0] sum;
output cout;
assign {cout, sum} = ina + inb +cin;
endmodule
下面这段代码同样是描述的四位全加器,不过是在过程块中对位拼接表达式进行的赋值。要注意的是,上一段代码使用了连续赋值语句对输出赋值,所以在端口声明时只能将输出声明为wire型或不写变量类型默认为wire型,而下面这一段代码在过程块中进行赋值,所以在端口声明时必须将输出声明为reg型。
module adder_4bit(ina,inb,sum,cout,cin);
input [3:0] ina, inb;
input cin;
output reg [3:0] sum;
output reg cout;
always@(*)
{cout,sum} = ina + inb + cin;
endmodule
接下来将对全加器的RTL代码进行功能仿真,testbench的参考代码如下,这里在initial块中列举了几种典型的输入组合,当然,也可以采用和之前类似的写法列举完所有的输入组合。
`timescale 1ns / 1ps
module adder_4bit_tb;
reg [3:0] ina,inb;
reg cin;
wire [3:0] sum;
wire cout;
adder_4bit uut(
.ina(ina),
.inb(inb),
.cin(cin),
.sum(sum),
.cout(cout)
);
initial begin
ina = 4'b0000; inb = 4'b0000; cin = 1'b0;
#10
ina = 4'b0011; inb = 4'b0100; cin = 1'b0;
#10
ina = 4'b1001; inb = 4'b1100; cin = 1'b0;
#10
ina = 4'b1010; inb = 4'b0001; cin = 1'b1;
#10
ina = 4'b0111; inb = 4'b1000; cin = 1'b1;
#10
$stop;
end
endmodule
下图为Modelsim软件中全加器的仿真波形,几种输入组合下都得到了正确的输出结果,功能仿真完成。
全加器波形
仿真完成后,可参考下表进行全加器实例的管脚约束。
之后便可完成后续流程上板进行现象的观察。下面给出了全加器上板演示的视频。视频中已按照上文的表格进行了管脚约束,将两个四比特的输入分别约束至拨码开关SW0至SW3、SW4至SW7,并将进位输入约束至拨码开关SW8,四比特的和输出由LED0置LED3观察,LED4则用于观察进位输出。
全加器演示
ALU
ALU全称Arithmetic and Logic Unit,即算术逻辑单元,是用于进行算术运算和逻辑运算的组合逻辑电路。对于一个基本的ALU,其输入为要进行运算操作的数据以及控制运算类型的控制码,输出为运算结果。
算术逻辑单元ALU
下表列举了一个简单的组合逻辑ALU根据不同的控制码opcode所具备的功能。这一节介绍的ALU实例其运算功能比较简单,便于FPGA的上板观察,读者也可在此基础上完善ALU的设计,例如添加更多的运算功能和添加输出标志位以得到功能更丰富完备的ALU。
下面给出了一段ALU的RTL参考代码,通过多路分支语句case就可以很容易描述ALU随操作码opcode变化的多种运算功能。这段代码中使用了parameter对位宽进行了常量参数化,能增强代码的可重用性和可读性,当需要修改位宽时只需要在例化的时候修改parameter的值即可。
module ALU #(
parameter DATA_WIDTH = 4
) (ina,inb,opcode,ALUout);
input [DATA_WIDTH-1:0] ina;
input [DATA_WIDTH-1:0] inb;
input [2:0] opcode;
output reg [DATA_WIDTH-1:0] ALUout;
always@(*)
case(opcode)
3'b000 : ALUout = ina + inb; //ADD
3'b001 : ALUout = ina - inb; //SUB
3'b010 : ALUout = ina & inb; //AND
3'b011 : ALUout = ina | inb; //OR
3'b100 : ALUout = ~(ina | inb); //NOR
3'b101 : ALUout = ina ^ inb; //XOR
default : ALUout = 0;
endcase
endmodule
在编写好ALU的RTL代码后,我们可以将其添加进Vivado的工程中,使用侧边栏的RTL Analysis功能,依次点击RTL Analysis→Open Elaborated Design→Schematic,可以得到与代码对应的RTL电路图,如下图所示。从图中可以很清晰地看到ALU的各种硬件组成部件,如负责算术运算功能的加法器和减法器,执行逻辑运算功能的门电路,以及对应case语句的多路复用器。有时候通过RTL电路图可以初步地判断代码是否正确地描述了我们想要的电路功能,也帮助我们将代码和硬件更好地对应起来。
Vivado RTL Analysis得到的RTL电路图
接下来是是进行ALU的功能仿真,testbench的参考写法如下,共设置了两组输入数据,并测试了操作码所有输入情况下的运算结果。
`timescale 1ns / 1ps
module ALU_tb;
reg [3:0] ina;
reg [3:0] inb;
reg [2:0] opcode;
wire [3:0] ALUout;
ALU UUT(
.ina(ina),
.inb(inb),
.opcode(opcode),
.ALUout(ALUout)
);
initial begin
ina = 4'b1001; inb = 4'b0001; opcode = 3'b000;
#100
ina = 4'b0111; inb = 4'b0101;
#100
$stop;
end
always #10 opcode = opcode + 1;
endmodule
在Modelsim软件中的部分仿真波形如下图所示,两个输入数据随着操作码opcode的变化依次进行了加法、减法、按位与、按位或、按位或非以及按位异或的运算。
ALU波形
仿真完成后,可参考下表进行ALU实例的管脚约束。
最后便可完成后续流程进行上板验证,如下为ALU上板演示的视频。视频中已按照上表进行了管脚约束,将两个四比特的输入分别约束至拨码开关SW0至SW3、SW4至SW7,三比特的操作码opcode输入约束至拨码开关SW9至SW11,运算结果由LED0至LED3显示。
ALU演示
至此,四个基础组合逻辑的实例就介绍完毕了,希望初学的读者可以多上手练习,在实际操作的过程中熟悉FPGA基本的开发流程。本期的文章到此结束,各位下期文章再见。
END
文章来源:https://zhuanlan.zhihu.com/p/163865080
推荐阅读
知乎:https://zhuanlan.zhihu.com/p/163865080
更多内容请关注其实我是老莫的网络书场专栏