特色:启用8个数码管中最右侧的4个显示。除法能计算出两位小数,而不一定非得是整除;考虑到小数的一般书写习惯,如果小数点后最末尾存在0可以自动删除并数据整体向右侧补位(例如18/3显示为6而非6.00)。输入的数可以是1位也可以是2位,输入灵活度增加。可以按下清零键后从头开始进行运算,无需再按复位键。
1.项目需求
实现一个两位十进制数加、减、乘、除运算的计算器,运算数和运算符(加、减、乘、除)由按键来控制,复位按键和4×4键盘按键分配如下图所示。
- /* 4×4键盘按键分配
- 7 8 9 +
- 4 5 6 -
- 1 2 3 *
- 0 C = /
- */
运算数和计算结果通过8个八段数码管显示。每个运算数使用两个数码管显示,左侧显示十位数,右侧显示个位数。输入两位十进制数时,最高位先在右侧显示,然后其跳变到左侧的数码管上,低位在刚才高位占据的数码管上显示。
2.需求分析
根据项目需求和给定的硬件资源,我们需要实现一个基于矩阵键盘和两个74HC595D芯片的两位十进制数加减乘除运算的计算器。下面是对需求的分析:
硬件需求:
4×4矩阵键盘:用于输入运算数和运算符。
8个八段数码管:用于显示运算数和计算结果。每个运算数使用两个数码管显示,分别表示十位数和个位数。
功能需求:
实现加、减、乘、除四种基本运算。
通过矩阵键盘输入两位十进制数和运算符。
显示输入的运算数和计算结果。
在输入两位十进制数时,最高位先在右侧显示,然后跳变到左侧的数码管上,低位在刚才高位占据的数码管上显示。
软件实现:
使用两个74HC595D芯片作为数码管的驱动器,通过串行方式控制8个数码管的显示。
实现输入的解析和运算逻辑,根据输入的运算符执行相应的运算。
控制数码管显示输入的运算数和计算结果,按照需求将输入的两位十进制数在数码管上显示。74HC595D是一种8位串行移位寄存器,通常用于驱动数码管或其他类似的数字显示设备。使用两个74HC595D可以实现8个数码管显示。
难点:系统应具有实时性,即时响应用户的按钮输入。同时一次按键不能导致多次输出,乘法器和除法器的设计也是难点。
3.实现的方式
按键扫描和识别:
使用Verilog编写按键扫描和识别的模块,该模块可以扫描矩阵键盘的按键状态,并将按下的键位进行识别。
将识别出的键位存储到适当的寄存器中,以备后续处理。
数值处理和转换为BCD码:
根据按键输入的顺序和规则,将识别出的按键值转换为两位十进制数。
实现加减乘除运算的逻辑,根据输入的运算符和运算数进行相应的运算。
将运算结果转换为BCD码(二进制码十进制),以便后续显示。
输出的BCD码转换为74HC595D的时序:
编写Verilog代码实现BCD码转换为74HC595D的时序控制逻辑。
使用串行方式将BCD码发送到74HC595D芯片,并控制时钟、数据和存储器时钟等引脚,将数据正确地加载到74HC595D中。
需要设计一个状态机来处理不同的状态:输入运算数状态、输入运算符状态、计算状态和显示结果状态。
在输入运算数状态和输入运算符状态,根据按键输入构建运算表达式,并处理格式问题。这里使用了一些的标志位来处理。小数点、0的显示都是避不开的问题。
在计算状态,对输入的运算表达式进行加减乘除运算,并将结果转换为BCD码。
在显示结果状态,将BCD码发送到74HC595D芯片进行数码管的显示。
4.功能框图
5.代码(内嵌到报告中)及说明
顶层文件:
module type_system(
input clk,
input rst_n,
input [3:0] col,
output [3:0] row,
output rclk_out, //74HC595的RCK管脚
output sclk_out, //74HC595的SCK管脚
output sdio_out //74HC595的SER管脚
);
wire [3:0] seg_data;
wire seg_data_flag;
wire [15:0] key_out;
wire [15:0] key_pulse;
//wire [3:0] seg_data;
//Array_KeyBoard
array_keyboard u1(
.clk(clk),
.rst_n(rst_n),
.col(col),
.row(row),
.key_out(key_out),
.key_pulse(key_pulse)
);
//key_decode
key_decode u2(
.clk (clk ),
.rst_n (rst_n ),
.key_pulse (key_pulse ),
.seg_data (seg_data ), //高4位代表十位,低4位代表个位
.seg_data_flag (seg_data_flag )
);
//wire [7:0] result; // 定义结果
calculator calculator_inst (
.clk(clk),
.input_data(seg_data),
.input_valid(seg_data_flag),
.rst_n_in(rst_n),
.rclk_out(rclk_out), //74HC595的RCK管脚
.sclk_out(sclk_out), //74HC595的SCK管脚
.sdio_out(sdio_out) //74HC595的SER管脚
);
endmodule
- 输入输出信号:
- 输入信号:
- clk:时钟信号
- rst_n:复位信号(低电平有效)
- col:列线输入,4位信号
- 输出信号:
- row:行线输出,4位信号
- rclk_out:74HC595芯片的RCK(存储时钟)管脚输出
- sclk_out:74HC595芯片的SCK(移位时钟)管脚输出
- sdio_out:74HC595芯片的SER(串行数据输入)管脚输出
- 模块实例化:
- array_keyboard u1:实例化了一个名为u1的array_keyboard模块,用于处理矩阵键盘输入。
- key_decode u2:实例化了一个名为u2的key_decode模块,用于解码按键并生成对应的数码管显示数据。
- calculator calculator_inst:实例化了一个名为calculator_inst的calculator模块,用于将解码后的数码管显示数据控制输出到74HC595芯片。
- 信号连接:
- seg_data和seg_data_flag:用于传递解码后的数码管显示数据和有效标志,用来保证数据有效性。
- key_out和key_pulse:用于处理从矩阵键盘输入得到的按键数据和按键脉冲信号。
- 代码功能:
- array_keyboard模块处理矩阵键盘输入,并生成按键数据和按键脉冲信号。
- key_decode模块解码按键数据,并生成对应的数码管显示数据和有效标志。
- calculator模块控制74HC595芯片,将数码管显示数据输出到对应的管脚上,实现数码管显示。
STEP BaseBoard V4.0底板上的4×4矩阵键盘电路图如下:
该电路图包括4根行线(ROW1、ROW2、ROW3、ROW4)和4根列线(COL1、COL2、COL3、COL4)。列线通过上拉电阻连接到VCC电压(3.3V)。矩阵按键的工作原理如下:
- 4根行线由FPGA控制,可以拉高或拉低。
- 4根列线则是输出线,它们的状态由4根行线的输入和按键状态共同决定,然后输出给FPGA。
在某一时刻,如果FPGA控制4根行线分别为ROW1=0、ROW2=1、ROW3=1、ROW4=1时,
- 对于K1、K2、K3、K4按键:当按下时,对应的4根列线输出COL1=0、COL2=0、COL3=0、COL4=0;未按下时,对应的4根列线输出COL1=1、COL2=1、COL3=1、COL4=1。
- 对于K5到K16之间的按键:无论按下与否,对应的4根列线输出COL1=1、COL2=1、COL3=1、COL4=1。
根据上述描述,只有在K1、K2、K3、K4按键被按下时,才会导致4根列线输出COL1=0、COL2=0、COL3=0、COL4=0;否则,输出为COL1=1、COL2=1、COL3=1、COL4=1。反过来,当FPGA检测到列线(COL1、COL2、COL3、COL4)中有低电平信号时,对应的K1、K2、K3、K4按键应该被按下了。
扫描的方式将这个过程分为4个时刻,对应4根行线中的一根被拉低。这样的循环扫描方式完成了对矩阵按键的全部扫描检测。程序中可以将这4个时刻映射到状态机的4个状态。
当然,这只是一行的情况,而我们有四行。将矩阵键盘的扫描周期分为4个时刻,对应4个状态,使得状态机在4个状态上循环跳转,最终通过扫描的方式获取矩阵键盘的操作状态。
扫描显示是一种节约I/O口资源的方法,适用于控制多个数码管的情况。在小脚丫底板上使用的是共阴极数码管中,扫描显示的原理如下:
在每个时刻,FPGA控制8根公共的段选接口输出对应数字1的数码管字库数据(例如8'h06,表示数字1的显示模式),同时只有1位数码管处于使能状态,其余数码管不显示。这通过控制数码管的位选信号来实现,例如第1位数码管为使能状态,其他位数码管不使能。
按照扫描的方式,总共分为8个时刻,每个时刻段选端口对应输出数码管需要显示的字库数据,位选端口则保持只有1位数码管处于使能状态。这8个时刻依次循环,当扫描频率足够高,则人眼看到的数码管显示是连续的,因为切换速度很快,我们会看到8个不同的数字在数码管上显示。
这种扫描显示方法有效地节约了I/O口资源,因为只需要8根段选信号和8根位选信号,共计16根信号,就可以控制多个数码管的显示,而不是每个数码管都需要独立的控制信号线。
74HC595是较为常用的串行转并行的芯片,内部集成了一个8位移位寄存器、一个存储器和8个三态缓冲输出。在最简单的情况下我们只需要控引脚输入得到8根引脚并行输出信号,我们使用3个I/O口控制两个级联的74HC595芯片,产生16路并行输出,连接到扫描显示的数码管上,完成数码管驱动任务。
参照给出的旧版本3.0的6个数码管电路和代码,我们需要增加两个数码管的显示。只需要增加两个状态,再在扫描中计算并加入对应的段选和位选即可。
旧版本3.0的状态转换和时序
整体运算和处理的逻辑需要大量标志位,下面是部分变量。
reg [2:0] state; // 定义状态寄存器,表示7个不同的状态
reg [7:0] num1, num2; // 定义两个输入数字
reg [3:0] operator; // 定义运算符
reg [14:0] result; // 定义结果
reg [7:0] seg_dot_en; //小数点标志位
reg seg_dot_en_flag; //小数点显示标志位
reg negative_flag; //负号标志位
reg div_error_flag; //错误标志位
这是我在处理数据的一些变量,就不过多介绍了,主要是显示和计算的逻辑处理。虽然这里的单个逻辑都不会很难,但是这里的逻辑很多,一定要有耐心。
if (!rst_n_in || input_data == 4'd14) begin
state <= STATE_NUM1_INPUT1;
num1 <= 4'b0;
num2 <= 4'b0;
operator <= 4'b0;
result <= 8'b0;
negative_flag <=0 ;
div_error_flag <= 0;
// seg_dot_en <= 0 ;
seg_dot_en_flag <= 0;
end
数值显示逻辑我写的比较复杂,用了很多变量,主要是理清逻辑,考虑每一种情况:
always @(posedge clk) begin
seg_dot_en = 8'd0 ;
seg_data_en[3:0] = 4'b0000;//不用
seg_data_en[7] = 1'b1;
if(seg_dot_en_flag == 1)//小数点显示
begin
if(tens == 4'd0 && units == 4'd0) begin display_units_ago = hundreds; display_tens_ago = thousands; display_hundreds_ago = 4'd0; display_thousands_ago = 4'd0; end
else if(units == 4'd0) begin display_units_ago = tens; display_tens_ago = hundreds; display_hundreds_ago = thousands; display_thousands_ago = 4'd0; seg_dot_en[6]=1; end
else begin display_units_ago = units; display_tens_ago = tens; display_hundreds_ago = hundreds; display_thousands_ago = thousands; seg_dot_en[5]=1; end
// 十位显示
if (display_tens_ago == 4'd0 && display_hundreds_ago == 4'd0 && display_thousands_ago == 4'd0 && seg_dot_en[6]==0 && seg_dot_en[5]==0)
seg_data_en[6] = 1'b0;
else
seg_data_en[6] = 1'b1;
// 百位显示
if (display_hundreds_ago == 4'd0 && display_thousands_ago == 4'd0 && seg_dot_en[5]==0)
seg_data_en[5] = 1'b0;
else
seg_data_en[5] = 1'b1;
// 千位显示
if (display_thousands_ago == 4'd0)
seg_data_en[4] = 1'b0;
else
seg_data_en[4] = 1'b1;
end
else//一般情况显示
begin
// 十位显示
if (tens == 4'd0 && hundreds == 4'd0 && thousands == 4'd0)
seg_data_en [6] = 1'b0;
else
seg_data_en[6] = 1'b1;
// 百位显示
if (hundreds == 4'd0 && thousands == 4'd0)
seg_data_en [5] = 1'b0;
else
seg_data_en [5] = 1'b1;
// 千位显示
if (thousands == 4'd0)
seg_data_en[4] = 1'b0;
else
seg_data_en[4] = 1'b1;
end
if(negative_flag==1)//最后阶段 && input_data == 4'd15 && state==STATE_NUM1_INPUT1)(state==STATE_OPERATOR_FINAL || )
begin
display_units = units;
if(seg_data_en[6:4] == 3'b000) begin display_tens = 4'd10; seg_data_en[6] = 1'b1; display_hundreds = hundreds; display_thousands = thousands; end
else if(seg_data_en[5:4] == 2'b00) begin display_tens = tens; display_hundreds = 4'd10; seg_data_en [5] = 1'b1; display_thousands = thousands; end
else if(seg_data_en[4:4] == 1'b0) begin display_tens = tens; display_hundreds = hundreds; display_thousands = 4'd10; seg_data_en[4] = 1'b1; end
end
else if(seg_dot_en_flag)//触发小数情况
begin
display_units = display_units_ago;
display_tens = display_tens_ago;
display_hundreds = display_hundreds_ago;
display_thousands = display_thousands_ago;
end
else if(div_error_flag)//(display_units_ago==4'b1000 && display_tens_ago==4'b1000 && display_hundreds_ago==4'b1000 && display_units_ago==4'b1000) //触发小数非法情况
begin
display_units = 4'd10;
display_tens = 4'd10;
display_hundreds = 4'd10;
display_thousands = 4'd10;
end
else
begin
display_units = units;
display_tens = tens;
display_hundreds = hundreds;
display_thousands = thousands;
end
seg_data_en_final = seg_data_en;
seg_dot_en_final = seg_dot_en;
end
BCD码的计算使用的是加3移位的方式。
module bcd_d(
input wire [13:0] binary,
output wire [3:0] g,
output wire [3:0] s,
output wire [3:0] b,
output wire [3:0] q
);
//***********************//
/*
* z 作为存储 BCD 码和 二进制码的寄存器
* 如果输入为 8 位,那么 z 需要的长度为
* 0xFF = 255 ---> 10-0101-0101 +++ ????-????
* 总共 18 位
*/
reg [29:0] z;
//***********************//
always @ (*)
begin
z = 29'b0; //置 0
z[13:0] = binary; //读入低 8 位
repeat (14) //重复 8 次
begin
if(z[17:14]>4) //大于 4 就加 3
z[17:14] = z[17:14] + 2'b11;
if(z[21:18]>4)
z[21:18] = z[21:18] + 2'b11;
if(z[25:22]>4)
z[25:22] = z[25:22] + 2'b11;
if(z[29:26]>4)
z[29:26] = z[29:26] + 2'b11;
z[29:1] = z[28:0]; //左移一位
end
end
assign q = z[29:26];
assign b = z[25:22]; //输出 BCD 码
assign s = z[21:18];
assign g = z[17:14];
endmodule
需要格外注意的是,中间的还许多情况需要考虑,例如除数为0输出错误,对数据显示的处理,但是这些主要是细节和逻辑,将它们理清还是不简单的。我的方法是先对它们单独处理,再统一逻辑,将相同的问题归类。例如,去除先导0的问题是4种运算的共同问题,但是小数点后的0只有除法出现,要单独处理。小数点只出现在除法中,乘法最大99*99。
6.仿真波形图
7.FPGA的资源利用说明
Design Summary
Number of registers: 258 out of 4635 (6%)
PFU registers: 258 out of 4320 (6%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 1564 out of 2160 (72%)
SLICEs as Logic/ROM: 1564 out of 2160 (72%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 626 out of 2160 (29%)
Number of LUT4s: 3111 out of 4320 (72%)
Number used as logic LUTs: 1859
Number used as distributed RAM: 0
Number used as ripple logic: 1252
Number used as shift registers: 0
Number of PIO sites used: 13 + 4(JTAG) out of 105 (16%)
Number of block RAMs: 0 out of 10 (0%)
Number of GSRs: 1 out of 1 (100%)
EFB used : No
JTAG used : No
Readback used : No
Oscillator used : No
Startup used : No
POR : On
Bandgap : On
Number of Power Controller: 0 out of 1 (0%)
Number of Dynamic Bank Controller (BCINRD): 0 out of 6 (0%)
Number of Dynamic Bank Controller (BCLVDSO): 0 out of 1 (0%)
Number of DCCA: 0 out of 8 (0%)
Number of DCMA: 0 out of 2 (0%)
Number of PLLs: 0 out of 2 (0%)
Number of DQSDLLs: 0 out of 2 (0%)
Number of CLKDIVC: 0 out of 4 (0%)
Number of ECLKSYNCA: 0 out of 4 (0%)
Number of ECLKBRIDGECS: 0 out of 2 (0%)
Notes:-
1. Total number of LUT4s = (Number of logic LUT4s) + 2*(Number of
distributed RAMs) + 2*(Number of ripple logic)
2. Number of logic LUT4s does not include count of distributed RAM and
ripple logic.
Number of clocks: 2
Net clk_c: 141 loads, 141 rising, 0 falling (Driver: PIO clk )
Net clk_200hz: 24 loads, 0 rising, 24 falling (Driver: u1/clk_200hz_38 )
Number of Clock Enables: 28
Net clk_c_enable_142: 5 loads, 5 LSLICEs
Net u1/clk_200hz_N_24_enable_42: 6 loads, 6 LSLICEs
Net u1/clk_200hz_N_24_enable_48: 6 loads, 6 LSLICEs
Net u1/clk_200hz_N_24_enable_39: 6 loads, 6 LSLICEs
Net u1/clk_200hz_N_24_enable_46: 6 loads, 6 LSLICEs
Net calculator_inst/clk_c_enable_124: 5 loads, 5 LSLICEs
Net calculator_inst/clk_c_enable_117: 4 loads, 4 LSLICEs
Net calculator_inst/clk_c_enable_110: 13 loads, 13 LSLICEs
Net calculator_inst/clk_c_enable_97: 3 loads, 3 LSLICEs
Net calculator_inst/clk_c_enable_68: 10 loads, 10 LSLICEs
Net calculator_inst/clk_c_enable_18: 1 loads, 1 LSLICEs
Net calculator_inst/clk_c_enable_24: 1 loads, 1 LSLICEs
Net calculator_inst/clk_c_enable_27: 1 loads, 1 LSLICEs
Net calculator_inst/clk_c_enable_77: 1 loads, 1 LSLICEs
Net seg_data_flag: 3 loads, 3 LSLICEs
Net calculator_inst/unsigned_mul_inst/clk_c_enable_93: 14 loads, 14 LSLICEs
Net calculator_inst/unsigned_mul_inst/clk_c_enable_41: 7 loads, 7 LSLICEs
Net calculator_inst/unsigned_mul_inst/state_1_N_483_1: 1 loads, 1 LSLICEs
Net calculator_inst/unsigned_mul_inst/state_1: 14 loads, 14 LSLICEs
Net calculator_inst/Segment_scan_inst/clk_c_enable_137: 10 loads, 10
LSLICEs
Net calculator_inst/Segment_scan_inst/clk_c_enable_89: 4 loads, 4 LSLICEs
Net calculator_inst/Segment_scan_inst/clk_c_enable_83: 1 loads, 1 LSLICEs
Net calculator_inst/Segment_scan_inst/state_1: 2 loads, 2 LSLICEs
Net calculator_inst/Segment_scan_inst/clk_c_enable_86: 1 loads, 1 LSLICEs
Net calculator_inst/Segment_scan_inst/clk_c_enable_87: 1 loads, 1 LSLICEs
Net calculator_inst/Segment_scan_inst/clk_c_enable_88: 1 loads, 1 LSLICEs
Net calculator_inst/Segment_scan_inst/clk_c_enable_90: 1 loads, 1 LSLICEs
Net u2/seg_data_3__N_149: 2 loads, 2 LSLICEs
Number of LSRs: 16
Net clk_200hz_N_26: 9 loads, 9 LSLICEs
Net u1/n12763: 1 loads, 1 LSLICEs
Net calculator_inst/n24509: 1 loads, 1 LSLICEs
Net calculator_inst/n24674: 33 loads, 33 LSLICEs
Net calculator_inst/n24496: 1 loads, 1 LSLICEs
Net calculator_inst/n11439: 1 loads, 1 LSLICEs
Net calculator_inst/seg_data_en_final_7_N_237_4: 1 loads, 1 LSLICEs
Net calculator_inst/n11936: 1 loads, 1 LSLICEs
Net calculator_inst/unsigned_mul_inst/n12718: 18 loads, 18 LSLICEs
Net calculator_inst/unsigned_mul_inst/n12720: 2 loads, 2 LSLICEs
Net calculator_inst/unsigned_mul_inst/state_0: 1 loads, 1 LSLICEs
Net calculator_inst/Segment_scan_inst/n12724: 6 loads, 6 LSLICEs
Net calculator_inst/Segment_scan_inst/n12729: 2 loads, 2 LSLICEs
Net calculator_inst/Segment_scan_inst/n12775: 1 loads, 1 LSLICEs
Net calculator_inst/Segment_scan_inst/n12767: 8 loads, 8 LSLICEs
Net calculator_inst/Segment_scan_inst/cnt_9__N_1118: 6 loads, 6 LSLICEs
Number of nets driven by tri-state buffers: 0
Top 10 highest fanout non-clock nets:
Net num2_6: 116 loads
Net num2_7: 99 loads
Net num2_3: 92 loads
Net num2_4: 92 loads
Net num2_0: 80 loads
Net n4231: 77 loads
Net n4243: 77 loads
Net n4252: 77 loads
Net n4253: 77 loads
Net n4255: 77 loads
Number of warnings: 0
Number of errors: 0
寄存器:共使用了258个寄存器,占4635个寄存器的6%。其中,PFU寄存器占用了258个,PIO寄存器未使用。
SLICEs:共使用了1564个SLICEs,占2160个SLICEs的72%。其中,SLICEs主要用作逻辑和ROM,占用了1564个;作为RAM和Carry的SLICEs均未使用。
LUT4s:共使用了3111个LUT4s,占4320个LUT4s的72%。其中,作为逻辑LUTs的个数为1859个,作为Ripple Logic的个数为1252个,没有作为分布式RAM和移位寄存器的LUT4s。
PIO sites:共使用了13个PIO sites,占105个PIO sites的16%。4个JTAG PIO sites也被使用。
其他资源(如RAM、PLLs等)均未使用或仅使用了一部分,占比很低。
综合来看,该设计对SLICEs和LUT4s的利用率较高,而对寄存器和PIO sites的利用率较低。没有出现资源占用上的警告或错误,整体资源利用比较合理。
8.演示视频
见开头