【六】矩阵键盘(发光二极管指示)实验
1. 项目概述
本项目演示如何使用 STM32F103C8T6 微控制器驱动 4×4 矩阵键盘,并通过8个LED指示灯显示按键状态。系统采用行列扫描方式检测按键,只需8个GPIO引脚即可实现16个按键的检测,大大节省了IO资源。
技术要点
- ✅ 4×4矩阵键盘扫描
- ✅ 行列扫描算法
- ✅ 软件消抖
- ✅ 按键值计算
- ✅ LED指示灯控制
- ✅ IO资源优化
- ✅ JTAG引脚重映射
应用场景
- 🎹 数字输入面板
- 🎹 电子密码锁
- 🎹 计算器键盘
- 🎹 电话拨号键盘
- 🎹 工业控制面板
- 🎹 游戏手柄
2. 硬件平台
主控芯片
- 型号: STM32F103C8T6
- 内核: ARM Cortex-M3
- 主频: 72 MHz
- Flash: 64 KB
- SRAM: 20 KB
- GPIO: 37个可用IO
外设资源
4×4矩阵键盘
行线(输出):
| 行线 | 引脚 | 说明 |
|---|---|---|
| ROW0 | PB8 | 第0行(推挽输出) |
| ROW1 | PB9 | 第1行(推挽输出) |
| ROW2 | PB10 | 第2行(推挽输出) |
| ROW3 | PB11 | 第3行(推挽输出) |
列线(输入):
| 列线 | 引脚 | 说明 |
|---|---|---|
| COL0 | PB12 | 第0列(上拉输入) |
| COL1 | PB13 | 第1列(上拉输入) |
| COL2 | PB14 | 第2列(上拉输入) |
| COL3 | PB15 | 第3列(上拉输入) |
按键布局:
COL0 COL1 COL2 COL3
↓ ↓ ↓ ↓
ROW0 → [1] [2] [3] [4]
ROW1 → [5] [6] [7] [8]
ROW2 → [9] [10] [11] [12]
ROW3 → [13] [14] [15] [16]
LED指示灯(8个)
| LED | 引脚 | 说明 |
|---|---|---|
| LED1 | PB0 | 按键1控制 |
| LED2 | PB1 | 按键2控制 |
| LED3 | PB2 | 按键3控制 |
| LED4 | PB3 | 按键4控制(需JTAG禁用) |
| LED5 | PB4 | 按键5控制(需JTAG禁用) |
| LED6 | PB5 | 按键6控制 |
| LED7 | PB6 | 按键7控制 |
| LED8 | PB7 | 按键8控制 |
硬件连接
矩阵键盘连接示意图:
┌─────────────────────────────────────────────┐
│ 4×4 矩阵键盘 │
│ │
│ COL0 COL1 COL2 COL3 │
│ │ │ │ │ │
│ ROW0─ ┼─[1]─┼─[2]─┼─[3]─┼─[4] │
│ │ │ │ │ │
│ ROW1─ ┼─[5]─┼─[6]─┼─[7]─┼─[8] │
│ │ │ │ │ │
│ ROW2─ ┼─[9]─┼─[10]┼─[11]┼─[12] │
│ │ │ │ │ │
│ ROW3─ ┼─[13]┼─[14]┼─[15]┼─[16] │
│ │ │ │ │ │
└─────────────────────────────────────────────┘
↓ ↓ ↓ ↓
PB12 PB13 PB14 PB15 (上拉输入)
↑ ↑ ↑ ↑
PB8 PB9 PB10 PB11 (推挽输出)
矩阵键盘工作原理:
未按键状态(所有行输出高电平):
ROW0 ─────────┬────┬────┬────┬──── VCC
│ │ │ │
ROW1 ─────────┼────┼────┼────┼──── VCC
│ │ │ │
ROW2 ─────────┼────┼────┼────┼──── VCC
│ │ │ │
ROW3 ─────────┼────┼────┼────┼──── VCC
│ │ │ │
COL0 COL1 COL2 COL3
↑ ↑ ↑ ↑
上拉 上拉 上拉 上拉 (全部为高电平)
按键6按下状态(ROW1输出低电平,扫描到COL1为低):
ROW0 ─────────┬────┬────┬────┬──── VCC
│ │ │ │
ROW1 ─────────┼────X────┼────┼──── GND (扫描行)
│ │ │ │ ↑ 输出低电平
│ 按键6 │ │
│ 按下 │ │
ROW2 ─────────┼────┼────┼────┼──── VCC
│ │ │ │
ROW3 ─────────┼────┼────┼────┼──── VCC
│ │ │ │
COL0 COL1 COL2 COL3
↑ ↓ ↑ ↑
高 低 高 高
↑ 检测到低电平,按键6按下!
注意事项:
- 行线配置为推挽输出,默认高电平
- 列线配置为上拉输入
- PB3、PB4需要禁用JTAG功能
- 扫描时逐行输出低电平
- 检测列线是否有低电平
3. 项目结构
4C_矩阵键盘(发光二极管指示)/
├── Libraries/ # STM32标准外设库
│ ├── CMSIS/ # Cortex微控制器软件接口标准
│ └── STM32F10x_StdPeriph_Driver/ # STM32F10x标准外设驱动
├── MDK/ # Keil工程文件
│ ├── TEST_CODE.uvproj # Keil项目文件
│ ├── TEST_CODE.uvopt # 项目配置选项
│ ├── Output/ # 编译输出目录
│ └── list/ # 列表文件目录
├── Readme/ # 说明文档目录
└── User/ # 用户代码目录
├── main.c # 主程序入口
├── stm32f10x_conf.h # 外设库配置文件
├── stm32f10x_it.c # 中断服务函数
├── stm32f10x_it.h # 中断服务函数头文件
├── system_stm32f10x.c # 系统初始化文件
└── TF_App/ # 应用层驱动
├── TF_KeyBoard.c # 矩阵键盘驱动实现
├── TF_KeyBoard.h # 矩阵键盘驱动头文件
├── TF_LED.c # LED驱动实现
├── TF_LED.h # LED驱动头文件
├── TF_Delay.c # 延时函数实现
├── TF_Delay.h # 延时函数头文件
├── TF_System_Cfg.c # 系统配置
└── TF_System_Cfg.h # 系统配置头文件
4. 功能说明
系统工作流程
系统上电
↓
LED初始化
(配置PB0-PB7为推挽输出)
↓
矩阵键盘初始化
(配置行线为推挽输出,列线为上拉输入)
↓
主循环开始
↓
矩阵键盘扫描
├─ 扫描第0行(ROW0=低,其他=高)
│ ├─ 检测COL0-COL3
│ └─ 发现按键返回键值
├─ 扫描第1行(ROW1=低,其他=高)
│ ├─ 检测COL0-COL3
│ └─ 发现按键返回键值
├─ 扫描第2行(ROW2=低,其他=高)
│ ├─ 检测COL0-COL3
│ └─ 发现按键返回键值
└─ 扫描第3行(ROW3=低,其他=高)
├─ 检测COL0-COL3
└─ 发现按键返回键值
↓
根据按键值控制LED
├─ 按键1-8:点亮对应LED
├─ 按键9-12:点亮2个LED(组合)
└─ 按键13-16:点亮2个LED(交叉)
↓
继续循环扫描
功能特点
-
高效扫描:
- 8个IO实现16个按键
- IO资源利用率高
- 扫描速度快
-
软件消抖:
- 按下延时10ms
- 释放延时10ms
- 有效过滤抖动
-
按键状态记忆:
- 使用
g_Key_old记录上次按键 - 避免重复触发
- 只在按键改变时响应
- 使用
-
灵活的LED控制:
- 按键1-8:单独控制
- 按键9-12:组合控制
- 按键13-16:交叉控制
-
JTAG重映射:
- 释放PB3、PB4作为GPIO
- 保留SW调试功能
- 最大化IO利用
按键与LED对应关系
单LED控制(按键1-8):
按键1 → LED1
按键2 → LED2
按键3 → LED3
按键4 → LED4
按键5 → LED5
按键6 → LED6
按键7 → LED7
按键8 → LED8
双LED组合(按键9-12):
按键9 → LED1 + LED2
按键10 → LED3 + LED4
按键11 → LED5 + LED6
按键12 → LED7 + LED8
双LED交叉(按键13-16):
按键13 → LED1 + LED3
按键14 → LED2 + LED4
按键15 → LED5 + LED7
按键16 → LED6 + LED8
5. 矩阵键盘原理
矩阵键盘结构
4×4矩阵键盘电路图:
┌─────────────────────────────────────┐
│ │
│ ROW0 ───┬───[S1]──┬───[S2]──┬───[S3]──┬───[S4]
│ │ │ │ │
│ ROW1 ───┼───[S5]──┼───[S6]──┼───[S7]──┼───[S8]
│ │ │ │ │
│ ROW2 ───┼───[S9]──┼───[S10]─┼───[S11]─┼───[S12]
│ │ │ │ │
│ ROW3 ───┼───[S13]─┼───[S14]─┼───[S15]─┼───[S16]
│ │ │ │ │
│ COL0 COL1 COL2 COL3
│ │ │ │ │
│ ┴──10K ┴──10K ┴──10K ┴──10K
│ 上拉 上拉 上拉 上拉
│ VCC VCC VCC VCC
│ │
└─────────────────────────────────────┘
工作原理:
- 行线:输出线,依次输出低电平进行扫描
- 列线:输入线,检测是否有低电平
- 按键:连接行线和列线,按下时导通
矩阵键盘扫描原理
扫描过程示例(检测按键6):
步骤1:扫描第0行(ROW0=0,其他=1)
ROW0=0 ───┬───[S1]──┬───[S2]──┬───[S3]──┬───[S4]
│ │ │ │
ROW1=1 ───┼───[S5]──┼───[S6]──┼───[S7]──┼───[S8]
│ │ │ │
COL0 COL1 COL2 COL3
1 1 1 1
结果:所有列线为高电平,第0行无按键按下
步骤2:扫描第1行(ROW1=0,其他=1)
ROW0=1 ───┬───[S1]──┬───[S2]──┬───[S3]──┬───[S4]
│ │ │ │
ROW1=0 ───┼───[S5]──X───[S6]──┼───[S7]──┼───[S8]
│ │ ↑按下 │ │
COL0 COL1 COL2 COL3
1 0 1 1
↑
检测到低电平!
结果:COL1为低电平,按键6被按下
键值计算:row=1, col=1 → key_value = 1*4 + 1 + 1 = 6
键值计算公式
键值计算公式:
key_value = row × 4 + col + 1
示例:
- 按键1: row=0, col=0 → 0×4 + 0 + 1 = 1
- 按键6: row=1, col=1 → 1×4 + 1 + 1 = 6
- 按键16: row=3, col=3 → 3×4 + 3 + 1 = 16
按键布局:
col: 0 1 2 3
row 0: [1] [2] [3] [4]
row 1: [5] [6] [7] [8]
row 2: [9] [10] [11] [12]
row 3: [13] [14] [15] [16]
矩阵键盘优势
IO资源对比:
┌──────────────┬─────────┬─────────┐
│ 按键数量 │ 独立按键 │ 矩阵键盘 │
├──────────────┼─────────┼─────────┤
│ 4个按键 │ 4个IO │ 4个IO │
│ 8个按键 │ 8个IO │ 6个IO │
│ 16个按键 │ 16个IO │ 8个IO │← 本项目
│ 64个按键 │ 64个IO │ 16个IO │
│ 100个按键 │ 100个IO │ 20个IO │
└──────────────┴─────────┴─────────┘
IO节省率:
16按键独立方式:16个IO
16按键矩阵方式:8个IO
节省率:(16-8)/16 × 100% = 50%
通用公式:
N×M矩阵键盘需要IO数:N + M
可实现按键数:N × M
最优配置:
N = M 时,IO利用率最高
例如:4×4 = 16个按键,8个IO
6. 代码详解
6.1 主程序 (main.c)
完整代码
#include "TF_System_Cfg.h"
#include "TF_KeyBoard.h"
#include "TF_LED.h"
int main(void)
{
LED_Init(); // LED初始化
GPIO_Key_Init(); // 矩阵键盘初始化
while(1)
{
KeyBoard_Scan(); // 查询方式实现键盘扫描
}
}
主函数要点:
-
初始化顺序:
LED_Init(); // 先初始化LED
GPIO_Key_Init(); // 再初始化矩阵键盘 -
主循环:
while(1)
{
KeyBoard_Scan(); // 持续扫描键盘
}
特点:
- 简单轮询方式
- 适合单任务应用
- 响应速度取决于循环周期
6.2 矩阵键盘驱动 (TF_KeyBoard)
TF_KeyBoard.h - 头文件
#ifndef __TF_KeyBoard_H__
#define __TF_KeyBoard_H__
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "TF_Delay.h"
/************************************************************************************************/
// 矩阵键盘行线定义 (PB8-PB11) - 输出
#define ROW0_PIN GPIO_Pin_8
#define ROW1_PIN GPIO_Pin_9
#define ROW2_PIN GPIO_Pin_10
#define ROW3_PIN GPIO_Pin_11
#define ROW_GPIO GPIOB
#define ROW_ALL_PINS (GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11)
// 矩阵键盘列线定义 (PB12-PB15) - 输入
#define COL0_PIN GPIO_Pin_12
#define COL1_PIN GPIO_Pin_13
#define COL2_PIN GPIO_Pin_14
#define COL3_PIN GPIO_Pin_15
#define COL_GPIO GPIOB
#define COL_ALL_PINS (GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15)
// 矩阵键盘GPIO时钟
#define MATRIX_KEY_RCC RCC_APB2Periph_GPIOB
/************************************************************************************************/
// 函数声明
extern void GPIO_Key_Init(void);
extern void KeyBoard_Scan(void);
extern uint8_t KeyBoard_GetKey(void);
#endif
矩阵键盘初始化
void GPIO_Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 打开GPIOB端口时钟
RCC_APB2PeriphClockCmd(MATRIX_KEY_RCC, ENABLE);
// 配置行线为推挽输出
GPIO_InitStructure.GPIO_Pin = ROW_ALL_PINS;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(ROW_GPIO, &GPIO_InitStructure);
// 配置列线为上拉输入
GPIO_InitStructure.GPIO_Pin = COL_ALL_PINS;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(COL_GPIO, &GPIO_InitStructure);
// 初始化所有行线输出高电平
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
}
初始化要点:
-
行线配置:
GPIO_Mode_Out_PP // 推挽输出
特点:
- 可输出高低电平
- 驱动能力强
- 默认输出高电平 -
列线配置:
GPIO_Mode_IPU // 上拉输入
特点:
- 内部上拉电阻
- 默认读取为高电平
- 按键按下时变为低电平 -
初始状态:
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
作用:
- 所有行线输出高电平
- 未扫描状态
- 所有列线读取为高电平
获取按键值函数
uint8_t KeyBoard_GetKey(void)
{
uint8_t row, col;
uint8_t key_value = 0;
uint16_t row_pins[4] = {ROW0_PIN, ROW1_PIN, ROW2_PIN, ROW3_PIN};
uint16_t col_pins[4] = {COL0_PIN, COL1_PIN, COL2_PIN, COL3_PIN};
// 扫描每一行
for(row = 0; row < 4; row++)
{
// 先将所有行设置为高电平
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
// 当前扫描行设置为低电平
GPIO_ResetBits(ROW_GPIO, row_pins[row]);
// 短暂延时,等待电平稳定
Delay_nms(1);
// 检测每一列
for(col = 0; col < 4; col++)
{
// 如果检测到某列为低电平,说明该位置的按键被按下
if(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0)
{
// 消抖延时
Delay_nms(10);
// 再次确认按键按下
if(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0)
{
// 计算键值: 键值 = 行号*4 + 列号 + 1
key_value = row * 4 + col + 1;
// 等待按键释放
while(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0);
// 释放后延时消抖
Delay_nms(10);
// 恢复所有行为高电平
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
return key_value;
}
}
}
}
// 恢复所有行为高电平
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
return 0; // 无按键按下
}
按键扫描要点:
-
行扫描:
for(row = 0; row < 4; row++)
{
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS); // 先全部高
GPIO_ResetBits(ROW_GPIO, row_pins[row]); // 当前行低
// 检测列线...
}
原理:
- 逐行输出低电平
- 其他行保持高电平
- 按键按下时,列线会变低 -
列检测:
for(col = 0; col < 4; col++)
{
if(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0)
{
// 发现按键按下
}
}
原理:
- 检测每一列的电平
- 低电平表示按键按下
- 高电平表示无按键 -
软件消抖:
Delay_nms(10); // 按下消抖
if(GPIO_ReadInputDataBit(...) == 0)
{
// 确认按键
}
Delay_nms(10); // 释放消抖
作用:
- 过滤按键抖动
- 提高检测可靠性 -
键值计算:
key_value = row * 4 + col + 1;
示例:
- row=0, col=0 → 0*4 + 0 + 1 = 1 (按键1)
- row=1, col=1 → 1*4 + 1 + 1 = 6 (按键6)
- row=3, col=3 → 3*4 + 3 + 1 = 16 (按键16) -
阻塞等待释放:
while(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0);
作用:
- 等待按键完全释放
- 防止重复触发
注意:
- 会阻塞程序
- 长按时间较长
- 可改用非阻塞方式
键盘扫描与LED控制
void KeyBoard_Scan(void)
{
g_Key_Value = KeyBoard_GetKey();
if(g_Key_Value != 0 && g_Key_Value != g_Key_old)
{
g_Key_old = g_Key_Value;
// 根据按键值控制LED
LED_Off(0); // 关闭所有LED
// 根据不同按键点亮不同LED
switch(g_Key_Value)
{
case 1: LED_On(1); break;
case 2: LED_On(2); break;
case 3: LED_On(3); break;
case 4: LED_On(4); break;
case 5: LED_On(5); break;
case 6: LED_On(6); break;
case 7: LED_On(7); break;
case 8: LED_On(8); break;
case 9: LED_On(1); LED_On(2); break; // 组合LED
case 10: LED_On(3); LED_On(4); break;
case 11: LED_On(5); LED_On(6); break;
case 12: LED_On(7); LED_On(8); break;
case 13: LED_On(1); LED_On(3); break; // 交叉LED
case 14: LED_On(2); LED_On(4); break;
case 15: LED_On(5); LED_On(7); break;
case 16: LED_On(6); LED_On(8); break;
default: break;
}
}
else if(g_Key_Value == 0)
{
g_Key_old = 0;
}
}
扫描函数要点:
-
按键状态记忆:
if(g_Key_Value != 0 && g_Key_Value != g_Key_old)
{
g_Key_old = g_Key_Value;
// 执行按键功能
}
作用:
- 只在按键改变时响应
- 避免重复触发
- 保存上次按键值 -
LED控制策略:
LED_Off(0); // 先关闭所有LED
switch(g_Key_Value)
{
case 1-8: // 单LED
case 9-12: // 组合LED
case 13-16: // 交叉LED
}
特点:
- 先全部关闭
- 再点亮对应LED
- 避免多次按键累积 -
状态清除:
else if(g_Key_Value == 0)
{
g_Key_old = 0; // 清除记忆
}
作用:
- 按键释放时清除状态
- 允许下次按相同键
6.3 LED驱动 (TF_LED)
LED初始化
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(LED_CLK, ENABLE);
// 使能AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// JTAG-DP禁用 + SW-DP使能(释放PB3、PB4为普通IO)
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
GPIO_InitStructure.GPIO_Pin = LED_PIN; // PB0-PB7
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(LED_PORT, &GPIO_InitStructure);
GPIO_ResetBits(LED_PORT, LED_PIN); // 关闭所有LED
}
JTAG重映射要点:
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
功能:
- 禁用JTAG-DP调试接口
- 保留SW-DP调试接口
- 释放PB3、PB4、PA15作为GPIO
引脚映射:
默认功能:
- PA13: SWDIO (SW调试)
- PA14: SWCLK (SW调试)
- PA15: JTDI (JTAG调试)
- PB3: JTDO (JTAG调试)
- PB4: JNTRST(JTAG调试)
重映射后:
- PA13: SWDIO (SW调试) ← 保留
- PA14: SWCLK (SW调试) ← 保留
- PA15: GPIO ← 可用
- PB3: GPIO ← 可用(LED4)
- PB4: GPIO ← 可用(LED5)
注意:
- 必须先使能AFIO时钟
- 只能使用SW方式调试
- 不影响程序下载
LED控制函数
// 点亮LED
void LED_On(unsigned char Led_PIN)
{
switch(Led_PIN)
{
case 1: GPIO_SetBits(LED1_PORT, LED1_PIN); break;
case 2: GPIO_SetBits(LED2_PORT, LED2_PIN); break;
case 3: GPIO_SetBits(LED3_PORT, LED3_PIN); break;
case 4: GPIO_SetBits(LED4_PORT, LED4_PIN); break;
case 5: GPIO_SetBits(LED5_PORT, LED5_PIN); break;
case 6: GPIO_SetBits(LED6_PORT, LED6_PIN); break;
case 7: GPIO_SetBits(LED7_PORT, LED7_PIN); break;
case 8: GPIO_SetBits(LED8_PORT, LED8_PIN); break;
default: GPIO_SetBits(LED_PORT, LED_PIN); break; // 全部点亮
}
}
// 关闭LED
void LED_Off(unsigned char Led_PIN)
{
switch(Led_PIN)
{
case 1: GPIO_ResetBits(LED1_PORT, LED1_PIN); break;
case 2: GPIO_ResetBits(LED2_PORT, LED2_PIN); break;
case 3: GPIO_ResetBits(LED3_PORT, LED3_PIN); break;
case 4: GPIO_ResetBits(LED4_PORT, LED4_PIN); break;
case 5: GPIO_ResetBits(LED5_PORT, LED5_PIN); break;
case 6: GPIO_ResetBits(LED6_PORT, LED6_PIN); break;
case 7: GPIO_ResetBits(LED7_PORT, LED7_PIN); break;
case 8: GPIO_ResetBits(LED8_PORT, LED8_PIN); break;
default: GPIO_ResetBits(LED_PORT, LED_PIN); break; // 全部关闭
}
}
7. 矩阵键盘与独立按键对比
详细对比表
| 特性 | 矩阵键盘(本项目) | 独立按键 |
|---|---|---|
| IO占用 | 8个(16按键) | 16个(16按键) |
| IO利用率 | 高(50%节省) | 低 |
| 硬件复杂度 | 中(需行列布线) | 低(直接连接) |
| 软件复杂度 | 高(需扫描算法) | 低(直接读取) |
| 扫描速度 | 中(逐行扫描) | 快(并行检测) |
| 响应时间 | 稍慢(扫描周期) | 快(立即响应) |
| 成本 | 低(节省IO) | 高(需要更多IO) |
| 适用场景 | 多按键应用 | 少按键应用 |
| 扩展性 | 好(易扩展到更多按键) | 差(受IO限制) |
IO资源对比
独立按键方式(16按键):
KEY1 KEY2 KEY3 KEY4 KEY5 KEY6 KEY7 KEY8
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
PB0 PB1 PB2 PB3 PB4 PB5 PB6 PB7 (8个IO)
KEY9 KEY10 KEY11 KEY12 KEY13 KEY14 KEY15 KEY16
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
PB8 PB9 PB10 PB11 PB12 PB13 PB14 PB15 (8个IO)
共需要:16个IO
矩阵键盘方式(16按键):
COL0 COL1 COL2 COL3
↓ ↓ ↓ ↓
ROW0 → 1 2 3 4
ROW1 → 5 6 7 8
ROW2 → 9 10 11 12
ROW3 → 13 14 15 16
↓
4行 + 4列 = 8个IO
节省率:(16-8)/16 = 50%
扫描速度对比
独立按键扫描(并行):
时间 →
t0: 读取KEY1-KEY16(同时检测)
↓ 立即得到结果
响应时间:微秒级
矩阵键盘扫描(串行):
时间 →
t0: 扫描ROW0(检测4个按键)
t1: 扫描ROW1(检测4个按键)
t2: 扫描ROW2(检测4个按键)
t3: 扫描ROW3(检测4个按键)
↓ 完成一轮扫描
响应时间:毫秒级(取决于扫描周期)
实际测试:
- 独立按键:<1ms
- 矩阵键盘:~50ms(包含消抖延时)
应用场景选择
选择独立按键:
✓ 按键数量少(<8个)
✓ IO资源充足
✓ 要求快速响应
✓ 简单应用
✓ 成本不敏感
选择矩阵键盘:
✓ 按键数量多(≥8个)
✓ IO资源紧张
✓ 对响应时间要求不高
✓ 复杂应用
✓ 成本敏感
推荐配置:
4-8按键: 独立按键
9-16按键: 矩阵键盘(4×4)
17-64按键:矩阵键盘(8×8)
>64按键: 矩阵键盘+IO扩展
8. 行列扫描算法详解
扫描算法流程图
开始扫描
↓
初始化:row=0
↓
┌─────────────────┐
│ 所有行输出高电平 │
└────────┬────────┘
↓
┌─────────────────┐
│ 当前行输出低电平 │
└────────┬────────┘
↓
┌─────────────────┐
│ 延时1ms等待稳定 │
└────────┬────────┘
↓
初始化:col=0
↓
┌─────────────────┐
│ 读取当前列电平 │
└────────┬────────┘
↓
┌─────────────────┐
│ 是否为低电平? │
└────┬──────┬─────┘
是 │ │ 否
↓ ↓
┌────────┐ col++
│ 消抖10ms│ │
└────┬───┘ │
↓ │
┌────────┐ │
│再次确认│ │
└────┬───┘ │
是 │ │
↓ │
┌────────┐ │
│计算键值│ │
└────┬───┘ │
↓ │
┌────────┐ │
│等待释放│ │
└────┬───┘ │
↓ │
返回键值 │
│ │
│ col<4?
│ ↓ 是
│ ┌───┘
│ │
│ ↓ 否
│ row++
│ │
│ row<4?
│ ↓ 是
│ ┌─┘
│ │
│ ↓ 否
└→返回0(无按键)
扫描时序图
扫描按键6的完整时序:
时间 →
0ms 1ms 2ms 12ms 13ms 23ms
↓ ↓ ↓ ↓ ↓ ↓
ROW0 高 低 高 高 高 高
│ 扫描 │ │ │ │
ROW1 高 高 低 低 低 高
│ │ 扫描 等待 等待 释放
ROW2 高 高 高 高 高 高
│ │ │ │ │ │
ROW3 高 高 高 高 高 高
│ │ │ │ │ │
COL0 高 高 高 高 高 高
COL1 高 高 低 低 低 高
│ │ ↑ ↑ ↑ ↑
│ │ 检测到 消抖确认 释放消抖 完成
│ │ 按键按下
│ 无按键
初始状态
说明:
1. t=0ms:初始状态,所有行高,所有列高
2. t=1ms:扫描ROW0,输出低,COL1保持高(无按键)
3. t=2ms:扫描ROW1,输出低,COL1变低(按键6按下)
4. t=2-12ms:消抖10ms,再次确认
5. t=12-23ms:等待按键释放
6. t=23ms:释放后消抖10ms,完成
伪码实现
# 矩阵键盘扫描伪码
def KeyBoard_Scan():
for row in range(4): # 扫描4行
# 设置行电平
set_all_rows_high()
set_row_low(row)
delay_ms(1) # 等待稳定
for col in range(4): # 检测4列
if read_col(col) == LOW: # 检测到低电平
delay_ms(10) # 消抖
if read_col(col) == LOW: # 再次确认
key_value = row * 4 + col + 1
# 等待释放
while read_col(col) == LOW:
pass
delay_ms(10) # 释放消抖
return key_value
return 0 # 无按键
# 键值计算
def calculate_key_value(row, col):
return row * 4 + col + 1
# 示例
# row=1, col=1 → 1*4 + 1 + 1 = 6 (按键6)
优化算法
// 优化1:快速扫描(减少延时)
uint8_t KeyBoard_FastScan(void)
{
uint8_t row, col;
for(row = 0; row < 4; row++)
{
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
GPIO_ResetBits(ROW_GPIO, row_pins[row]);
// 去掉1ms延时,直接检测
for(col = 0; col < 4; col++)
{
if(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0)
{
Delay_nms(5); // 减少消抖时间到5ms
if(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0)
{
return row * 4 + col + 1;
}
}
}
}
return 0;
}
// 优化2:非阻塞扫描(不等待释放)
uint8_t KeyBoard_NonBlockingScan(void)
{
static uint8_t last_key = 0;
uint8_t current_key = 0;
// 快速扫描获取当前按键
current_key = KeyBoard_FastScan();
// 只在按键改变时返回
if(current_key != last_key && current_key != 0)
{
last_key = current_key;
return current_key;
}
last_key = current_key;
return 0;
}
// 优化3:查表法(提高计算速度)
const uint8_t key_table[4][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
uint8_t get_key_value(uint8_t row, uint8_t col)
{
return key_table[row][col];
}
9. 使用说明
9.1 硬件连接
连接步骤
-
连接矩阵键盘
行线(输出):
ROW0 → PB8
ROW1 → PB9
ROW2 → PB10
ROW3 → PB11
列线(输入):
COL0 → PB12
COL1 → PB13
COL2 → PB14
COL3 → PB15 -
连接LED
LED1-LED8 → PB0-PB7(高电平点亮) -
连接调试器
SWDIO → PA13
SWCLK → PA14
注意:禁用JTAG后,只能使用SW调试
9.2 编译与下载
编译工程
- 打开
MDK/TEST_CODE.uvproj - 选择目标芯片:STM32F103C8
- 编译:Project → Build Target (F7)
- 检查编译结果:0 Error(s), 0 Warning(s)
下载程序
- 配置调试器:Options → Debug → 选择 J-Link/ST-Link
- 选择SW调试方式(不能使用JTAG)
- 下载程序:Flash → Download (F8)
- 复位运行
9.3 运行效果
预期现象:
- 按键1-8 → 对应LED1-LED8单独点亮
- 按键9 → LED1+LED2同时点亮
- 按键10 → LED3+LED4同时点亮
- 按键11 → LED5+LED6同时点亮
- 按键12 → LED7+LED8同时点亮
- 按键13 → LED1+LED3交叉点亮
- 按键14 → LED2+LED4交叉点亮
- 按键15 → LED5+LED7交叉点亮
- 按键16 → LED6+LED8交叉点亮
9.4 测试方法
测试1:基本功能
步骤:
1. 上电复位
2. 依次按下按键1-16
3. 观察对应LED是否正确点亮
预期结果:
- 按键1-8:单LED点亮
- 按键9-12:双LED组合点亮
- 按键13-16:双LED交叉点亮
测试2:按键响应速度
步骤:
1. 快速连续按下不同按键
2. 观察LED切换是否流畅
预期结果:
- LED立即切换
- 无明显延迟
- 不会出现错乱
测试3:消抖测试
步骤:
1. 缓慢按下按键(模拟抖动)
2. 观察LED是否闪烁
预期结果:
- LED稳定点亮
- 不会闪烁
- 只响应一次
10. 常见问题
问题1:所有按键无响应
可能原因:
- ❌ 行列线连接错误
- ❌ GPIO配置错误
- ❌ 上拉电阻缺失
解决方案:
// 检查1:行线是否配置为输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 必须是推挽输出
// 检查2:列线是否配置为上拉输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 必须是上拉输入
// 检查3:初始状态是否正确
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS); // 所有行初始为高
// 调试:测试行列线电平
void Test_Matrix_Key(void)
{
// 测试行线输出
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
// 用万用表测量PB8-PB11,应为3.3V
GPIO_ResetBits(ROW_GPIO, ROW_ALL_PINS);
// 用万用表测量PB8-PB11,应为0V
// 测试列线输入
// 按下按键,用万用表测量对应列线,应从3.3V变为0V
}
问题2:部分按键无响应
原因: 特定行或列线故障
解决方案:
// 定位故障行/列
void Test_Row_Col(void)
{
// 测试每一行
for(int row = 0; row < 4; row++)
{
GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS);
GPIO_ResetBits(ROW_GPIO, row_pins[row]);
printf("ROW%d: ", row);
// 测试每一列
for(int col = 0; col < 4; col++)
{
uint8_t state = GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]);
printf("COL%d=%d ", col, state);
}
printf("\r\n");
}
}
// 检查硬件连接
// 如果某行全部无响应:检查该行线连接
// 如果某列全部无响应:检查该列线连接
问题3:按键值错误
原因: 键值计算错误或布局理解错误
解决方案:
// 方案1:添加调试输出
uint8_t KeyBoard_GetKey(void)
{
// ...
if(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0)
{
key_value = row * 4 + col + 1;
printf("Row=%d, Col=%d, Key=%d\r\n", row, col, key_value);
return key_value;
}
// ...
}
// 方案2:使用查表法
const uint8_t key_map[4][4] = {
{1, 2, 3, 4}, // 第0行
{5, 6, 7, 8}, // 第1行
{9, 10, 11, 12}, // 第2行
{13, 14, 15, 16} // 第3行
};
key_value = key_map[row][col];
问题4:按键重复触发
原因: 消抖时间不足或状态记忆失效
解决方案:
// 方案1:增加消抖时间
Delay_nms(20); // 从10ms增加到20ms
// 方案2:使用硬件消抖电路
// 在每个按键上并联0.1uF电容
// 方案3:改进状态记忆逻辑
static uint8_t key_pressed = 0;
if(g_Key_Value != 0)
{
if(!key_pressed)
{
key_pressed = 1;
// 执行按键功能
}
}
else
{
key_pressed = 0;
}
问题5:LED无法点亮
原因: JTAG功能未禁用或LED驱动错误
解决方案:
// 检查JTAG是否禁用
void LED_Init(void)
{
// 必须使能AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 必须禁用JTAG
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
// ...
}
// 测试LED
void Test_LED(void)
{
// 逐个测试LED1-LED8
for(int i = 1; i <= 8; i++)
{
LED_On(i);
Delay_nms(500);
LED_Off(i);
Delay_nms(500);
}
}
// 如果PB3、PB4的LED无法点亮,说明JTAG未正确禁用
11. 扩展应用
11.1 自定义键盘映射
// 十六进制键盘映射
const char hex_key_map[4][4] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
char KeyBoard_GetChar(void)
{
uint8_t key = KeyBoard_GetKey();
if(key > 0 && key <= 16)
{
uint8_t row = (key - 1) / 4;
uint8_t col = (key - 1) % 4;
return hex_key_map[row][col];
}
return 0;
}
// 电话键盘映射
const char phone_key_map[4][4] = {
{'1', '2', '3', 'F1'},
{'4', '5', '6', 'F2'},
{'7', '8', '9', 'F3'},
{'*', '0', '#', 'F4'}
};
11.2 密码输入系统
#define PASSWORD_LENGTH 4
char password[PASSWORD_LENGTH] = {'1', '2', '3', '4'};
char input_buffer[PASSWORD_LENGTH];
uint8_t input_index = 0;
void Password_Input(void)
{
char key_char = KeyBoard_GetChar();
if(key_char != 0 && key_char != '*' && key_char != '#')
{
input_buffer[input_index++] = key_char;
printf("*"); // 显示星号
if(input_index >= PASSWORD_LENGTH)
{
// 验证密码
if(memcmp(input_buffer, password, PASSWORD_LENGTH) == 0)
{
printf("\r\nPassword Correct!\r\n");
LED_On(0); // 所有LED点亮
}
else
{
printf("\r\nPassword Error!\r\n");
// LED闪烁3次
for(int i = 0; i < 3; i++)
{
LED_On(0);
Delay_nms(200);
LED_Off(0);
Delay_nms(200);
}
}
input_index = 0;
}
}
else if(key_char == '*') // 清除
{
input_index = 0;
printf("\r\nCleared\r\n");
}
}
11.3 计算器功能
typedef enum {
OP_NONE,
OP_ADD,
OP_SUB,
OP_MUL,
OP_DIV
} Operator_t;
int32_t operand1 = 0, operand2 = 0;
Operator_t operator = OP_NONE;
uint8_t input_stage = 0; // 0=输入第一个数, 1=输入第二个数
void Calculator_Process(void)
{
char key = KeyBoard_GetChar();
if(key >= '0' && key <= '9')
{
if(input_stage == 0)
{
operand1 = operand1 * 10 + (key - '0');
printf("%c", key);
}
else
{
operand2 = operand2 * 10 + (key - '0');
printf("%c", key);
}
}
else if(key == 'A') // +
{
operator = OP_ADD;
input_stage = 1;
printf("+");
}
else if(key == 'B') // -
{
operator = OP_SUB;
input_stage = 1;
printf("-");
}
else if(key == 'C') // *
{
operator = OP_MUL;
input_stage = 1;
printf("*");
}
else if(key == 'D') // /
{
operator = OP_DIV;
input_stage = 1;
printf("/");
}
else if(key == '#') // =
{
int32_t result = 0;
switch(operator)
{
case OP_ADD: result = operand1 + operand2; break;
case OP_SUB: result = operand1 - operand2; break;
case OP_MUL: result = operand1 * operand2; break;
case OP_DIV: result = (operand2 != 0) ? (operand1 / operand2) : 0; break;
default: break;
}
printf("=%d\r\n", result);
// 复位
operand1 = 0;
operand2 = 0;
operator = OP_NONE;
input_stage = 0;
}
else if(key == '*') // 清除
{
operand1 = 0;
operand2 = 0;
operator = OP_NONE;
input_stage = 0;
printf("\r\nC\r\n");
}
}
11.4 定时器扫描(非阻塞)
// 使用定时器中断扫描矩阵键盘
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 在定时器中断中扫描键盘
g_Key_Value = KeyBoard_GetKey();
}
}
// 主循环中处理按键
int main(void)
{
LED_Init();
GPIO_Key_Init();
TIM2_Init(10); // 10ms定时器
while(1)
{
if(g_Key_Value != 0)
{
// 处理按键
Process_Key(g_Key_Value);
g_Key_Value = 0;
}
// 执行其他任务
Other_Task();
}
}
11.5 按键音效
// 不同按键发出不同音调
void Key_Beep(uint8_t key_value)
{
uint16_t freq_table[16] = {
262, 294, 330, 349, // 按键1-4: C D E F
392, 440, 494, 523, // 按键5-8: G A B C
587, 659, 698, 784, // 按键9-12: D E F G
880, 988, 1047, 1175 // 按键13-16: A B C D
};
if(key_value > 0 && key_value <= 16)
{
Buzzer_Play(freq_table[key_value - 1], 100); // 播放100ms
}
}
void KeyBoard_Scan(void)
{
g_Key_Value = KeyBoard_GetKey();
if(g_Key_Value != 0 && g_Key_Value != g_Key_old)
{
g_Key_old = g_Key_Value;
// 按键音效
Key_Beep(g_Key_Value);
// LED控制
LED_Control(g_Key_Value);
}
}
11.6 按键宏功能
typedef struct {
uint8_t key;
void (*func)(void);
} KeyMacro_t;
void Macro_Function1(void) { /* 功能1 */ }
void Macro_Function2(void) { /* 功能2 */ }
void Macro_Function3(void) { /* 功能3 */ }
KeyMacro_t key_macros[] = {
{1, Macro_Function1},
{2, Macro_Function2},
{3, Macro_Function3},
// ...
};
void Process_Key_Macro(uint8_t key)
{
for(int i = 0; i < sizeof(key_macros)/sizeof(KeyMacro_t); i++)
{
if(key_macros[i].key == key)
{
key_macros[i].func();
break;
}
}
}