矩阵键盘与电子幽灵

矩阵键盘与电子幽灵

SingleDog Other Waste

最近在调试矩阵键盘,遇到了很多不可言说的Bug,在这里记录一下。顺便介绍下矩阵键盘吧。

矩阵键盘

部分图文源于:STM32 4*4矩阵键盘实现原理(附程序)_矩阵键盘原理图-CSDN博客

矩阵键盘由 4x4 个按键组成,外部引出 8 个引脚。通过对这 8 个引脚 io 状态的读写,可以定位到被按下的按键。下面介绍其原理及实现。

原理

一般 4x4 矩阵键盘的原理图如下:

我们先看行 1PD3。将 PD3 拉低到地,PD4-7 置上拉输入。假设 KEY1 被按下,那么 PD4 被拉低,即可确认 KEY1 被按下。KEY2-4 同理。

以上只针对某一行的情况。在使用整个键盘时,我们默认将 PD0-PD7 全部拉高,然后每次检测时将 PD0-PD3 中的某一个单独拉低。如检测第二行 PD2 时,将 PD3,1,0 拉高,PD2 拉低,随后如上读取 PD4-7 的电平情况,即可定位。这一方法被称为逐行扫描。

非阻塞式检测

众所周知,按键在按下时会有一段时间的抖动,需要进行消抖。在没有外部电路消抖时,我们常采用软件消抖的方式。通常,我们在检测到引脚电平变化时,会先 delay 一段时间,再读取引脚电平,以此来跳过抖动。这种方法被称为阻塞式检测。它其实造成了一定的资源浪费:delay 时,cpu 处于阻塞状态,无法处理其它任务。

抖动时的电平会多次变化

为了解决这一问题,我们可以采用非阻塞式按键检测。下面以上拉输入为前提。当检测到引脚电平变低时,我们令变量 down_cnt 自增 1。一段时间 (CPU 执行其它任务) 后,再次记录。如果引脚电平仍为低,令 down_cnt 再自增 1。重复以上过程,直到 down_cnt == x,那么我们返回按键被按下的信号。这里的 x 是一个合理的能跳过抖动的时间。假设抖动持续 10ms,cpu 执行其它任务需要 5ms,那么令 x=2 即可较好地跳过抖动。

松开抖动的处理与上类似。

这只是我的理解,有佬使用状态机更清晰地描述了这一过程,详见:STM32按键消抖——入门状态机思维 - 知乎 (zhihu.com)

代码实现

初始化配置:使用 GPIOA,Pin0-3 为推挽输出,对应行 4-1Pin4-7 为上拉输入,对应列 1-4

按键消抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
u8 Key_Scan(u8 curRow, u8 i)

{

    // TIM_SetCounter(KeyTim, 0);

    static u8 down_cnt[4] = {0};

    static u8 up_cnt[4] = {0};

    static u8 downFlag[4] = {0};

    u8 temp = GPIOA->IDR;

    /*debug

    u8 gp5 = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5);

    u8 gp4 = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4);*/

    if (((GPIOA->IDR) & (0b11111111)) != curRow)//取出低8位

    {

        down_cnt[i]++;

        if (down_cnt[i] == 2)

        {

            down_cnt[i] = 0;

            downFlag[i] = temp;

        }

    }

    else

    {

        up_cnt[i]++;

        if (up_cnt[i] == 2 && downFlag[i] != 0)//按下后松手,才认为一次按键完成

        {

            up_cnt[i] = 0;

            temp = downFlag[i];

            downFlag[i] = 0;

            return temp;

        }

        up_cnt[i] %= 3;//未按下时防止溢出

    }

    return 0;

}

GPIOA->IDR,管脚输入寄存器,值为 32 位 uint,低 16 位有效,对应 GPIOA 的 Pin0-15。为 0 代表输入为低电平,为 1 代表高电平。
GPIOA->ODR,管脚输出寄存器,配置它可以控制引脚输出电平的高低。参数与 IDR 类似。

这个函数其实有一定缺陷,因为每一行的扫描完成一次后,需要等待其它三行的扫描完成,再进行对该行的第二次扫描,这就导致按下的采样间隔过大:按键如果按下的时间太短,也会被认作是抖动,不能正常识别。解决方案是每次多按一会 按下时不使用消抖,直接读取到变化就将 downFlag[i]=temp,弹起消抖不变。

矩阵键盘

按照上述原理,扫描行 1 时,将 ODR 配置为 row[0]=0b1111_0111;如果 IDR!=row[0],令 down_cnt[0]++。假设 IDR=0b1110_0111,证明 Pin4 被拉低,即第一个按键被按下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
u8 Keyboard_Scan(void)

{

    GPIOA->ODR = 0b00000000;

    u8 rows[] = {0b11110111, 0b11111011, 0b11111101, 0b11111110};

    u8 temp;

    for (u8 i = 0; i < 4; i++)

    {

        GPIOA->ODR = rows[i];

        temp = Key_Scan(rows[i], i);
/*Debug
        u8 t1 = GPIOA->IDR;

        u8 t2 = GPIOA->ODR;

       
        OLED_ShowString(62, 10, "IDR", OLED_6X8);

        OLED_ShowString(62, 20, "ODR", OLED_6X8);

        OLED_ShowBinNum(0, 10, t1, 8, OLED_6X8);

        OLED_ShowBinNum(0, 20, t2, 8, OLED_6X8);

        OLED_Update();*/

        if (temp != rows[i] && temp != 0)

        { /*1110

         1101

         1011

         0111*/

            temp = temp >> 4;//取4-7位

            u8 j = 0;

            for (j = 0; j < 4; j++) // 返回按键值,>>移temp

            {

                if (((temp >> j) & 0x01) == 0)//检测是哪一位被拉低

                {

                    return i * 4 + j;

                }

            }

        }

    }

    return NoRes;//宏定义的信号,表明没有按下

}

