一、项目需求
基于小脚丫核心板和小脚丫FPGA套件STEP BaseBoard V4.0实现一个两位十进制数加、减、乘、除运算的计算器,
运算数和运算符(加、减、乘、除)由按键来控制。
二、需求分析
输入模块:首先,需要一个输入装置,比如按键,用于输入两位数。按键的信息需要被编码并储存,通常这会用
到寄存器来暂存这些输入的数字。
转码模块:当某个按键按下时,转换成相应键值。
存储和计算:第一个数字的输入可以通过一个暂存器来暂存,并通过控制信号的变化触发数据从右侧的数码管移
动到左侧的数码管。同时,第二个数字(低位)被加载到右侧的数码管上显示。
二进制转BCD码:将需要显示的二进制数据转换成BCD码存储。
数码管显示:数码管的驱动通常需要译码器来将存储的数字转换为对应的显示信号。每个数码管都有其对应的译
码器输入,由此,当数字在内部寄存器中跳转时,相应的显示也会更新。
三、实现方式
(1)矩阵按键
图1 4*4矩阵按键原理图
上图为4×4矩阵按键的硬件电路图,可以看到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
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); //通过前后两个时刻的值判断
对按键前后两个时刻对比按键是否按下。
(2)按键转码
判断按键是否按下,相应转换成对应的键值。
图2 4*4矩阵按键分配图
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
seg_data <= 4'h0;
key_flag <= 1'b0;
end else begin
case(key_pulse) //key_pulse脉宽等于clk_in的周期
16'h0001: begin seg_data <= 4'h7; key_flag <= 1'b1; end //编码
16'h0002: begin seg_data <= 4'h8; key_flag <= 1'b1; end
16'h0004: begin seg_data <= 4'h9; key_flag <= 1'b1; end
16'h0008: begin seg_data <= 4'hd; key_flag <= 1'b1; end
16'h0010: begin seg_data <= 4'h4; key_flag <= 1'b1; end
16'h0020: begin seg_data <= 4'h5; key_flag <= 1'b1; end
16'h0040: begin seg_data <= 4'h6; key_flag <= 1'b1; end
16'h0080: begin seg_data <= 4'hc; key_flag <= 1'b1; end
16'h0100: begin seg_data <= 4'h1; key_flag <= 1'b1; end
16'h0200: begin seg_data <= 4'h2; key_flag <= 1'b1; end
16'h0400: begin seg_data <= 4'h3; key_flag <= 1'b1; end
16'h0800: begin seg_data <= 4'hb; key_flag <= 1'b1; end
16'h1000: begin seg_data <= 4'he; key_flag <= 1'b1; end
16'h2000: begin seg_data <= 4'h0; key_flag <= 1'b1; end
16'h4000: begin seg_data <= 4'hf; key_flag <= 1'b1; end
16'h8000: begin seg_data <= 4'ha; key_flag <= 1'b1; end
default: begin seg_data <= seg_data; key_flag <= 1'b0; end //无按键按下时保持
endcase
end
end
按下相应按键,译码为所对应的键值。
(3)存储和计算
运算数和计算结果通过8个八段数码管显示。每个运算数使用两个数码管显示,左侧显示十位数,右侧显示个位
数。输入两位十进制数时,最高位先在右侧显示,然后其跳变到左侧的数码管上,低位在刚才高位占据的数码管上显示。通过状态机实现各状态的切换,数据存储到寄存器。
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0) begin
c_state <= STATE0;
num1 <= 8'h0;
num2 <= 8'h0;
opcode <= 4'h0;
result <= 16'h0;
data <= 16'h0;
end
else begin
case(c_state)
STATE0: if(key_flag == 1'b1) begin //有按键按上
if(key_data < 4'd10) begin //按下操作数
num1 <= num1*10 + key_data; //产生操作数
data <= data*10 + key_data; //数码管显示操作数1
c_state <= STATE0;
end
else if(key_data < 4'd14) begin //按下操作数
opcode <= key_data; //产生操作数
c_state <= STATE1;
data <= 16'h0;
end
else
c_state <= STATE0; //无动作
end
STATE1: if(key_flag == 1'b1) begin //有按键按上
if(key_data < 4'd10) begin //按下操作数
num2 <= num2*10 + key_data; //产生操作数
data <= data*10 + key_data; //数码管显示操作数2
c_state <= STATE1;
end
else if(key_data == 4'd15) begin //按下等号
case(opcode)
4'd10 : result <= num1 + num2; //加运算
4'd11 : result <= num1 - num2; //减运算
4'd12 : result <= num1 * num2; //乘运算
4'd13 : result <= num1 / num2; //除运算
endcase
c_state <= STATE2;
end
else
c_state <= STATE1; //操作符误按,如:1++
end
else //没有按键按下
c_state <= STATE1; //状态在s1等待
STATE2: begin
data[15:0] <= result;
c_state <= STATE3;
num1 <= 15'b0;
num2 <= 15'b0;
end
STATE3: begin
if(key_flag == 1'b1) begin //有按键按上
if(key_data > 4'd9 && key_data < 4'd14) begin //按下操作敍
if(result < 10000) begin
num1 <= result[15:0];
opcode <= key_data;
data <= 16'h0;
c_state <= STATE1;
end
else begin
data <= 16'h0;
c_state <= STATE0;
end
end
else if(key_data < 4'd10) begin
data <= num1*10 + key_data;
num1 <= num1*10 + key_data; //产生操作数
c_state <= STATE0;
end
else
c_state <= STATE3;
end
else
c_state <= STATE3;
end
default: c_state <= STATE0;
endcase
end
end
在不同状态间实现切换,从而实现存储和计算功能。
(4)进制转换
将需要显示的数据转换成BCD码,然后输出到数码管显示。
reg [35:0] shift_reg;
always@(bin_code or rst_n)begin
shift_reg = {20'h0,bin_code};
if(!rst_n)
bcd_code = 0;
else begin
repeat(16) begin //循环16次
//BCD码各位数据作满5加3操作
if (shift_reg[19:16] >= 5) shift_reg[19:16] = shift_reg[19:16] + 2'b11;
if (shift_reg[23:20] >= 5) shift_reg[23:20] = shift_reg[23:20] + 2'b11;
if (shift_reg[27:24] >= 5) shift_reg[27:24] = shift_reg[27:24] + 2'b11;
if (shift_reg[31:28] >= 5) shift_reg[31:28] = shift_reg[31:28] + 2'b11;
if (shift_reg[35:32] >= 5) shift_reg[35:32] = shift_reg[35:32] + 2'b11;
shift_reg = shift_reg << 1;
end
bcd_code = shift_reg[35:16];
end
end
(5)数码管显示
图3 数码管显示原理图
底板上有8位数码管,根据驱动方法不同,有以下比较:
1)独立显示:控制每个数码管至少需要8个I/O口控制,8位数码管就需要8 * 8 = 64根信号线才能分别显示。独立显示实现简单,但是需要大量的信号线。
2)扫描显示:将每位数码管的同一段选信号连接在一起,这样我们就只需要8根段选信号和6根位选信号,共计14根信号。扫描显示可以有效节约I/O口资源,实现起来稍显复杂。
小脚丫底板上使用的8位共阴极数码管,分析扫描显示的原理如下:
当某一时刻,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)这样我们会看到第1位数码管显示数字1,其余7位数码管不显示。按照扫描的方式,一共分为8个时刻,段选端口分别对应输出8位数码管需要显示的字库数据,位选端口保持每个时刻只有1位数码管处于使能状态,8个时刻依次循环,当扫描频率足够高(例如当扫描频率等于100Hz)时,则在人眼看到的数码管显示就是连续的,我们看到的就是8个不同的数字。
上面为大家介绍了数码管的独立显示和扫描显示两种方法,扫描显示的方式使用了14个I/O口控制,相对于简单的
处理器来讲14个I/O口也是非常多了,这里我们又使用了一款常见的驱动芯片74HC595,下面我们一起了解一下:
74HC595是较为常用的串行转并行的芯片,内部集成了一个8位移位寄存器、一个存储器和8个三态缓冲输出。在
最简单的情况下我们只需要控制3根引脚输入得到8根引脚并行输出信号,而且可以级联使用,我们使用3个I/O口控制两个级联的74HC595芯片,产生16路并行输出,连接到扫描显示的8位数码管上,可以轻松完成数码管驱动任务。
图4 74HC595原理图
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
驱动双级联74HC595芯片,从而控制数码管点亮。
四、功能框图
图5 功能框图
如图按键按下后转换成相应键值,通过计算模块输出需要显示的数据,数据通过转换成BCD码在数码管显示。
五、仿真波形图
图6 仿真波形图
如图,当第一次按键键值为1时num1为1,再次按键键值为2时num1为12,第三次按键值为10时为操作符“ +”,第
四次按键键值为1时num2为1,第五次按键键值为2时num2为12,此时拿按下等号,计算num1加num2的值为result。
第六次按键值为12时为操作符“ *”,同时把result的值赋给num1,num2此时清零。第七次按键键值为1时num2为1,第八次按键键值为0时num2为10,按下等号计算结果result等于num1 * num2 = 240。
六、FPGA的资源利用
图7 FPGA的资源利用图