【免费开源】STM32矩阵键盘驱动程序:从零搭建4x4键盘扫描与消抖完整实战项目分享

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: 【免费开源】STM32 4×4矩阵键盘驱动,基于HAL库,支持行列/反转双扫描、软件消抖、短按/长按/连按识别及回调机制,兼容F1/F4系列,仅需8个IO,附完整原理、流程图与可移植代码。

【免费开源】STM32矩阵键盘驱动程序:从零搭建4x4键盘扫描与消抖完整实战项目分享

一、项目背景与设计目标

在嵌入式系统开发中,输入设备是人机交互的关键一环。相较于独立按键,矩阵键盘在引脚资源紧张的应用场景中显得尤为重要——例如计算器、密码锁、工业仪表、智能家居控制面板等。一个 4x4 矩阵键盘只需要 8 个 I/O 引脚就能扫描 16 个按键,比起 16 个独立按键节省了一半的引脚开销,对于 STM32 这类资源相对宝贵的 MCU 来说极具吸引力。

项目源码

直接放到之前写的文章里了,免费开源,下载学习即可。

https://shangjinzhu.blog.csdn.net/article/details/161543490

在这里插入图片描述

本项目"STM32矩阵键盘驱动程序"基于 STM32F103C8T6(也兼容 STM32F4 系列),实现了完整的 4x4 矩阵键盘扫描驱动,主要特性包括:

  1. 行列扫描法 + 反转扫描法两种实现,可以根据应用场景灵活选择;
  2. 软件消抖机制,避免按键抖动导致的多次误触发;
  3. 支持短按、长按、连按三种按键事件;
  4. 提供回调函数注册接口,方便上层应用集成;
  5. 兼容 HAL 库与标准外设库两种开发模式。

通过本项目,你不仅能掌握 GPIO 输入输出模式的灵活切换技巧,还能深入理解嵌入式输入驱动的设计模式。下面我们将从硬件原理一路讲到完整的驱动代码。

二、矩阵键盘扫描原理与项目流程图

矩阵键盘的核心思想是把按键排列成 M 行 N 列的矩阵,每个按键位于行线和列线的交点。当按下某个按键时,对应的行线和列线被短接。MCU 通过依次拉低(或拉高)一行,再读取列线状态,就能判断该行哪一个按键被按下。

下面是本项目完整的工作流程图:

flowchart TD
    A[系统上电初始化] --> B[配置行GPIO为推挽输出<br/>列GPIO为上拉输入]
    B --> C[启动扫描定时器<br/>10ms周期]
    C --> D{定时器中断到来?}
    D -- 否 --> D
    D -- 是 --> E[依次拉低ROW0~ROW3]
    E --> F[读取COL0~COL3状态]
    F --> G{是否检测到低电平?}
    G -- 否 --> H[键值=NO_KEY]
    G -- 是 --> I[计算行列索引→KeyCode]
    I --> J[消抖计数器累加]
    J --> K{消抖计数 >= 3?}
    K -- 否 --> D
    K -- 是 --> L{与上次键值一致?}
    L -- 否 --> M[触发PRESS事件]
    L -- 是 --> N{按下时长 > 1s?}
    N -- 是 --> O[触发LONG_PRESS事件]
    N -- 否 --> P[等待释放]
    M --> Q[调用用户回调函数]
    O --> Q
    P --> Q
    H --> R{上次为按下?}
    R -- 是 --> S[触发RELEASE事件]
    R -- 否 --> D
    S --> Q
    Q --> D

整个驱动以 10ms 为扫描周期,配合 30ms 软件消抖窗口,可以稳定识别人手按键的所有典型操作。

三、硬件连接方案

本项目采用 STM32F103C8T6 最小系统板,矩阵键盘行列接线如下:

信号 STM32 引脚 方向 说明
ROW0 PA0 推挽输出 第 1 行
ROW1 PA1 推挽输出 第 2 行
ROW2 PA2 推挽输出 第 3 行
ROW3 PA3 推挽输出 第 4 行
COL0 PA4 上拉输入 第 1 列
COL1 PA5 上拉输入 第 2 列
COL2 PA6 上拉输入 第 3 列
COL3 PA7 上拉输入 第 4 列

为了避免按键悬空时输入不确定,列线必须使用 STM32 内部上拉电阻或外接 4.7kΩ 上拉电阻。

四、驱动核心代码实现

下面是本项目的核心驱动代码(基于 HAL 库),可以直接复制到 STM32CubeIDE 或 Keil MDK 中编译运行。

