FPGA矩阵键盘驱动设计:从扫描时序到Verilog实现与调试

发布时间:2026/10/13 2:34:50

FPGA矩阵键盘驱动设计:从扫描时序到Verilog实现与调试 做FPGA开发的人基本都会在某一天遇到“4*4矩阵键盘”这个外设。可能是在课程设计里可能是在某个仪表面板上也可能是单纯想给自己的小项目加一个人机交互入口。我之前有个朋友在某国产FPGA开发板上接了一块4x4矩阵键盘查了半天教程引脚也约束了代码也烧进去了结果按键怎么按都没反应。后来发现问题出在列输入引脚没有做上拉按下按键时引脚等于悬空FPGA读回来的全是高电平——按键信息根本没进到芯片里面。这种问题光看代码是看不出来的。矩阵键盘驱动听起来是一个很简单的东西但真要在FPGA上跑稳会牵扯到扫描时序、按键消抖、状态机设计、引脚电气特性、板级排错等一系列细节。这篇文章我把完整的设计思路、Verilog实现和调试经验整理一遍适合两类人看一类是新手想把矩阵键盘在FPGA上跑通另一类是已经写好了驱动但上板各种不工作想找排查思路的朋友。文章里所有代码都是我在实际工程里用过的写法不是那种“为了写教程现攒”的代码。1. 项目整体思路与设计拆解1.1 矩阵结构解决的是“引脚不够用”的问题先搞清楚最基础的问题4x4矩阵键盘为什么叫“矩阵”因为16个按键被分成了4行4列每一个按键实际上是一个开关位于某一行和某一列的交点。比如左上角那个键当你按下它时第0行和第0列就被这个开关短接在一起。这样设计的好处非常直观16个独立按键需要16个IO引脚而矩阵键盘只需要8个引脚——4个行引脚、4个列引脚。在FPGA开发板上IO资源虽然比单片机宽裕但引脚往往被分配给了数码管、LED、ADC、Flash等外设能省一个是一个。这也是矩阵键盘至今仍然是各种开发板和仪器面板主力输入方案的原因。不过省引脚是有代价的代价就是不能像独立按键那样“直接读”必须靠扫描来定位到底按的是哪个键。扫描的逻辑可以理解为我一次只把一根行线拉低然后去读四根列线看哪根被拉低了。第0行拉低时如果列1也被拉低说明是第0行第1列的键被按下。依次把四行都扫一遍就能知道16个按键里哪个处于按下状态。这就是矩阵键盘驱动的核心工作。1.2 我的方案选型逐行拉低 vs 行列反转扫描的具体实现网上常见的有两类思路。一类是逐行拉低法一类是行列反转法。逐行拉低法就是我上面描述的思路让一行输出低电平其余行输出高电平或高阻然后读取列状态。这种方式结构清晰状态机很好写也方便随时扩展成8x8更大规模的矩阵。行列反转法则是先把所有行输出低、读出列状态锁存后交换角色再把所有列输出低、读出行状态两次读结果组合出按键位置。它扫描速度快但代码稍绕而且对IO方向切换有要求。我最终选择的是逐行拉低法。原因是它在FPGA上实现最自然状态转换一目了然后续要加释放检测、组合键功能也容易扩展。而且对于4x4这种规模扫描速度完全是冗余的不需要为了省几个周期去用行列反转。1.3 为什么用FPGA而不是单片机写驱动有人可能会问矩阵键盘驱动这种东西单片机上一堆现成库干嘛要用FPGA其实FPGA在这里的优势不是“简单”而是“可控”。FPGA可以精确地控制每个扫描周期的时长、每个消抖计数的参数而且所有逻辑都在一块芯片里完成不需要外部依赖。更重要的是FPGA往往不是一个独立的系统它要和其它逻辑共存——比如你需要用同一个按键去切换显示模式、触发数据采集、控制状态机跳转。这时候如果按键驱动只是系统里的一小部分自然希望它是一个可靠的硬件模块而不是靠处理器反复查询。我在这篇文章里写的这个驱动就是作为一个独立模块使用对外只暴露“时钟、复位、列输入、行输出、键值和有效脉冲”这几个端口。这个设计定位也决定了代码要尽量独立不依赖任何厂商的软核只要FPGA资源够几个Slice就能跑起来。2. 核心原理与关键设计细节2.1 先看原理图行和列到底谁输入谁输出这是我踩过最深的一个坑。很多教程会在代码里默认把“某一行拉低、读取列”但到了你的板子上行和列的电气方向可能完全相反。有的开发板是行接FPGA输出、列接输入有的则反过来。这个不查原理图光靠“猜”是不行的。写驱动之前第一件事就是打开开发板的原理图找到矩阵键盘部分确认以下几点行线ROW接的是FPGA哪些引脚列线COL接的又是哪些引脚列线或行线上是否已经有上拉电阻按键按下的时候是把两个节点短接还是把某个节点拉低/拉高。一般来说常见接法是“行输出、列输入”。键盘按下时被拉低的列信号传回FPGA。如果列输入引脚本身没有外部上拉就需要在FPGA内部开启弱上拉。Xilinx的约束写法是给对应引脚加PULLUP属性Altera则在QSF文件里设置WEAK_PULL_UP_RESISTOR。有些教程完全忽略这一点结果就是引脚悬空读回来全是一扫描函数怎么跑都扫不到按键。我当时帮朋友排查时发现他的列引脚在约束文件里什么都没设FPGA内部又没有上拉等于让列引脚直接悬空读。按下按键时相当于把悬空引脚接到低电平理论上有变化但实际由于噪声和输入缓冲读回来的值很不稳定。把PULLUP加上之后问题立刻消失。所以这一条我必须放在最前面讲。2.2 消抖的本质和参数计算机械按键按下时金属触点不是一次就稳定接触的而是会在几毫秒内反复弹跳电平也跟着上下抖动。这个时间通常在5ms到20ms不等取决于按键质量、按下速度。如果不处理FPGA会在几毫秒内看到“按下、释放、按下、释放”的连续变化一个按键被识别成多次。处理方法就是“消抖”。单片机里常用的做法是延时十几毫秒再读一次FPGA里本质也一样只是用计数器来实现。以50MHz系统时钟为例1ms是50000个时钟周期20ms就是1000000个周期。这个数需要20bit寄存器来计数2^201048576。如果你用的时钟是100MHz20ms就是2000000个周期需要21bit。具体公式就是计数上限 消抖时间(秒) × 时钟频率。我的习惯是消抖时间取20ms并且把“按下消抖”和“释放消抖”都做。按键按下后延时20ms再确认避免按下瞬间的抖动释放时同样延时20ms再回到空闲避免释放瞬间的抖动导致重复触发。有些实现只做按下消抖结果松开的时候又触发一次就会造成“按一次识别两次”的怪现象。2.3 扫描时序安排兼顾实时性和稳定性消抖和扫描其实是两回事。如果每次扫描一行都要消抖20ms扫完四行要80ms虽然人感受不到但整个系统的响应就慢了。更好的做法是把流程拆成两段第一段是“按下确认”。初始时让所有行输出低电平只要检测到任何一列被拉低就进入消抖流程。消抖结束后确认确实有键按下。第二段才是“逐行定位”。这时候不再需要20ms消抖因为我们已经知道有个键稳定按下了只需要快速判断它在哪一行哪一列。每一行拉低后等一小段时间让电平稳定我一般取10us也就是500个周期再采样列值。四行扫完最多也就几十微秒人完全无法感知。这里有个容易被忽略的细节扫描某一行时其余行一定要输出高电平而不是悬空或高阻否则按键按下时可能形成意外的通路产生“串键”。比如第0行拉低、第1行悬空如果你同时按了(0,1)和(1,1)两个键电流就可能通过列线串到第1行导致读出来的结果混乱。保证未扫描行都置高是最简单的防串扰手段。3. 实操过程与Verilog实现3.1 模块接口和内部参数定义我写驱动的时候习惯把时钟频率作为参数传进来这样以后换到不同板子上只需要改一个参数。下面是模块接口module keypad_4x4 #( parameter CLK_FREQ 50_000_000 )( input wire clk, input wire rst_n, input wire [3:0] col_in, // 列输入默认高电平 output reg [3:0] row_out, // 行输出低有效 output reg [3:0] key_code, // 按键编码 0~15 output reg key_valid // 按键有效脉冲 );注意这里的row_out是低有效驱动即拉低表示选中某一行。col_in默认通过上拉电阻或内部弱上拉保持高电平按下按键后被拉低。如果你的板子电气极性相反代码里的判断逻辑要相应取反这就是前面强调先看原理图的原因。内部参数如下// 20ms 消抖计数50MHz下 1000000 localparam DEBOUNCE_TOP 20 * CLK_FREQ / 1000; // 行扫描稳定时间约 10us localparam SETTLE_TOP CLK_FREQ / 100000; localparam S_IDLE 3d0; localparam S_DEBOUNCE 3d1; localparam S_SCAN0 3d2; localparam S_SCAN1 3d3; localparam S_SCAN2 3d4; localparam S_SCAN3 3d5; localparam S_READ 3d6; localparam S_RELEASE 3d7;DEBOUNCE_TOP的计算前面说过了50MHz下是1000000。SETTLE_TOP取500也就是行输出拉低后等500个时钟周期再采样10us足够让信号稳定。3.2 列输入同步防止亚稳态按键输入是外部异步信号直接采样有可能让FPGA进入亚稳态。虽然矩阵键盘这种低频信号实际出问题的概率不高但养成好习惯总没错。我给列输入加了两级同步寄存器这是几乎所有跨时钟域信号处理的标准做法reg [3:0] col_sync_r; reg [3:0] col_sync_rr; always (posedge clk or negedge rst_n) begin if (!rst_n) begin col_sync_r 4b1111; col_sync_rr 4b1111; end else begin col_sync_r col_in; col_sync_rr col_sync_r; end end // 是否任意键按下 wire any_key ~(col_sync_rr); // 按下按键对应的列位为1 wire [3:0] col_neg ~col_sync_rr;col_sync_rr是两级同步后的稳定值后面所有逻辑都基于它判断。这里还有一个隐含的好处驱动模块内部统一使用时钟同步后的信号不会因为直接操作外部引脚而产生毛刺。3.3 主状态机从按下检测到逐行扫描主状态机是整个驱动的骨架我把关键部分拆开讲。一开始在S_IDLE让所有行都拉低监听列S_IDLE: begin row_out 4b0000; if (any_key) begin deb_cnt 20d0; state S_DEBOUNCE; end end只要有任意一列变低就认为可能有键按下进入消抖。消抖计数到20ms后再次检查any_key如果仍然有效就真正开始逐行扫描如果已经恢复高说明是抖动干扰回到S_IDLE。扫描过程我直接用四个状态分别对应四行这样写比用一个循环变量在状态机里“转”更直观S_SCAN0: begin row_out 4b1110; // 第0行拉低 settle_cnt 10d0; state S_READ; end S_READ: begin if (settle_cnt SETTLE_TOP - 1) begin settle_cnt settle_cnt 1b1; end else begin // 稳定时间到采样列值 if (col_neg ! 4b0000) begin // 当前行有按键按下输出键码这里以 S_SCAN0 为例 key_code 4d0 key_encode(4d0, col_neg); key_valid 1b1; state S_RELEASE; end else begin // 这一行没有按键继续下一行 state S_SCAN1; end end end为了精简上面我只写了第0行的分支。完整代码里S_SCAN1、S_SCAN2、S_SCAN3结构完全一样只是行输出分别变为1101、1011、0111并且把对应的行号传给key_encode。这里有一个细节扫描阶段为什么用了S_READ单独等待因为拉低行输出之后硬件电路需要一点时间才能让列电平稳定下来立刻采样可能读到旧值。加10us的稳定等待代价很小但能显著提高可靠性。3.4 键值编码从行列位置到按下的是哪个键4x4键盘的物理布局通常是这样的行\列列0列1列2列3行0123A行1456B行2789C行3*0#D驱动的输出key_code我习惯直接给行列索引组合即row*4 col。比如行0列0是0行0列1是1行3列2是14。这样在FPGA内部继续做逻辑判断时最方便数值越小越整齐。如果后级需要把键码转换成ASCII码或者特定指令字可以再单独做一层映射。有些朋友喜欢直接输出“123A456B...”的ASCII码也可以只是映射表写起来长一点。我的建议是驱动只负责“物理位置编码”不做业务映射。因为物理位置到业务含义的映射往往跟具体应用强相关把两者耦合在一起换一个项目就要改驱动代码很麻烦。key_encode这个组合函数可以很简单地实现用case语句把列值对应的列号找出来再拼上当前行号。四输入一输出的编码逻辑资源占用可以忽略不计。3.5 释放检测避免“一次按键多次触发”扫描到按键后驱动进入S_RELEASE状态。这一步很多人会忽略。按键不可能一直按着总会松开如果在松开的过程中不做处理下一次检测就可能再次识别同一个按键表现在外设上就是“按一下跳两下”。我的做法是确认键码后把所有行都拉低重新回到全局检测方式等待any_key消失。而且不是一消失就立刻回S_IDLE同样给它一段消抖时间我复用S_DEBOUNCE这一套计数逻辑在释放期间再计20ms。释放消抖结束且确认没有任何按键按下状态机才回到空闲。如果你不需要连续按键功能这个流程已经非常完整。3.6 仿真测试需要注意的点写Testbench的时候我建议重点关注三个场景第一单个按键按下和释放的完整时序。仿真时用force或直接给col_in赋带抖动的波形看key_valid是否只拉高一个周期。第二抖动干扰场景。让col_in在10ms内反复跳变看状态机是否会被误触发进入扫描流程。第三串键场景。两个按键同时按下看能否正确扫描并识别其中一个。对于矩阵键盘两个按键同时按下的处理能力有限这个后面还会细说。Testbench里我用两个独立的任务模拟“按下”和“释放”按下的时候给col_in加一小段随机抖动释放时也加抖动这样比较接近真实按键行为。仿真代码一般写得比较糙核心是能产生类似真实按键的时序即可。4. 板级调试实录按键不灵、乱触发、重复计次怎么办4.1 头号问题完全没反应先去量电气上板之后按键完全没反应90%的情况不是状态机写错而是电气问题。我建议的排查顺序是固定的第一步用万用表二极管档或电压档量行线和列线对地电压。列线应该是高电平通常3.3V如果量到0V说明引脚配置有问题或者外部短路。第二步按下按键量对应行线和列线之间是否变成导通。注意矩阵键盘的按键是行和列的交叉点按下后行线和列线短接这个和独立按键“一端接地”不一样。第三步检查引脚约束里有没有设PULLUP。如果你在代码里已经用了内部上拉但是忘记在XDC或QSF里声明实际上不会生效。这个细节非常坑代码看起来没问题引脚也用对了可就是读不到低电平。第四步才是回头查代码里行和列的扫描顺序跟原理图是否一致。4.2 按键乱触发、连击问题出在消抖和释放“按一下识别好几次”是高发问题。常见原因有两个。第一个是消抖时间不够。如果你把消抖时间设成3ms甚至1ms可能出现按下瞬间的抖动还没结束计数器就开始扫描了。我推荐至少10ms常规用20ms。如果按键质量差甚至可能要加到30ms。这个值不是越大越好太大响应会变慢但20ms是综合下来比较稳的起点。第二个是释放没消抖。前面说过如果松开按键时不做延迟处理释放瞬间的抖动可能被当成“又按了一下”。这个排查起来也容易在S_RELEASE状态下加一个计数器确认松开稳定后再回空闲问题自然消失。4.3 列引脚悬空导致的随机触发还有一种情况是按键没按但系统偶尔自己报一个键值。这个基本可以断定是列引脚悬空没有上拉引脚电平受外部电磁干扰随机浮动。解决方法跟“没反应”那条一样加上拉。有些开发板原理图里已经放了上拉电阻就不会有这个问题如果没放只能靠FPGA内部上拉或者外接电阻。我遇到过一些键盘模组出厂自带弱上拉但文档没标代码里再开一次内部上拉也没有坏处。4.4 排查速查表现象可能原因排查/解决建议所有按键没反应列引脚悬空、上拉未生效检查PULLUP约束量列线电平特定行或列没反应引脚约束错误、排线接触不良对照原理图检查引脚分配检查排线按键连击/重复触发释放消抖缺失、消抖时间过短增加释放消抖延长时间到20ms随机误触发引脚悬空、电磁干扰开启内部上拉必要时外部加10k上拉两个键同时按出错矩阵扫描固有局限扫描时未扫描行置高或改用多键检测方案key_valid拉高不止一拍确认状态没有只保持一个周期在状态机里用寄存器延迟清零4.5 用一个LED回环Demo快速验证驱动如果你不想一上来就接UART可以先做一个最简单的验证把key_code直接接到四个LED上按下键LED就显示对应的二进制编码。这样一个4x4键盘驱动是否工作一眼就能看出来。我自己的习惯是接一个按键后让LED保持显示按下一个新键才更新。这样便于观察是不是真的读到了。注意key_valid是单周期脉冲如果直接用组合逻辑去驱动LED可能一闪而过最好把key_code锁存住。这个Demo虽然简单但能过滤掉一大部分问题如果按键位置能对应上LED显示值说明扫描逻辑和键值编码都没问题剩下的就是上层应用的事了。5. 一些我踩过的坑和最后的扩展建议做这个驱动的时候我印象最深的一个坑就是上拉。当时在某国产FPGA开发板上键盘模组自带原理图是“行输入、列输出”我一开始照着网上的代码默认“行输出、列输入”结果整块板子怎么调都不对。后来把行和列对调又把列引脚反着读才反应过来。所以“看原理图”这件事我真的怎么强调都不为过不同板子接法完全不同。代码结构上我建议把驱动模块跟业务逻辑彻底分开。键盘驱动只负责“行列扫描、消抖、编码、输出有效脉冲”至于这个键码是控制LED、切换显示还是触发数据采集全部放到上一层。这个模块写一次以后换到任何工程只要引脚约束跟上直接复制就能用。如果以后要扩展可以考虑把4x4改成5x4、6x4状态机多加几行即可也可以在驱动里加一个“长按”检测把按下时长超过比如1秒的按键输出一个长按标志这样在菜单系统里就可以用短按和长按做不同操作。这些扩展都不复杂前提是最底层的扫描和消抖要把地基打稳。现在我回头再想那个朋友遇到的问题他其实只差一个PULLUP。矩阵键盘驱动不是玄学电气、时序、状态机三关都过了它就能安安稳稳跑上很久。也希望这篇文章能帮你在自己的板子上少花几个晚上的排查时间。
延伸阅读

