
简介针对51单片机开发学习者这份资源提供了基于4*4矩阵键盘中断扫描的完整Keil工程与C源文件主要面向需要理解按键中断机制与行列扫描原理的初学者。工程内包含C源文件、中断服务程序、按键扫描函数与数码管显示函数详细展示了中断允许寄存器IE、优先级寄存器IP的配置并在中断服务程序中完成行线与列线状态读取、现场保护、按键去抖和编码判断逻辑清晰便于跟踪调试。压缩包内含11个文件包括C源文件、Keil工程文件uv2/opt/plg、编译输出hex/obj/lst/m51及备份整体仅19KB可直接在Keil中打开工程运行适合快速学习和实验。已有1645人浏览学习对于单片机课程设计、实验室教学或自学中断系统与矩阵键盘的读者来说是一份很好的实务参考。通过对比轮询与中断的扫描差异还能加深对嵌入式系统资源利用的理解。1. 先搞懂为什么矩阵键盘非要用中断扫描刚把矩阵键盘接到51单片机上时我最先写的是while(1)循环里一层层翻转IO口轮询按键。当时还觉得没什么问题直到往主循环里塞了数码管动态刷新和串口打印才发现按键响应变得时好时坏按下一次经常被识别成两三次甚至丢失。改成定时器中断扫描后所有IO读取和消抖都被隔离在固定的时间片里主循环再乱也不影响按键状态。下面以“64-矩阵键盘中断扫描”这个Keil工程为例拆开讲解4×4矩阵键盘中断扫描的完整实现包括硬件引脚连接、C源文件中的中断服务程序和扫描函数、Keil工程文件里的每个文件类型以及实际调试时最值钱的几个边界参数。2. 4×4矩阵键盘的硬件连接与中断触发原理矩阵键盘能省IO的核心原因是把16个按键排成了4行4列的交叉矩阵每个按键跨接一根行线和一根列线。这样16个键只需要8个IO而不是16个。单片机的工作分两步先控制列线输出电平再读取行线状态通过行列组合确定具体按键。这个工程里我们使用P1口P1.0-P1.3接4根列线P1.4-P1.7接4根行线中断源使用定时器0每2ms触发一次扫描而不是用外部中断引脚的硬件方式。2.1 行列引脚分配与上拉确认常见的51单片机矩阵键盘模块会引出8个引脚其中4根行走一边、4根列走一边。接线的对应关系如下矩阵键盘引脚51单片机P1口工作模式R1P1.4输入读行线R2P1.5输入读行线R3P1.6输入读行线R4P1.7输入读行线C1P1.0扫描输出C2P1.1扫描输出C3P1.2扫描输出C4P1.3扫描输出51的P1口是准双向口写1时引脚呈高电平并且具备输入能力写0时输出低电平。所以扫描时只让当前列输出0其他列和所有行线保持1然后读取P1口高四位的电平某个行线被按下拉到0说明这个键正好落在当前列与对应行的交叉处。为了减小长导线干扰每根行线上最好并一个10kΩ排阻到VCC开发板矩阵键盘模块如果自带排阻这一步可以跳过。如果手上板子的行和列正好反接不改电路把后面扫描函数里的输出和读取方向对调就行。2.2 外部中断和定时器中断怎么选很多人看到“中断扫描”会下意识以为是外部中断。51单片机确实有两个外部中断INT0和INT1但矩阵键盘有16个按键不可能把每个键都接到P3.2或P3.3。一个折中方案是把4根行线经过四输入与门合成一路任意按键按下都会触发外部中断这种硬方案响应速度极快但会增加一颗逻辑芯片而且按键抖动会把中断服务函数打得措手不及需要额外处理。对于课程设计和自制小项目采用定时器0中断做周期扫描是最稳的定时器每2ms触发一次在中断里扫描一遍键盘消抖后更新键值主循环只负责消费结果。这样既不增加硬件成本又能保证即使主循环里跑延时函数按键动作也不会丢。2.3 一个中断周期里的时间预算在12MHz晶振下51单片机一个机器周期是1us。定时器0采用模式1也就是16位定时器初值计算公式如下TH0 (65536 - 2000) / 256; // 2ms初值高8位12MHz下2000个机器周期 TL0 (65536 - 2000) % 256; // 2ms初值低8位从初值计数到65536溢出正好经过2000个机器周期即2ms。中断服务函数里除了重装初值还要执行一次完整的键盘扫描。扫描函数需要遍历4列每列读取一次P1口并判断4个行位总共不超过50条指令折算下来大约几十微秒只占2ms中断周期的2.5%。就算加入连续4次采样消抖单次中断执行时间也不会超过200us。这个时间预算决定了中断扫描不会拖累主循环同时也说明为什么不能把防抖延时直接写进中断里否则一次中断可能占用上毫秒主循环就被卡死了。3. Keil工程里的C源文件到底写了什么“64-矩阵键盘中断扫描”这个工程在Keil里编译时核心代码就一个.c文件。下面这段代码是提取出来的最小可运行版本包含了定时器0初始化、键盘扫描、中断消抖、数码管段码输出四个部分芯片头文件使用reg52.h兼容AT89S52和STC89C52系列。函数/变量作用scan_keyboard()逐列拉低扫描线读取行线状态返回键值1~16或0xFFtimer0_isr()定时器0中断服务函数每2ms调用一次扫描并消抖g_key_value中断与主循环共享的键值变量g_key_pressed新按键标志主循环处理后清零#include reg52.h #define KEY_PORT P1 #define DB_PORT P0 typedef unsigned char u8; u8 code seg_table[17] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x00 }; u8 g_key_value 0xFF; bit g_key_pressed 0; u8 scan_keyboard(void) { u8 col, row; for (col 0; col 4; col) { KEY_PORT 0xF0 | (0x0F ^ (0x01 col)); for (row 0; row 4; row) { if ((KEY_PORT (0x10 row)) 0x00) { return row * 4 col 1; } } } return 0xFF; } void timer0_isr(void) interrupt 1 { static u8 last_key 0xFF; static u8 stable_cnt 0; u8 cur_key; TH0 0xF8; TL0 0x30; cur_key scan_keyboard(); if (cur_key ! 0xFF cur_key last_key) { if (stable_cnt 4) { g_key_value cur_key; g_key_pressed 1; stable_cnt 0; } } else { last_key cur_key; stable_cnt 0; } } void main(void) { TMOD 0xF0; TMOD | 0x01; TH0 0xF8; TL0 0x30; ET0 1; TR0 1; EA 1; while (1) { if (g_key_pressed) { g_key_pressed 0; DB_PORT seg_table[g_key_value - 1]; } } }3.1 寄存器初始化的顺序为什么不能反过来初始化代码先清除TMOD的低四位再把定时器0配置为模式1接着写TH0和TL0初值最后依次打开ET0、TR0和EA。如果先把TR0置1再写初值定时器可能在初值写入完成前就开始计数第一次中断时间不准如果忘记EA1即使ET0打开中断也不会被响应。这个顺序在很多51单片机教材里都是固定套路建议直接背下来。中断服务函数里的两个static局部变量用于保存消抖状态它们不会被主循环访问作用域也只在中断函数内部合理规避了全局变量被随意修改的问题。3.2 扫描函数里的列扫描与行读取KEY_PORT 0xF0 | (0x0F ^ (0x01 col));这句话需要拆开看。0xF0保证了高四位全部为1也就是P1.4到P1.7处于输入状态后面的0x0F ^ (0x01 col)只影响低四位让当前列对应的位为0其他列为1。比如col0时P1口写0xFEP1.0输出低P1.1到P1.3输出高P1.4到P1.7保持输入。接着KEY_PORT (0x10 row)检查的是P1.4到P1.7对应位如果某一行读到0说明这个键已经按下直接返回row * 4 col 1作为键值1~16。这里特意用0x0F ^ (0x01 col)而不是~(0x01 col)是因为取反运算会把高四位也变成0导致行线被强制置为输出低电平整个矩阵键盘就无法正常读入了。3.3 中断里的消抖是如何工作的按键按下瞬间会产生大约5~10ms的机械抖动这期间接触点会反复通断。如果每次扫描都直接上报键值一次按键可能被识别成多次。中断服务函数里的做法是先调用扫描函数获得本次键值如果和上一次相同说明按键状态稳定累加计数连续4次读到同一个键值也就是8ms后才更新全局键值并置位标志。如果期间读到不同值或0xFF说明抖动还在继续计数清零重来。这样做的代价是按键响应有8ms延迟对矩阵键盘应用场景来说可以接受。主循环轮询到g_key_pressed后立即清零并更新数码管避免同一键值被重复处理。4. 工程文件逐项讲解与Keil编译配置解压“64-矩阵键盘中断扫描.rar”后能看到一堆后缀名不同的文件它们不是都需要手动处理。Keil工程的核心只有.Uv2和.c文件其余大多是编译过程中生成的中间产物。搞清楚每个文件的作用遇到报错时才知道去哪里看。4.1 文件类型表扩展名文件作用是否需要备份.c源码中断扫描的核心逻辑需要.Uv2Keil工程文件双击打开需要.OptKeil窗口布局和断点配置不必手动修改.plg编译输出日志记录warning和error出问题时查看.obj编译器生成的目标文件自动生成.lst列表文件包含每条C语句对应的汇编查错时使用.m51存储器映射文件查看ROM/RAM占用分析空间时使用.hex最终烧录文件下载用工程里的.Opt文件经常被忽略但它保存了工程窗口状态和断点位置。如果打开工程发现一直停在某个奇怪的位置删掉同名的.Opt再重新打开即可。.plg是文本格式可以在没有打开Keil的情况下快速查看上一次编译结果尤其在自动化构建场景下很有用。4.2 Keil里中断号与中断源的对应C51编译器用interrupt关键字扩展中断服务函数后面的数字是中断向量号不是引脚编号。定时器0对应的中断号是1外部中断0对应0外部中断1对应2定时器1对应3串口对应4。如果代码里ET01开的是定时器0中断函数却写成interrupt 0那么进入的是外部中断0的向量地址烧录后不会按预期工作。在Keil uVision5里这种错误编译时不会报错只有运行时才会暴露排查起来很隐蔽。正确的写法如下void timer0_isr(void) interrupt 1 // 1号中断是定时器0 { TH0 0xF8; TL0 0x30; }还需要注意C51编译器不允许两个函数声明同一个中断号否则链接阶段会报多重定义错误。如果工程里已经有其他中断函数务必检查中断号是否重复。4.3 编译和下载的完整步骤用Keil uVision5打开.Uv2工程后先确认三件事Device选型是否正确Output选项卡里的Create HEX File是否勾选以及主文件是否被包含在工程里。选型不一定要完全精确STC89C52可以选Atmel AT89C52因为指令集兼容但如果使用到STC扩展的SFR必须换成STC的头文件。编译按F7正常结束会看到0 Error(s), 0 Warning(s)。如果出现UNCALLED SEGMENT警告通常只是某个函数未被调用不影响生成hex但要检查是不是忘记在中断或main里调用扫描函数。最后用STC-ISP或开发板自带下载工具把hex烧进单片机波特率建议选低一点待机电流不稳时下载容易失败。ls -l 矩阵键盘中断扫描.hex烧录前先确认hex文件存在且有最近修改时间。如果你在Windows下直接看文件大小也可以一般在几百字节到几KB之间。hex内容过小说明链接时可能丢了某些段过大的话要检查是否把调试信息也编了进去。实际项目中我习惯在编译后顺手看一眼hex生成时间确保烧录的是刚编译出来的固件避免“改了代码没生效”这种低级问题。5. 中断扫描的边界情况与调试技巧中断扫描做到能跑只是第一步真正把响应手感调好还要处理几个边界情况。这里说三个我在51单片机矩阵键盘教程和实际调试中都会验证的点。5.1 用IO翻转量出ISR执行时间想知道中断服务函数到底跑了多久不要在Keil里打断点因为断点会冻结整个芯片看不到真实时序。正确做法是在中断入口和出口各翻转一个空闲IO比如P3.0用示波器看高电平宽度。这个宽度就是扫描加消抖的实际执行时间。12MHz晶振下前面那段代码的运行时间一般在100us以内如果测到超过500us说明扫描函数里有除法、延时或库函数调用需要优化。中断服务函数里不要调用printf和delay前者会占用大量堆栈后者是在中断里硬耗时间都会破坏实时性。逻辑分析仪也可以看但至少要16MHz采样率才能准确捕捉微秒级脉冲。5.2 长按与连击的判据调整前面代码中连续4次读到相同键后会把stable_cnt清零因此只触发一次按键事件适合菜单选择。如果希望长按后自动重复改一行就可以if (stable_cnt 4) { g_key_value cur_key; g_key_pressed 1; stable_cnt KEY_AUTO_REPEAT_TICKS; // 例如20代表40ms后再上报 }这样第一次按键确认后不立即清零而是继续累加到20次也就是40ms后再次上报同一键值实现自动重复。KEY_AUTO_REPEAT_TICKS建议定义成宏放在文件头部连击速度和消抖次数互不干扰。实际调试中我用2ms中断周期稳定次数等于4时手感偏“钝”按键后要明显等待一小段才显示改成2之后碳膜按键也能稳定识别但需要保证硬件上拉电阻足够强否则噪声可能被当成按键。5.3 行列反接时只改宏定义很多学习板为了走线方便把矩阵键盘的行线和列线接到P1口的高四位和低四位时会互换。这时不需要重新改扫描函数把输出掩码和读取掩码抽象成两个宏即可#define SCAN_OUT_MASK 0x0F // 列线低四位输出扫描信号 #define READ_IN_MASK 0xF0 // 行线高四位读取按键状态扫描函数里统一使用这两个宏做位运算行列反接时只需要交换宏值扫描方向自动反转。顺手把键值编码也做成查表不要依赖固定的row * 4 col公式这样硬件布局再怎么变软件侧只是换一张表的事。调试时先用万用表确认空闲状态下所有行线都是高电平按下任意键时至少有一根行线被拉到低电平如果测量结果不符合问题一定在接线或上拉不在中断代码。本文还有配套的精品资源点击获取