FPGA.矩阵键盘

矩阵键盘是外部设备中所使用的排布类似于矩阵的键盘组。矩阵式结构的键盘显然比直接法要复杂一些,识别也要复杂一些,列线通过电阻接正电源,并将行线所接的芯片的I/O口作为输出端,而列线所接的I/O口则作为输入。

本文简单的讲诉下矩阵键盘的扫描原理,给出相应的代码和测试文件。

矩阵键盘是常用的输入设备,由于相较于传统的单个按键,它能很好的节省宝贵的I/O接口。怎么节省的呢?下面以4x4矩阵键盘举例说明原因。

原理解释

首先说明,FPGA中我们常说的写矩阵键盘,其实是在书写判断键盘输入是哪一个按键的,并译码输出相应数据的芯片,是一个芯片。键盘就是键盘,我们不可能把它“写”出来。

键盘整体图示

我们写的就是左边的芯片,那该怎么书写?我们需要先弄清楚矩阵键盘的工作原理,看下图,通过图片理解比较直观点。

键盘电路

从上图我们可以看出,对于4x4矩阵键盘来说,它是四输出,四输入。

而且键盘的输出即芯片的输入,键盘的输入即芯片的输出,有点拗口,不过看图还是不难理解的。

为了方便讲解,我先设定一些值,说明原理时比较好说明。
设输入的四根线,也就是行,设为[3:0]col;
输出的四根线,也就是列,设为[3:0]row;
将它们组合一下,行列就能决定一个点 设键值 row_col = {row,col}。

我们设定一个初始状态,对于芯片来说,输出[3:0]col是能控制的,那没有按键按下时,我让它都是 0 就好了,为什么不是 1,因为键盘的另一端,是高电平,不能和它一样啊,不然键盘按下的时候,反馈给芯片的输入就没有变化了。当你读到这或许还不能理解这一点,不过没关系,我尽力去解释清楚。

看键盘的四输入[3:0]col , 四输出[3:0]row。
假如没有按键按下时,芯片给的四个输出都是0,输入的按键由于外部连的高电平的作用,给芯片的输入都是1,本来呢,要是你有按键按下,就能把0接到输入,输入也就不用全是1了,但是你没有, 所以在没有按键按下时,芯片中的数据 row_col = 1111_0000。

要是现在想按了,而且还按了,就是下面这个键。会发生什么呢?

键盘电路

由于按下了,也就是接通了按键左边的0 ,那么反馈回去的数据就变成了 row_col=1101_0000,令前面的1111 ,变成了1101的“凶手”,肯定是你后面四个0的某一个造成的。
但对于芯片来说我是不知道你是哪个 0 造成的,也就是说目前芯片知道了是哪一列,但哪一行不知道。为了找出是那一个0,我让芯片的四个输出,也就是四行,只有一个是0,一个个排查,也就是键盘行扫描。

先从1110开始,1111不变成1101,那就换一行变成1101试试,不行再变,变成1011,直到找到使芯片输入变成了1011的那一行,这样行,列都知道了就能确定一个点。显然,我按下的那个键 row_col = 1101_1101。

按键消抖这部分,在这里不属于重点,不多解释了,主要就是个延时,在程序中也不难看懂。

具体的操作看代码吧。