更多相关文章

2026/10/13 2:34:50

Linux内核加固:从__user指针校验到LKRG运行时防护

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 2:34:50

ES6继承方法:从原型链到class的手写实现与面试全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 3:54:55

UE5视差材质原理与实操:POM节点、参数调优与避坑指南

简介:这套UE5视差材质资源包面向虚幻引擎开发者和视觉设计师,系统展示基于高度图的视差贴图技术,用于低成本营造物体表面深度感,提升场景真实度,可广泛应用于游戏开发、建筑可视化与电影制作。压缩包共478个文件&#…

2026/10/13 3:54:55

mfc140.dll丢失怎么修复?官方免费方法+深度排查思路

“由于找不到mfc140.dll,无法继续执行代码。重新安装程序可能会修复此问题。”如果你最近被这个弹窗卡住过,那多半是某个软件或游戏客户端突然打不开了。每次遇到这种弹窗,第一反应都是先上网搜一下“mfc140.dll丢失怎么解决”,结…

2026/10/13 3:54:55

软件配置管理实战:从版本控制到变更治理的完整解析

软件配置管理(SCM)这个东西,刚入行的开发可能觉得它就是“用Git管代码”,干了两三年以后,会发现事情远没有这么简单。我在带团队和做技术管理的这些年里,见过太多次因为配置管理没做好而引发的线上事故、团…

2026/10/13 3:54:55

Linux磁盘管理全攻略:分区、挂载、LVM、配额与排障实践

做运维这些年,我越来越发现一个事实:很多看似诡异的问题,最后都能在磁盘管理上找到病灶。要么是分区规划不合理导致空间浪费,要么是挂载配置写错开机直接进不了系统,要么是日志把根分区撑爆才发现当初没做配额。磁盘管…

2026/10/13 3:49:55

进程通信全解析:从管道到Socket的选型与实践

写进程通信这块内容,我心里一直有个念头:教科书把管道、消息队列、共享内存、信号量、Socket一个个平铺开来,讲完定义讲API,讲完API讲例子,但真到自己写多进程程序时,最常遇到的困惑是“我到底该用哪个”。…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/12 12:01:26

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介:这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师,聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案,解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件,约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型,最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支,让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图,十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