赞
踩
前文通过FPGA实现了乘法器、除法器、二进制转BCD码、矩阵键盘驱动、数码管驱动等等模块,但是都是仿真,并没有实际上板测试过。本文就通过一个计算器使用这些模块,验证其功能正确性,本工程没有调用任何IP,也没有直接使用乘法和除法。
由于手里的altera和xilinx的开发板均没有矩阵键盘和8个数码管,刚好电子森林又有一个白嫖开发板的活动,使用的是lattice的芯片,开发板的外设比较多。就报名了该活动,最后上板是在lattice的开发板,由于没有使用IP,因此在其余任何厂家的FPGA开发板上均可以直接使用该工程。
本文通过FPGA实现8位十进制数的加、减、乘、除运算,通过矩阵键盘输入数据和运算符,矩阵键盘的布局图如下所示。该计算器可以进行连续运算,当按下等号后,可以直接按数字进行下次运算,或者按运算符,把上次运算结果作为本次运算的第一个操作数。
通过clr可以清除之前输入的所有数据,在输入运算符时,可以输入多次,但是只有最后一次输入的运算符有效。计算器输入的数字和计算结果通过8个数码管进行显示。
该工程的资源消耗图如下所示,总共消耗六百多个LUT,实现27位除法运算和14位乘法运算,以及二进制转BCD码等。消耗的其余资源均不超过百分之二十,总体资源利用率还可以。
顶层模块的信号列表如下所示:
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
key_row | O | 4 | 矩阵键盘行输出信号。 |
key_col | I | 4 | 矩阵键盘列输入信号,低电平有效。 |
sclk | O | 1 | 74HC595移位时钟信号,上升沿有效。 |
rclk | O | 1 | 74HC595锁存时钟信号,上升沿有效。 |
ds | O | 1 | 74HC595串行数据信号,在sclk下降沿更新数据。 |
顶层模块只对子模块进行连线,框图如下所示,总共包括7个子模块,每个子模块执行不同功能。
Key_scan模块:对按键消抖,并且检测被按下按键的位号,按键编号取值范围[0,15],key_vld为高电平表示有按键被按下一次,key_out表示被按下按键的位号。
Operation_ctrl模块:计算器的控制和计算模块,通过检测被按下的按键,执行相应的功能,并且把数码管需要显示的数据通过operat_out信号输出,该信号为二进制数据。
Mult模块:当使能信号为高电平时开始对输入的数据进行乘法运算。乘数和被乘数均支持14位(4位十进制数大小),最大输出27位数据(8位十进制最大数据的位宽)。因为只有8个数码管,所以最大支持8位十进制数据输出。该模块通过移位和加法器实现乘法,能够运行的频率会更高。
Div模块:当使能信号为高电平时对输入的数据进行除法运算,除数和被除数均支持27位,通过移位和加法器实现除法运算。当quotient_vld为高电平表示除法运算结束。
Hex2bcd模块:通过移位的方式实现二进制转bcd码,输入数据高达27位,如果使用除法和取余实现二进制转BCD码,将消耗大量资源且时钟运行频率较低。
Seg_disp:数码管的刷新控制模块,8个数码共用同一组数据线,每个数码管通过位选的方式工作,该模块实现位选及数据的控制,将输入的8个BCD码显示在对应的8个数码管上。
Hc595_drive模块:该模块实现74HC595芯片的驱动,通过三个管脚控制8个数码管的显示。
本文只对模块设计方法进行简要概括,具体细节可以查看我的公众号文章,端口信号含义如下表所示。
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
key_row | O | 4 | 矩阵键盘行输出信号。 |
key_col | I | 4 | 矩阵键盘列输入信号,低电平有效。 |
key_out | O | 4 | 被按下按键的位号。 |
key_vld | O | 1 | 高电平表示有按键被按下。 |
通过一个状态机采用逐行扫描的方式对按键进行检测,首先初始状态四行全部输出低电平,检测列输入信号是否全部为高电平(列信号被上拉到VCC)。如果列输入不全为高电平,则表示有按键被按下,则启用一个计数器cnt,对列输入不全为高电平的时间进行计数。
如果时间能够达到20MS,则认为按键真的被按下。如果没有达到20MS就检测到列输入全为高电平,则判定为抖动,此时将计数器清零,继续检测。当确认按键被按下后,状态机跳转到行检测,此时需要逐行输出低电平,其余行输出高电平,对所有行检测一遍,从而确定出被按下按键的位置,输出位号。最后状态机跳转到一个等待状态,直到所有按键被释放后,状态机回到空闲状态继续检测。
我只对顶层模块写了测试文件,所以其余模块仿真通过modelsim添加对应模块信号即可,该模块仿真结果如下图所示。TestBench文件里面通过调用编写的任务实现按键按下的信号模拟,按下按键的前后都是模拟了抖动的,如下图所示。红框部分都是按键按下前的抖动,计数器shake_cnt没有计数到最大值key_col所有位都变为高电平了,此时计数器就会清零重新检测,直到检测到橙色框处,按键按下是俺才超过设定时间,状态机才会跳转。
行扫描的细节如下所示,此时对每行进行检测,每行扫描时间持续16个时钟周期,通过计数器row_cnt记录一行扫描的时间,计数器row_index记录扫描第几行了。当扫描第0行时,列输入不全为高电平,表示被按下的按键在第0行。列输入值为4’hd,第一列为低电平,表示被按下按键在第1列,则计算出位号为1,此时key_out输出1,key_vld拉高一个时钟周期,表示1号按键被按下一次。
该模块主要实现被按下按键的识别,然后进行相应的计算,并且输出对应的数据给数码管进行显示,模块端口信号如下所示:
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
din_key | I | 4 | 被按下按键位号。 |
din_key_vld | I | 1 | 高电平表示有按键被按下1次。 |
div_vld | I | 1 | 高电平表示除法器计算结束。 |
div_in | I | 27 | 除法器运算结果。 |
mult_vld | I | 1 | 高电平表示乘法器计算结束。 |
mult_in | I | 27 | 乘法器计算结果。 |
div_en | O | 1 | 高电平驱动除法器开始工作。 |
mult_en | O | 1 | 高电平驱动乘法器开始工作。 |
data1 | O | 27 | 计算器的1号运算数,作为被除数/被乘数/被加数/被减数。 |
data2 | O | 27 | 计算器的2号运算数,作为除数/乘数/加数/减数。 |
dout | O | 27 | 数码管需要显示的二进制数据。 |
该模块的功能稍微复杂一点,既要实现按键的识别,又要进行加法、减法计算,还要进行数码管显示数据的控制。均通过一个状态机实现,该状态机的跳转与按下的按键有关。
状态机的状态转换图如下所示,状态机初始位于空闲状态(IDLE),当有数字按键被按下后,跳转到输入操作数1的状态(IDTA1),在此状态下,需要数据1,最多输入8位十进制数据,检测到有加减乘除运算符按下时,跳转到运算符输入状态(OPERAT),该状态下可以输入很多运算符,但是只有离开该状态时,输入的最后一个运算符有效。
在输入运算符的状态下,如果检测到数字按键被按下,则跳转到输入数字的状态(IDTA2),也就是输入第2个操作数,该状态最多输入8位十进制数据,否则会溢出。在该状态下,如果检测到等号被按下,则跳转到计算结果的状态(RESULT),该状态会根据输入的运算符,对输入的数据进行相应运算,运算符为乘法或除法时,将乘法器使能或除法器使能信号拉高一个时钟,让乘法器或除法器模块工作,当乘法器或者除法器计算结束时,更新计算结果。在该状态下,如果检测到数字按键被按下,则跳转到输入数据1状态,进行下次运算。如果检测到运算符按键被按下,说明用户进行连续运算,则将本次运算结果赋值给操作数1,状态机跳转到输入运算符状态,继续下次运算。
所有状态,只要清零按键被按下,状态机回到空闲状态。
再说模块输出信号dout,当状态机处于IDTA1状态时,数码管需要显示输入的数据1,则dout等于data1数值。当状态机处于IDATA2状态时,需要显示输入的数据2,则dout等于data2数值。当状态机处于运算结果状态时,则dout根据运算符不同,进行相应运算,输出不同数值。
该模块内部还包括一些数字信号、运算符检测、信号对齐、输入数据的变化等等,这些不做细讲,可以通过源代码自行阅读,源代码均有注释,阅读应该没有难度。
对该模块进行总体仿真,在TestBench文件中依次按下这些按键,先实现26+290,然后再减82。
仿真结果如下所示,dout是数码管需要显示的数据,din_key是按键消抖模块检测被按下的按键,通过位号译码,首先输入26,数码管现需要先显示2,然后显示26,3号按键代表加号,然后输入290作为第二运算数,14号按键是等号,计算结果316。然后7号按键是减号,之后dout的值赋值给data1,然后输入82,完成316-82的运算,最后数码管显示234。
连续乘法运算仿真,如图所示,上次运算结果为234,然后按下乘号(11号按键),将上次运算结果赋值给data1,然后输入乘数为13,按下等号后,mult_en拉高,使能乘法器模块,乘法器计算结束(mult_vld为高),输出运算结果3042。
连续除法运算仿真,如图所示,生词运算结果为3042,然后按下除号(15号按键),将上次运算结果赋值给data1,然后输入除数为71,按下等号后,div_en拉高,使能除法器模块,除法器计算结束(div_vld为高),输出运算结果42。
由于篇幅原因,本文不对乘法器的具体实现做讲解,具体实现方式已经在我另一篇文章进行详细讲解了,需要了解原理的可以自行查看。端口信号列表如下所示:
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
start | I | 1 | 开始计算,高电平有效。 |
multiplicand | I | 14 | 被乘数输入。 |
multiplier | I | 14 | 乘数输入。 |
product | O | 27 | 乘积输出。 |
product_vld | O | 1 | 乘积有效指示信号。 |
rdy | O | 1 | 高电平表示模块空闲,可以进行运算。 |
由于人使用计算器的频率较低,所以没有使用rdy信号,没有影响。
乘法器模块仿真如下所示,start为高电平时,被乘数为234,乘数为13,通过几个时钟周期后,product_vld拉高,表示计算结束,输出乘积为3042。仿真正常,这个模块输出延迟与乘数的值有关,最多不会超过乘数位宽那么多个时钟周期,最少1个时钟周期。
由于篇幅原因,本文不对除法器的具体实现做讲解,需要了解原理的可以查看我写的除法器实现原理。端口信号列表如下所示:
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
start | I | 1 | 开始计算,高电平有效。 |
dividend | I | 27 | 被除数输入。 |
divisor | I | 27 | 除数输入。 |
quotient | O | 27 | 商输出。 |
remainder | O | 27 | 余数输出。 |
quotient_vld | O | 1 | 商和余数除数有效指示信号。 |
ready | O | 1 | 高电平表示模块空闲,可以进行运算。 |
error | O | 1 | 输入除数为0。 |
该模块没有使用余数、error、ready信号,对应仿真结果如下图所示。开始信号有效时,被除数为3042,除数为71,经过几个时钟周期后,quobient_vld拉高,表示除法计算结束,计算商为42,余数为60,经过验算后没有问题。
该模块输入27位二进制数据,输出32位BCD码,如果直接使用除法和取余操作,将消耗大量逻辑资源,并且时钟频率还不能提高。所以就采用移位和加法的算法来实现转换,这个模块我之前就已经设计过,具体细节还是挺多的,可以通过文章查看,本文不对该模块具体实现方式进行讲解,设计的时候考虑了参数化,直接修改输入参数位宽即可实现任意位宽的转换,端口列表如下:
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
din | I | 27 | 二进制输入数据。 |
din_vld | I | 27 | 二进制输入数据有效指示信号。 |
dout | O | 36 | 转换后BCD码 |
dout_vld | O | 1 | 转换后的BCD码有效指示信号。 |
该模块的din_vld恒为高电平,需要对前文提到模块稍作修改,修改后的代码如下所示:
module hex2bcd #(
parameter IN_DATA_W = 27 ,//输入数据位宽;
parameter OUT_DATA_W = clogb2({{IN_DATA_W}{1'b1}})//自动计算输出数据对应的十进制位数;
)(
input clk ,//系统时钟;
input rst_n ,//系统复位,低电平有效;
input [IN_DATA_W-1:0] din ,//输入二进制数据;
input din_vld ,//输入数据有效指示信号,高电平有效;
output reg [4*OUT_DATA_W-1:0] dout ,//输出8421BCD码;
output reg dout_vld //输出数据有效指示信号,高电平有效;
);
localparam CNT_W = clogb(IN_DATA_W-3);//根据输入数据的位宽自动计算需要移动的轮数;
//localparam OUT_DATA_W = clogb2({{IN_DATA_W}{1'b1}});//自动计算输出数据对应的十进制位数;
reg [IN_DATA_W-1:0] din_ff0 ;
reg flag ;
reg [CNT_W-1:0] cnt ;
reg [IN_DATA_W+OUT_DATA_W*4-1:0]data_shift ;
reg end_cnt_ff0 ;
wire [OUT_DATA_W*4-1:0] data_compare;
wire add_cnt ;
wire end_cnt ;
function integer clogb2(input integer depth);
begin
if(depth==0)
clogb2 = 1;
else if(depth!=0)
for(clogb2=0;depth>0;clogb2=clogb2+1)
depth=depth/10;
end
endfunction
//自动计算位宽
function integer clogb(input integer depth);begin
if(depth==0)
clogb = 1;
else if(depth!=0)
for(clogb=0;depth>0;clogb=clogb+1)
depth=depth>>1;
end
endfunction
//当输入数据有效并且此时该模块空闲时保存输入数据,否则不保存输入数据,这样可以保证本次转换数据完全正确;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
din_ff0 <= 0;
end
else if(din_vld)begin
din_ff0 <= din;
end
end
//标志信号flag,当输入数据有效时拉高,当计数器计数完成时清零;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
flag <= 1'b0;
end
else if(din_vld)begin
flag <= 1'b1;
end
else if(end_cnt)begin
flag <= 1'b0;
end
end
//移位计数器,每次转换需要移动IN_DATA_W-2次,初始值为0,加一条件flag信号有效,结束条件是计数到IN_DATA_W-2次;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 0;
else
cnt <= cnt + 1;
end
end
assign add_cnt = flag;
assign end_cnt = add_cnt && cnt == IN_DATA_W-3;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
data_shift <= 0;
end
else if(add_cnt)begin
if(cnt==0)begin//初始时将输入数据左移三位保存;
data_shift <= {{{OUT_DATA_W-3}{1'b0}},din_ff0,3'b0};
end
else begin//计数器加一条件有效时,将移位寄存器数据左移一位;
data_shift <= {data_compare[OUT_DATA_W*4-2:0],data_shift[IN_DATA_W-1:0],1'b0};
end
end
end
//移位后大于等于5之后加3;
generate
genvar bit_num;
for(bit_num = 0 ; bit_num < OUT_DATA_W ; bit_num = bit_num + 1)begin : DATA
assign data_compare[4*bit_num+3 : 4*bit_num] = data_shift[IN_DATA_W+4*bit_num+3 : IN_DATA_W+4*bit_num] + (data_shift[IN_DATA_W+4*bit_num+3 : IN_DATA_W+4*bit_num]>=5 ? 4'd3 : 4'd0);
end
endgenerate
//将计数器延迟一拍,用于生成输出信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
end_cnt_ff0 <= 1'b0;
end
else begin
end_cnt_ff0 <= end_cnt;
end
end
//通过计数器结束条件产生输出信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
dout <= 0;
end
else if(end_cnt_ff0)begin
dout <= data_shift[IN_DATA_W+OUT_DATA_W*4-1 : IN_DATA_W];
end
end
//通过计数器结束条件生成输出有效指示信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
dout_vld <= 1'b0;
end
else begin
dout_vld <= end_cnt_ff0;
end
end
endmodule
该模块的端口仿真如下图所示,输入数据按照十进制显示,输出数据按照十六进制数据显示,输入数据为17’d29时,输出数据为36’h000000029,转换完成。
该模块用的也比较多了,8个数码管共用同一组数据线,那么这组数据线就只能通过时分复用的方式传递数据。该模块我也发布过专门的模块驱动文章,需要的可以自行查看。
对应的端口列表如下所示:
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
din | I | 32 | 需要显示的32位BCD码 |
segment | O | 8 | 数码管的数据线 |
seg_sel | O | 8 | 数码管位选信号 |
dout_vld | O | 1 | 数码管刷新指示信号 |
该模块后面还有一个74HC595驱动模块,所以需要固定一段时间产生数据和位选信号,一般数码管刷新时间采用20us,所以每隔20us产生一个刷新信号,输出对应数码管数据和位选信号给74HC595模块的驱动,再生成相应输出,修改后的代码如下所示:
module seg_disp #(
parameter TCLK = 20 ,//系统时钟周期,单位ns。
parameter TIME_20US = 20_000 ,//数码管刷新时间,默认20us。
parameter SEG_NUM = 8 //需要显示的数码管个数。
)(
//输入信号定义
input clk ,//系统时钟,50MHz。
input rst_n ,//系统复位,低电平有效。
input [(SEG_NUM * 4) - 1 :0] din ,//需要数码管显示的BCD码数码;
//输出信号定义
output reg [7 : 0] segment ,//数码管的数据线;
output reg [SEG_NUM - 1 : 0] seg_sel ,//数码管的位选信号;
output reg dout_vld //为高电平时,表示段选和位选信号有效;
);
//参数定义
localparam TIME = TIME_20US/TCLK ;
localparam TIME_W = clogb2(TIME-1) ;//计算数码管扫描时间的时钟数据位宽;
localparam SEG_W = clogb2(SEG_NUM) ;
localparam ZERO = 8'h3F ; //8'hC0;前面的数据是共阴数码管使用的,后面数据是共阳数码管使用的;
localparam ONE = 8'h06 ; //8'hF9;
localparam TWO = 8'h5B ; //8'hA4;
localparam THREE = 8'h4F ; //8'hB0;
localparam FOUR = 8'h66 ; //8'h99;
localparam FIVE = 8'h6D ; //8'h92;
localparam SIX = 8'h7D ; //8'h82;
localparam SEVEN = 8'h07 ; //8'hF8;
localparam EIGHT = 8'h7F ; //8'h80;
localparam NINE = 8'h6F ; //8'h90;
localparam ERR = 8'h77 ; //8'h86;
//中间信号定义
reg [3 : 0] sel_result ;
reg [SEG_W - 1 : 0] sel ;
reg [SEG_W - 1 : 0] sel_ff0 ;
reg [TIME_W - 1 : 0] cnt_20us ;
reg add_sel_r ;//
wire end_cnt_20us;
wire add_sel ;
wire end_sel ;
//自动计算位宽的函数;
function integer clogb2(input integer depth);
begin
if(depth==0)
clogb2=1;
else if(depth!=0)
for(clogb2=0; depth>0;clogb2=clogb2+1)
depth=depth>>1;
end
endfunction
//20us计数器,用于对一个数码管点亮的持续时间进行计数,计数器初始值为0,对
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin//计数器初始值为0;
cnt_20us <= 0;
end
else if(end_cnt_20us)begin//当计数器计数到20us时,表示一个数码管已经被点亮20US了,将计数器清零;
cnt_20us <= 0;
end
else begin//否则,计数器加一;
cnt_20us <= cnt_20us + 1'b1;
end
end
//计数器结束条件,当计数器计数到TIME-1时表示20US已经到了,将计数器清零;
assign end_cnt_20us = cnt_20us == TIME - 1;
//计数器sel,用于计数此时点亮的时第几个数码管,上电复位时点亮第零个数码管,所以初始值为0,之后当计数器cnt_20us计数结束时,表示一个数码管点亮时间已经到了,此时计数器sel加一,表示该点亮下一个计数器了,当点亮SEG_NUM-1个计数器完成(end_sel有效)时表示数码管都被点亮了一次,此时计数器sel清零,又从第一个数码管开始点亮;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
sel <= 0;
end
else if(add_sel) begin
if(end_sel)//当计数器sel计数结束时,计数器清零;
sel <= 0;
else
sel <= sel + 1;
end
end
assign add_sel = end_cnt_20us;//计数器sel的加一条件是,计数器cnt_20us计数器结束;
assign end_sel = add_sel && sel == SEG_NUM - 1;//计数器sel计数到SEL_NUM-1时,计数器sel清零;
//sel_result信号是当前被点亮数码管需要显示的数据,根据计数器sel的值确定此时应该将输入信号的哪几位数据译码输出给数码管进行显示;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
sel_result <= 4'd0;
end
else if(add_sel)begin//取输入信号din[4*sel+3 : 4*sel]信号给译码部分进行译码,之后输出给数码管数据信号驱动数码管显示该数据;
sel_result <= din[4*sel+3 -: 4];//{din[4*sel+3],din[4*sel+2],din[4*sel+1],din[4*sel]};
end
end
//译码器部分,将sel_result十进制信号译码成数码管显示该数字对应的八位数据信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始上电时,所有数码管显示数据0;
segment <= ZERO;
end
else if(add_sel_r)begin
case(sel_result)//将sel_result译码成对应的segment数据,segment数据驱动数码管才能显示sel_result代表的数字;
0: segment <= ZERO ;//想要数码管显示0,就要给数码管数据信号segment输入ZERO数据,其余类似;
1: segment <= ONE ;
2: segment <= TWO ;
3: segment <= THREE;
4: segment <= FOUR ;
5: segment <= FIVE ;
6: segment <= SIX ;
7: segment <= SEVEN;
8: segment <= EIGHT;
9: segment <= NINE ;
default: segment <= ERR;
endcase
end
end
//为了与段选动态扫描,保持同步,此时位选应该打一拍再赋给位选信号 seg_sel
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
sel_ff0 <= 0;
end
else if(add_sel)begin
sel_ff0 <= sel;
end
end
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0,全部数码管被点亮;
seg_sel <= {{SEG_NUM}{1'b0}};
end
else if(add_sel_r)begin//将1右移sel_ff0位之后取反,seg_sel的第sel_ff0输出低电平,对应的第sel_ff0个数码管被点亮了,其余位输出高电平,对应的数码管熄灭;
seg_sel <= ~({1'b1,{{SEG_NUM-1}{1'b0}}} >> sel_ff0);//~(6'h1<<sel_ff0);
end
end
//移位寄存器,将数据更新的指示信号暂存;
//dout_vld与segment、seg_sel对齐。
always@(posedge clk)begin
add_sel_r <= add_sel;
dout_vld <= add_sel_r;
end
endmodule
该模块仿真比较简单,如下图所示,需要显示的数据为32’h00000234,由于底板上位选最低位对应的是最左边的数码管,第1个十进制数需要与位选的最高位对齐,第8个十进制数需要与位选的最低位对齐,仿真结果如下所示。
位选信号为8’h7f是,此时数据段应该输出8’h66,数码管显示4这个数字。
数码管的驱动电路如下所示,通过两片74HC595驱动8个数码管,关于74HC595的驱动,我也有相应文章进行讲解,由于篇幅问题,本文只对该模块进行仿真。
信号端口列表如下所示。
信号名 | I/O | 位宽 | 含义 |
---|---|---|---|
clk | I | 1 | 系统时钟频率,默认12MHz。 |
rst_n | I | 1 | 系统复位信号,低电平有效。 |
segment | I | 8 | 数码管的数据线 |
seg_sel | I | 8 | 数码管位选信号 |
dout_vld | I | 1 | 数码管刷新指示信号 |
ds | O | 1 | 74hc595芯片串行数据。 |
sclk | O | 1 | 74hc595芯片移位寄存器时钟信号。 |
rclk | O | 1 | 74hc595芯片锁存器时钟信号。 |
该模块整体仿真结果如下所示:
该模块细节仿真如下所示:
上述仿真了各个模块,下文在开发板上对这个工程进行实测。由于我手里的altera和xilinx的开发板并没有矩阵键盘和数据管这些外设,刚好电子森林当时又推出了一块开发板,刚好有这些外设,并且还可以白嫖,就有了这个项目。
因此最终在lattice的开发板上验证该工程,由于工程没有使用任何IP,在其余FPGA厂家的芯片上依旧可以直接使用。
首先验证正常的加减乘除运算,如下视频所示,每次运算后使用清除按键清零运算结果,然后开始下次运算。
单次计算
下面是进行连续运算的演示,使用该计算器实现以下运算((3320 + 2551) - 771)* 7 / 9,得到计算结果为3966,与真实结果一致。
连续运算
同时在输入运算符时,如果运算符输入错误,则可以再次输入运算符,以最后输入的运算符为准,之后输入第二个操作数,与一般的计算器机制一致。
该工程的源码在公众号后台回复“基于FPGA的计算器”(不包括引号)即可。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。