这一篇在干嘛?

16 个独立按键要 16 根 FPGA 引脚,太奢侈了;4×4 矩阵键盘把按键排成 4 行 4 列,只用 8 根线就能识别全部 16 个键。本实验用「逐列扫描 + 行读取」的经典方法找出被按下的键,并把键值译码到数码管上显示。

实验目的

了解 4×4 键盘用 8 根线扫描 16 个键的方法。

实验原理:行列坐标定位按键

4×4 阵列键盘十分常见,其电路和接口如下图。键盘的两个 4 位口 A[3:0] 和 B[3:0] 都接有上拉电阻:

矩阵键盘 40 芯接口原理图

矩阵键盘原理图

16 个按键排成 4×4 的交叉矩阵:行线与列线互不相连,每个按键位于一个「行 × 列」交叉点上,按键按下时把对应的行线与列线接通。识别按键的思路是给行线轮流送低电平,再看列线上哪个变成了低电平

  1. 向 A 口扫描输入一组「只含一个 0」的 4 位数据,如 1110、1101、1011、0111——每次只有一行被拉低;
  2. 若该行上有键按下,按下键所在的列线被接通到低电平,B 口就会输出对应含 0 的数据;若没有键按下,B 口因上拉电阻保持全 1;
  3. 结合 A、B 两个口的数据,就能唯一确定按键的位置。

举个例子:当键 S0 按下时,对于输入的 A=1110,那么输出的 B=0111。把两个口拼在一起,{B,A}=0111_1110 就成了 S0 的专属代码。每个键都有唯一的 {B,A} 编码,检测到某个编码时输出对应的键盘码即可。

上拉电阻是扫描能够工作的前提

B 口(以及 A 口)靠上拉电阻在没有按键、没有被驱动时保持高电平。若上拉缺失,未按下的列线会处于悬空高阻状态,读回来的是随机值,扫描就会误判。这也是为什么引脚约束中 A 口的 4 根线全部配置了 PULLUP。

实验程序

模块 K4X4 用一个 2 位计数器 C 产生 4 个节拍,节拍间轮流把 4 个「单 0 码」送到 B 口扫描;同时用 case 对 {B,A} 查表,命中哪个 8 位编码就输出对应的键值 R:

module K4X4 (CLK,A,B,R); input CLK; input [3:0] A;
output [3:0] B;
output [3:0] R;
reg [1:0] C ;    reg [3:0] R,B ;
always @ (posedge CLK) begin
    C<=C+1;
    case(C)
    0: B=4'B0111; 1: B=4'B1011; 2: B=4'B1101; 3: B=4'B1110;
    endcase
    case( {B,A } )
    8'B0111_1110 : R=4'H0;    8'B0111_1101 : R=4'H1;
    8'B0111_1011 : R=4'H2;    8'B0111_0111 : R=4'H3;
    8'B1110_1110 : R=4'H4;    8'B1110_1101 : R=4'H5;
    8'B1110_1011 : R=4'H6;    8'B1110_0111 : R=4'H7;
    8'B1101_1110 : R=4'H8;    8'B1101_1101 : R=4'H9;
    8'B1101_1011 : R=4'HA;    8'B1101_0111 : R=4'HB;
    8'B1011_1110 : R=4'HC;    8'B1011_1101 : R=4'HD;
    8'B1011_1011 : R=4'HE;    8'B1011_0111 : R=4'HF;
endcase end endmodule

代码结构非常紧凑,逐行拆解:

  • C<=C+1:时钟每拍自增,C 在 0/1/2/3 间循环,作为扫描节拍;
  • 第一个 case:按 C 的值向 B 口输出 0111 → 1011 → 1101 → 1110,四拍覆盖四列扫描;
  • 第二个 case:16 个 {B,A} 组合对应 16 个键,命中即输出 4 位键值 R(4’H0~4’HF)。

本例程没有消抖与松键检测

这段例程刻意保持最简:它没有按键消抖、也没有「等待按键释放」的逻辑,所以按住不放时键值会随扫描节拍持续输出,机械触点的抖动也可能造成短暂的误码。正式产品里通常在检测到稳定键值后加 20ms 左右的消抖窗并等待松键,学习时可以先理解扫描原理,再自行叠加消抖逻辑。

另外注意:CLK 直接用了开发板时钟 SCLK,扫描节拍就是时钟节拍,频率非常高,足以覆盖抖动并保证每次按键至少被扫描到很多次。

实验仿真

根据描述语言进行仿真测试,分析波形:把 A 口固定为某个含 0 的值,观察 B 口轮换扫描时 R 是否输出正确的键值;再模拟一次按键接通(A 的某位被 B 拉低),检查查表结果。

管脚约束

A 口 4 根行扫描线(HA[0]HA[3])配置上拉;B 口 4 根列读取线(LB[0]LB[3])为普通输出;dataout[0][6] 与 en[0][7] 用于数码管显示键值:

set_pin_assignment { HA[0] } { LOCATION = H16; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { HA[1] } { LOCATION = H15; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { HA[2] } { LOCATION = J16; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { HA[3] } { LOCATION = K15; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { LB[0] } { LOCATION = G14; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LB[1] } { LOCATION = G16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LB[2] } { LOCATION = F15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { LB[3] } { LOCATION = F14; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SCLK } { LOCATION = R7; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { dataout[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { dataout[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[1] } { LOCATION = E11; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[6] } { LOCATION = E16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { en[7] } { LOCATION = F16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }

HA(行)全上拉、LB(列)带上驱动强度的配置,与原理图中的上拉电阻一一对应。

下载测试

  1. 将 4×4 键盘模块插在开发板 J1 口上;
  2. 下载程序之后,点击任意键,会看到数码管显示译码数(0~F 的键值)。

现象与总结

按下任意一个键,数码管立即显示对应的 0~F 键值。回头看,这个实验用 8 根线做成了 16 个键的输入,核心是一个「两步问路」的过程:先问「哪一行被拉低了」(B 口发出的单 0 码),再问「哪一列被接通了」(A 口读回的数据),两者拼成唯一的 8 位编码。行列扫描是所有矩阵键盘(包括 PC 键盘内部)的通用原理,配合前面的 PS/2 实验对比学习,可以完整理解「按键如何变成数据」的全链路。

自测