[技术问答] 如何用单片机高效处理矩阵按键?

[复制链接]
 楼主| qiufengsd 发表于 2025-4-5 22:32 | 显示全部楼层 |阅读模式
假设有一个4×4的矩阵按键,它由4行(Row)和4列(Column)组成,共16个按键。
通常,行连接到单片机的GPIO输出端,列连接到GPIO输入端,且列端口通常需要上拉电阻来保持默认高电平。
硬件连接示例:



1、矩阵按键的基本扫描方法

依次拉低每一行的电平,并读取列信号,判断是否有按键按下。
实现步骤:
  • 设定所有行(Row)为高电平,所有列(Column)为输入模式,并上拉。
  • 依次将每一行拉低(低电平),然后读取所有列的状态。
  • 如果某列检测到低电平,说明该行与该列的交点处按键被按下。
  • 记录按键位置,并等待去抖动处理。
  • 继续扫描下一行,直到所有行扫描完毕。
示例代码(基于C语言):



  1. #define ROWS 4
  2. #define COLS 4
  3. constuint8_t row_pins[ROWS] = {ROW1, ROW2, ROW3, ROW4};
  4. constuint8_t col_pins[COLS] = {COL1, COL2, COL3, COL4};
  5. void scan_matrix_keypad() {
  6.     for (int i = 0; i < ROWS; i++) {
  7.         // 设定当前行为低电平
  8.         gpio_write(row_pins[i], LOW);
  9.         delay_us(5);  // 确保稳定
  10.         // 读取列状态
  11.         for (int j = 0; j < COLS; j++) {
  12.             if (gpio_read(col_pins[j]) == LOW) {
  13.                 printf("按键[%d,%d]被按下\n", i, j);
  14.             }
  15.         }
  16.         // 恢复当前行为高电平
  17.         gpio_write(row_pins[i], HIGH);
  18.     }
  19. }



2、低功耗优化

如果单片机支持外部中断,可以利用外部中断检测按键按下,降低CPU负载。
方法如下:
  • 初始状态:所有行设为高电平,所有列配置为带上拉输入,并开启中断。
  • 进入低功耗模式,等待外部中断。
  • 当按键按下时,列引脚的电平变化触发中断。
  • 进入中断后,采用行列扫描法识别具体按键。
  • 处理按键逻辑后,恢复低功耗状态。
示例代码(基于C语言):



  1. void EXTI_Handler() {
  2.     for (int j = 0; j < COLS; j++) {
  3.         if (gpio_read(col_pins[j]) == LOW) {
  4.             scan_matrix_keypad(); // 仅在有按键按下时扫描
  5.             break;
  6.         }
  7.     }
  8. }
  9. void setup() {
  10.     for (int i = 0; i < ROWS; i++) {
  11.         gpio_mode(row_pins[i], OUTPUT);
  12.         gpio_write(row_pins[i], HIGH);
  13.     }
  14.     for (int j = 0; j < COLS; j++) {
  15.         gpio_mode(col_pins[j], INPUT_PULLUP);
  16.         attach_interrupt(col_pins[j], EXTI_Handler, FALLING);
  17.     }
  18. }



3、按键去抖动策略

按键在机械接触时会出现抖动,可能会误触发多次按键事件,因此需要去抖动处理。
3.1、软去抖动通过软件延迟来过滤抖动信号,例如检测到按键按下后,延迟20ms再次检测是否仍然按下。



  1. bool is_key_pressed(uint8_t row, uint8_t col) {
  2.     if (gpio_read(col_pins[col]) == LOW) {
  3.         delay_ms(20); // 20ms去抖
  4.         if (gpio_read(col_pins[col]) == LOW) {
  5.             return true;
  6.         }
  7.     }
  8.     return false;
  9. }



3.2、硬件去抖动可在矩阵按键电路中增加一个小电容(如0.1uF)或者使用施密特触发器来稳定按键信号。



在资源受限的嵌入式系统中,如果单片机 没有足够的外部中断资源,可以使用 定时器 进行周期性扫描矩阵按键,以减少CPU占用。
同时,为了避免主循环(while(1))中阻塞等待按键事件,使用FIFO(First In, First Out)队列 存储按键事件,以提高系统响应速度。
4、进一步优化

基本原理:
  • 定时器周期性触发扫描,间隔通常设为 10~20ms,以确保能及时捕获按键事件,同时避免过于频繁地占用CPU资源。
  • 在定时器中断函数内,执行一次完整的行列扫描,如果检测到按键按下,则将其加入FIFO队列。
以下是基于 STM32 的 定时器中断方式 进行按键扫描的示例代码:



  1. #define ROWS 4
  2. #define COLS 4
  3. constuint8_t row_pins[ROWS] = {ROW1, ROW2, ROW3, ROW4};
  4. constuint8_t col_pins[COLS] = {COL1, COL2, COL3, COL4};
  5. // FIFO 队列结构体
  6. #define KEY_FIFO_SIZE 10
  7. typedefstruct {
  8.     uint8_t keys[KEY_FIFO_SIZE];  // 按键事件队列
  9.     uint8_t head;  // 队列头
  10.     uint8_t tail;  // 队列尾
  11. } KeyFIFO;
  12. KeyFIFO key_fifo = {{0}, 0, 0};
  13. // 按键事件入队
  14. void key_fifo_enqueue(uint8_t key) {
  15.     uint8_t next = (key_fifo.tail + 1) % KEY_FIFO_SIZE;
  16.     if (next != key_fifo.head) {  // 队列未满
  17.         key_fifo.keys[key_fifo.tail] = key;
  18.         key_fifo.tail = next;
  19.     }
  20. }
  21. // 读取FIFO队列中的按键
  22. uint8_t key_fifo_dequeue() {
  23.     if (key_fifo.head == key_fifo.tail) {
  24.         return0; // 队列为空
  25.     }
  26.     uint8_t key = key_fifo.keys[key_fifo.head];
  27.     key_fifo.head = (key_fifo.head + 1) % KEY_FIFO_SIZE;
  28.     return key;
  29. }
  30. // 定时器中断回调函数,每10ms扫描按键
  31. void TIM2_IRQHandler() {
  32.     for (int i = 0; i < ROWS; i++) {
  33.         gpio_write(row_pins[i], LOW);
  34.         delay_us(5); // 确保稳定
  35.         for (int j = 0; j < COLS; j++) {
  36.             if (gpio_read(col_pins[j]) == LOW) {
  37.                 uint8_t key_id = (i * COLS) + j + 1;
  38.                 key_fifo_enqueue(key_id);
  39.             }
  40.         }
  41.         gpio_write(row_pins[i], HIGH);
  42.     }
  43.     TIM_ClearITPendingBit(TIM2, TIM_IT_Update);  // 清除定时器中断标志
  44. }
  45. // 定时器初始化
  46. void timer2_init() {
  47.     TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  48.     NVIC_InitTypeDef NVIC_InitStructure;
  49.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
  50.     TIM_TimeBaseStructure.TIM_Period = 10000 - 1;  // 10ms定时
  51.     TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;  // 1MHz时钟
  52.     TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
  53.     TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
  54.     TIM_Cmd(TIM2, ENABLE);
  55.     NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
  56.     NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  57.     NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  58.     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  59.     NVIC_Init(&NVIC_InitStructure);
  60. }



这样,我们就能在 低资源占用 和 高响应速度 之间取得 良好平衡,构建更高效的 单片机矩阵按键控制系统
jcky001 发表于 2025-4-8 15:13 | 显示全部楼层
行扫描法常用方法。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

34

主题

3400

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部