谈到按键,我们首先应该想到消抖,在一般设计中,认为抖动的总时间会持续20ms
以内(这个数值在程序消抖时会用到),具体情况如下图所示:
硬件消抖
一方面我们可以通过RS触发器来完成消抖:
但是比较明显,这种电路只适用于单刀双掷的情况,而通常我们使用更多的是两脚或四脚按键,针对此类按键目前常用下面两种电路来实现消抖:
其原理是利用电阻和电容对波形进行积分,不过硬件方法一般只在按键数量较少时使用,不然会使用到大量的电阻电容;更为经济和简洁的方法是通过编程实现消抖。
软件消抖
对于单按键的消抖模块,其接口如下图所示:
接口声明功能描述如下:
接口名称 | I/O | 功能描述 |
---|---|---|
CLK | I | 50M时钟 |
nRST | I | 复位信号 |
KEY_IN | I | 按键输入 |
KEY_FLAG | O | 按键状态的切换 |
KEY_STATE | O | 按键状态 |
软件消抖主要涉及信号跳变沿检测、计数器和状态机
//======判断按键输入信号跳变沿========//
always @(posedge CLK or negedge nRST)
if(!nRST)
begin
key_a <= 1'b0;
key_b <= 1'b0;
end
else
begin
key_a <= KEY_IN;
key_b <= key_a;
end
assign flag_H2L = key_b && (!key_a);//下降沿
assign flag_L2H = (!key_b) && key_a;//上升沿
当前状态 | 下一个状态 | 跳转条件 |
---|---|---|
Key_up | Key_up | (!flag_H2L) — 没检测的下降沿 |
Key_up | Filter_Up2Down | (flag_H2L) — 检测到下降沿 |
Filter_Up2Down | Key_up | (!cnt_full).(flag_L2H) — 还没达到消抖时间就遇到上升沿 |
Filter_Up2Down | Filter_Up2Down | (!flag_L2H).(!cnt_full) — 没到达消抖时间也没遇到上升沿 |
Filter_Up2Down | Key_down | (cnt_full) — 到达消抖时间 |
Key_down | Filter_Down2Up | (flag_L2H) — 遇到上升沿 |
Key_down | Key_down | (!flag_L2H) — 没遇到上升沿 |
Filter_Down2Up | Key_down | (flag_H2L).(!cnt_full) — 还没达到消抖时间就遇到下升沿 |
Filter_Down2Up | Filter_Down2Up | (!flag_H2L).(!cnt_full) — 没到达消抖时间也没遇到下升沿 |
Filter_Down2Up | Key_up | (cnt_full) — 到达消抖时间 |
按键消抖完整模块 KeyPress.v 如下:
module KeyPress(
CLK,
nRST,
KEY_IN,
KEY_FLAG,
KEY_STATE
);
input CLK;
input nRST;
input KEY_IN;
output reg KEY_FLAG;
output reg KEY_STATE;
reg key_a, key_b;
reg en_cnt, cnt_full;
reg [3:0]state;
reg [19:0]cnt;
wire flag_H2L, flag_L2H;
localparam
Key_up = 4'b0001,
Filter_Up2Down = 4'b0010,
Key_down = 4'b0100,
Filter_Down2Up = 4'b1000;
//======判断按键输入信号跳变沿========//
always @(posedge CLK or negedge nRST)
if(!nRST)
begin
key_a <= 1'b0;
key_b <= 1'b0;
end
else
begin
key_a <= KEY_IN;
key_b <= key_a;
end
assign flag_H2L = key_b && (!key_a);
assign flag_L2H = (!key_b) && key_a;
//============计数使能模块==========//
always @(posedge CLK or negedge nRST)
if(!nRST)
cnt <= 1'b0;
else if(en_cnt)
cnt <= cnt + 1'b1;
else
cnt <= 1'b0;
//=============计数模块=============//
always @(posedge CLK or negedge nRST)
if(!nRST)
cnt_full <= 1'b0;
else if(cnt == 20'd999_999)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
//=============有限状态机============//
always @(posedge CLK or negedge nRST)
if(!nRST)
begin
en_cnt <= 1'b0;
state <= Key_up;
KEY_FLAG <= 1'b0;
KEY_STATE <= 1'b1;
end
else
case(state)
//保持没按
Key_up: begin
KEY_FLAG <= 1'b0;
if(flag_H2L) begin
state <= Filter_Up2Down;
en_cnt <= 1'b1;
end
else
state <= Key_up;
end
//正在向下按
Filter_Up2Down: begin
if(cnt_full) begin
en_cnt <= 1'b0;
state <= Key_down;
KEY_STATE <= 1'b0;
KEY_FLAG <= 1'b1;
end
else if(flag_L2H) begin
en_cnt <= 1'b0;
state <= Key_up;
end
else
state <= Filter_Up2Down;
end
//保持按下状态
Key_down: begin
KEY_FLAG <= 1'b0;
if(flag_L2H) begin
state <= Filter_Down2Up;
en_cnt <= 1'b1;
end
else
state <= Key_down;
end
//正在释放按键
Filter_Down2Up: begin
if(cnt_full) begin
en_cnt <= 1'b0;
state <= Key_up;
KEY_FLAG <= 1'b1;
KEY_STATE <= 1'b1;
end
else if(flag_H2L) begin
en_cnt <= 1'b0;
state <= Key_down;
end
else
state <= Filter_Down2Up;
end
//其他未定义状态
default: begin
en_cnt <= 1'b0;
state <= Key_up;
KEY_FLAG <= 1'b0;
KEY_STATE <= 1'b1;
end
endcase
endmodule
矩阵键盘的原理图如下所示:
我们将行端口
接到FPGA的输出端口,列端口
接到FPFA的输入端口。当行端口被全部赋值为低电平之后,如果有某一个键按下则对应的列端口会被拉低,按键所在列被确定下来;然后我们可以先将行端口全部赋值为高电平(则列端口检测到的电平全为高),再依次赋值为低电平,当列端口的电平不全为高时对应的行即为按键所在行。
接口声明功能描述如下:
接口名称 | I/O | 功能描述 |
---|---|---|
CLK | I | 50M时钟 |
nRST | I | 复位信号 |
KEY_COL | I | 列端口信号 |
KEY_ROW | O | 行端口信号 |
KEY_Value | O | 扫描到的键值 |
Value_en | O | 键值有效性标识 |
当前状态 | 下一个状态 | 跳转条件 |
---|---|---|
NO_KEY | NO_KEY | (!KeyPress:u0).(!KeyPress:u1).(!KeyPress:u2).(!KeyPress:u3) |
NO_KEY | ROW_ONE | (!KeyPress:u0).(!KeyPress:u1).(!KeyPress:u2).(KeyPress:u3) + (!KeyPress:u0).(!KeyPress:u1).(KeyPress:u2) + (!KeyPress:u0).(KeyPress:u1) + (KeyPress:u0) |
ROW_ONE | NO_KEY | (!KEY_COL[0]) + (KEY_COL[0]).(!KEY_COL[1]) + (KEY_COL[0]).(KEY_COL[1]).(!KEY_COL[2]) + (KEY_COL[0]).(KEY_COL[1]).(KEY_COL[2]).(!KEY_COL[3]) |
ROW_ONE | ROW_TWO | (KEY_COL[0]).(KEY_COL[1]).(KEY_COL[2]).(KEY_COL[3]) |
ROW_TWO | NO_KEY | (!KEY_COL[0]) + (KEY_COL[0]).(!KEY_COL[1]) + (KEY_COL[0]).(KEY_COL[1]).(!KEY_COL[2]) + (KEY_COL[0]).(KEY_COL[1]).(KEY_COL[2]).(!KEY_COL[3]) |
ROW_TWO | ROW_THREE | (KEY_COL[0]).(KEY_COL[1]).(KEY_COL[2]).(KEY_COL[3]) |
ROW_THREE | NO_KEY | (!KEY_COL[0]) + (KEY_COL[0]).(!KEY_COL[1]) + (KEY_COL[0]).(KEY_COL[1]).(!KEY_COL[2]) + (KEY_COL[0]).(KEY_COL[1]).(KEY_COL[2]).(!KEY_COL[3]) |
ROW_THREE | ROW_FOUR | (KEY_COL[0]).(KEY_COL[1]).(KEY_COL[2]).(KEY_COL[3]) |
ROW_FOUR | NO_KEY |
矩阵扫描完整模块 Key_Value.v 如下:
module KeyValue(
CLK,
nRST,
KEY_ROW,
KEY_COL,
KEY_Value,
Value_en
);
input CLK;
input nRST;
input [3:0]KEY_COL;
output reg Value_en;
output reg [3:0]KEY_ROW;
output reg [3:0]KEY_Value;
wire [3:0]key_flag;
wire [3:0]key_state;
reg [4:0]state;
reg row_flag;//标识已定位到行
reg [1:0]rowIndex;
reg [1:0]colIndex;
localparam
NO_KEY = 5'b00001,
ROW_ONE = 5'b00010,
ROW_TWO = 5'b00100,
ROW_THREE = 5'b01000,
ROW_FOUR = 5'b10000;
KeyPress u0(
.CLK(CLK),
.nRST(nRST),
.KEY_IN(KEY_COL[0]),
.KEY_FLAG(key_flag[0]),
.KEY_STATE(key_state[0])
);
KeyPress u1(
.CLK(CLK),
.nRST(nRST),
.KEY_IN(KEY_COL[1]),
.KEY_FLAG(key_flag[1]),
.KEY_STATE(key_state[1])
);
KeyPress u2(
.CLK(CLK),
.nRST(nRST),
.KEY_IN(KEY_COL[2]),
.KEY_FLAG(key_flag[2]),
.KEY_STATE(key_state[2])
);
KeyPress u3(
.CLK(CLK),
.nRST(nRST),
.KEY_IN(KEY_COL[3]),
.KEY_FLAG(key_flag[3]),
.KEY_STATE(key_state[3])
);
//==========通过状态机判断行===========//
always @(posedge CLK or negedge nRST)
if(!nRST)
begin
state <= NO_KEY;
row_flag <= 1'b0;
KEY_ROW <= 4'b0000;
end
else
case(state)
NO_KEY: begin
row_flag <= 1'b0;
KEY_ROW <= 4'b0000;
if(key_flag != 4'b0000) begin
state <= ROW_ONE;
KEY_ROW <= 4'b1110;
end
else
state <= NO_KEY;
end
ROW_ONE: begin
//这里做判断只能用KEY_COL而不能用key_state
//因为由于消抖模块使得key_state很稳定
//不会因为KEY_ROW的短期变化而变化
//而KEY_COL则会伴随KEY_ROW实时变化
if(KEY_COL != 4'b1111) begin
state <= NO_KEY;
rowIndex <= 4'd0;
row_flag <= 1'b1;
end
else begin
state <= ROW_TWO;
KEY_ROW <= 4'b1101;
end
end
ROW_TWO: begin
if(KEY_COL != 4'b1111) begin
state <= NO_KEY;
rowIndex <= 4'd1;
row_flag <= 1'b1;
end
else begin
state <= ROW_THREE;
KEY_ROW <= 4'b1011;
end
end
ROW_THREE: begin
if(KEY_COL != 4'b1111) begin
state <= NO_KEY;
rowIndex <= 4'd2;
row_flag <= 1'b1;
end
else begin
state <= ROW_FOUR;
KEY_ROW <= 4'b0111;
end
end
ROW_FOUR: begin
if(KEY_COL != 4'b1111) begin
rowIndex <= 4'd3;
row_flag <= 1'b1;
end
state <= NO_KEY;
end
endcase
//===========判断按键所在列=============//
always @(posedge CLK or negedge nRST)
if(!nRST)
colIndex <= 2'd0;
else if(key_state != 4'b1111)
case(key_state)
4'b1110: colIndex <= 2'd0;
4'b1101: colIndex <= 2'd1;
4'b1011: colIndex <= 2'd2;
4'b0111: colIndex <= 2'd3;
endcase
//===========通过行列计算键值==========//
always @(posedge CLK or negedge nRST)
if(!nRST)
Value_en <= 1'b0;
else if(row_flag)
begin
Value_en <= 1'b1;
KEY_Value <= 4*rowIndex + colIndex;
end
else
Value_en <= 1'b0;
endmodule
数码管的连接图如下:
从图上我们可以看出8个8段数码管通过共阳的方式连接,其位选端
和段选端
分为用8个I/O进行控制。
对于显示控制模块,其接口如下图所示:
接口声明功能描述如下:
接口名称 | I/O | 功能描述 |
---|---|---|
CLK | I | 50M时钟 |
nRST | I | 复位信号 |
KEY_Value | I | 键盘键值 |
Value_en | I | 键值有效性标识 |
SEL | O | 位选信号 |
SEG | O | 段选信号 |
数码管显示控制完整模块 ShowControl.v 如下:
module ShowControl(
CLK,
nRST,
KEY_Value,
Value_en,
SEL,
SEG
);
input CLK;
input nRST;
input Value_en;
input [3:0]KEY_Value;
output reg [7:0]SEL;
output reg [7:0]SEG;
reg clock_1k;
reg [14:0]cnt;
reg [3:0]data_tmp;
reg [31:0]disp_data;
//=========产生数码管驱动脉冲=======//
always @(posedge CLK or negedge nRST)
if(!nRST)
begin
cnt <= 15'b0;
clock_1k <= 1'b1;
end
else if(cnt == 15'd24_999)
begin
cnt <= 15'b0;
clock_1k <= ~clock_1k;
end
else
cnt <= cnt + 1'b1;
//==========更新要显示的数据=========//
always @(posedge CLK or negedge nRST)
if(!nRST)
disp_data <= 32'd0;
else if(Value_en)
disp_data <= {disp_data[27:0],KEY_Value};
else
disp_data <= disp_data;
//=============位选控制============//
always @(posedge clock_1k or negedge nRST)
if(!nRST)
SEL <= 8'b1111_1111;
else if(SEL == 8'b1111_1111)
SEL <= 8'b1111_1110;
else
SEL <= {SEL[6:0],SEL[7]};
//=============段选控制============//
always @(*)
case(SEL)
8'b1111_1110: data_tmp <= disp_data[3:0];
8'b1111_1101: data_tmp <= disp_data[7:4];
8'b1111_1011: data_tmp <= disp_data[11:8];
8'b1111_0111: data_tmp <= disp_data[15:12];
8'b1110_1111: data_tmp <= disp_data[19:16];
8'b1101_1111: data_tmp <= disp_data[23:20];
8'b1011_1111: data_tmp <= disp_data[27:24];
8'b0111_1111: data_tmp <= disp_data[31:28];
endcase
//=============段选解析============//
always @(*)
case(data_tmp)
4'h0: SEG <= 8'b11000000;
4'h1: SEG <= 8'b11111001;
4'h2: SEG <= 8'b10100100;
4'h3: SEG <= 8'b10110000;
4'h4: SEG <= 8'b10011001;
4'h5: SEG <= 8'b10010010;
4'h6: SEG <= 8'b10000010;
4'h7: SEG <= 8'b11111000;
4'h8: SEG <= 8'b10000000;
4'h9: SEG <= 8'b10010000;
4'ha: SEG <= 8'b10001000;
4'hb: SEG <= 8'b10000011;
4'hc: SEG <= 8'b11000110;
4'hd: SEG <= 8'b10100001;
4'he: SEG <= 8'b10000110;
4'hf: SEG <= 8'b10001110;
endcase
endmodule
对于顶层模块,其接口如下图所示:
其接口功能如前文所述-------------------------------------------------------------------
本设计顶层模块完整代码 DigitalTube.v 如下:
module DigitalTube(
CLK,
nRST,
KEY_ROW,
KEY_COL,
SEL,
SEG
);
input CLK;
input nRST;
input [3:0]KEY_COL;
output [3:0]KEY_ROW;
output [7:0]SEL; //位选
output [7:0]SEG; //段选
wire value_en;
wire [3:0]key_value;
KeyValue keyValue1(
.CLK(CLK),
.nRST(nRST),
.KEY_ROW(KEY_ROW),
.KEY_COL(KEY_COL),
.KEY_Value(key_value),
.Value_en(value_en)
);
ShowControl showControl1(
.CLK(CLK),
.nRST(nRST),
.KEY_Value(key_value),
.Value_en(value_en),
.SEL(SEL),
.SEG(SEG)
);
endmodule