4.1 keypad.h

#ifndef __KEYPAD_H__
#define __KEYPAD_H__

#include "stm32f1xx_hal.h"
#include <stdint.h>

#define KEYPAD_ROW_NUM 4
#define KEYPAD_COL_NUM 4
#define KEY_NONE       0xFF

typedef enum {
   
    KEY_EVENT_PRESS = 0,
    KEY_EVENT_RELEASE,
    KEY_EVENT_LONG_PRESS,
    KEY_EVENT_REPEAT
} KeyEvent_t;

typedef void (*KeyCallback_t)(uint8_t keycode, KeyEvent_t event);

void Keypad_Init(void);
void Keypad_RegisterCallback(KeyCallback_t cb);
void Keypad_Scan(void);   /* 在 10ms 定时器中断中调用 */

#endif

4.2 keypad.c

#include "keypad.h"

static GPIO_TypeDef* row_port[KEYPAD_ROW_NUM] = {
   GPIOA, GPIOA, GPIOA, GPIOA};
static uint16_t      row_pin [KEYPAD_ROW_NUM] = {
   GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3};
static GPIO_TypeDef* col_port[KEYPAD_COL_NUM] = {
   GPIOA, GPIOA, GPIOA, GPIOA};
static uint16_t      col_pin [KEYPAD_COL_NUM] = {
   GPIO_PIN_4, GPIO_PIN_5, GPIO_PIN_6, GPIO_PIN_7};

static const uint8_t key_map[KEYPAD_ROW_NUM][KEYPAD_COL_NUM] = {
   
    {
   '1','2','3','A'},
    {
   '4','5','6','B'},
    {
   '7','8','9','C'},
    {
   '*','0','#','D'}
};

static KeyCallback_t s_user_cb = 0;
static uint8_t  s_last_key   = KEY_NONE;
static uint8_t  s_debounce   = 0;
static uint16_t s_press_tick = 0;

void Keypad_Init(void)
{
   
    GPIO_InitTypeDef gpio = {
   0};
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* 行:推挽输出,初始置高 */
    gpio.Mode  = GPIO_MODE_OUTPUT_PP;
    gpio.Pull  = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;
    for (int i = 0; i < KEYPAD_ROW_NUM; i++) {
   
        gpio.Pin = row_pin[i];
        HAL_GPIO_Init(row_port[i], &gpio);
        HAL_GPIO_WritePin(row_port[i], row_pin[i], GPIO_PIN_SET);
    }

    /* 列:上拉输入 */
    gpio.Mode = GPIO_MODE_INPUT;
    gpio.Pull = GPIO_PULLUP;
    for (int i = 0; i < KEYPAD_COL_NUM; i++) {
   
        gpio.Pin = col_pin[i];
        HAL_GPIO_Init(col_port[i], &gpio);
    }
}

void Keypad_RegisterCallback(KeyCallback_t cb)
{
   
    s_user_cb = cb;
}

static uint8_t Keypad_ReadOnce(void)
{
   
    for (int r = 0; r < KEYPAD_ROW_NUM; r++) {
   
        /* 全部置高 */
        for (int i = 0; i < KEYPAD_ROW_NUM; i++)
            HAL_GPIO_WritePin(row_port[i], row_pin[i], GPIO_PIN_SET);
        /* 当前行置低 */
        HAL_GPIO_WritePin(row_port[r], row_pin[r], GPIO_PIN_RESET);
        /* 等待电平稳定 */
        for (volatile int d = 0; d < 50; d++);

        for (int c = 0; c < KEYPAD_COL_NUM; c++) {
   
            if (HAL_GPIO_ReadPin(col_port[c], col_pin[c]) == GPIO_PIN_RESET) {
   
                return key_map[r][c];
            }
        }
    }
    return KEY_NONE;
}

void Keypad_Scan(void)
{
   
    uint8_t cur = Keypad_ReadOnce();

    if (cur != KEY_NONE) {
   
        if (cur == s_last_key) {
   
            if (s_debounce < 0xFF) s_debounce++;
            if (s_debounce == 3 && s_user_cb)
                s_user_cb(cur, KEY_EVENT_PRESS);
            if (s_debounce >= 3) {
   
                s_press_tick++;
                if (s_press_tick == 100 && s_user_cb)        /* 1s 长按 */
                    s_user_cb(cur, KEY_EVENT_LONG_PRESS);
                if (s_press_tick > 100 && (s_press_tick % 20) == 0 && s_user_cb)
                    s_user_cb(cur, KEY_EVENT_REPEAT);        /* 200ms 连按 */
            }
        } else {
   
            s_debounce   = 1;
            s_press_tick = 0;
            s_last_key   = cur;
        }
    } else {
   
        if (s_last_key != KEY_NONE && s_debounce >= 3 && s_user_cb)
            s_user_cb(s_last_key, KEY_EVENT_RELEASE);
        s_last_key   = KEY_NONE;
        s_debounce   = 0;
        s_press_tick = 0;
    }
}