代码难度也不大,就不详细解释了。

电 子 幽 灵

这份代码折磨了我几个晚上,总是会出现一些意想不到的 Bug。虽然大部分 Bug 是因为我是 cb。感谢 袁神,帮我解决了大部分问题。

电磁兼容初试

杜邦线一般长这样:

矩阵键盘的八个引脚在板子上是连成一排的。我直接拆了相连的 8 根线来用,没有把这 8 根线彼此分开。毕竟我连 STLink 的散着的 4 根线都接不太明白

调试的时候,电子幽灵出现了:

  • 偶发性某一行/某一列完全不识别
  • 右上角的按键和旁边两个按键识别不出来。这个问题稳定触发。打断点看见 temp 返回的是 0b1100_0111,也就是 Pin4 和 Pin5 被同时拉低了。我单独测试这两个 Pin 又是正常的,插上矩阵键盘就短路,让我一度怀疑是不是我写出 Bug 了,或者寄存器读取的值不对,或者这个键盘设计有问题。
    赞美 袁神,他让我把这 8 根线拆散,以上问题迎刃而解。

袁神曾说:8 根连在一起的杜邦线会产生电磁干扰。

打通 OLED 的二脉

我测试过程中换了一块 OLED,不出意外地点不亮。看了几遍代码以后,最后把 VCC 接到 5V 才正常点亮;但是 OLED 的测试代码正常,移植到我的项目的代码又不正常了。最后连 Keil 的断点调试都不正常,单步开始乱跳,程序入口都找不到了。最后 Rebuild 了整个 Project 才正常。代码小改 Build,大改还是 ReBuild 吧。

有趣的是,在第一次接 5v 点亮 OLED 以后,再接 3.3v 也能点亮这块 OLED 了。不知道我是不是把里面的什么东西烧掉了。

CV 浓度检测

经常 CV 代码的朋友都知道,CV 容易自己写难。

在测试 PA4 和 PA5 时,这两个脚的电平不管我怎么配置都没有用,最后发现推挽输出连灯都点不亮了。

我经历过这种事,当时是 PA12 的下拉输入不起作用,最后知道这个引脚设计上比较特殊,电平拉不低。我又开始怀疑 PA4 和 PA5 是不是也比较特殊,但是百度下来也没有遇到相似的情况。袁神看了我的码一眼,就赞道:

你初始化没写 GPIO_Speed。

我是 cb

补充一个比较全的 C8T6 的引脚图:
STM32F103C8T6引脚图及引脚功能说明及避坑指南-腾讯云开发者社区-腾讯云 (tencent.com)

具有 AC5 特色的 C99 标准

  1. gcc 中,if(a != 0) 是等价于 if(a) 的;但是在 AC5 中,它俩不等效。感谢 STLink 的断点调试,让我少被折磨了几个小时。
  2. C99 中,for(int i=0;i<xx;i++) 是合理的写法。在 AC5 开启 C99 支持后,编译同样可以通过,但是 i 的值变得不可预料。在 if (((temp >> j) & 0x01) == 0)//检测是哪一位被拉低 这句,for(int i=0;i<xx;i++) 始终无法满足条件;最后把 i 先声明,再使用,才恢复正常。

当然,AC5 现在岁数比我都大一轮多了,arm 官方也不再支持 AC5 了,但是我的 Keil 默认配的还是 AC5,很多教程也是基于 AC5 写的。等我有空一定迁移到 AC6。


最后说一点调试的心得吧。

首先就是不要碰 stm32,用 Arduino 调试的流程。我比较习惯从软件 - 硬件的流程来调试,毕竟我比较擅长写 Bug。实际写的时候,我写出了很多位运算的 bug,比如 16 位的数直接和 8 位比较;这类还好,可以用断点调试试出来。但是逻辑上的 Bug 还是不可避免的,写之前最好先把状态转移图画明白。

硬件上,当比较基础的代码出现 Bug 的时候,先看看自己的配置有没有写好;然后再看看外部电路连错没有;最后再考虑引脚的特殊性。单片机调试和一般软件调试区别很大,寄存器的值不对就是不对,也很难说是哪一步没写好或者电路没连对。有时候可能就是这种时候只能发挥工匠精神,慢慢检查。学电子的是这样的

最后就是,代码和人,能跑就行。我还遇到过注释掉某一行就发生硬错误,不注释就正常的情形。Pin, TIM 这些实在调不通就换一个,很多 Bug 很难说到底是 MCU 自身缺陷还是电路问题。

赞美袁神,驱散一切电子幽灵。

  • Title: 矩阵键盘与电子幽灵
  • Author: SingleDog
  • Created at : 2024-02-28 00:03:00
  • Updated at : 2024-04-12 21:56:53
  • Link: https://www.singledog233.top/GPIO-Keyboard/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments