3.语音板的按钮编程

0x00 语音板按钮介绍

语音板上除了一颗可以编程控制的LED灯,我们还增加了一颗可以编程控制的按钮。这样我们就可以在语音板上做出更多有趣的功能,例如不通过语音唤醒,直接通过按钮来唤醒。因为在某些环境中,由于周边的噪音导致比较难唤醒,这样我们就可以通过按钮来进行语音交互了。当然,我们还可以按钮配合着led灯,这样就能做出各种好玩的效果。首先让我们来认识下语音板上的按钮长什么样的,如下图所示:

3.语音板的按钮编程 - 第1张
可编程按钮位置

跟led灯一样,我们除了知道物理位置还得知道这个按钮是连在GPIO的哪个引脚上。这样我们就可以来编程获取到按钮的状态了,如下图所示:

3.语音板的按钮编程 - 第2张
按钮连接的GPIO引脚

由图中可知,这颗按钮是接在物理引脚36上,wiringPi编号27引脚上,BCM编号16上面。现在我们已经知道了按钮需要编程的GPIO引脚,那么就可以来编程了。


0x01 按钮编程

这里我们可以先从最简单的按钮编程开始入手,我们只需要当按下按钮的时候,有一个打印信息即可,表明按钮被按了一下。那现在开始编程,我们先使用python来编程看看,python代码如下:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import RPi.GPIO as GPIO
import time

#GPIO BOARD mode
#BTN_PIN = 36

#GPIO BCM mode
BTN_PIN = 16

number = 0

#GPIO.setmode(GPIO.BOARD)
GPIO.setmode(GPIO.BCM)
GPIO.setup(BTN_PIN, GPIO.IN, GPIO.PUD_UP)

try:
    while True:
        time.sleep(0.1)
        if GPIO.input(BTN_PIN) == 0:
            print("Button Click:%s"%number)
            number = number + 1
except KeyboardInterrupt:
    pass

#GPIO资源清理
GPIO.cleanup()

对该代码进行下简单的解析,从BTN_PIN可以看出,我这里定义了两个编号,一个是36,另外一个是16。这两个编号在不同的模式下使用,这个36是物理引脚,所以在初始化GPIO.setmode()时候就需要GPIO.BOARD参数。如果想使用BCM编码模式,这个参数就需要改为GPIO.BCM,那么对应的按钮编号也要改为16了。

GPIO.setup()就是为了配置按钮的GPIO引脚,这里设置为输入模式,因为按钮是一个输入模块,当按下或者弹起的时候,作为信号传输到GPIO引脚上。像LED灯就是输出模块,因为LED接在GPIO上,它不能像GPIO输入什么信号,而是我们编程控制led灯的GPIO高低电平来控制LED灯的亮灭。这里设置按钮为上拉模式,上拉就是将不确定的信号通过一个电阻钳位在高电平,电阻同时起限流作用。这样我们的按钮在不按下的时候,该GPIO就应该是一个高电平模式,所以当我们按下按钮的时候就会成低电平,这样我们就可以写代码来检测了。当GPIO.input(BTN_PIN)==0,说明按钮被按下了,我们就可以来打印信息了。

我们可以来看看,代码运行起来的效果是什么样的,如下视频所示:

python代码按钮测试

仔细观看上面视频的同学可能会注意到,有时间我按一下按钮打印一次日志,有时候按一次按钮就打印两次。这是为什么呢?其实这里的原因比较简单,这是因为我代码里是通过一个while循环,每隔100ms就检测一下按钮,只要按钮现在处于被按下的状态就会打印一次日志。那打印两次的就是因为我按下的时间有200ms了所以就打印了两次,那我们可以想象一下只要我一直按着按钮,这样肯定就会一直有日志打印出来。

大家再深入的思考一下这个代码,现在是通过不断的死循环,每隔100ms就要检测一下按钮有没有按下。可以想想这样有多么的浪费CPU的资源,因为CPU每隔100ms就要过来看一眼。你可以想想一下,你正在家里吃饭,你妈妈现在是个CPU。如果她每隔100ms都要跑过去看看你有没有吃饱饭,那你可以想想她有多么的累,她估计其他事情也不用干了。那你妈妈有没有更高级一点的方式来知道你有没有吃饱饭呢?那很简单,不用她一直盯着你吃饭,只要你吃饱饭的时候通知她一声就可以了,这样她再跑过来给你收拾一下桌子。这样你吃饭也安心,她也有时间干点其他事情。这种高级的方式是什么呢?这就是中断的方式,当有事件发生的时候再去通知CPU来干活,平时没事不要叫它。

那我们这次使用中断的方式来检测按键,代码稍微有一点点复杂了,参考代码如下:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import RPi.GPIO as GPIO
import time

