1、概括
本文通过FPGA实现8位十进制数的加、减、乘、除运算,通过矩阵键盘输入数据和运算符,矩阵键盘的布局图如下所示。该计算器可以进行连续运算,当按下等号后,可以直接按数字进行下次运算,或者按运算符,把上次运算结果作为本次运算的第一个操作数,具体细节在控制模块处讲解。
图1 矩阵键盘
通过clr可以清除之前输入的所有数据,在输入运算符时,可以输入多次,但是只有最后一次输入的运算符有效。计算器输入的数字和计算结果通过8个数码管进行显示。
该工程的资源消耗图如下所示,总共消耗六百多个LUT,实现27位除法运算和14位乘法运算,以及二进制转BCD码等。消耗的其余资源均不超过百分之二十,总体资源利用率还可以。
图2 资源利用率
2、顶层模块设计
顶层模块的信号列表如下所示:
表1 顶层模块信号列表
信号名 | 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个子模块,每个子模块执行不同功能。
图3 顶层框图
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个数码管的显示。
顶层模块参考代码如下所示:
module top #(
parameter TCLK = 83 ,//系统时钟周期,默认83ns.
parameter TIME_20MS = 20_000_000 ,//按键消抖时间,默认20ms,单位ns。
parameter TIME_20US = 20_000 ,//每个数码管刷新时间,默认20us。
parameter SEG_NUM = 8 ,//数码管的个数,默认8个;
parameter SCLK_DIV = 4 //sclk与系统时钟的分频系数。
)(
input clk ,//系统时钟信号,12MHz,周期约83ns。
input rst_n ,//系统复位信号,低电平有效;
input [3 : 0] key_col ,//矩阵键盘列输入;
output [3 : 0] key_row ,//矩阵键盘行输出;
output ds ,//74HC595串行数据线;
output sclk ,//74HC595移位寄存器时钟;
output rclk //74HC595锁存器时钟;
);
wire [3 : 0] key_out ;
wire key_vld ;
wire div_en ;
wire mult_en ;
wire [26 : 0] data1 ;
wire [26 : 0] data2 ;
wire [26 : 0] product ;
wire product_vld ;
wire [26 : 0] quotient ;
wire quotient_vld;
wire [26 : 0] operat_out ;
wire [31 : 0] bcd_out ;
wire [7 : 0] segment ;//八段数码管段选信号;
wire [SEG_NUM - 1 : 0] seg_sel ;//八段数码管位选信号;
wire sel_vld ;
//例化矩阵键盘,按键检测模块;
key_scan #(
.TCLK ( TCLK ),//系统时钟clk周期,单位ns;
.TIME_20MS ( TIME_20MS ) //按键消抖时间,默认20ms,单位ns。
)
u_key_scan (
.clk ( clk ),//系统时钟信号,默认50MHz。
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.key_col ( key_col ),//矩阵键盘列输入信号;
.key_row ( key_row ),//矩阵键盘行输出信号;
.key_out ( key_out ),//矩阵键盘被按下按键的编号;
.key_vld ( key_vld ) //矩阵键盘被按下键盘编号有效指示信号;
);
//例化运算控制模块;
operation_ctrl u_operation_ctrl (
.clk ( clk ),//系统时钟,50MHz。
.rst_n ( rst_n ),//系统复位,低电平有效。
.din_key ( key_out ),//按键按下的数值;
.din_key_vld ( key_vld ),//按下指示信号,高电平有效;
.div_vld ( quotient_vld ),//除法模块计算完成指示信号;
.div_in ( quotient ),//除法模块计算结果;
.mult_vld ( product_vld ),//乘法模块运算结果有效指示信号;
.mult_in ( product ),//乘法模块运算结果;
.div_en ( div_en ),//除法模块使能信号;
.mult_en ( mult_en ),//乘法模块使能信号;
.data1 ( data1 ),//被乘数或者被除数数据;
.data2 ( data2 ),//乘数或者除数数据;
.dout ( operat_out ) //输出数码管需要显示的数据;
);
//例化乘法运算模块;
mult #(
.MULT_D ( 14 ),//被乘数位宽;
.MULT_R ( 14 ) //乘数位宽;
)
u_mult (
.clk ( clk ),//系统时钟信号;
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.start ( mult_en ),//开始运算信号,高电平有效;
.multiplicand ( data1[13:0] ),//被乘数;
.multiplier ( data2[13:0] ),//乘数;
.product ( product ),//乘积输出;
.product_vld ( product_vld ),//乘积有效指示信号,高电平有效;
.rdy ( ) //模块忙闲指示信号,高电平表示空闲;
);
//例化除法运算模块;
div #(
.L_DIVN ( 27 ),//被除数的位宽;
.L_DIVR ( 27 ) //除数的位宽;
)
u_div (
.clk ( clk ),//时钟信号;
.rst_n ( rst_n ),//复位信号,低电平有效;
.start ( div_en ),//开始计算信号,高电平有效;
.dividend ( data1 ),//被除数输入;
.divisor ( data2 ),//除数输入;
.ready ( ),//高电平表示此模块空闲。
.error ( ),//高电平表示输入除数为0,输入数据错误。
.quotient_vld ( quotient_vld ),//商和余数输出有效指示信号,高电平有效;
.remainder ( ),//余数,余数的大小不会超过除数大小。
.quotient ( quotient ) //商。
);
//例化二进制转BCD模块;
hex2bcd #(
.IN_DATA_W ( 27 )//输入27位2进制数据;
)
u_hex2bcd (
.clk ( clk ),//系统时钟信号,默认50MHz。
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.din ( operat_out),//需要被转换的二进制数据;
.din_vld ( 1'b1 ),//输入数据一直有效;
.dout ( bcd_out ),//转换完成的BCD码数据;
.dout_vld ( )
);
//例化数码管显示模块;
seg_disp #(
.TIME_20US ( TIME_20US ),//每个数码管刷新时间,默认20us。
.TCLK ( TCLK ),//系统时钟周期,默认83ns.
.SEG_NUM ( SEG_NUM ) //数码管的个数,默认8个;
)
u_seg_disp (
.clk ( clk ),//系统时钟信号,默认12MHz。
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.din ( bcd_out ),//将BCD码进行显示;
.segment ( segment ),//数码管的段选信号;
.seg_sel ( seg_sel ),//数码管的位选信号;
.dout_vld ( sel_vld )
);
//例化74hc595驱动模块;
hc595_drive #(
.SEG_NUM ( SEG_NUM ),//需要显示的数码管个数。
.SCLK_DIV ( SCLK_DIV ) //sclk与系统时钟的分频系数。
)
u_hc595_drive (
.clk ( clk ),//系统时钟,50MHz。
.rst_n ( rst_n ),//系统复位,低电平有效。
.segment ( segment ),//数码管的数据线;
.seg_sel ( seg_sel ),//数码管的位选信号;
.din_vld ( sel_vld ),//段选和位选有效指示信号;
.ds ( ds ),//74HC595串行数据线;
.sclk ( sclk ),//74HC595移位寄存器时钟;
.rclk ( rclk ) //74HC595锁存器时钟;
);
endmodule
3、矩阵键盘检测模块
本文只对模块设计方法进行简要概括,具体细节可以查看我的公众号文章,端口信号含义如下表所示。
表2 按键检测模块信号列表
信号名 | 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所有位都变为高电平了,此时计数器就会清零重新检测,直到检测到橙色框处,按键按下是俺才超过设定时间,状态机才会跳转。
图4 抖动检测仿真
行扫描的细节如下所示,此时对每行进行检测,每行扫描时间持续16个时钟周期,通过计数器row_cnt记录一行扫描的时间,计数器row_index记录扫描第几行了。当扫描第0行时,列输入不全为高电平,表示被按下的按键在第0行。列输入值为4’hd,第一列为低电平,表示被按下按键在第1列,则计算出位号为1,此时key_out输出1,key_vld拉高一个时钟周期,表示1号按键被按下一次。
图5 按键检测
该模块代码如下所示:
module key_scan #(
parameter TCLK = 20 ,//系统时钟周期,单位ns。
parameter TIME_20MS = 20_000_000 //按键消抖时间,单位为ns。
)(
input clk ,//系统时钟信号,默认50MHz。
input rst_n ,//系统复位,低电平有效;
input [3 : 0] key_col ,//矩阵键盘的列号;
output reg [3 : 0] key_row ,//矩阵键盘的行号;
output reg [3 : 0] key_out ,//矩阵键盘被按下按键的数值;
output reg key_vld //矩阵键盘被按下按键数据输出有效指示信号;
);
//自定义参数;
localparam CHK_COL = 4'b0001 ;//状态机的列扫描状态;
localparam CHK_ROW = 4'b0010 ;//状态机的的行扫描状态;
localparam DELAY = 4'b0100 ;//状态机的延时状态;
localparam WAIT_END = 4'b1000 ;//状态机的等待状态;
localparam TIME_20MS_NUM = TIME_20MS / TCLK ;//计算出TIME_20MS对应的系统时钟个数;
localparam TIME_20MS_W = clogb2(TIME_20MS_NUM-1) ;//利用函数计算出TIME_20MS_NUM对应的寄存器位宽;
reg [3 : 0] key_col_ff0 ;
reg [3 : 0] key_col_ff1 ;
reg [1 : 0] key_col_get ;
reg [3 : 0] state_c ;
reg [TIME_20MS_W - 1 : 0] shake_cnt ;
reg [3 : 0] state_n ;
reg [1 : 0] row_index ;
reg [3 : 0] row_cnt ;
wire end_shake_cnt ;
wire col2row_start ;
wire row2del_start ;
wire del2wait_start ;
wire wait2col_start ;
wire add_row_cnt ;
wire end_row_cnt ;
wire add_shake_cnt ;
wire add_row_index ;
wire end_row_index ;
//自动计算位宽函数;
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
//将输入的列信号打两拍,降低亚稳态出现的机率。
always@(posedge clk)begin
{key_col_ff1,key_col_ff0} <= {key_col_ff0,key_col};
end
//计数器shake_cnt,如果有按键被按下,则key_col_ff1!=4'hf,此时计数器计数。
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)begin
shake_cnt <= 0;
end
else if(add_shake_cnt)begin
if(end_shake_cnt)//按键被按下20ms时,计数器清零;
shake_cnt <= 0;
else//否则当按键被按下时,计数器进行计数;
shake_cnt <= shake_cnt + 1;
end
else begin//没有按键被按下时,计数器清零;
shake_cnt <= 0;
end
end
assign add_shake_cnt = (key_col_ff1!=4'hf);
assign end_shake_cnt = add_shake_cnt && shake_cnt == TIME_20MS_NUM-1 ;
//当列检查结束时,将被按下按键所在列寄存;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始为0,没有按键被按下;
key_col_get <= 0;
end
else if(col2row_start)begin//当状态机从列检查跳转到行检查时,将按键对应列保存;
if(key_col_ff1==4'b1110)//最低位为0,则表示第0列按键被按下;
key_col_get <= 0;
else if(key_col_ff1==4'b1101)//第1位位0,则表示第1列按键被按下;
key_col_get <= 1;
else if(key_col_ff1==4'b1011)//第2位位0,则表示第2列按键被按下;
key_col_get <= 2;
else//否则表示第3列按键被按下;
key_col_get <= 3;
end
end
//状态机的第一段;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
state_c <= CHK_COL;
end
else begin
state_c <= state_n;
end
end
always@(*)begin
case(state_c)
CHK_COL: begin//检查列触发;
if(col2row_start)begin
state_n = CHK_ROW;
end
else begin
state_n = CHK_COL;
end
end
CHK_ROW: begin//检查行触发;
if(row2del_start)begin
state_n = DELAY;
end
else begin
state_n = CHK_ROW;
end
end
DELAY : begin//这个状态的存在是为了等待行扫描结束后,计算结果输出。
if(del2wait_start)begin
state_n = WAIT_END;
end
else begin
state_n = DELAY;
end
end
WAIT_END: begin//此时四行全部输出低电平,如果按键被按下,没有松开,那么会持续之前的状态,就需要一致等待按键松开;
if(wait2col_start)begin
state_n = CHK_COL;
end
else begin
state_n = WAIT_END;
end
end
default: state_n = CHK_COL;
endcase
end
//状态机第三段,描述
assign col2row_start = (state_c==CHK_COL ) && end_shake_cnt;//检查到有对应列持续20MS被按下。
assign row2del_start = (state_c==CHK_ROW ) && end_row_index;//行扫描完成;
assign del2wait_start= (state_c==DELAY ) && end_row_cnt;
assign wait2col_start= (state_c==WAIT_END) && key_col_ff1==4'hf;//4'hf表示前面的按键已经被松开,状态机重新回到列检测状态。
//控制行数据的输出,在检查被按下按键所在行时,进行行循环扫描。
//从第一行开始一次拉低,其余行拉高,其余时刻所有行全部拉低。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_row <= 4'b0;
end
else if(state_c==CHK_ROW)begin//行扫描,依次将每行的电平拉低。
key_row <= ~(1'b1 << row_index);
end
else begin
key_row <= 4'b0;
end
end
//行扫描的计数器,对行进行扫描。
//每行扫描持续时间为行计数器row_cnt的计数值,目前为16个时钟周期。
//当4行全部扫面完毕时,计数器清零;
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)begin
row_index <= 0;
end
else if(add_row_index) begin
if(end_row_index)
row_index <= 0;
else
row_index <= row_index + 1;
end
else if(state_c!=CHK_ROW)begin
row_index <= 0;
end
end
assign add_row_index = state_c==CHK_ROW && end_row_cnt;
assign end_row_index = add_row_index && row_index == 4-1 ;
//每行扫描持续时间,初始值为0,此处设置每行扫面16个时钟周期;
//状态机位于行扫描或者等待状态时进行计数,当计数到最大值16时清零。
always@(posedge clk or negedge rst_n)begin
if(rst_n==0)begin
row_cnt <= 0;
end
else if(add_row_cnt)begin
if(end_row_cnt)
row_cnt <= 0;
else
row_cnt <= row_cnt + 1;
end
end
assign add_row_cnt = state_c==CHK_ROW || state_c==DELAY;
assign end_row_cnt = add_row_cnt && row_cnt == 16-1;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_out <= 0;
end//计算被按下按键的数值;
else if(state_c==CHK_ROW && end_row_cnt && key_col_ff1[key_col_get]==1'b0)begin
key_out <= {row_index,key_col_get};
end
end
//按键数值有效指示信号,高电平时表示key_out输出的值是有效的。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
key_vld <= 1'b0;
end
else begin//当没扫描一行,前面暂存的列为低电平的时候,表示这一行,这一列的按键被按下。
key_vld <= (state_c==CHK_ROW && end_row_cnt && key_col_ff1[key_col_get]==1'b0);
end
end
endmodule
4、控制模块
该模块主要实现被按下按键的识别,然后进行相应的计算,并且输出对应的数据给数码管进行显示,模块端口信号如下所示:
表3 控制模块信号列表
信号名 | 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),该状态下可以输入很多运算符,但是只有离开该状态时,输入的最后一个运算符有效。
图6 状态转换图
在输入运算符的状态下,如果检测到数字按键被按下,则跳转到输入数字的状态(IDTA2),也就是输入第2个操作数,该状态最多输入8位十进制数据,否则会溢出。在该状态下,如果检测到等号被按下,则跳转到计算结果的状态(RESULT),该状态会根据输入的运算符,对输入的数据进行相应运算,运算符为乘法或除法时,将乘法器使能或除法器使能信号拉高一个时钟,让乘法器或除法器模块工作,当乘法器或者除法器计算结束时,更新计算结果。在该状态下,如果检测到数字按键被按下,则跳转到输入数据1状态,进行下次运算。如果检测到运算符按键被按下,说明用户进行连续运算,则将本次运算结果赋值给操作数1,状态机跳转到输入运算符状态,继续下次运算。
当按下的按键是0,1,2,4,5,6,8,9,10,12表示数字按键,3,7,11,15按键表示运算符按键,14表示等号,13清零,数字按键被按下后会被译码成对应数字,比如8号按键对应数字7,12号按键对应数字0。
在输入操作数1和操作数2的状态下,每次数字按键按下,都会被先译码成对应数值,然后之前输入的值乘以10然后加上本次输入的数值,得到的数值就是用户在这个状态下输入计算器的数值,从而实现每输入一个数据,数码管之前的数据就会左移一个位置,输入的数据始终在最右边的数码管位置上。
所有状态,只要清零按键被按下,状态机回到空闲状态。
再说模块输出信号dout,当状态机处于IDTA1状态时,数码管需要显示输入的数据1,则dout等于data1数值。当状态机处于IDATA2状态时,需要显示输入的数据2,则dout等于data2数值。当状态机处于运算结果状态时,则dout根据运算符不同,进行相应运算,输出不同数值。
对该模块进行总体仿真,在TestBench文件中依次按下这些按键,先实现26+290,然后再减82。
图7 仿真按下按键
仿真结果如下所示,dout是数码管需要显示的数据,din_key是按键消抖模块检测被按下的按键,通过位号译码,首先输入26,数码管现需要先显示2,然后显示26,3号按键代表加号,然后输入290作为第二运算数,14号按键是等号,计算结果316。然后7号按键是减号,之后dout的值赋值给data1,然后输入82,完成316-82的运算,最后数码管显示234。
图8 加减法仿真
连续乘法运算仿真,如图所示,上次运算结果为234,然后按下乘号(11号按键),将上次运算结果赋值给data1,然后输入乘数为13,按下等号后,mult_en拉高,使能乘法器模块,乘法器计算结束(mult_vld为高),输出运算结果3042。
图9 乘法仿真
连续除法运算仿真,如图所示,生词运算结果为3042,然后按下除号(15号按键),将上次运算结果赋值给data1,然后输入除数为71,按下等号后,div_en拉高,使能除法器模块,除法器计算结束(div_vld为高),输出运算结果42。
图10 除法仿真
该模块仿真到此结束,该模块的代码如下所示:
module operation_ctrl (
//输入信号定义
input clk ,//系统时钟,50MHz。
input rst_n ,//系统复位,低电平有效。
input [3 : 0] din_key ,//按键按下的数值;
input din_key_vld ,//按下指示信号,高电平有效;
input div_vld ,//除法模块计算完成指示信号;
input [26 : 0] div_in ,//除法模块计算结果;
input mult_vld ,//乘法模块运算结果有效指示信号;
input [26 : 0] mult_in ,//乘法模块运算结果;
//输出信号定义
output reg div_en ,//除法模块使能信号;
output reg mult_en ,//乘法模块使能信号;
output reg [26 : 0] data1 ,//被乘数或者被除数数据;
output reg [26 : 0] data2 ,//乘数或者除数数据;
output reg [26 : 0] dout //输出数码管需要显示的数据;
);
localparam IDLE = 5'b00001 ;//状态机的空闲状态编码;
localparam IDATA1 = 5'b00010 ;//状态机的输入参数1状态编码;
localparam OPERAT = 5'b00100 ;//状态机的输入运算符状态编码;
localparam IDATA2 = 5'b01000 ;//状态机的输入参数2状态编码;
localparam RESULT = 5'b10000 ;//状态机的计算结果状态编码;
reg number_flag ;//高电平表示由数字按键被按下;
reg operat_flag ;//高电平表示有运算符按键被按下;
reg din_key_vld_r0 ;//
reg din_key_vld_r1 ;
reg [1 : 0] operation ;//
reg [3 : 0] din_key_r0 ;//
reg [3 : 0] din_key_r1 ;//
reg [4 : 0] state_n ;
reg [4 : 0] state_c ;
reg [3 : 0] key_value ;//
reg operat_flag_r ;
wire idl2idata1_start ;
wire operat2idata2_start ;
wire idata12operat_start ;
wire idata22result_start ;
wire result2idata1_start ;
wire result2operat_start ;
//The first section: synchronous timing always module, formatted to describe the transfer of the secondary register to the live register�?
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
state_c <= IDLE;
end
else if(din_key_vld && din_key==13)begin//按下清除按键时,状态机回到空闲状态;
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
//The second paragraph: The combinational logic always module describes the state transition condition judgment.
always@(*)begin
case(state_c)
IDLE:begin
if(idl2idata1_start)begin
state_n = IDATA1;
end
else begin
state_n = state_c;
end
end
IDATA1:begin
if(idata12operat_start)begin
state_n = OPERAT;
end
else begin
state_n = state_c;
end
end
OPERAT:begin
if(operat2idata2_start)begin
state_n = IDATA2;
end
else begin
state_n = state_c;
end
end
IDATA2:begin
if(idata22result_start)begin
state_n = RESULT;
end
else begin
state_n = state_c;
end
end
RESULT:begin
if(result2idata1_start)begin
state_n = IDATA1;
end
else if(result2operat_start)begin
state_n = OPERAT;
end
else begin
state_n = state_c;
end
end
default:begin
state_n = IDLE;
end
endcase
end
// Third paragraph: Design transfer conditions;
assign idl2idata1_start = ((state_c == IDLE) && (number_flag));//空闲状态下有数字按键按下;
assign idata12operat_start = ((state_c == IDATA1) && (operat_flag));//在输入数据1状态下,有运算符按键按下;
assign operat2idata2_start = ((state_c == OPERAT) && (number_flag));//在输入运算符状态下,有数字按键被按下;
assign idata22result_start = ((state_c == IDATA2) && (din_key_vld && din_key==14));//在输入数据2状态下,等号被按下;
assign result2idata1_start = ((state_c == RESULT) && (number_flag));//在输出运算结果的状态下,有数字按键被按下;
assign result2operat_start = ((state_c == RESULT) && (operat_flag));//在输出运算结果的状态下,有运算符被按下;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
number_flag <= 1'b0;
end
else begin//数字按键按下时位高电平,其余时间均为低电平;
number_flag <= din_key_vld && (din_key != 3) && (din_key != 7) && (din_key != 11) && (din_key != 13) && (din_key != 14) && (din_key != 15);
end
end
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
operat_flag <= 1'b0;
end
else begin//运算符按键按下时位高电平,其余时间均为低电平;
operat_flag <= din_key_vld && ((din_key == 3) || (din_key == 7) || (din_key == 11) || (din_key == 15));
end
end
//对按键的输入信号进行暂存;
always@(posedge clk)begin
operat_flag_r <= operat_flag;
{din_key_r1,din_key_r0} <= {din_key_r0,din_key};//将输入的按键信号延迟两个时钟周期,与state_c信号对齐;
{din_key_vld_r1,din_key_vld_r0} <= {din_key_vld_r0,din_key_vld};//将输入的按键有效指示信号延迟两个时钟周期,与state_c信号对齐;
end
//当按下数字按键时,将数字按键译码成对应数字,key_value与din_key_vld_r1对齐;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
key_value <= 4'd0;
end
else if(din_key_vld_r0)begin
case(din_key_r0)//当按下数字按键时,将数字按键译码成对应数字;
4'd0 : key_value <= 4'd1;
4'd1 : key_value <= 4'd2;
4'd2 : key_value <= 4'd3;
4'd4 : key_value <= 4'd4;
4'd5 : key_value <= 4'd5;
4'd6 : key_value <= 4'd6;
4'd8 : key_value <= 4'd7;
4'd9 : key_value <= 4'd8;
4'd10 : key_value <= 4'd9;
4'd12 : key_value <= 4'd0;
default : key_value <= 4'd0;
endcase
end
end
//对运算进行保存,0表示加法运算,1表示减法运算,2表示乘法运算,3表示除法运算。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
operation <= 2'd0;
end
else if((state_c == OPERAT) && din_key_vld_r1)begin
case(din_key_r1)//当在符号状态下按下对应符号按键时,进行译码保存。
4'd3 : operation <= 2'd0;
4'd7 : operation <= 2'd1;
4'd11 : operation <= 2'd2;
4'd15 : operation <= 2'd3;
default: operation <= 2'd0;//默认进行加法运算;
endcase
end
end
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
data1 <= 14'd0;
end//当跳转到IDATA1状态时,将该数据清零;
else if(idl2idata1_start || result2idata1_start)begin
data1 <= 14'd0;
end//如果从输出结果状态跳转到输入符号状态,说明是进行连续运算,则将上次输出结果作为数据1;
else if(result2operat_start)begin
data1 <= dout;
end
//将原始数据乘以10,此处采用移位实现,然后加上输入数据,就可以表示当前数据;
else if(din_key_vld_r1 && (state_c == IDATA1))begin
data1 <= (data1 << 3) + (data1 << 1) + key_value;
end
end
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
data2 <= 14'd0;
end//当跳转到IDATA2状态时,将该数据清零;
else if(operat2idata2_start)begin
data2 <= 14'd0;
end//将原始数据乘以10,此处采用移位实现,然后加上输入数据,就可以表示当前数据;
else if(~operat_flag_r && din_key_vld_r1 && (state_c == IDATA2))begin
data2 <= (data2 << 3) + (data2 << 1) + key_value;
end
end
//对输出结果进行运算;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
dout <= 27'd0;
end
else if(state_c == IDATA1)begin//在输入数据1阶段显示输入的数据1;
dout <= data1;
end
else if(state_c == IDATA2)begin//在输入数据2阶段显示输入的数据2;
dout <= data2;
end
else if(state_c == RESULT)begin//在输出结果阶段,进行相关运算并输出进行显示;
case (operation)
2'd0 : dout <= data1 + data2;
2'd1 : dout <= data1 - data2;
2'd2 : if(mult_vld) dout <= mult_in;
2'd3 : if(div_vld) dout <= div_in;
default : dout <= dout;
endcase
end
else if(state_c == IDLE)begin//其他时候数码管显示0;
dout <= 27'd0;
end
end
//生成乘法模块或者除法模块使能信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
mult_en <= 1'b0;
div_en <= 1'b0;
end
else begin//当状态机跳转到运算结果的状且进行乘法运算,则将乘法运算的使能信号拉高;
mult_en <= (idata22result_start && (operation == 2'd2));
div_en <= (idata22result_start && (operation == 2'd3));
end
end
endmodule
5、乘法器模块
由于篇幅原因,本文不对乘法器的具体实现做讲解,具体实现方式已经在我另一篇文章进行详细讲解了,需要了解原理的可以自行查看。端口信号列表如下所示:
表4 乘法器模块信号列表
信号名 | 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个时钟周期。
图11 乘法器仿真
该模块代码如下所示:
module mult #(
parameter MULT_D = 8 ,//被乘数位宽;
parameter MULT_R = 4 //乘数位宽;
)(
input clk ,//系统时钟信号;
input rst_n ,//系统复位信号,低电平有效;
input start ,//开始运算信号,高电平有效;
input [MULT_D - 1 : 0] multiplicand ,//被乘数;
input [MULT_R - 1 : 0] multiplier ,//乘数;
output reg [MULT_D + MULT_R - 1 : 0] product ,//乘积输出;
output reg product_vld ,//乘积有效指示信号,高电平有效;
output reg rdy //模块忙闲指示信号,高电平表示空闲;
);
reg flag ;
reg [MULT_D - 1 : 0] multiplier_r ;//乘数的寄存器
reg [MULT_D + MULT_R - 1 : 0] multiplicand_r ;//被乘数的寄存器。
reg [MULT_D + MULT_R - 1 : 0] product_r ;//乘积寄存器;
wire start_f ;
//开始计算信号有效且乘数和被乘数均不等于0;
assign start_f = (~flag) && (start && (multiplicand != 0) && (multiplier != 0));
//运算标志信号,
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
flag <= 1'b0;
end
else if(start_f)begin//开始运算时拉高
flag <= 1'b1;
end
else if(multiplier_r == 1)begin//运算结束时拉低;
flag <= 1'b0;
end
end
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
multiplicand_r <= {{MULT_D + MULT_R}{1'b0}};
multiplier_r <= {{MULT_R}{1'b0}};
end
else if(start_f)begin//当计算开始时;
multiplicand_r <= multiplicand;//将被乘数加载到被乘数寄存器中。
multiplier_r <= multiplier;//将乘数加载到乘积寄存器中。
end
else if(flag)begin//正常计算标志信号有效时,被乘数左移一位,乘数右移一位。
multiplicand_r <= multiplicand_r << 1;
multiplier_r <= multiplier_r >> 1;
end
end
//计算乘法运算结果,开始信号有效时,将乘积清零。
//当乘数寄存器最低位为1时,加上此时被乘数的值。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
product_r <= {{MULT_D + MULT_R}{1'b0}};
end
else if(start)//当乘数或者被乘数为0时,乘积输出0.
product_r <= {{MULT_D + MULT_R}{1'b0}};
else if(flag && multiplier_r[0])begin//如果乘积的最低位为1,则把乘积的高位数据与被乘数相加。
product_r <= product_r + multiplicand_r;
end
end
//输出乘积和乘积有效指示信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
product <= {{MULT_D + MULT_R}{1'b0}};
product_vld <= 1'b0;
end
else if((~flag) && (start && ((multiplicand == 0) || (multiplier == 0))))begin
product <= {{MULT_D + MULT_R}{1'b0}};//如果开始计算时,乘数或者被乘数为0,则直接输出0;
product_vld <= 1'b1;
end
else if(flag && (multiplier_r == 1))begin//计算完成时,把计算结果输出,且乘积有效指示信号拉高;
product <= product_r + multiplicand_r;
product_vld <= 1'b1;
end
else begin//其余时间把有效指示信号拉低;
product_vld <= 1'b0;
end
end
//生成模块忙闲指示信号;
always@(*)begin//当开始信号有效或者标志信号有效时,模块处于工作状态;
if(start || flag)
rdy = 1'b0;
else//否则模块处于空闲状态;
rdy = 1'b1;
end
endmodule
6、除法器模块
由于篇幅原因,本文不对除法器的具体实现做讲解,需要了解原理的可以查看我写的除法器实现原理。端口信号列表如下所示:
表5 除法器模块信号列表
信号名 | 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,经过验算后没有问题。
图12 除法器仿真
该模块代码如下所示:
//注意此模块默认被除数的位宽大于等于除数的位宽。
//当quotient_vld信号为高电平且error为低电平时,输出的数据是除法计算的正确结果。
//当输入除数为0时,error信号拉高,且商和余数为0;
//当ready信号为低电平时,不能将开始信号start拉高,此时拉高start信号会被忽略。
module div #(
parameter L_DIVN = 8 ,//被除数的位宽;
parameter L_DIVR = 4 //除数的位宽;
)(
input clk ,//时钟信号;
input rst_n ,//复位信号,低电平有效;
input start ,//开始计算信号,高电平有效,必须在ready信号为高电平时输入才有效。
input [L_DIVN - 1 : 0] dividend ,//被除数输入;
input [L_DIVR - 1 : 0] divisor ,//除数输入;
output reg ready ,//高电平表示此模块空闲。
output reg error ,//高电平表示输入除数为0,输入数据错误。
output reg quotient_vld ,//商和余数输出有效指示信号,高电平有效;
output reg [L_DIVR - 1 : 0] remainder ,//余数,余数的大小不会超过除数大小。
output reg [L_DIVN - 1 : 0] quotient //商。
);
localparam L_CNT = clogb2(L_DIVN) ;//利用函数自动计算移位次数计数器的位宽。
localparam IDLE = 3'b001 ;//状态机空闲状态的编码;
localparam ADIVR = 3'b010 ;//状态机移动除数状态的编码;
localparam DIV = 3'b100 ;//状态机进行减法计算和移动被除数状态的编码;
reg vld ;//
reg [2 : 0] state_c ;//状态机的现态;
reg [2 : 0] state_n ;//状态机的次态;
reg [L_DIVN : 0] dividend_r ;//保存被除数;
reg [L_DIVR - 1 : 0] divisor_r ;//保存除数。
reg [L_DIVN - 1 : 0] quotient_r ;//保存商。
reg [L_CNT - 1 : 0] shift_dividend ;//用于记录被除数左移的次数。
reg [L_CNT - 1 : 0] shift_divisor ;//用于记录除数左移的次数。
wire [L_DIVR : 0] comparison ;//被除数的高位减去除数。
wire max ;//高电平表示被除数左移次数已经用完,除法运算基本结束,可能还需要进行一次减法运算。
//自动计算计数器位宽函数。
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
//max为高电平表示被除数左移的次数等于除数左移次数加上被除数与除数的位宽差;
assign max = (shift_dividend == (L_DIVN - L_DIVR) + shift_divisor);
//用来判断除数和被除数第一次做减法的高位两者的大小,当被除数高位大于等于除数时,comparison最高位为0,反之为1。
//comparison的计算结果还能表示被除数高位与除数减法运算的结果。
//在移动除数时,判断的是除数左移一位后与被除数高位的大小关系,进而判断能不能把除数进行左移。
assign comparison = ((divisor[L_DIVR-1] == 0) && ((state_c == ADIVR))) ?
dividend_r[L_DIVN : L_DIVN - L_DIVR] - {divisor_r[L_DIVR-2 : 0],1'b0} :
dividend_r[L_DIVN : L_DIVN - L_DIVR] - divisor_r;//计算被除数高位减去除数,如果计算结果最高位为0,表示被除数高位大于等于除数,如果等于1表示被除数高位小于除数。
//状态机次态到现态的转换;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为空闲状态;
state_c <= IDLE;
end
else begin//状态机次态到现态的转换;
state_c <= state_n;
end
end
//状态机的次态变化。
always@(*)begin
case(state_c)
IDLE : begin//如果开始计算信号为高电平且除数和被除数均不等于0。
if(start & (dividend != 0) & (divisor != 0))begin
state_n = ADIVR;
end
else begin//如果开始条件无效或者除数、被除数为0,则继续处于空闲状态。
state_n = state_c;
end
end
ADIVR : begin//如果除数的最高位为高电平或者除数左移一位大于被除数的高位,则跳转到除法运算状态;
if(divisor_r[L_DIVR-1] | comparison[L_DIVR])begin
state_n = DIV;
end
else begin
state_n = state_c;
end
end
DIV : begin
if(max)begin//如果被除数移动次数达到最大值,则状态机回到空闲状态,计算完成。
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
default : begin//状态机跳转到空闲状态;
state_n = IDLE;
end
endcase
end
//对被除数进行移位或进行减法运算。
//初始时需要加载除数和被除数,然后需要判断除数和被除数的高位,确定除数是否需要移位。
//然后根据除数和被除数高位的大小,确认被除数是移位还是与除数进行减法运算,注意被除数移动时,为了保证结果不变,商也会左移一位。
//如果被除数高位与除数进行减法运算,则商的最低位变为1,好比此时商1进行的减法运算。经减法结果赋值到被除数对应位。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
divisor_r <= 0;
dividend_r <= 0;
quotient_r <= 0;
shift_divisor <= 0;
shift_dividend <= 0;
end//状态机处于加载状态时,将除数和被除数加载到对应寄存器,开始计算;
else if(state_c == IDLE && start && (dividend != 0) & (divisor != 0))begin
dividend_r <= dividend;//加载被除数到寄存器;
divisor_r <= divisor;//加载除数到寄存器;
quotient_r <= 0;//将商清零;
shift_dividend <= 0;//将移位的被除数寄存器清零;
shift_divisor <= 0; //将移位的除数寄存器清零;
end//状态机处于除数左移状态,且除数左移后小于等于被除数高位且除数最高位为0。
else if(state_c == ADIVR && (~comparison[L_DIVR]) && (~divisor_r[L_DIVR-1]))begin
divisor_r <= divisor_r << 1;//将除数左移1位;
shift_divisor <= shift_divisor + 1;//除数总共被左移的次数加1;
end
else if(state_c == DIV)begin//该状态需要完成被除数移位和减法运算。
if(comparison[L_DIVR] && (~max))begin//当除数大于被除数高位时,被除数需要移位。
dividend_r <= dividend_r << 1;//将被除数左移1位;
quotient_r <= quotient_r << 1;//同时把商左移1位;
shift_dividend <= shift_dividend + 1;//被除数总共被左移的次数加1;
end
else if(~comparison[L_DIVR])begin//当除数小于等于被除数高位时,被除数高位减去除数作为新的被除数高位。
dividend_r[L_DIVN : L_DIVN - L_DIVR] <= comparison;//减法结果赋值给被除数进行减法运算的相应位。
quotient_r[0] <= 1;//因为做了一次减法,则商加1。
end
end
end
//生成状态机从计算除结果的状态跳转到空闲状态的指示信号,用于辅助设计输出有效指示信号。
always@(posedge clk)begin
vld <= (state_c == DIV) && (state_n == IDLE);
end
//生成商、余数及有效指示信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
quotient <= 0;
remainder <= 0;
quotient_vld <= 1'b0;
end//如果开始计算时,发现除数或者被除数为0,则商和余数均输出0,且将输出有效信号拉高。
else if(state_c == IDLE && start && ((dividend== 0) || (divisor==0)))begin
quotient <= 0;
remainder <= 0;
quotient_vld <= 1'b1;
end
else if(vld)begin//当计算完成时。
quotient <= quotient_r;//把计算得到的商输出。
quotient_vld <= 1'b1;//把商有效是指信号拉高。
//移动剩余部分以补偿对齐变化,计算得到余数;
remainder <= (dividend_r[L_DIVN - 1 : 0]) >> shift_dividend;
end
else begin
quotient_vld <= 1'b0;
end
end
//当输入除数为0时,将错误指示信号拉高,其余时间均为低电平。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
error <= 1'b0;
end
else if(state_c == IDLE && start)begin
if(divisor==0)//开始计算时,如果除数为0,把错误指示信号拉高。
error <= 1'b1;
else//开始计算时,如果除数不为0,把错误指示信号拉低。
error <= 1'b0;
end
end
//状态机处于空闲且不处于复位状态;
always@(*)begin
if(start || state_c != IDLE || vld)
ready = 1'b0;
else
ready = 1'b1;
end
endmodule
7、二进制转BCD模块
该模块输入27位二进制数据,输出32位BCD码,如果直接使用除法和取余操作,将消耗大量逻辑资源,并且时钟频率还不能提高。所以就采用移位和加法的算法来实现转换,这个模块我之前就已经设计过,具体细节还是挺多的,可以通过文章查看,本文不对该模块具体实现方式进行讲解,设计的时候考虑了参数化,直接修改输入参数位宽即可实现任意位宽的转换,端口列表如下:
表6 二进制转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码有效指示信号。 |
该模块的端口仿真如下图所示,输入数据按照十进制显示,输出数据按照十六进制数据显示,输入数据为17’d29时,输出数据为36’h000000029,转换完成。
图13 二进制转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
8、数码管刷新模块
该模块用的也比较多了,8个数码管共用同一组数据线,那么这组数据线就只能通过时分复用的方式传递数据。该模块我也发布过专门的模块驱动文章,需要的可以自行查看。该模块对应的端口列表如下所示:
表7 数码管刷新模块信号列表
信号名 | 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模块的驱动,再生成相应输出。
该模块仿真比较简单,如下图所示,需要显示的数据为32’h00000234,由于底板上位选最低位对应的是最左边的数码管,第1个十进制数需要与位选的最高位对齐,第8个十进制数需要与位选的最低位对齐,仿真结果如下所示。
位选信号为8’h7f是,此时数据段应该输出8’h66,数码管显示4这个数字。
图14 数码管刷新模块仿真
修改后的代码如下所示:
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
9、74H595模块驱动
数码管的驱动电路如下所示,通过两片74HC595驱动8个数码管,关于74HC595的驱动,我也有相应文章进行讲解,由于篇幅问题,本文只对该模块进行仿真。
图15 数码管驱动电路
信号端口列表如下所示。
表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芯片锁存器时钟信号。 |
该模块整体仿真结果如下所示:
图16 整体仿真
该模块细节仿真如下所示:
图17 细节仿真
该模块的代码如下所示:
module hc595_drive #(
parameter SEG_NUM = 8 ,//需要显示的数码管个数。
parameter SCLK_DIV = 4 //sclk与系统时钟的分频系数。
)(
//输入信号定义
input clk ,//系统时钟,50MHz。
input rst_n ,//系统复位,低电平有效。
input [7 : 0] segment ,//数码管的数据线;
input [SEG_NUM - 1 : 0] seg_sel ,//数码管的位选信号;
input din_vld ,//段选和位选有效指示信号;
output reg ds ,//74HC595串行数据线;
output reg sclk ,//74HC595移位寄存器时钟;
output reg rclk //74HC595锁存器时钟;
);
//参数定义
localparam SCLK_DIV_W = clogb2(SCLK_DIV - 1);//利用函数自动计算位宽;
//中间信号定义
reg flag ;//
reg [15 : 0] din_r ;//
reg [SCLK_DIV_W - 1 : 0] div_cnt ;//
reg [4 : 0] cnt ;//
wire add_cnt ;
wire end_cnt ;
wire add_div_cnt ;
wire end_div_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>>1;
end
endfunction
//标志信号,当需要刷新时拉高,当刷新完成时拉低;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
flag <= 1'b0;
end
else if(end_cnt)begin//计数器计数结束,表示刷新完成;
flag <= 1'b0;
end
else if(din_vld)begin//有数据需要刷新;
flag <= 1'b1;
end
end
//当输入数据有效时,将需要显示的数据暂存;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
din_r <= 16'd0;
end
else if(din_vld)begin//数据信号存在高位,先输出;
din_r <= {segment[7:0],seg_sel[7:0]};
end
end
//分频系数计数器,当flag信号为高电平时有效;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//
div_cnt <= 0;
end
else if(add_div_cnt)begin
if(end_div_cnt)
div_cnt <= 0;
else
div_cnt <= div_cnt + 1;
end
end
assign add_div_cnt = flag;//处于刷新状态时,计数器对系统时钟计数;
assign end_div_cnt = add_div_cnt && div_cnt == SCLK_DIV - 1;//计数到分频系数清零;
//计数发送数据的位数,需要发送16位数据,且需要将锁存时钟拉高,所以需要计数17;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 0;
else
cnt <= cnt + 1;
end
end
assign add_cnt = end_div_cnt;//当分频计数器计数结束表示1位数据发送完成,此计数器加1。
assign end_cnt = add_cnt && cnt == 17 - 1;//当发送完16位数据且锁存时钟拉高后清零,表示完成刷新;
//产生74hc595的移位时钟信号;
//add_div_cnt && div_cnt == 0表示sclk的下降沿;
//add_div_cnt && div_cnt == SCLK_DIV/2-1表示sclk的上升沿;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
sclk <= 1'b0;
end
else if(add_div_cnt)begin
if(div_cnt == 0)//当分频计数器为0且计数条件有效时拉低。
sclk <= 1'b0;
else if(div_cnt == (SCLK_DIV >> 1))//当分频计数器计数到一半时拉高;
sclk <= 1'b1;
end
end
//在SCLK下降沿输出数据;
//FPGA需要在SCLK下降沿更新数据,74HC595在上升沿才能采集稳定的数据。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
ds <= 1'b0;
end//当分频计数器为0,且发送的数据小于16时,输出数据;
else if(add_div_cnt && (div_cnt == 0) && (cnt < 16))begin
ds <= din_r[15 - cnt];//其实就是在sclk下降沿输出数据;
end
end
//产生锁存时钟信号,当数据全部发送完毕后,将锁存时钟拉高一个时钟周期。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
rclk <= 1'b0;
end
else if(end_cnt)begin
rclk <= 1'b0;
end
else if(add_div_cnt && (cnt == 16))begin
rclk <= 1'b1;
end
end
endmodule
10、仿真激励文件
本设置只有一个仿真激励文件,也就是顶层模块的激励文件,通过驱动顶层,对其余子模块进行仿真,把按键信号写成了然乌,只需要调用该任务就可以模拟按键被按下,简化仿真激励文件的编写,仿真激励文件的代码在下面。
`timescale 1 ns/1 ns
module test();
localparam CYCLE = 20 ;//系统时钟周期,单位ns,默认20ns;
localparam RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
localparam TIME_20MS = 4_000 ;//按键消抖时间,默认20ms,单位ns,仿真时将时间缩短;
localparam TIME_20US = 4000 ;//每个数码管刷新时间,默认20us;
reg clk ;//系统时钟,默认100MHz;
reg rst_n ;//系统复位,默认低电平有效;
reg [3 : 0] key_col ;
reg [3 : 0] key_col_r ;
reg key_col_sel ;
reg [1 : 0] now_row ;//当前行
reg [1 : 0] now_col ;//当前列;
wire [3 : 0] key_row ;
wire ds ;
wire sclk ;
wire rclk ;
//例化需要测试的模块;
top #(
.TCLK ( CYCLE ),
.TIME_20MS ( TIME_20MS ),
.TIME_20US ( TIME_20US )
)
u_top (
.clk ( clk ),
.rst_n ( rst_n ),
.key_col ( key_col ),
.key_row ( key_row ),
.ds ( ds ),
.sclk ( sclk ),
.rclk ( rclk )
);
//生成周期为CYCLE数值的系统时钟;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
rst_n = 1;key_col = 4'hf;key_col_sel=0;
key_col_r = 4'hf;now_col = 0;now_row = 0;
#1;
rst_n = 0;//开始时复位10个时钟;
#(RST_TIME*CYCLE);
rst_n = 1;
#(20*CYCLE);
key_task(0,1);//按下第0行第1列按键;输入2
key_task(1,2);//按下第1行第2列按键;输入6
key_task(0,3);//按下第0行第3列按键,输入加号
key_task(0,1);//按下第0行第1列按键;输入2
key_task(2,2);//按下第2行第2列按键;输入9
key_task(3,0);//按下第3行第0列按键,输入0
key_task(3,2);//按下第3行第2列按键,输入等号
key_task(1,3);//按下第1行第3列按键,输入减号
key_task(2,1);//按下第2行第1列按键;输入8
key_task(0,1);//按下第0行第1列按键,输入2
key_task(3,2);//按下第3行第2列按键,输入等号
key_task(2,3);//按下第2行第3列按键,输入乘号
key_task(0,0);//按下第0行第0列按键;输入1
key_task(0,2);//按下第0行第2列按键,输入3
key_task(2,3);//按下第2行第3列按键,输入乘号
key_task(3,3);//按下第3行第3列按键,输入除号
key_task(3,2);//按下第3行第2列按键,输入等号
key_task(3,3);//按下第3行第3列按键,输入除号
key_task(2,0);//按下第2行第0列按键;输入7
key_task(0,0);//按下第0行第0列按键,输入1
key_task(3,2);//按下第3行第2列按键,输入等号
#(20*CYCLE);
$stop;//停止仿真;
end
//生成对应按键的信号;
task key_task(
input [1 : 0] row ,//被按下按键的行号;
input [1 : 0] col //被按下按键的列号;
);
begin
now_col <= col;
now_row <= row;
key_col_r <= 4'hf;//初始时,没有按键被按下;
key_col_sel <= 1'b0;
@(posedge clk);
key_col_r[col] <= 1'b0;//
repeat(20) begin//将信号随机翻转20次,模拟按键按下时的抖动。
#(({$random} % (TIME_20MS/(CYCLE+5))) * CYCLE);
key_col_r[col] <= ~key_col_r[col];
end
key_col_sel <= 1'b1;//列选通信号拉高;
repeat(TIME_20MS/7)begin//按键按下的保持时间;
@(posedge clk);
end
key_col_sel <= 1'b0;//列选通信号拉低;
key_col_r <= 4'hf;//按键被释放;
repeat(20) begin//将信号随机翻转20次,模拟按键释放时的抖动。
#(({$random} % (TIME_20MS/(CYCLE+5))) * CYCLE);
key_col_r[col] <= ~key_col_r[col];
end
#(({$random} % (TIME_20MS/(CYCLE+20))) * CYCLE);
key_col_r[col] <= 1'b1;//确保按键被释放;
repeat(TIME_20MS)begin//释放按键的保持时间;
@(posedge clk);
end
end
endtask
always@(*)begin
if(key_col_sel)begin
case (now_col)
2'd0 : key_col <= {3'd7,key_row[now_row]};
2'd1 : key_col <= {2'd3,key_row[now_row],1'd1};
2'd2 : key_col <= {1'd1,key_row[now_row],2'd3};
2'd3 : key_col <= {key_row[now_row],3'd7};
endcase
end
else begin
key_col <= key_col_r;
end
end
endmodule
11、总结
我做的是最简单的一个题目,所以完成的速度比较快,加上很多模块都是以前积累的,算是给大家探路吧。通过本次设计,我觉得IDE综合工具的管脚分配图形化界面做的很棒,而且还实现了图形化的对应,相当不错的创新设计。仿真的画还是差点意思,不能添加自己想要的信号,会直接吧设计中的所有信号添加到放着中,并且多余多bit信号的仿真特别不友好,希望改进。