跳到主要内容

2.4 实验原理

2.4.1 键盘类型

嵌入式设计中常见的键盘有两种类型,独立键盘与矩阵键盘。

alt text
  • 独立式按键:每个按键单独连接到一个I/O口上,通过判断按键端口的电位识别按键的操作,编程简单,需要更多I/O资源

  • 矩阵式按键:通过行列交叉编码连接,通过分时扫描的方法识别按键的操作,节约I/O资源,编程相对复杂

关于独立键盘的驱动设计,我们在基础数字电路实验部分已经系统学习过,这里主要介绍一下矩阵键盘的原理及驱动方法。

在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式,使用行线和列线分别连接到按键开关的两端,这样我们就可以通过4根行线和4根列线(共8个I/O口)连接16个按键,而且按键数量越多优势越明显,比如再多加一条线就可以构成20键的键盘,而直接用端口线则只能多出一键(9键)。由此可见,在需要的键数比较多时,采用矩阵法来做键盘是更加合适的。

2.4.2 矩阵键盘连接

这里我们以STEP BaseBoard V4.0底板上的4x4矩阵键盘为例,其电路图如下:

alt text
4x4矩阵键盘电路

上图为4×4矩阵按键的硬件电路图,可以看到4根行线(ROW1、ROW2、ROW3、ROW4)和4根列线(COL1、COL2、COL3、COL4),同时列线通过上拉电阻连接到VCC电压(3.3V),对于矩阵按键来讲:

  • 4根行线是输入的,是由FPGA控制拉高或拉低,
  • 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按键应该是被按下了。

按照扫描的方式,一共分为4个时刻,分别对应4根行线中的一根拉低,4个时刻依次循环,这样就完成了矩阵按键的全部扫描检测,我们在程序中以这4个时刻对应状态机的4个状态。

2.4.3 矩阵键盘驱动设计

通过上节的描述,大家对于矩阵键盘工作的原理应该都没有问题了,那么我们怎么编程实现矩阵键盘驱动设计呢?我们将矩阵键盘的扫描周期分为4个时刻,对应4个状态,使得状态机在4个状态上循环跳转,最终通过扫描的方式获取矩阵键盘的操作状态。

alt text
状态机各状态逻辑

状态机程序实现如下:

reg     [1:0]       c_state;     //状态机状态
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) //循环跳转,输出逻辑
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

至于状态机循环的周期,根据我们基础教程里可知,按键抖动的不稳定时间在10ms以内,所以对同一个按键采样的周期大于10ms,这里同样取20ms时间。20ms时间对应4个状态,每5分钟进行一次状态转换。所以我们在状态机之前先增加分频模块,得到200Hz的分频时钟,然后状态机按照200Hz分频时钟的节拍做状态跳转和键盘采样。

alt text

分频功能程序实现如下:

parameter			CNT_200HZ = 60000;
//count for clk_200hz,计数分频
reg [15:0] cnt;
reg clk_200hz;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt <= 16'd0;
clk_200hz <= 1'b0;
end else begin
if(cnt >= ((CNT_200HZ>>1) - 1)) begin
cnt <= 16'd0;
clk_200hz <= ~clk_200hz;
end else begin
cnt <= cnt + 1'b1;
clk_200hz <= clk_200hz;
end
end
end

通过以上的程序我们实现了状态机的4个状态循环跳转,每个状态都有对应的逻辑输出,接下来我们需要将矩阵键盘的输出采集回来,并以此判断键盘的操作状态。采样也是需要按照状态的,4个状态的采样数据合并后得到一个16位数,代表16个按键的操作状态,是不是非常简单呢?比如下面的程序,是不是就搞定了?

键盘采样功能程序实现如下:

//通过对列接口的电平状态采样得到对应4个按键的状态,依次循环
always@(negedge clk_200hz or negedge rst_n) begin
if(!rst_n) begin
key_out <= 16'hffff;
end else begin
case(c_state)
//采集当前状态的列数据赋值给对应的寄存器位
STATE0: key_out[ 3: 0] <= col;
STATE1: key_out[ 7: 4] <= col;
STATE2: key_out[11: 8] <= col;
STATE3: key_out[15:12] <= col;
default: key_out <= 16'hffff;
endcase
end
end //结束了吗?没有,还差一点,请往下看