#GPIO BOARD mode
BTN_PIN = 36

#GPIO BCM mode
#BTN_PIN = 16

#统计按键次数
number=0

def btnInterrupt(BTN_PIN):
    global number
    print("Button Click:%s"%number)
    number = number + 1

GPIO.setmode(GPIO.BOARD)
#GPIO.setmode(GPIO.BCM)
GPIO.setup(BTN_PIN, GPIO.IN, GPIO.PUD_UP)

GPIO.add_event_detect(BTN_PIN, GPIO.RISING, btnInterrupt, 400)

try:
    while True:
        time.sleep(3)
except KeyboardInterrupt:
    pass

#GPIO资源清理
GPIO.remove_event_detect(BTN_PIN)
GPIO.cleanup()

对代码进行下简单的解析,这里我们使用了一个新的API,GPIO.add_event_detect()函数,这里其实就是一个事件监听的函数。它使用检测中断边沿触发的方式来调用函数,这样就可以在主线程中执行一些其他重要的事情。当该引脚的电平有变化时,就会触发中断函数。这里检测的电平变化有三种方式:一种就是GPIO.RISING上升沿触发,就是说电平从低变到高的时候才会触发中断。另外一种GPIO.FALLING下降沿触发,就是电平从高降到低的时候会触发中断。最后一种就是GPIO.BOTH,就是上升沿下降沿电平都检测。拿我们这里的按钮来说,设置为上升沿触发,那现象就是当按下按钮的时候,电平从高到低不会触发中断函数,当我们松开按钮的时候,电平就会从低变为高,此时就会检测到上升沿电平,那么就会触发中断函数了。

GPIO.add_event_detect()的第一个函数就是检测的GPIO引脚,第二个参数就是检测的电平触发方式,上升、下降、还是都上升下降都检测,第三个参数就是当检测到电平变化的时候调用的中断函数,最后一个参数也比较重要就是bounce time。这个可以理解为按钮消抖时间,那这个按钮消抖是什么呢?

按键消抖通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。这是一个很重要的时间参数,在很多场合都要用到。按键稳定闭合时间的长短则是由操作人员的按键动作决定的,一般为零点几秒至数秒。键抖动会引起一次按键被误读多次。为确保CPU对键的一次闭合仅作一次处理,必须去除键抖动。在键闭合稳定时读取键的状态,并且必须判别到键释放稳定后再作处理。当按钮按下的时候,电平一般都是这样变化的,如下图所示

3.语音板的按钮编程 - 第3张
按键抖动状态图
3.语音板的按钮编程 - 第4张

可以看出来,如果我们不加个消抖时间的话,那么一般情况下按下一次按钮就会有多次电平边沿触发中断。这里的时间我设置为400ms,意思就是说当电平变化小于400ms的边沿触发都忽略掉,认为电平没有变化。就拿上图来说的话,1,2电平虽然电平有从低电平变到高电平的上升沿触发,但是时间太短,就认为电平没有变化仍然是高电平。当到3的时候虽然变为低电平了,但是一直要持续400ms,才好认为电平变化了。然后到达4前面的高电平,这里虽然是高电平,但是持续时间太短了,就认为仍然是低电平,到达4时候也有上升沿,但是仍然认为时间太短,只有上升沿时间有400ms了,才认为真是有了上升沿。这样大家也可以想象出来,这样的好处是可以消除抖动,但是坏处就是程序不能实时检测到按钮状态变化,必须要有一点点延时。但是我认为这点延时是值得等待的,这样可以消除按钮的抖动。

那最后我们来对比下这两个代码的运行效果吧,看看轮询方式和中断检测按钮的效果,如下视频所示:

中断和轮询方式的差别

0x02 按钮控制led灯

介绍完按钮的简单操作后,我们这次来稍微负责点的。我们通过按钮来控制语音板上的led灯,我们要实现的效果是,当按下按钮,led灯亮。不按按钮led灯灭。听起来很简单,那先来想想代码怎么写,然后再来看看完整参考代码吧:

#include <wiringPi.h>
#include <stdio.h>

#define LED_PIN  26
#define BTN_PIN  27

#define DELY_MS  100

int main(void)
{
    wiringPiSetup();
    pinMode(LED_PIN, OUTPUT);
    pinMode(BTN_PIN, INPUT);

    digitalWrite(LED_PIN, HIGH);
    pullUpDnControl(BTN_PIN, PUD_UP);

    while(1)
    {
        //检测按钮是否被按下
        if(digitalRead(BTN_PIN) == 0)
        {
            //led GPIO引脚置低电平,灯亮
            digitalWrite(LED_PIN, LOW);
            printf("O\n");
        }
        else
        {
            //led GPIO引脚置高电平,灯灭
            digitalWrite(LED_PIN, HIGH);
            printf("X\n");
        }

        //增加延时函数,减少cpu消耗
        delay(DELY_MS);
    }

    return 0;
}

这里我们换了使用wiringPi的方式来控制led灯,因为前面介绍了python的方式。下面对代码进行简要解析,这里使用最初级的方式来检测按键是否被按下。轮询方式代码逻辑简单,但是效率太低,消耗CPU资源太多。通过不断的digitalRead(BTN_PIN)读取按钮GPIO当前状态来判断是否按下按钮,当按下的话就点亮led灯。没有按下就灭led灯。这份代码需要使用gcc来编译才能运行,该源码文件命名为wiringpi_btn_led.c,生成的执行文件叫btn_led,编译的时候需要链接wiringPi动态库,完整的编译命令如下:

gcc wiringpi_btn_led.c -o btn_led -Wall -lwiringPi

当编译完成后,我们就可以来运行该代码,运行效果视频如下:

按键控制led亮灭

通过上述代码我们可以看出来这里使用使用轮询方式效率比较低,太消耗CPU资源。当没有按下按钮的时候,屏幕也在不停的输入日志,因为代码是检测到没有按下按钮那就灭led灯,这个灭led灯的操作一直在不断的做。那使用wiringPi的方式怎么来实现中断方式控制Led灯亮灭呢?参考代码如下:

#include <wiringPi.h>
#include <stdio.h>

#define LED_PIN  26
#define BTN_PIN  27
#define DELY_MS  1000

//在中断中要使用易失性变量
volatile int cnt = 0;

void btnInterrupt()
{
    if(cnt%2 == 0)
    {
        //led GPIO引脚置低电平,灯亮
        digitalWrite(LED_PIN, LOW);
    }
    else
    {
        //led GPIO引脚置高电平,灯灭
        digitalWrite(LED_PIN, HIGH);
    }
    cnt++;
}

int main(void)
{
    wiringPiSetup();
    pinMode(LED_PIN, OUTPUT);
    pinMode(BTN_PIN, INPUT);

    digitalWrite(LED_PIN, HIGH);
    pullUpDnControl(BTN_PIN, PUD_UP);

    //配置按键的边沿触发方式和中断函数
    wiringPiISR(BTN_PIN, INT_EDGE_RISING, &btnInterrupt);

    while(1)
    {
        //主线程可以做其他事情
        delay(DELY_MS);
    }

    return 0;

在这里大家可能会发现按键没有做消抖,这是因为wiringPiISR()这个函数它不带有这个消抖的参数,需要我们自己来处理抖动了。我这里并没有做处理,如果真正要做的话,就需要新开一个线程来单独处理这个led灯的控制了,但是那样就有点稍微复杂了。我们这里的代码就是为了让大家对按键的编程有个初步认识即可。

那这里我们是在中断函数中进行led灯的控制,根据代码逻辑大家应该也可以想象出来,当我们按下按钮应该是什么样的状态了。这里我们实现的按钮效果就有点像家里的电灯开关了,按一下灯一直亮,再按一下灯一直灭。最后就是这份源码的编译命令了,其实这份源码编译命令跟上面的是一样的。


0x03 源码下载

本次课程设计的源码,我都会上传到语音板的代码仓库中。大家可以查看下源码目录下的example文件下的四个文件:

3.语音板的按钮编程 - 第5张
本次课程的四份源码

0x04 参考资料

[1].(九)树莓派3B+ wiringPi库的使用-button按钮操作. https://blog.csdn.net/zhuming3834/article/details/82184046

[2].树莓派GPIO高级控制方法. https://blog.csdn.net/huayucong/article/details/78834493

[3].百度百科-按键消抖. https://baike.baidu.com/item/%E6%8C%89%E9%94%AE%E6%B6%88%E6%8A%96

[4].树莓派wiringPi库详解. https://www.cnblogs.com/lulipro/p/5992172.html


0x05 问题反馈

大家在按照教程操作过程中有任何问题,可以直接在文章末尾给我留言,或者关注ROS小课堂的官方微信公众号,在公众号中给我发消息反馈问题也行。我基本上每天都会处理公众号中的留言!当然如果你要是顺便给ROS小课堂打个赏,我也会感激不尽的,打赏30块还会被邀请进ROS小课堂的微信群,与更多志同道合的小伙伴一起学习和交流!

[wshop_reward]

本文原创,作者:corvin_zhang,其版权均为ROS小课堂所有。
如需转载,请注明出处:https://www.corvin.cn/1571.html