1 项目需求
- 实现一个两位十进制数加、减、乘、除运算的计算器,运算数和运算符(加、减、乘、除)由按键来控制,4×4键盘按键分配如下图所示,小数点代表清零。
4×4键盘按键分配图
基本要求:
- 运算数和计算结果通过8个八段数码管显示。每个运算数使用两个数码管显示,左侧显示十位数,右侧显示个位数。输入两位十进制数时,最高位先在右侧显示,然后其跳变到左侧的数码管上,低位在刚才高位占据的数码管上显示。
STEP BaseBoard V4.0介绍
STEP BaseBoard V4.0是第4代小脚丫FPGA扩展底板,可以用于全系列小脚丫核心板的功能扩展,采用100mm*161.8mm的黄金比例尺寸,板子集成了存储器、温湿度传感器、接近式传感器、矩阵键盘、旋转编码器、HDMI接口、RGBLCD液晶屏、8个7位数码管、蜂鸣器模块、UART通信模块、ADC模块、DAC模块和WIFI通信模块,配合小脚丫FPGA板能够完成多种实验,是数字逻辑、微机原理、可编程逻辑语言以及EDA设计工具等课程完美的实验平台。
电路框图:
搭配采用的小脚丫核心模块是采用Type C接口的基于Lattice MXO2的小脚丫FPGA核心板 ——STEP-MXO2-LPC。
STEP小脚丫FPGA学习平台是苏州思得普信息科技公司专门针对FPGA初学者(尤其是学习数字电路的在校同学)打造的一系列性价比最高、学习门槛最低的学习模块系列。板上选用的芯片兼具了FPGA和CPLD的优点,瞬时上电启动,无需外部重新配置FPGA,是学习数字逻辑绝佳的选择。系列中所有板子的大小兼容标准的DIP40封装,尺寸只有52mm x 18mm,非常便于携带,而且能够直接插在面包板上或以模块的方式放置在其它电路板上以即插即用的方式,大大简化系统的设计。
2 完成的功能
2.1数字键盘输入:
系统能够接收数字键盘的输入信号,将按键信息转换为数字形式并进行处理。
2.2数码管显示:
根据数字键盘输入的信息,将其转换为数码管的段码数据,通过与 74HC595 芯片的通信,实现了数据的串行输出,控制数码管的显示。
2.3数据处理:
对按键信息进行解码处理,根据按键信息,计算最终结果。
3 实现思路
- 按键输入和解码:
- 使用
array_keyboard模块处理按键输入,将按键状态传递给key_out和key_pulse信号。 - 在
key_decode模块中,根据按键输入进行解码,实现对两个两位十进制数的输入和运算符的选择。将输入的数字和运算符存储在适当的寄存器中。
- 使用
- 数码管显示:
- 使用
segment_led模块分别显示两个两位十进制数的十位数和个位数。 - 在
segment_scan模块中,根据解码后的数据,控制数码管显示结果。根据需要显示的数值,将对应的数码管段数据发送到数码管显示模块。
- 使用
- 计算逻辑:
- 在
key_decode模块中,根据输入的数字和运算符进行加、减、乘、除运算。将计算结果存储在适当的寄存器中。
- 在
- 显示结果:
- 将计算结果经过处理后显示在数码管上,确保结果能正确显示十位和个位数。
- 清零和重置:
- 提供清零功能,使用户能够清除已输入的数字和运算符,以便开始新的计算。
- 考虑添加复位功能,以确保系统能够在需要时返回到初始状态。
- 测试和调试:
- 在设计完成后,进行综合、布局和时序分析,然后通过仿真验证功能是否按预期工作。
- 在实际硬件上进行测试和调试,确保计算器在实际使用中能够正确运行。
利用webide实现设计。
webide无需下载即可在网页设计,并且可以非常方便的分配管脚,一键下载jed文件,非常值得推荐
利用电子森林AI助手,可以生成简单Verilog代码,大大减少设计时间,提高效率。
4 实现过程
4.1 程序流程图
4.2 按键输入模块
- 该模块实现了一个基于状态机的按键输入模块,用于处理一个4x4矩阵按键的输入。
- 通过状态机的方式,周期性扫描按键矩阵的行,以检测按键的按下状态。
- 采样按键状态,生成按键输出和按键脉冲信号,用于后续按键解码和处理。
工作方式:
- 时钟和复位:
- 使用系统时钟
clk和复位信号rst_n。 - 通过时钟信号生成 200Hz 的周期性信号
clk_200hz用于按键扫描。
- 使用系统时钟
- 状态机设计:
- 使用
c_state寄存器实现状态机,共有 4 个状态。 - 每个状态下,对应矩阵按键的行输出有效,以便扫描按键状态。
- 使用
- 按键扫描:
- 在每个状态下,通过采样列接口的电平状态来获取按键状态。
- 采样得到的按键状态存储在
key寄存器中,并与之前的状态进行比较,用于检测按键按下。
- 按键输出和按键脉冲:
key_out寄存器存储当前按键状态,key_out_r延迟一个时钟周期以锁存前一刻的按键状态。- 通过比较前后两个时刻的按键状态,生成按键脉冲信号
key_pulse,用于检测按键的按下和释放。
module array_keyboard #
(
parameter CNT_200HZ = 60000
)
(
input clk,
input rst_n,
input [3:0] col,
output reg [3:0] row,
output reg [15:0]key_out,
output [15:0]key_pulse
);
localparam STATE0 = 2'b00;
localparam STATE1 = 2'b01;
localparam STATE2 = 2'b10;
localparam STATE3 = 2'b11;
/*
因使用4x4矩阵按键,通过扫描方法实现,所以这里使用状态机实现,共分为4种状态
在其中的某一状态时间里,对应的4个按键相当于独立按键,可按独立按键的周期采样法采样
周期采样时每隔20ms采样一次,对应这里状态机每隔20ms循环一次,每个状态对应5ms时间
对矩阵按键实现原理不明白的,请去了解矩阵按键实现原理
*/
//计数器计数分频实现5ms周期信号clk_200hz,系统时钟12MHz
reg [15:0] cnt;
reg clk_200hz;
always@(posedge clk or negedge rst_n) begin //复位时计数器cnt清零,clk_200hz信号起始电平为低电平
if(!rst_n) begin
cnt <= 16'd0;
clk_200hz <= 1'b0;
end else begin
if(cnt >= ((CNT_200HZ>>1) - 1)) begin //数字逻辑中右移1位相当于除2
cnt <= 16'd0;
clk_200hz <= ~clk_200hz; //clk_200hz信号取反
end else begin
cnt <= cnt + 1'b1;
clk_200hz <= clk_200hz;
end
end
end
reg [1:0] c_state;
//状态机根据clk_200hz信号在4个状态间循环,每个状态对矩阵按键的行接口单行有效
always@(posedge clk_200hz or negedge rst_n) begin
if(!rst_n) begin
c_state <= STATE0;
row <= 4'b1110;
end else begin
case(c_state)
//状态c_state跳转及对应状态下矩阵按键的row输出
STATE0: begin c_state <= STATE1; row <= 4'b1101; end
STATE1: begin c_state <= STATE2; row <= 4'b1011; end
STATE2: begin c_state <= STATE3; row <= 4'b0111; end
STATE3: begin c_state <= STATE0; row <= 4'b1110; end
default:begin c_state <= STATE0; row <= 4'b1110; end
endcase
end
end
reg [15:0] key,key_r;
//因为每个状态中单行有效,通过对列接口的电平状态采样得到对应4个按键的状态,依次循环
always@(negedge clk_200hz or negedge rst_n) begin
if(!rst_n) begin
key_out <= 16'hffff; key_r <= 16'hffff; key <= 16'hffff;
end else begin
case(c_state)
//采集当前状态的列数据赋值给对应的寄存器位
//对键盘采样数据进行判定,连续两次采样低电平判定为按键按下
STATE0: begin key_out[ 3: 0] <= key_r[ 3: 0]|key[ 3: 0]; key_r[ 3: 0] <= key[ 3: 0]; key[ 3: 0] <= col; end
STATE1: begin key_out[ 7: 4] <= key_r[ 7: 4]|key[ 7: 4]; key_r[ 7: 4] <= key[ 7: 4]; key[ 7: 4] <= col; end
STATE2: begin key_out[11: 8] <= key_r[11: 8]|key[11: 8]; key_r[11: 8] <= key[11: 8]; key[11: 8] <= col; end
STATE3: begin key_out[15:12] <= key_r[15:12]|key[15:12]; key_r[15:12] <= key[15:12]; key[15:12] <= col; end
default:begin key_out <= 16'hffff; key_r <= 16'hffff; key <= 16'hffff; end
endcase
end
end
reg [15:0] key_out_r;
//Register low_sw_r, lock low_sw to next clk
always @ ( posedge clk or negedge rst_n )
if (!rst_n) key_out_r <= 16'hffff;
else key_out_r <= key_out; //将前一刻的值延迟锁存
//wire [15:0] key_pulse;
//Detect the negedge of low_sw, generate pulse
assign key_pulse= key_out_r & ( ~key_out); //通过前后两个时刻的值判断
endmodule
4.3 运算模块 key_decode
key_decode 模块是用来处理按键输入并进行解码的模块,根据用户按下的按键来实现对两个两位十进制数的输入和选择加、减、乘、除运算符的功能。
模块输入和输出:
- 输入:
- clk: 时钟信号,用于同步按键输入处理。
- rst_n: 复位信号,低有效,用于重置模块状态。
- key_pulse: 按键脉冲信号,根据按键输入的状态产生的脉冲信号。
- 输出:
- seg_data: 用于控制数码管显示的信号。
模块功能:
- key_decode 模块根据按键输入的状态进行解码,实现对两个两位十进制数的输入和选择加、减、乘、除运算符的功能。
- 模块内部包含了寄存器来存储输入的数字、运算符以及计算结果。
- 当按下按键时,根据按键的值进行相应的操作,例如输入数字、选择运算符、清零等。
- 模块根据按键输入和状态机逻辑来更新存储的数字和运算符,并将结果传递给 seg_data 信号,以便在数码管上显示。
工作方式:
- 模块内部包含状态机逻辑,根据当前状态和按键输入进行状态转移和操作。
- 使用寄存器来存储输入的数字、运算符和计算结果,以便进行计算和显示。
- 根据不同的按键输入,执行相应的操作,例如输入数字、选择运算符、进行计算等。
- 在计算完成后,将结果传递给 seg_data 信号,以便在数码管上显示。
module key_decode(
input clk,
input rst_n,
input [15:0] key_pulse,
output reg [31:0] seg_data
);
reg [31:0] seg_data_med;
reg [3:0] num; //加减乘除
reg numd;
reg [31:0] num1;
reg [31:0] num2;
reg [31:0] nummedi;
reg [35:0] data;
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
seg_data_med <= 8'h00;
else
begin
seg_data <= 16'h0000;
if (num == 4'h0) //未按下加减乘除
begin
case (key_pulse)
16'h0001: num1 <= (seg_data_med *10) + 32'h07; // 编码
16'h0002: num1 <= (seg_data_med *10) + 32'h08;
16'h0004: num1 <= (seg_data_med *10) + 32'h09;
16'h0008: num <= 4'h8; // 除
16'h0010: num1 <= (seg_data_med *10) + 32'h04;
16'h0020: num1 <= (seg_data_med *10) + 32'h05;
16'h0040: num1 <= (seg_data_med *10) + 32'h06;
16'h0080: num <= 4'h4; // 乘
16'h0100: num1 <= (seg_data_med *10) + 32'h03;
16'h0200: num1 <= (seg_data_med *10) + 32'h02;
16'h0400: num1 <= (seg_data_med *10) + 32'h01;
16'h0800: num <= 4'h2; // 减
16'h1000: num1 <= (seg_data_med *10) + 32'h00;
16'h2000:
begin
seg_data_med <= 0; // 清零
num2 <= 0;
num1 <= 0;
numd <= 0;
num <= 0;
end
16'h4000:
begin
numd <= 1'd1; // 等于
end
16'h8000: num <= 4'h1; // 加
default: seg_data_med <= seg_data_med; // 无按键按下时保持
endcase
seg_data_med <= num1;
if (numd == 1'd1 )
begin
seg_data_med <= num1;
numd <= 1'd0;
end
end
else
begin
if ( numd != 1)// 未按下等于
begin
case (key_pulse)
16'h0001: num2 <= (seg_data_med *10) + 32'h07; // 编码
16'h0002: num2 <= (seg_data_med *10) + 32'h08;
16'h0004: num2 <= (seg_data_med *10) + 32'h09;
16'h0008: num <= 4'h8; // 除
16'h0010: num2 <= (seg_data_med *10) + 32'h04;
16'h0020: num2 <= (seg_data_med *10) + 32'h05;
16'h0040: num2 <= (seg_data_med *10) + 32'h06;
16'h0080: num <= 4'h4; // 乘
16'h0100: num2 <= (seg_data_med *10) + 32'h03;
16'h0200: num2 <= (seg_data_med *10) + 32'h02;
16'h0400: num2 <= (seg_data_med *10) + 32'h01;
16'h0800: num <= 4'h2; // 减
16'h1000: num2 <= (seg_data_med *10) + 32'h00;
16'h2000:
begin
seg_data_med <= 0; // 清零
num2 <= 0;
num1 <= 0;
numd <= 0;
num <= 0;
end
16'h4000:
begin
numd <= 1'b1; // 等于
end
16'h8000: num <= 4'h1; // 加
default: seg_data_med <= num2; // 无按键按下时保持
endcase
if(numd != 1)
seg_data_med <= num2;
end
if ( numd == 1)
begin
if (num == 4'h1 )
begin
num2 <= num2 + num1;
end
if (num == 4'h2 )
begin
num2 <= (num1 - num2);
end
if (num == 4'h4 )
begin
num2 <= num1 * num2;
end
if (num == 4'h8 )
begin
num2 <= num1 / num2;
end
numd<=0;
case (key_pulse)
16'h2000:
begin
seg_data_med <= 0; // 清零
num2 <= 0;
num1 <= 0;
numd <= 0;
num <= 0;
end
default: seg_data_med <= num2; // 无按键按下时保持
endcase
end
end
end
data = {20'd0,seg_data_med[15:0]};
repeat(16) begin //二进制码总共8位,所以循环位数是8
if(data[19:16]>=5)
data[19:16] = data[19:16] + 3;
if(data[23:20]>=5)
data[23:20] = data[23:20] + 3;
if(data[27:24]>=5)
data[27:24] = data[27:24] + 3;
if(data[31:28]>=5)
data[31:28] = data[31:28] + 3;
if(data[35:32]>=5)
data[35:32] = data[35:32] + 3;
data = data << 1;
end
seg_data <= data[35:16];
end
endmodule
4.4 数码管显示模块
分为segment_led 模块(核心板数码管)和segment_scan 模块(扩展底板数码管)。
segment_led 模块:
模块功能:
- 数码管段码转换:根据输入的
seg_data数据,将其转换为对应的数码管段码输出。 - 数码管显示:将转换后的段码输出到
segment_led,控制数码管显示特定数字。 - 输入:4位输入信号
seg_data,用于表示要显示的数字(0 到 9)。 - 输出:9位输出信号
segment_led,用于控制数码管的各段(SEG, DP, G, F, E, D, C, B, A)。
模块工作方式:
- 组合逻辑:使用
always@(seg_data)块来对输入的seg_data进行处理,根据不同的输入值,将对应的段码数据存储在segment_led中。 - 段码转换:根据输入的数字,将其转换为对应的数码管段码数据,以便正确显示该数字。
module segment_led
(
input [3:0] seg_data, //seg_data input
output reg [8:0] segment_led //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
always@(seg_data)
case(seg_data)
4'd0: segment_led = 9'h3f;
4'd1: segment_led = 9'h06;
4'd2: segment_led = 9'h5b;
4'd3: segment_led = 9'h4f;
4'd4: segment_led = 9'h66;
4'd5: segment_led = 9'h6d;
4'd6: segment_led = 9'h7d;
4'd7: segment_led = 9'h07;
4'd8: segment_led = 9'h7f;
4'd9: segment_led = 9'h6f;
default: segment_led = 9'h00;
endcase
endmodule
segment_scan 的模块,用于控制扩展底板数码管的显示。
模块功能:
- 数码管显示控制:通过输入的数据信号
dat_1到dat_8控制8个数码管显示的内容。 - 数据使能控制:通过
dat_en和dot_en信号控制每个数码管的数据位和小数点的显示使能。 - 74HC595串行输出:使用
seg_rck、seg_sck、seg_din控制 74HC595 芯片的输出,实现数据的串行输出。
- 输入:
- 系统时钟信号
clk(12MHz)和系统复位信号rst_n。 - 每个数码管的数据输入
dat_1到dat_8,数据位显示使能信号dat_en和小数点显示使能信号dot_en。
- 系统时钟信号
- 输出:
- 74HC595 的控制信号
seg_rck、seg_sck、seg_din。
- 74HC595 的控制信号
字库数据初始化:
- 在复位时,对数码管的字库进行初始化,将不同数字对应的段码数据存储在
seg中。
模块工作方式:
- 时钟分频:通过计数器
cnt和clk_40khz信号实现对系统时钟的分频,生成 40kHz 的时钟信号。 - 状态机:使用状态机实现数码管的扫描和数据传输时序的控制。
- 数码管数据处理:根据状态机的状态,将对应的数码管数据和显示使能信号传送给 74HC595 芯片,实现数码管的显示。
module segment_scan(
input clk, //系统时钟 12MHz
input rst_n, //系统复位 低有效
input [3:0] dat_1, //SEG1 显示的数据输入
input [3:0] dat_2, //SEG2 显示的数据输入
input [3:0] dat_3, //SEG3 显示的数据输入
input [3:0] dat_4, //SEG4 显示的数据输入
input [3:0] dat_5, //SEG5 显示的数据输入
input [3:0] dat_6, //SEG6 显示的数据输入
input [3:0] dat_7, //SEG7 显示的数据输入
input [3:0] dat_8, //SEG8 显示的数据输入
input [7:0] dat_en, //数码管数据位显示使能,[MSB~LSB]=[SEG1~SEG8]
input [7:0] dot_en, //数码管小数点位显示使能,[MSB~LSB]=[SEG1~SEG8]
output reg seg_rck, //74HC595的RCK管脚
output reg seg_sck, //74HC595的SCK管脚
output reg seg_din //74HC595的SER管脚
);
localparam CNT_40KHz = 300; //分频系数
localparam IDLE = 3'd0;
localparam MAIN = 3'd1;
localparam WRITE = 3'd2;
localparam LOW = 1'b0;
localparam HIGH = 1'b1;
//创建数码管的字库,字库数据依段码顺序有关
//这里字库数据[MSB~LSB]={G,F,E,D,C,B,A}
reg[6:0] seg [15:0];
always @(negedge rst_n) begin
seg[0] = 7'h3f; // 0
seg[1] = 7'h06; // 1
seg[2] = 7'h5b; // 2
seg[3] = 7'h4f; // 3
seg[4] = 7'h66; // 4
seg[5] = 7'h6d; // 5
seg[6] = 7'h7d; // 6
seg[7] = 7'h07; // 7
seg[8] = 7'h7f; // 8
seg[9] = 7'h6f; // 9
seg[10] = 7'h77; // A
seg[11] = 7'h7c; // b
seg[12] = 7'h39; // C
seg[13] = 7'h5e; // d
seg[14] = 7'h79; // E
seg[15] = 7'h71; // F
end
//计数器对系统时钟信号进行计数
reg [9:0] cnt = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) cnt <= 1'b0;
else if(cnt>=(CNT_40KHz-1)) cnt <= 1'b0;
else cnt <= cnt + 1'b1;
end
//根据计数器计数的周期产生分频的脉冲信号
reg clk_40khz = 1'b0;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) clk_40khz <= 1'b0;
else if(cnt<(CNT_40KHz>>1)) clk_40khz <= 1'b0;
else clk_40khz <= 1'b1;
end
//使用状态机完成数码管的扫描和74HC595时序的实现
reg [15:0] data;
reg [2:0] cnt_main;
reg [5:0] cnt_write;
reg [2:0] state = IDLE;
always@(posedge clk_40khz or negedge rst_n) begin
if(!rst_n) begin //复位状态下,各寄存器置初值
state <= IDLE;
cnt_main <= 3'd0; cnt_write <= 6'd0;
seg_din <= 1'b0; seg_sck <= LOW; seg_rck <= LOW;
end else begin
case(state)
IDLE:begin //IDLE作为第一个状态,相当于软复位
state <= MAIN;
cnt_main <= 3'd0; cnt_write <= 6'd0;
seg_din <= 1'b0; seg_sck <= LOW; seg_rck <= LOW;
end
MAIN:begin
cnt_main <= cnt_main + 1'b1;
state <= WRITE; //在配置完发给74HC595的数据同时跳转至WRITE状态,完成串行时序
case(cnt_main)
//对8位数码管逐位扫描
//data [15:8]为段选, [7:0]为位选
3'd0: data <= {{dot_en[7],seg[dat_1]},dat_en[7]?8'hfe:8'hff};
3'd1: data <= {{dot_en[6],seg[dat_2]},dat_en[6]?8'hfd:8'hff};
3'd2: data <= {{dot_en[5],seg[dat_3]},dat_en[5]?8'hfb:8'hff};
3'd3: data <= {{dot_en[4],seg[dat_4]},dat_en[4]?8'hf7:8'hff};
3'd4: data <= {{dot_en[3],seg[dat_5]},dat_en[3]?8'hef:8'hff};
3'd5: data <= {{dot_en[2],seg[dat_6]},dat_en[2]?8'hdf:8'hff};
3'd6: data <= {{dot_en[1],seg[dat_7]},dat_en[1]?8'hbf:8'hff};
3'd7: data <= {{dot_en[0],seg[dat_8]},dat_en[0]?8'h7f:8'hff};
default: data <= {8'h00,8'hff};
endcase
end
WRITE:begin
if(cnt_write >= 6'd33) cnt_write <= 1'b0;
else cnt_write <= cnt_write + 1'b1;
case(cnt_write)
//74HC595是串行转并行的芯片,3路输入可产生8路输出,而且可以级联使用
//74HC595的时序实现,参考74HC595的芯片手册
6'd0: begin seg_sck <= LOW; seg_din <= data[15]; end //SCK下降沿时SER更新数据
6'd1: begin seg_sck <= HIGH; end //SCK上升沿时SER数据稳定
6'd2: begin seg_sck <= LOW; seg_din <= data[14]; end
6'd3: begin seg_sck <= HIGH; end
6'd4: begin seg_sck <= LOW; seg_din <= data[13]; end
6'd5: begin seg_sck <= HIGH; end
6'd6: begin seg_sck <= LOW; seg_din <= data[12]; end
6'd7: begin seg_sck <= HIGH; end
6'd8: begin seg_sck <= LOW; seg_din <= data[11]; end
6'd9: begin seg_sck <= HIGH; end
6'd10: begin seg_sck <= LOW; seg_din <= data[10]; end
6'd11: begin seg_sck <= HIGH; end
6'd12: begin seg_sck <= LOW; seg_din <= data[9]; end
6'd13: begin seg_sck <= HIGH; end
6'd14: begin seg_sck <= LOW; seg_din <= data[8]; end
6'd15: begin seg_sck <= HIGH; end
6'd16: begin seg_sck <= LOW; seg_din <= data[7]; end
6'd17: begin seg_sck <= HIGH; end
6'd18: begin seg_sck <= LOW; seg_din <= data[6]; end
6'd19: begin seg_sck <= HIGH; end
6'd20: begin seg_sck <= LOW; seg_din <= data[5]; end
6'd21: begin seg_sck <= HIGH; end
6'd22: begin seg_sck <= LOW; seg_din <= data[4]; end
6'd23: begin seg_sck <= HIGH; end
6'd24: begin seg_sck <= LOW; seg_din <= data[3]; end
6'd25: begin seg_sck <= HIGH; end
6'd26: begin seg_sck <= LOW; seg_din <= data[2]; end
6'd27: begin seg_sck <= HIGH; end
6'd28: begin seg_sck <= LOW; seg_din <= data[1]; end
6'd29: begin seg_sck <= HIGH; end
6'd30: begin seg_sck <= LOW; seg_din <= data[0]; end
6'd31: begin seg_sck <= HIGH; end
6'd32: begin seg_rck <= HIGH; end //当16位数据传送完成后RCK拉高,输出生效
6'd33: begin seg_rck <= LOW; state <= MAIN; end
default: ;
endcase
end
default: state <= IDLE;
endcase
end
end
endmodule
4.5 top模块 type_system
type_system 的模块,该模块实现了一个数字键盘输入系统,将按键输入转换为数码管的显示。
模块功能:
- 数字键盘输入:通过输入的
col和row信号表示数字键盘的行列输入,将按键信息转换为key_out和key_pulse。 - 数码管段码转换:根据按键信息,将其转换为数码管的段码数据并输出到
seg_1和seg_2。 - 74HC595控制:通过输出信号
seg_rck、seg_sck、seg_din控制 74HC595 芯片的输出,实现数据的串行输出。
模块输入和输出:
- 输入:
- 系统时钟信号
clk和系统复位信号rst_n。 - 数字键盘的列信号
col和行信号row。
- 系统时钟信号
- 输出:
- 数码管显示的两组段码输出
seg_1和seg_2。 - 74HC595 的控制信号
seg_rck、seg_sck、seg_din。
- 数码管显示的两组段码输出
模块工作方式:
- 键盘输入处理:使用
array_keyboard模块处理数字键盘输入,将按键信息转换为key_out和key_pulse。 - 数据处理:通过
key_decode模块处理按键信息,将其转换为数码管的段码数据存储在seg_data中。 - 数码管显示:使用两个
segment_led模块将seg_data分别输出到seg_1和seg_2控制数码管显示。 - 数码管扫描:使用
segment_scan模块控制数码管的显示,包括数据输入、显示使能和 74HC595 控制信号输出。
module type_system(
input clk,
input rst_n,
input [3:0] col,
output [3:0] row,
output [8:0] seg_1, //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
output [8:0] seg_2, //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
output seg_rck, //74HC595的RCK管脚
output seg_sck, //74HC595的SCK管脚
output seg_din //74HC595的SER管脚
);
wire [15:0] key_out;
wire [15:0] key_pulse;
wire [31:0] seg_data_in;
wire [31:0] seg_data;
wire [7:0] dat_en; //控制数码管点亮
assign dat_en[7] = seg_data[31:28]? 1'b1:1'b0;
assign dat_en[6] = dat_en[7]|(seg_data[27:24]? 1'b1:1'b0);
assign dat_en[5] = dat_en[6]|(seg_data[23:20]? 1'b1:1'b0);
assign dat_en[4] = dat_en[5]|(seg_data[19:16]? 1'b1:1'b0);
assign dat_en[3] = dat_en[4]|(seg_data[15:12]? 1'b1:1'b0);
assign dat_en[2] = dat_en[3]|(seg_data[11:8]? 1'b1:1'b0);
assign dat_en[1] = dat_en[2]|(seg_data[7:4]? 1'b1:1'b0);
assign dat_en[0] = 1'b1;
array_keyboard u1(
.clk(clk),
.rst_n(rst_n),
.col(col),
.row(row),
.key_out(key_out),
.key_pulse(key_pulse)
);
//processing data
key_decode u2(
.clk (clk ),
.rst_n (rst_n ),
.key_pulse (key_pulse ),
.seg_data (seg_data ) //高4位代表十位,低4位代表个位
);
//Segment led display module
segment_led u3
(
.seg_data (seg_data[7:4] ), //seg_data input
.segment_led (seg_1 ) //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
//Segment led display module
segment_led u4
(
.seg_data (seg_data[3:0] ), //seg_data input
.segment_led (seg_2 ) //MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
//segment_scan display module
segment_scan u5(
.clk(clk), //系统时钟 12MHz
.rst_n(rst_n), //系统复位 低有效
.dat_1(seg_data[31:28] ), //SEG1 显示的数据输入
.dat_2(seg_data[27:24] ), //SEG2 显示的数据输入
.dat_3(seg_data[23:20] ), //SEG3 显示的数据输入
.dat_4(seg_data[19:16]), //SEG4 显示的数据输入
.dat_5(seg_data[15:12] ), //SEG5 显示的数据输入
.dat_6(seg_data[11:8]), //SEG6 显示的数据输入
.dat_7(seg_data[7:4] ), //SEG7 显示的数据输入
.dat_8(seg_data[3:0] ), //SEG8 显示的数据输入
.dat_en(dat_en ), //数码管数据位显示使能,[MSB~LSB]=[SEG1~SEG8]
.dot_en(8'b0001_0001 ), //数码管小数点位显示使能,[MSB~LSB]=[SEG1~SEG8]
.seg_rck(seg_rck ), //74HC595的RCK管脚
.seg_sck(seg_sck ), //74HC595的SCK管脚
.seg_din(seg_din ) //74HC595的SER管脚
);
endmodule
4.6资源利用率
- 寄存器数量:225个寄存器,占总数的5%。
- SLICE数量:2047个SLICE,占总数的95%。其中,95%用于逻辑/ROM,70%用于Carry。
- LUT4数量:4090个LUT4,占总数的95%。其中,1060个用于逻辑运算,3030个用于时序逻辑。
- PIO站点数量:31个PIO站点和4个JTAG站点,占总数的33%。
5 遇到的主要难题
5.1 always语句赋值问题
在Verilog中,确保敏感信号具有相同的触发方式,并且在always块中只使用reg类型的变量进行赋值操作,并且这些赋值操作需要保持一致,即要么都是阻塞赋值,要么都是非阻塞赋值。这些规则有助于确保时序逻辑的正确性和避免潜在的问题,最重要的是可以减少大量调试时间
建议:
- 相同触发方式:
- 在
always块中,确保所有敏感信号具有相同的触发方式,例如都是在时钟的上升沿或下降沿触发。混合使用不同的触发方式可能会导致意外的行为和时序问题。
- 在
- 只使用
reg类型变量: - 在
always块中,应该只使用reg类型的变量进行赋值操作。reg类型的变量用于存储时序逻辑的状态,并且在时钟信号触发时更新。
- 在
- 保持一致的赋值方式:
- 在
always块中,确保所有的赋值操作要么都是阻塞赋值(=),要么都是非阻塞赋值(<=)。混合使用这两种赋值方式可能会导致意外的逻辑错误和时序问题。
- 在
5.2 时序逻辑和时钟域交叉问题
在数字电路设计中,时钟信号的稳定性和时序逻辑的正确性至关重要。确保各模块在正确的时钟域中操作,避免时钟域交叉可能是一个巨大的工程。
5.3 注意webide的run.log文件报错
在进行fpga设计时,难免会出现因代码问题导致的映射错误。但是在使用webide设计时,如果有映射错误,只会在run.log文件的最后一行报错,并且报错后可以正常下载上一次jed文件,这是难以发现的,这会使得
6 未来的计划建议
该项目已经成功实现了二位十进制计算器的功能,并达到了预期指标。然而,还有许多可以提升与扩展的地方:
功能扩展与优化:
- 考虑增加更多功能,如连续运算,以提升计算器的实用性和功能性。
- 优化除法算法,使得资源占用降低。
性能优化:
- 对代码进行优化,降低资源占用。