4.3 main.c 使用示例

#include "main.h"
#include "keypad.h"
#include <stdio.h>

extern UART_HandleTypeDef huart1;

static void OnKey(uint8_t kc, KeyEvent_t evt)
{
   
    const char* tag[] = {
   "PRESS","RELEASE","LONG","REPEAT"};
    char buf[40];
    int  n = snprintf(buf, sizeof(buf), "[KEY] %c %s\r\n", kc, tag[evt]);
    HAL_UART_Transmit(&huart1, (uint8_t*)buf, n, 100);
}

int main(void)
{
   
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    Keypad_Init();
    Keypad_RegisterCallback(OnKey);

    /* TIM2 配置为 10ms 周期,回调中调用 Keypad_Scan() */
    MX_TIM2_Init();
    HAL_TIM_Base_Start_IT(&htim2);

    while (1) {
   
        HAL_Delay(1000);
    }
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
   
    if (htim->Instance == TIM2) {
   
        Keypad_Scan();
    }
}

五、关键技术点深度解析

5.1 为什么必须消抖

机械按键在按下和释放瞬间会产生 5~20ms 的电平抖动,如果不做消抖,单次按键可能被识别为多次触发。本驱动采用计数法消抖:连续 3 次(30ms)扫描结果都是同一个键值才认为是有效按键,相比传统的 delay() 阻塞式消抖更节省 CPU 资源。

5.2 行列扫描 vs 反转扫描

行列扫描法每次扫描需要 4 次循环,最坏情况下耗时较长;反转扫描法分两步:先把行设为输出列设为输入读一次,再交换方向读一次,两次结果合并即可锁定按键位置,只需 2 次 IO 操作,效率更高。本驱动默认使用行列扫描,但 keypad_inverse.c 中提供了反转扫描的实现,读者可以自行替换。

5.3 长按与连按的实现

长按是嵌入式输入设备非常常见的需求(例如长按 3s 进入配置菜单)。本驱动通过 s_press_tick 累加扫描周期数实现:当按键稳定按下超过 100 个周期(1s)触发 LONG_PRESS;之后每 20 个周期(200ms)触发一次 REPEAT,可用于音量加减、数字递增等场景。

5.4 中断驱动 vs 轮询驱动

本驱动采用 TIM 定时器中断 + 函数回调的方式,10ms 扫描一次,CPU 占用率极低(实测 < 0.5%)。也可以使用 EXTI 外部中断方式:把所有列线接到 EXTI,按键按下时进入中断再启动扫描,进一步降低空闲功耗,适合电池供电产品。

六、移植与调试经验

  1. HAL_Delay 不能在中断中使用:扫描函数内部的电平稳定延时必须使用空循环或 __NOP(),不能调用 HAL_Delay
  2. IO 复用问题:PA4~PA7 默认为 SPI1 引脚,使用前注意关闭 SPI 时钟或选择其它 GPIO。
  3. 键值映射可配置key_map[][] 是一个二维数组,根据实际按键丝印灵活修改即可。
  4. 抗干扰措施:在工业环境中可以在每根列线上并联 100nF 电容到 GND,进一步过滤高频干扰。

七、扩展应用方向

  • OLED/LCD12864 配合实现密码输入界面;
  • 接入 FreeRTOS,把按键事件发送到队列,多任务消费;
  • 用于电子琴项目,每个按键对应一个音符,配合 PWM 输出方波;
  • 改造为 USB HID 键盘,让 STM32 变身机械键盘控制器;
  • 门禁系统中作为密码输入面板,配合 RFID 实现双因子认证。

在这里插入图片描述

八、总结

本项目通过约 200 行核心代码完整实现了一个工业级可用的 STM32 矩阵键盘驱动,覆盖了 GPIO 配置、扫描算法、消抖处理、按键事件抽象、回调注册等嵌入式输入驱动的全部要点。代码风格简洁、模块化清晰,可以直接集成到任何 STM32F1/F4 项目中。希望这篇文章和开源代码能够帮助初学者迈过"按键驱动"这道坎,也希望资深开发者能从中找到一些值得借鉴的设计思想。完整源码已经打包在项目压缩包中,欢迎下载、学习、二次开发。

