**这是本文档旧的修订版!**

=====矩阵键盘键入系统设计=====

实验任务

  • 任务:基于 STEP-MAX10M08核心板 和 STEP BaseBoard V3.0底板 设计矩阵键盘键入系统并观察调试结果
  • 要求:按动矩阵键盘按键,通过核心板上的数码管显示按键的键值。
  • 解析:通过FPGA编程驱动矩阵键盘电路,获取矩阵键盘键入的信息,然后通过编码将键盘输出的信息译码成对应的键值信息,最后通过驱动核心板独立数码管,将键盘按键的键值显示在数码管上。

实验目的

在基础数字电路实验部分我们已经掌握了FPGA驱动独立显示数码管的原理及方法,掌握了有限状态机的设计实现思想,本实验主要学习矩阵键盘的原理及驱动设计。

  • 熟悉独立显示数码管驱动模块的应用
  • 熟悉状态机FSM的编程方法
  • 掌握矩阵键盘的工作原理
  • 完成FPGA驱动矩阵键盘的设计实现

设计框图

根据前面的实验解析我们可以得知,该设计可以拆分成三个功能模块实现,

  • ArrayKeyBoard:通过驱动矩阵键盘工作获取键盘的操作信息数据。 * Decoder:通过编码方式将键盘的操作信息译码成对应的键值信息。 * Segmentled:通过驱动核心板独立数码管将键盘按键的键值显示在数码管上。

顶层模块Type_system通过实例化三个子模块并将对应的信号连接,最终实现矩阵键盘键入系统的总体设计。
top-down层次设计模块结构设计

实验原理

键盘类型

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

独立键盘 矩阵键盘

  • 独立式按键:每个按键单独连接到一个I/O口上,通过判断按键端口的电位识别按键的操作,编程简单,需要更多I/O资源
  • 矩阵式按键:通过行列交叉编码连接,通过分时扫描的方法识别按键的操作,节约I/O资源,编程相对复杂 关于独立键盘的驱动设计,我们在基础数字电路实验部分已经系统学习过,这里主要介绍一下矩阵键盘的原理及驱动方法。 在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式,使用行线和列线分别连接到按键开关的两端,这样我们就可以通过4根行线和4根列线(共8个I/O口)连接16个按键,而且按键数量越多优势越明显,比如再多加一条线就可以构成20键的键盘,而直接用端口线则只能多出一键(9键)。由此可见,在需要的键数比较多时,采用矩阵法来做键盘是更加合适的。

矩阵键盘连接

这里我们以STEP BaseBoard V3.0底板上的4×4矩阵键盘为例,其电路图如下: 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个状态。

矩阵键盘驱动设计

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

状态机各状态逻辑

状态机程序实现如下:

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

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

状态转移图

状态机程序实现如下:

parameter			CNT_200HZ = 60000;
//计数器计数分频实现5ms周期信号clk_200hz
reg		[15:0]		cnt;
reg					clk_200hz;
always@(posedge clk or negedge rst_n) begin  //复位时计数器cnt清零,clk_200hz信号起始电平为低电平
	if(!rst_n) begin
		cnt <= 16'd0;
		clk_200hz <= 1'b0;
	end else begin
		if(cnt >= ((CNT_200HZ>>1) - 1)) begin  //数字逻辑中右移1位相当于除2
			cnt <= 16'd0;
			clk_200hz <= ~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_in) begin
	if(!rst_n_in) 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

结束了吗? 没有,还差一点,继续往下看

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

按键抖动示意

上图是市面上常见按键抖动的模型,有三个参数,按下抖动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

将此程序实现方法更新到上一部分程序中,最后keyout就是采样消抖后的直接输出。当按键被按下时keyout对应位输出低电平,松开按键时keyout对应位恢复高电平输出。 由以上程序我们完成了矩阵键盘的驱动,但是keyout这种类型的输出有时在后级时序电路设计中不好直接使用,例如对于当前矩阵键盘键入系统设计来讲,我们需要按键按动一次(与按下保持的时间长短无关)就输入对应的键值,按键松开后键值也不能消失,我们就需要一个寄存器变量来储存按过的按键键值,考虑到可能存在多个按键在极短时间内被先后按下,这样一来我们最好将按键按动这种长时间事件转化成一个瞬间的脉冲,方法就是对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位脉冲信号,平时为低电平,当按键被按下时刻keypulse产生一个高脉冲,脉冲的宽度为模块系统时钟clkin的一个周期。

状态机状态转移图

系统总体实现

在基础数字电路实验部分我们已经掌握了FPGA驱动独立显示数码管的原理及方法, 模块通过一个4位的输入传递要显示的数值,通过9位的输出控制数码管显示该数值,这里我们不再重复。 矩阵键盘驱动模块输出的是脉冲信号,后面数码管驱动模块输入的是用4位位宽表示的数据,所以中两个实例之间就需要一个编码的功能块,主要功能是根据矩阵键盘的脉冲输出(keypulse)判定键盘的操作,通过编码对应提供按键的键值数据(segdata),最后通过连线将键值数据连接到数码管模块的输入端口。

键值显示转码程序实现

//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

综合后的设计框图如下:

rtl设计框图

实验步骤

  1. 双击打开Quartus Prime工具软件;
  2. 新建工程:File → New Project Wizard(工程命名,工程目录选择,设备型号选择,EDA工具选择);
  3. 新建文件:File → New → Verilog HDL File,键入设计代码并保存;
  4. 设计综合:双击Tasks窗口页面下的Analysis & Synthesis对代码进行综合;
  5. 管脚约束:Assignments → Assignment Editor,根据项目需求分配管脚;
  6. 设计编译:双击Tasks窗口页面下的Compile Design对设计进行整体编译并生成配置文件;
  7. 程序烧录:点击Tools → Programmer打开配置工具,Program进行下载;
  8. 观察设计运行结果。

实验现象