基础 -4- 矩阵键盘实验
约 807 个字 145 行代码 1 张图片 预计阅读时间 5 分钟
章节导读
本章将介绍矩阵键盘检测电路的设计与实现方法,通过 Verilog HDL 语言完成 4×4 矩阵键盘的扫描识别模块,掌握多键输入设备的行列扫描原理、消抖机制以及按键编码处理方式。
矩阵键盘作为常见的人机交互接口之一,广泛应用于嵌入式系统、数字电路和微控制器项目中。与独立按键不同,矩阵键盘在节省 IO 资源的同时,对扫描逻辑和时序处理提出了更高的要求。实验中我们将采用逐行扫描法,结合状态机与延时消抖手段,确保按键信息的准确采集。

Figure 1. 矩阵键盘原理图
实验板 8 个引脚分别连接矩阵键盘的 KEY1~KEY8,该矩阵键盘的原理如下:将 KEY1~KEY4 脚设置为输出引脚,KEY5~KEY8 设置为输入引脚。以 KEY1 和 KEY5 为例,当没有按键按下时,KEY1 和 VCC 之间是断路,此时 R1 为上拉电阻,电路几乎没有电流流过,KEY5 检测到的电压恰好是 VCC,为 1。所以按键不按下,KEY5~KEY8 检测到 1。
如果按键按下。此时 KEY5~KEY8 检测到的值与 KEY1~KEY4 的输出电压有关。以 KEY1 和 KEY5 为例,如果 KEY1 输出为 0,按键 1 按下,VCC 和 KEY1 之间形成通路,KEY5 检测到 0。但如果 KEY1 输出为 1,此时即使按键按下,VCC 和 KEY1 之间也几乎没有电流,此时 KEY5 检测到高阻态,也就是 1。所以,如果行输出电平为 0,并且按键按下,KEY5~8 会检测到 0;如果行输出电平为 1,按键按下,KEY5~8 检测到 1。
现在我们看懂了原理图就可以开始设计 verilog,根据原理图我们知道,只有行电平(KEY1~4 的输出电平)为 0 时,按键按下,KEY5~8 才会检测到 0。那么我们可以用行扫描的逻辑设计:
- FPGA 按顺序将 1 到 4 行中的一行输出为低电平,其余 3 行为高电平(或高阻态)。
- FPGA 逐个读取每列引脚(KEY5~8)的电平,若某列为低电平,则说明该行和该列交汇处的按键被按下。
- 可以在没有按键按下时,把所有行的输出电平都拉低,直到有按键按下时,重复 1~2 的步骤扫描。
实战演练
系统架构
| Verilog |
|---|
| 系统框图:
[Top模块] = {矩阵键盘扫描模块 → 按键上升沿检测模块}
|
模块设计
根据上述原理,设计行扫描矩阵键盘检测模块如下:
matrix_key
| Verilog |
|---|
| module matrix_key #(
parameter ROW_NUM = 4,
parameter COL_NUM = 4,
parameter DEBOUNCE_TIME = 2000,
parameter DELAY_TIME = 200
) (
input wire clk,
input wire rstn,
output reg [ROW_NUM-1:0] row,
input wire [COL_NUM-1:0] col,
output reg [ROW_NUM*COL_NUM-1:0] key_out
);
localparam ROW_ACTIVE = 1'b0; // 行有效电平
localparam ROW_INACTIVE = 1'b1; // 行无效电平
localparam COL_PRESSED = 1'b0; // 列按下电平
localparam COL_RELEASED = 1'b1; // 列释放电平
reg [ROW_NUM-1:0][COL_NUM-1:0] key; // 按键状态寄存器
reg [2:0] cu_st, nt_st;
localparam [2:0] ST_IDLE = 3'b001;
localparam [2:0] ST_SCAN = 3'b010;
localparam [2:0] ST_DEBOUNCE = 3'b100;
wire btn_pressed = ((|(\~(col ^ {COL_NUM{COL_PRESSED}}))) && (cu_st == ST_IDLE)) || (key_out != 0); // 只要有一个按键按下,btn_pressed为1
reg [31:0] delay_cnt; // 延时计数器
reg [31:0] debounce_cnt; // 消抖计数器
reg [ROW_NUM-1:0] row_cnt; // 行计数器
always @(posedge clk or negedge rstn) begin
if(!rstn) delay_cnt <= 0;
else if(cu_st == ST_SCAN) begin
if(delay_cnt == DELAY_TIME) delay_cnt <= 0;
else delay_cnt <= delay_cnt + 1;
end else delay_cnt <= 0;
end
always @(posedge clk or negedge rstn) begin
if(!rstn) row_cnt <= 0;
else if(cu_st == ST_SCAN) begin
if(delay_cnt == DELAY_TIME) row_cnt <= row_cnt + 1;
else row_cnt <= row_cnt;
end else row_cnt <= 0;
end
always @(posedge clk or negedge rstn) begin
if(!rstn) debounce_cnt <= 0;
else if(cu_st == ST_DEBOUNCE) begin
if(debounce_cnt == DEBOUNCE_TIME) debounce_cnt <= 0;
else debounce_cnt <= debounce_cnt + 1;
end else debounce_cnt <= 0;
end
/*
处理逻辑
ROW作为输出,COL作为输入
1. ST_IDLE状态,所有ROW都拉至有效电平
2. 若没有按键按下,所有COL都为释放电平
3. 若有按键按下,按下的按键所在的COL会变为按下电平
4. 进入ST_SCAN状态,启动扫描,ROW全部置为无效电平,并逐次改变为有效电平。(此时,COL会都变成列释放电平)
5. 如果某一个ROW行有效电平时,COL变成了列按下电平,则说明该ROW和COL交点的按键被按下
6. 每一行都扫描一遍。
7. 进入ST_DEBOUNCE状态,所有ROW都拉至行有效电平,在此期间不进行扫描。
8. DEBOUNCE时间到后,进入IDLE状态。
*/
always @(posedge clk or negedge rstn) begin
if(!rstn) cu_st <= ST_IDLE;
else cu_st <= nt_st;
end
always @(*) begin
if(!rstn) nt_st <= ST_IDLE;
else case(cu_st)
ST_IDLE: begin
if(btn_pressed) nt_st <= ST_SCAN;
else nt_st <= ST_IDLE;
end
ST_SCAN: begin
if((delay_cnt == DELAY_TIME) && (row_cnt == ROW_NUM-1)) nt_st <= ST_DEBOUNCE;
else nt_st <= ST_SCAN;
end
ST_DEBOUNCE: begin
if(debounce_cnt == DEBOUNCE_TIME) nt_st <= ST_IDLE;
else nt_st <= ST_DEBOUNCE;
end
default: nt_st <= ST_IDLE;
endcase
end
integer i, j;
always @(posedge clk or negedge rstn) begin
if(!rstn) key <= 0;
else for(i=0; i<ROW_NUM; i=i+1)
for(j=0; j<COL_NUM; j=j+1)
if((cu_st == ST_SCAN) && (delay_cnt == DELAY_TIME) && (row_cnt == i)) key[i][j] <= (col[j] == COL_PRESSED)?(1'b1):(1'b0);
else key[i][j] <= key[i][j]; // 其他情况不变
end
always @(*) begin
for(i=0;i<ROW_NUM;i=i+1) begin
for(j=0;j<COL_NUM;j=j+1) begin
key_out[i*COL_NUM+j] <= key[i][j];
end
end
end
always @(posedge clk or negedge rstn) begin
if(!rstn) row <= {ROW_NUM{ROW_ACTIVE}};
else if(cu_st == ST_IDLE && nt_st == ST_SCAN) row <= {{(ROW_NUM-1){ROW_INACTIVE}}, ROW_ACTIVE};
else if(cu_st == ST_SCAN) begin
if(delay_cnt == DELAY_TIME) row <= {row[ROW_NUM-1:0],ROW_INACTIVE};
else row <= row;
end else row <= {ROW_NUM{ROW_ACTIVE}};
end
endmodule //matrix_key
|
为了能够观察到现象,使用板载 8 个 led 和实验箱 8 个 led 进行显示,按下矩阵键盘的按键,对应 led 就会亮,顶层文件如下所示:
matrix_key_top
| Verilog |
|---|
| module matrix_key_top(
//system io
input wire external_clk ,
input wire external_rstn,
input wire [ 3:0] col,
output wire [ 3:0] row,
output wire [15:0] led
);
wire [15:0] key_out;
assign led = key_out;
matrix_key #(
.ROW_NUM ( 4 ),
.COL_NUM ( 4 ),
.DEBOUNCE_TIME ( 10000 ),
.DELAY_TIME ( 2000 ))
u_matrix_key(
.clk ( external_clk ),
.rstn ( external_rstn ),
.row ( row ),
.col ( col ),
.key_out ( key_out )
);
endmodule
|
上板验证步骤
- 设置参数:CLK_CYCLE=5000(对应 200Hz 扫描频率)
- 绑定管脚:连接 led 和矩阵键盘管脚
章末总结
关键收获:
- 掌握矩阵键盘行扫描原理,能看懂原理图
- 学习时序控制中计数器的重要作用
拓展训练
可以将数码管与矩阵键盘相结合