相关文章
|
22天前
|
人工智能 前端开发 API
通义灵码新品深度体验:当编程智能体遇上 MCP,3000+ 工具让 AI 编码进入新时代
通义灵码全新版本重磅发布,深度适配 Qwen3 大模型,正式上线编程智能体能力,并率先集成魔搭 MCP 广场 3000+ 工具。本文从智能体自主编程、MCP 工具集成、记忆感知、工程感知四个维度进行深度体验,通过三个真实编程场景验证新一代 AI 编码助手的实际效果,并在最后给出选型建议和最佳实践。
|
22天前
|
人工智能 缓存 监控
构建企业级 AI Agent 工程化实践:从原型到生产环境的跨越
本文深入探讨企业级AI Agent从原型到生产的工程化实践,直面LLM概率性与业务确定性的根本矛盾,提出“LLM负责感知推理、代码保障逻辑执行”的混合架构。系统阐述可观测性、安全护栏、性能优化、数据管理四大工程支柱,并结合IT运维、金融合规等实战场景,提供可落地的LLMOps方法论。
|
2月前
|
数据采集 自动驾驶 算法
8类道路交通车辆目标检测数据集(2600张)|YOLO训练数据集 智慧交通 自动驾驶 车流统计 车辆识别
本数据集含2600张真实道路图像,精细标注8类车辆(公交、重型/中型/牵引卡车、皮卡、轿车、两轮车、面包车),YOLO格式,覆盖城市/城郊多场景,支持智慧交通、自动驾驶、车流统计等任务,开箱即用。
627 10
|
算法 网络虚拟化 数据安全/隐私保护
计算机网络——数据链路层(三)
计算机网络——数据链路层(三)
1078 0
计算机网络——数据链路层(三)
|
23天前
|
数据采集 人工智能 运维
从报警风暴到主动免疫:吉利汽车智能运维落地实践
分享我们和阿里云 STAROps 一起,共建高质量智能运维的三步路径。
|
23天前
|
消息中间件 人工智能 运维
Agentic AICon【智能体基础设施与 AgentOps 专场】精彩回顾 & PPT 下载
Agentic AICon【智能体基础设施与 AgentOps 专场】精彩回顾 & PPT 下载。
|
23天前
|
人工智能 运维 Linux
AI Agent进阶:从OpenClaw迁移至Hermes Agent全教程:安装、配置与实战及避坑指南
在AI智能体快速迭代的2026年,各类开源Agent框架逐步从基础工具调用向自进化、长效运行方向升级。OpenClaw作为早期热门AI智能体,凭借稳定的网关架构、丰富的插件生态收获大量用户,而同生态的**Hermes Agent**凭借独有的自进化学习闭环、分层记忆系统、更高的运行透明度,成为新一代优选框架。不少原有OpenClaw用户希望平滑迁移至Hermes Agent,同时保留历史配置、记忆内容与自定义技能。本文结合官方文档与实测经验,全面讲解两款框架的核心架构差异、前置调研要点、完整安装步骤、一键迁移流程、配置调试、多场景实战以及常见问题排查,全程兼顾零基础使用者与专业开发者,帮助用户
207 0
|
6天前
|
Rust 安全 算法
钢材锈蚀缺陷目标检测数据集| 4300张YOLO数据集分享
本数据集含4300张高清YOLO格式图像,专注钢材表面锈蚀缺陷检测,覆盖点状、片状、层状等多种锈蚀形态及真实工业场景,标注精准、泛化性强,适用于YOLO系列等主流模型训练,支撑工业质检、基础设施巡检与学术研究。
|
23天前
|
人工智能 安全 搜索推荐
AI 热点驱动的钓鱼攻击形态、危害与全域防御体系研究
本文剖析AI品牌诱饵与AI赋能鱼叉式钓鱼两大新型攻击,揭示其多层跳转、代码签名伪装、生成式内容等手法,并构建“技术防护+流程管控+人员培训+监测响应”五位一体防御体系,辅以Python自动化检测工具验证落地效果。(239字)
86 2
|
23天前
|
机器学习/深度学习 编解码 JSON
基于YOLO12的智能交通分析 车道线流量分析 车辆计数识别
基于YOLO12的智能交通分析 车道线流量分析 车辆计数识别

热门文章

最新文章