跳到主要内容

【六】矩阵键盘(发光二极管指示)实验

1. 项目概述

本项目演示如何使用 STM32F103C8T6 微控制器驱动 4×4 矩阵键盘,并通过8个LED指示灯显示按键状态。系统采用行列扫描方式检测按键,只需8个GPIO引脚即可实现16个按键的检测,大大节省了IO资源。

技术要点

  • ✅ 4×4矩阵键盘扫描
  • ✅ 行列扫描算法
  • ✅ 软件消抖
  • ✅ 按键值计算
  • ✅ LED指示灯控制
  • ✅ IO资源优化
  • ✅ JTAG引脚重映射

应用场景

  1. 🎹 数字输入面板
  2. 🎹 电子密码锁
  3. 🎹 计算器键盘
  4. 🎹 电话拨号键盘
  5. 🎹 工业控制面板
  6. 🎹 游戏手柄

2. 硬件平台

主控芯片

  • 型号: STM32F103C8T6
  • 内核: ARM Cortex-M3
  • 主频: 72 MHz
  • Flash: 64 KB
  • SRAM: 20 KB
  • GPIO: 37个可用IO

外设资源

4×4矩阵键盘

行线(输出):

行线引脚说明
ROW0PB8第0行(推挽输出)
ROW1PB9第1行(推挽输出)
ROW2PB10第2行(推挽输出)
ROW3PB11第3行(推挽输出)

列线(输入):

列线引脚说明
COL0PB12第0列(上拉输入)
COL1PB13第1列(上拉输入)
COL2PB14第2列(上拉输入)
COL3PB15第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引脚说明
LED1PB0按键1控制
LED2PB1按键2控制
LED3PB2按键3控制
LED4PB3按键4控制(需JTAG禁用)
LED5PB4按键5控制(需JTAG禁用)
LED6PB5按键6控制
LED7PB6按键7控制
LED8PB7按键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按下!

注意事项:

  1. 行线配置为推挽输出,默认高电平
  2. 列线配置为上拉输入
  3. PB3、PB4需要禁用JTAG功能
  4. 扫描时逐行输出低电平
  5. 检测列线是否有低电平

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(交叉)

继续循环扫描

功能特点

  1. 高效扫描:

    • 8个IO实现16个按键
    • IO资源利用率高
    • 扫描速度快
  2. 软件消抖:

    • 按下延时10ms
    • 释放延时10ms
    • 有效过滤抖动
  3. 按键状态记忆:

    • 使用g_Key_old记录上次按键
    • 避免重复触发
    • 只在按键改变时响应
  4. 灵活的LED控制:

    • 按键1-8:单独控制
    • 按键9-12:组合控制
    • 按键13-16:交叉控制
  5. 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(); // 查询方式实现键盘扫描
}
}

主函数要点:

  1. 初始化顺序:

    LED_Init();         // 先初始化LED
    GPIO_Key_Init(); // 再初始化矩阵键盘
  2. 主循环:

    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);
}

初始化要点:

  1. 行线配置:

    GPIO_Mode_Out_PP  // 推挽输出

    特点:
    - 可输出高低电平
    - 驱动能力强
    - 默认输出高电平
  2. 列线配置:

    GPIO_Mode_IPU  // 上拉输入

    特点:
    - 内部上拉电阻
    - 默认读取为高电平
    - 按键按下时变为低电平
  3. 初始状态:

    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; // 无按键按下
}

按键扫描要点:

  1. 行扫描:

    for(row = 0; row < 4; row++)
    {
    GPIO_SetBits(ROW_GPIO, ROW_ALL_PINS); // 先全部高
    GPIO_ResetBits(ROW_GPIO, row_pins[row]); // 当前行低

    // 检测列线...
    }

    原理:
    - 逐行输出低电平
    - 其他行保持高电平
    - 按键按下时,列线会变低
  2. 列检测:

    for(col = 0; col < 4; col++)
    {
    if(GPIO_ReadInputDataBit(COL_GPIO, col_pins[col]) == 0)
    {
    // 发现按键按下
    }
    }

    原理:
    - 检测每一列的电平
    - 低电平表示按键按下
    - 高电平表示无按键
  3. 软件消抖:

    Delay_nms(10);  // 按下消抖
    if(GPIO_ReadInputDataBit(...) == 0)
    {
    // 确认按键
    }

    Delay_nms(10); // 释放消抖

    作用:
    - 过滤按键抖动
    - 提高检测可靠性
  4. 键值计算:

    key_value = row * 4 + col + 1;

    示例:
    - row=0, col=00*4 + 0 + 1 = 1 (按键1)
    - row=1, col=11*4 + 1 + 1 = 6 (按键6)
    - row=3, col=33*4 + 3 + 1 = 16 (按键16)
  5. 阻塞等待释放:

    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;
}
}

扫描函数要点:

  1. 按键状态记忆:

    if(g_Key_Value != 0 && g_Key_Value != g_Key_old)
    {
    g_Key_old = g_Key_Value;
    // 执行按键功能
    }

    作用:
    - 只在按键改变时响应
    - 避免重复触发
    - 保存上次按键值
  2. LED控制策略:

    LED_Off(0);  // 先关闭所有LED

    switch(g_Key_Value)
    {
    case 1-8: // 单LED
    case 9-12: // 组合LED
    case 13-16: // 交叉LED
    }

    特点:
    - 先全部关闭
    - 再点亮对应LED
    - 避免多次按键累积
  3. 状态清除:

    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 硬件连接

连接步骤

  1. 连接矩阵键盘

    行线(输出):
    ROW0 → PB8
    ROW1 → PB9
    ROW2 → PB10
    ROW3 → PB11

    列线(输入):
    COL0 → PB12
    COL1 → PB13
    COL2 → PB14
    COL3 → PB15
  2. 连接LED

    LED1-LED8 → PB0-PB7(高电平点亮)
  3. 连接调试器

    SWDIO → PA13
    SWCLK → PA14
    注意:禁用JTAG后,只能使用SW调试

9.2 编译与下载

编译工程

  1. 打开 MDK/TEST_CODE.uvproj
  2. 选择目标芯片:STM32F103C8
  3. 编译:Project → Build Target (F7)
  4. 检查编译结果:0 Error(s), 0 Warning(s)

下载程序

  1. 配置调试器:Options → Debug → 选择 J-Link/ST-Link
  2. 选择SW调试方式(不能使用JTAG)
  3. 下载程序:Flash → Download (F8)
  4. 复位运行

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:所有按键无响应

可能原因:

  1. ❌ 行列线连接错误
  2. ❌ GPIO配置错误
  3. ❌ 上拉电阻缺失

解决方案:

// 检查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;
}
}
}