对于大多数需要循环扫描的硬件来说,程序写到这里应该就完成了,但是大家想想我们之前基础数字电路实验关于按键消抖部分的内容,因为我们是在对按键采样,按键是会抖动的,所以我们还要想办法对采集回来的数据做一些判定,怎么判定呢? 这就得回到矩阵键盘工作原理上来了。

alt text

上图是市面上常见按键抖动的模型,有三个参数,按下抖动10ms以内,松开抖动10ms以内,按键周期数百ms;前面说过键盘的采样周期为20ms,可以得到以下结论:

  • 按键周期内至少有4个FPGA采样点同时落在按键稳定区域内
  • 按键周期内不会有相邻FPGA采样点同时落在按键抖动区域内
  • 如果FPGA连续两次采样到低电平即可判定按键按下,否则判定为按键松开

键盘采样判定功能程序实现如下:

/STATE0: begin 
key[3:0] <= col; //矩阵键盘采样
key_r[3:0] <= key[3:0]; //键盘数据锁存
key_out[3:0] <= key_r[3:0]|key[3:0]; //连续两次采样判定
end

将此程序实现方法更新到上一部分程序中,最后key_out就是采样消抖后的直接输出。当按键被按下时key_out对应位输出低电平,松开按键时key_out对应位恢复高电平输出。

由以上程序我们完成了矩阵键盘的驱动,但是key_out这种类型的输出有时在后级时序电路设计中不好直接使用,例如对于当前矩阵键盘键入系统设计来讲,我们需要按键按动一次(与按下保持的时间长短无关)就输入对应的键值,按键松开后键值也不能消失,我们就需要一个寄存器变量来储存按过的按键键值,考虑到可能存在多个按键在极短时间内被先后按下,这样一来我们最好将按键按动这种长时间事件转化成一个瞬间的脉冲,方法就是对key_out信号中的每一位进行下降沿(或上升沿)检测,方法如下:

下降沿检测程序实现如下:

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); //通过前后两个时刻的值判断

经过上面程序的处理,我们就得到了16位脉冲信号,平时为低电平,当按键被按下时刻key_pulse产生一个高脉冲,脉冲的宽度为模块系统时钟clk_in的一个周期。

alt text
状态机状态转移图

2.4.4.系统总体实现

在基础数字电路实验部分我们已经掌握了FPGA驱动独立显示数码管的原理及方法, 模块通过一个4位的输入传递要显示的数值,通过9位的输出控制数码管显示该数值,这里我们不再重复。

矩阵键盘驱动模块输出的是脉冲信号,后面数码管驱动模块输入的是用4位位宽表示的数据,所以中两个实例之间就需要一个编码的功能块,主要功能是根据矩阵键盘的脉冲输出(key_pulse)判定键盘的操作,通过编码对应提供按键的键值数据(seg_data),最后通过连线将键值数据连接到数码管模块的输入端口。

键值显示转码程序实现

reg             [7:0]   seg_data;   //高4位代表十位,低4位代表个位
//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: seg_data <= 8'h01; //编码
16'h0002: seg_data <= 8'h02;
16'h0004: seg_data <= 8'h03;
16'h0008: seg_data <= 8'h04;
16'h0010: seg_data <= 8'h05;
16'h0020: seg_data <= 8'h06;
16'h0040: seg_data <= 8'h07;
16'h0080: seg_data <= 8'h08;
16'h0100: seg_data <= 8'h09;
16'h0200: seg_data <= 8'h10;
16'h0400: seg_data <= 8'h11;
16'h0800: seg_data <= 8'h12;
16'h1000: seg_data <= 8'h13;
16'h2000: seg_data <= 8'h14;
16'h4000: seg_data <= 8'h15;
16'h8000: seg_data <= 8'h16;
default: seg_data <= seg_data; //无按键按下时保持
endcase
end
end

综合后的设计框图如下:

alt text