基于STM32的4*4矩阵软键盘驱动

本文将分享一套完整的矩阵键盘驱动设计,重点分析如何解决按键"按一次触发多次"的经典问题,并提供可直接使用的代码。


一、引言

本文将给出一个基于STM32 HAL库的矩阵键盘驱动,采用硬件抽象层(HAL) 思想,让键盘核心逻辑与具体引脚完全解耦。经过改进后的代码,能够保证按键按下时只触发一次回调,直到松开后再次按下才会重新触发。


二、硬件连接与工作原理

2.1 矩阵键盘原理

4×4矩阵键盘由4行(ROW)和4列(COL)组成。通过逐行拉低,读取列电平来判断哪个按键被按下。以常见的"行列扫描法"为例:

  • 将所有行输出高电平,列配置为上拉输入。

  • 依次将某一行拉低,读取所有列的电平。

  • 若某列读取到低电平(因为列上拉,按键按下时行拉低会把列也拉低),则说明该行该列的按键被按下。

2.2 硬件连接(示例)

本驱动使用GPIOA的0-3作为行输出,4-7作为列输入。用户可根据实际板子修改keyboard_port.h中的引脚定义,无需改动核心代码。

行/列 引脚
R0 PA0
R1 PA1
R2 PA2
R3 PA3
C0 PA4
C1 PA5
C2 PA6
C3 PA7

三、软件架构设计

为了使驱动易于移植和维护,我们将代码分为三层:

  1. 硬件抽象层(HAL)keyboard_port.hkeyboard_port.c

    定义引脚宏、GPIO初始化、行/列操作宏。移植时只需修改这里的引脚映射。

  2. 核心扫描层keyboard.ckeyboard.h

    实现行列扫描、消抖、状态机、事件回调。与具体硬件无关。

  3. 定时器层tim_key.ctim_key.h

    配置一个10ms定时中断,周期性调用 KEY_TimerTick()。可根据实际需求更换定时器或改用RTOS任务。

这样的分层使得驱动可以轻松迁移到其他STM32系列甚至其他MCU(只需实现相应的GPIO操作宏)。


四、关键代码解析