源程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
`timescale 1ns / 1ps
module key_scan(
input clk,
input rst_n, //系统复位
input [3:0]row, //FPGA输入,行
output reg flag, //输出值有效标志位
output reg [3:0] data , //按键值
output reg [3:0] y,//根据按键功能给出相应功能
output reg [3:0]col //FPGA 输出 ,列
);

///////////////////////////////////
// 分频电路 //
/////////////////////////////////
//当然需不需要分频,看你具体需求了,但这里给出代码
reg clk_1k;//1k Hz的时钟
reg [20:0] count; //计数器
parameter T1k = 10;//这个分频为了方便仿真,随意定了个较小的值,10分分频

always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
clk_1k <= 1;
count <= 0;
end
else
if(count < T1k/2)
count <= count + 1;
else
begin
count <= 0 ;
clk_1k <= ~clk_1k;
end
end
////////////////////////////////////
// 键盘扫描电路 //
///////////////////////////////////
reg [4:0]cnt_time;//按键按下的时间
reg [1:0]state; //状态寄存器
reg [7:0]row_col; //按键对应的行列值

always @(posedge clk_1k or negedge rst_n)
begin
if(!rst_n)
begin
flag <= 0;
state <= 0;
cnt_time <= 0;
row_col <= 0;
col <= 4'b0_000;
end
else
begin
case(state)
// 状态 0 是判断是否有按键按下,如果有,开始列扫描
0 : begin
if(row != 4'b1111) //检测是否有按键按下,row是输入的。
begin
if(cnt_time < 5) //按键时间大于一定的值,认为确实是按下了
cnt_time <= cnt_time + 1;
else
begin
cnt_time <= 0;
state <=1;
col <= 4'b1110; //行扫描的初始值
end
end
else
cnt_time = 0;
end

//状态 0 是对列进行逐列扫描
1:begin
if(row != 4'b1111)
begin
row_col <= {row,col};//键值的行列记录下来
flag <= 1 ; //拉高有效标志位,确定扫描点
state <= 2;
col <= 4'b0_000;//回到没有按下的状态
end
else
begin
col <= {col[2:0],col[3]};
end
//不是初始按键的列 1110,换一列,变为1101,相当于一个循环移位
end

//状态 2 检测按键是否被释放,如果被释放,回到初始状态
2:begin
if(row == 4'b1111)
begin
if(cnt_time < 5)
begin
cnt_time <= cnt_time + 1;
end
else
begin
cnt_time <= 0;
state <= 0;
col <= 4'b000;
end
end
else
begin
cnt_time <=0;
flag <= 0;
end
end

default : state <= 0;
endcase
end
end
//////////////////////////////////
// 编码 //
/////////////////////////////////
always @(*)
begin
if(!rst_n)
begin
data = 0;
end
else
begin
case(row_col)
8'b1110_1110: data = 0;
8'b1110_1101: data = 1;
8'b1110_1011: data = 2;
8'b1110_0111: data = 3;

8'b1101_1110: data = 4;
8'b1101_1101: data = 5;
8'b1101_1011: data = 6;
8'b1101_0111: data = 7;

8'b1011_1110: data = 8;
8'b1011_1101: data = 9;
8'b1011_1011: data = 10;
8'b1011_0111: data = 11;

8'b0111_1110: data = 12;
8'b0111_1101: data = 13;
8'b0111_1011: data = 14;
8'b0111_0111: data = 15;
default: data = 16;
endcase
end
end
//////////////////////////////////
// 译码 //
/////////////////////////////////
//对按键译码就是把按键的功能安排好,我随便把它变为数字的16进制了
always @(*)
begin
if(!rst_n)
begin
data = 0;
end
else
begin
case(data)
0: y=4'b0_000;
1: y=4'b0_001;
2: y=4'b0_010;
3: y=4'b0_011;

4: y=4'b0_101;
5: y=4'b0_101;
6: y=4'b0_110;
7: y=4'b0_111;

8: y=4'b1_000;
9: y=4'b1_001;
10: y=4'b1_010;
11: y=4'b1_011;

12: y=4'b1_100;
13: y=4'b1_101;
14: y=4'b1_110;
15: y=4'b1_111;
default: y=4'b0_000;
endcase
end
end
//译码电路,当个例子,实际情况请根据自身需要修改。
endmodule

tb测试文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
module tb_keyscan;
reg clk;
reg rst_n;
reg [3:0]row;

wire flag;
wire [3:0]data;
wire [3:0]y;
wire [3:0]col;

//例化
key_scan uut(
.clk(clk),.rst_n(rst_n),.row(row),
.flag(flag),.data(data),.y(y),.col(col)
);

initial
begin
clk = 0;
rst_n = 0;
#10;
rst_n = 1; //先复位一段时间再启动
end

always #10 clk=~clk; //50M时钟信息

reg [4:0]pnumber; //按键值
initial
begin
pnumber = 16;//无按键按下
#10 pnumber = 1;
#10 pnumber = 16; //模拟抖动
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10;

pnumber = 1;//按键 1 按下
#2500 pnumber = 16; //模拟释放时的抖动
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#2000; //松开

pnumber = 2;//按键 2 按下
#2500 pnumber = 16; //模拟释放时的抖动
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#2000; //松开

pnumber = 3;//按键 3 按下
#2500 pnumber = 16; //模拟释放时的抖动
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#10 pnumber = 1;
#10 pnumber = 16;
#2000; //松开
end

always @(*)
begin
case(pnumber)
0: row = {1'b1,1'b1,1'b1,col[0]};
1: row = {1'b1,1'b1,1'b1,col[1]};
2: row = {1'b1,1'b1,1'b1,col[2]};
3: row = {1'b1,1'b1,1'b1,col[3]};

4: row = {1'b1,1'b1,col[0],1'b1};
5: row = {1'b1,1'b1,col[1],1'b1};
6: row = {1'b1,1'b1,col[2],1'b1};
7: row = {1'b1,1'b1,col[3],1'b1};

8: row = {1'b1,col[0],1'b1,1'b1};
9: row = {1'b1,col[1],1'b1,1'b1};
10: row = {1'b1,col[2],1'b1,1'b1};
11: row = {1'b1,col[3],1'b1,1'b1};

12: row = {col[0],1'b1,1'b1,1'b1};
13: row = {col[1],1'b1,1'b1,1'b1};
14: row = {col[2],1'b1,1'b1,1'b1};
15: row = {col[3],1'b1,1'b1,1'b1};

16: row = 4'b1111;
default : row = 4'b1111;
endcase
end

endmodule

代码的写法有很多,各有优劣,我这也只是给出我的想法,有错误请斧正,欢迎大家在评论区与我交流,打赏就更好了。

本文完。

请我喝杯咖啡~
------ 本文结束------
0%