1.项目需求
本次项目的主要需求是基于小脚丫FPGA套件STEP BaseBoard V4.0实现两位十进制数加、减、乘、除运算。
2.需求分析
在本项目中,不考虑两位以上的十进制数,只对两位及以下的十进制数进行加、减、乘、除运算,需要通过矩阵键盘键入数字和符号,并通过数码管显示。
3.实现方式
根据项目的需求,分析得出本项目共需要用到矩阵键盘模块、译码器模块、计算器核心模块、bcd转码模块、数码管显示模块。
(1)矩阵键盘模块
根据如下图所示的原理图
可以看到4根行线(ROW1、ROW2、ROW3、ROW4)和4根列线(COL1、COL2、COL3、COL4),同时列线通过上拉电阻连接到VCC电压(3.3V),对于矩阵按键来讲:
1、4根行线是输入的,是由FPGA控制拉高或拉低
2、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按键应该是被按下了。
因此,我们可以使用一个状态机来实现这样的功能,状态机的状态转换图如下
状态机部分具体实现代码如下
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
(2)译码器模块
本模块主要用于将键盘输入的信号转化为对应的数字信号和符号信号,以便于后面计算的进行。实现的方式即通过case进行判断。具体代码如下
module key_decode(
input clk,
input rst_n,
input [15:0] key_pulse, //按键消抖后动作脉冲信号
output reg [7:0] seg_data,
output reg flag //按键检测信号
);
//key_pulse transfer to seg_data
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
seg_data <= 8'h00;
end else begin
case(key_pulse) //key_pulse脉宽等于clk_in的周期
16'h0001:begin seg_data <= 8'h09;flag <= 1'b1;end//9
16'h0002:begin seg_data <= 8'h08;flag <= 1'b1;end//8
16'h0004:begin seg_data <= 8'h07;flag <= 1'b1;end//7
16'h0008:begin seg_data <= 8'h10;flag <= 1'b1;end//÷
16'h0010:begin seg_data <= 8'h06;flag <= 1'b1;end//6
16'h0020:begin seg_data <= 8'h05;flag <= 1'b1;end//5
16'h0040:begin seg_data <= 8'h04;flag <= 1'b1;end//4
16'h0080:begin seg_data <= 8'h11;flag <= 1'b1;end//×
16'h0100:begin seg_data <= 8'h03;flag <= 1'b1;end//3
16'h0200:begin seg_data <= 8'h02;flag <= 1'b1;end//2
16'h0400:begin seg_data <= 8'h01;flag <= 1'b1;end//1
16'h0800:begin seg_data <= 8'h12;flag <= 1'b1;end//-
16'h1000:begin seg_data <= 8'h00;flag <= 1'b1;end//0
16'h2000:begin seg_data <= 8'h13;flag <= 1'b1;end//.
16'h4000:begin seg_data <= 8'h14;flag <= 1'b1;end//=
16'h8000:begin seg_data <= 8'h15;flag <= 1'b1;end//+
default:begin seg_data <= seg_data;flag <= 1'b0;end //无按键按下时保持
endcase
end
end
endmodule
(3)bcd转码模块
本模块采用的是移位加3法,即在移位的过程中,如果移位出的数值大于等于5,则将数值加3,再进行移位。对于n位二进制数,就进行n次移位。具体实现代码如下
module bin2bcd (
input rst_n,
input [15:0] bitcode,
output reg [31:0] bcdcode
);
reg [47:0] data;
always@(bitcode or rst_n) begin
data = {32'd0,bitcode};
if(!rst_n)
bcdcode = 0;
else begin
repeat(16) begin //二进制码总共16位,所以循环位数是16
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;
if(data[39:36]>=5)
data[39:36] = data[39:36] + 3;
if(data[43:40]>=5)
data[43:40] = data[43:40] + 3;
if(data[47:44]>=5)
data[47:44] = data[47:44] + 3;
data = data << 1;
end
bcdcode = data[47:16];
end
end
endmodule
仿真截图如下
(4)计算器核心模块
经分析得,计算机核心可用一个状态机来实现,状态机的状态转换图如下
第一个状态的代码如下:
STATE0:begin
if(flag == 1'b1) begin
if(key_input <= 9) begin
num_1 <= (num_1 << 3) + (num_1 << 1) + key_input;//第一个十进制数读取
digit <= (digit << 3) + (digit << 1) + key_input;//数码管显示信号
c_state <= STATE0;//继续读取
end
else begin
if(key_input == 8'h14) begin
c_state <= STATE0;//等号误触
end
else if(key_input == 8'h13) begin
digit <=32'b0;//小数点待更新
c_state <= STATE0;//
end
else begin
sgn <= key_input;//录入运算符号
c_state <= STATE1;//跳至下一状态
digit <= 32'b0;
end
end
end
else c_state <= STATE0;
end
在本状态时,若输入的信号为0~9的数字,则将该数字读取进num_1中,并返回本状态继续读取,若输入等号信号,则判定为误触,保持本状态,否则录入运算符号并跳转至下一状态,读取第二个数字。
第二个状态的代码如下:
STATE1:begin
if(flag == 1'b1) begin
if(key_input <=9) begin
num_2 <= (num_2 << 3) + (num_2 << 1) + key_input;//读取第二个数字
digit <= (digit << 3) + (digit << 1) + key_input;//数码管显示信号
c_state <= STATE1;
end
else begin
if(key_input == 8'h14) begin
case(sgn)
8'h15:result = num_1 + num_2;
8'h12:result = num_1 - num_2;
8'h11:result = num_1 * num_2;
8'h10:result = num_1 / num_2;
default:result = result;
endcase
c_state <= STATE2;
end
end
end
else c_state <= STATE1;
end
在本状态时,若输入的信号为0~9的数字,则将该数字读取进num_2中,并返回本状态继续读取,若输入等号信号,
则根据之前输入的符号跳转至第三状态进行计算,然后跳转至第四状态显示计算结果。
第四个状态的代码如下:
STATE3:begin
if(flag == 1'b1) begin
if(key_input <= 9)begin
num_1 <= (num_1 << 3) + (num_1 << 1) + key_input;
digit <= key_input;
c_state <= STATE0;
end
else begin
if(result < 100) begin
if(key_input == 8'h14) begin
num_1 <= result;
c_state <= STATE3;
end
else if(key_input > 9) begin
num_1 <= result;
digit <= 32'b0;
sgn <= key_input;
c_state <= STATE1;
end
end
else begin
digit <= 32'b0;
c_state <= STATE0;
end
end
end
else c_state <= STATE3;
end
在第四状态中,继续对下一个信号保持读取,如果计算结果超过100,则自动跳转至第一个状态并清零;如果输入的是数字,则将结果自动清零并将数字读取至num_1中,同时跳转至第一个状态;如果输入的是符号,则保存符号到sgn中并跳转至第二个状态。
(5)数码管显示模块
本项目中,数码管的显示通过两个74hc595芯片实现,原理图如下
由原理图,我们使用3个I/O口控制两个级联的74HC595芯片,产生16路并行输出,连接到扫描显示的8位数码管上。但本项目中,我们最大只需要显示到千位数,故只需要使用其中四个数码管,而另外四个数码管的使能信号始终为0。
下面是数码管显示的原理详解。
独立控制数码管显示非常简单,但是控制每个数码管至少需要8个I/O口控制,8位数码管就需要8*8 = 64根信号线才能分别显示,因此我们选择扫描显示的方法。扫描显示将每位数码管的同一段选信号连接在一起,这样就只需要8根段选信号和8根位选信号,共计16根信号。扫描显示可以有效节约I/O口资源,但是实现起来稍显复杂。
当某一时刻,FPGA控制8根公共的段选接口输出数字1对应的数码管字库数据8'h06(DP=0、G=0、F=0、E=0、D=0、C=1、B=1、A=0)时,同时控制8位数码管只有第1位使能(DIG1=0、DIG2=1、DIG3=1、DIG4=1、DIG5=1、DIG6=1、DIG7=1、DIG8=1)这样我们会看到第1位数码管显示数字1,其余7位数码管不显示。按照扫描的方式,一共分为8个时刻,段选端口分别对应输出6位数码管需要显示的字库数据,位选端口保持每个时刻只有1位数码管处于使能状态,8个时刻依次循环,当扫描频率足够高(例如当扫描频率等于100Hz)时,则在人眼看到的数码管显示就是连续的,我们看到的就是8个不同的数字。
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