4.1 硬件抽象层(keyboard_port.h

复制代码
// 行输出引脚配置
#define R0_PORT      GPIOA
#define R0_PIN       GPIO_PIN_0
// ... 类似定义R1~R3

// 列输入引脚配置
#define C0_PORT      GPIOA
#define C0_PIN       GPIO_PIN_4
// ... 类似定义C1~C3

// 硬件操作宏
#define R_SET_LOW(port, pin)    HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET)
#define R_SET_HIGH(port, pin)   HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET)
#define C_READ(port, pin)       HAL_GPIO_ReadPin(port, pin)

所有与GPIO有关的操作都封装为宏,移植时只需修改引脚宏和这些宏内部实现即可。

4.2 核心扫描逻辑(keyboard.c

4.2.1 单次扫描函数 KEY_ScanOnce()
复制代码
static key_value_t KEY_ScanOnce(void) {
    for(uint8_t row=0; row<KEY_ROWS; row++) {
        // 所有行置高
        for(uint8_t i=0; i<KEY_ROWS; i++)
            R_SET_HIGH(R_Ports[i], R_Pins[i]);
        // 当前行拉低
        R_SET_LOW(R_Ports[row], R_Pins[row]);

        // 读取列
        for(uint8_t col=0; col<KEY_COLS; col++) {
            if(C_READ(C_Ports[col], C_Pins[col]) == 0)
                return Key_Map[row][col];
        }
    }
    return KEY_NONE;
}

该函数一次完整扫描,返回第一个检测到的按键值。注意:扫描完后所有行恢复高电平,避免影响下次扫描。

4.2.2 定时扫描与消抖(改进版)

原始的消抖逻辑存在"按住时反复触发"的缺陷,原因在于触发后立即清零 last_key,导致按键持续期间重新开始消抖计数。改进后的代码引入 triggered 标志:

复制代码
static uint8_t triggered = 0;   // 当前按键是否已触发

void KEY_TimerTick(void) {
    key_value_t cur_key = KEY_ScanOnce();

    if(cur_key != KEY_NONE) {
        if(cur_key == last_key) {
            if(++debounce_cnt >= KEY_DEBOUNCE_CNT) {
                debounce_cnt = 0;
                if(!triggered) {
                    triggered = 1;
                    if(g_key_callback != NULL)
                        g_key_callback(cur_key);
                }
                // 注意:不清空 last_key
            }
        } else {
            // 按键改变,重置所有状态
            debounce_cnt = 0;
            last_key = cur_key;
            triggered = 0;
        }
    } else {
        // 无按键,重置所有状态
        debounce_cnt = 0;
        last_key = KEY_NONE;
        triggered = 0;
    }
}

关键改动

  • 增加 triggered 标志,记录当前按键是否已经触发过事件。

  • 消抖完成后,若未触发过才执行回调,并置位 triggered

  • 不再在触发后清空 last_key,避免重新积累计数。

  • 只有当按键变化(cur_key != last_key)或按键释放(cur_key == KEY_NONE)时,才重置 triggeredlast_key

这样,在按键按下的整个过程中,无论按住多久,只会触发一次回调。松开后再按才会再次触发。

4.2.3 回调注册

用户可以通过 KEY_SetCallback() 注册自己的按键处理函数,驱动会在检测到有效按键时调用该函数。

复制代码
void KEY_SetCallback(KeyEventCallback_t cb) {
    g_key_callback = cb;
}

4.3 定时器配置(tim_key.c

以STM32F1为例,配置TIM2产生10ms中断:

复制代码
void TIM_KeyScan_Init(void) {
    __HAL_RCC_TIM2_CLK_ENABLE();
    htim_key.Instance = TIM2;
    htim_key.Init.Prescaler = 7200 - 1;   // 72MHz / 7200 = 10kHz
    htim_key.Init.Period    = 100 - 1;    // 10kHz / 100 = 100Hz → 10ms
    // ...
    HAL_TIM_Base_Init(&htim_key);
    HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
}

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

用户只需调用 TIM_KeyScan_Start() 即可开始扫描。


五、使用方法

  1. 修改引脚定义 :在 keyboard_port.h 中根据实际电路修改行/列的端口和引脚。

  2. 初始化 :在 main() 中调用 KEY_InitGPIO()TIM_KeyScan_Init()

  3. 启动扫描 :调用 TIM_KeyScan_Start()

  4. 注册回调 :在某个地方调用 KEY_SetCallback(YourKeyHandler),其中 YourKeyHandler 原型为 void YourKeyHandler(key_value_t key)

  5. 处理按键:在回调函数中实现具体逻辑,例如将按键值发送到串口、控制LED等。


六、常见问题与解决

Q1:为什么按下按键有时不触发,有时触发多次?

A:可能是消抖次数设置不当或硬件抖动过大。建议将 KEY_DEBOUNCE_CNT 调整为2或3(对应20ms或30ms),并确保定时器周期为10ms左右。如果硬件抖动严重,可适当增加消抖次数。

Q2:如何实现长按重复触发?

A:可以在 KEY_TimerTick() 中增加长按计时器,当按键持续按下超过一定时间后,每隔固定周期触发一次回调。但注意不要与单次触发逻辑混淆。

Q3:能否支持更多按键?

A:可以。只需修改 KEY_ROWSKEY_COLS 宏,并扩展 Key_Map 数组。同时,确保硬件引脚配置正确。


七、总结

本文分享的矩阵键盘驱动采用分层设计,具有良好的可移植性。通过引入"触发标志"改进消抖逻辑,彻底解决了"按一次触发多次"的经典问题。整个代码简洁高效,非常适合作为嵌入式项目的基础组件。读者可以根据自己的硬件平台稍作修改,即可快速实现键盘输入功能。

如果你有任何疑问或改进建议,欢迎在评论区留言交流!

八、参考代码

通过网盘分享的文件:keyBoard_STM32F103C8T6.zip

链接: https://pan.baidu.com/s/13emfCdVl8Ld59cXzRMh3SQ?pwd=wjmm 提取码: wjmm

--来自百度网盘超级会员v8的分享

相关推荐
电子工程师成长日记-C512 小时前
51单片机低频信号发生器
单片机·嵌入式硬件·51单片机
Frostnova丶2 小时前
LeetCode 48 & 1886.矩阵旋转与判断
算法·leetcode·矩阵
逐步前行3 小时前
STM32_DMA_寄存器操作
stm32·单片机·嵌入式硬件
Funing74 小时前
无法打开 源 文件 “esp_err.h“
嵌入式硬件·esp32
Hello World . .4 小时前
51单片机基础外设:中断、定时器/计数器(PWM控制蜂鸣器、电机)
单片机·嵌入式硬件·51单片机
FakeOccupational5 小时前
【电路笔记 STM32】Cortex-M7 内核上的数据缓存结构图 + MPU内存保护单元 + Cache基本操作 + Cache&DMA 时序图
笔记·stm32·缓存
LCG元5 小时前
基于STM32CubeMX的HAL库串口通信与DMA传输深度优化
stm32·单片机·嵌入式硬件
阿Y加油吧6 小时前
力扣打卡——搜索二维矩阵、相交链表
线性代数·leetcode·矩阵
嵌入小生0076 小时前
硬件 --- GPIO/中断/定时器/蜂鸣器
单片机·嵌入式硬件·定时器·pwm·gpio·蜂鸣器·中断