Raspberry Pi的GPIO中断编程

背景介绍

树莓派的GPIO引脚不仅可以输出高低电平,也可以当做输入端口(可以想象成键盘输入),当GPIO接入的是高电平,GPIO的值可以认为是1,如果是低电平则是0。如下图所示,可以使用一个Push Button开关按键来控制GPIO 25(BCM Numbering)的高低电平以达到控制的目的。

GPIO 25和VCC(3.3V)之间通过R1(10K欧姆)和R2(1K欧姆)上拉电阻相连,当按键未被按下时,GPIO 25上拉到VCC,程序可以读到1,当按键按下时,GPIO 25被下拉电阻R2拉到GND(0V),程序可以读到0。如果不加R1,而GPIO 25不小心被设置成输出低电平时,将直接和VCC相连而造成短路,这样可能会烧掉这个引脚,所以加上限流电阻R1后,即使发生这样的情况,也不会出现短路情况。

应用

如果我们需要根据GPIO 25的值来控制树莓派,比如按下按钮时希望点亮某个LED或在液晶上显示当前时间,就需要通过程序来获取状态的变化。

一种常见的做法是在循环里不断读取该引脚的状态,当发生对应的变化的时执行控制逻辑,但显而易见,这种做法很消耗CPU,如果在循环增加sleep(1000)这样的调用,又很容易错过按键变化。较好的做法则是通过中断来实现。

最新的树莓派Raspbian和Arch Linux内核都已经包含了GPIO的中断处理支持。但使用前需要将指定GPIO引脚输出,方法如下:

首先可以通过命令echo 25 > /sys/class/gpio/export导出GPIO 25端口,执行成功后在相应的目录下看到以下文件,得益于Linux下一切都是文件的设计理念,GPIO的状态可以通过value文件来获取,这样就可以利用Linux的poll/epoll来获取value文件的变化(这点和Linux高性能网络编程是类似的)。

root@raspberrypi2 ~/projects/interrupt_test # ls -l /sys/class/gpio/gpio25/
total 0
-rw-r--r-- 1 root root 4096 Apr  8 23:56 active_low
-rw-r--r-- 1 root root 4096 Apr  8 22:29 direction
-rw-r--r-- 1 root root 4096 Apr  8 22:29 edge
drwxr-xr-x 2 root root    0 Apr  8 23:56 power
lrwxrwxrwx 1 root root    0 Apr  8 23:56 subsystem -> ../../../../class/gpio
-rw-r--r-- 1 root root 4096 Apr  8 22:08 uevent
-rw-r--r-- 1 root root 4096 Apr  8 22:29 value
root@raspberrypi2 ~/projects/interrupt_test # 

wiringPi

wiringPi库封装了一个简单的接口,传入一个回调函数,当事件发生时传入的函数将被调用。

int wiringPiISR (int pin, int edgeType,  void (*function)(void)) ;

其最主要的部分的实现代码是:

int waitForInterruptSys (int pin, int mS)
{
  int fd, x ;
  uint8_t c ;
  struct pollfd polls ;
  if ((fd = sysFds [pin & 63]) == -1)
    return -2 ;
// Setup poll structure
  polls.fd     = fd ;
  polls.events = POLLPRI ;      // Urgent data!
// Wait for it ...
  x = poll (&polls, 1, mS) ;
// Do a dummy read to clear the interrupt
//      A one character read appars to be enough.
  (void)read (fd, &c, 1) ;
  return x ;
}

示例代码

#include 
#include 
#include 
#include 
#include 
// What GPIO input are we using?
//      This is a wiringPi pin number
#define BUTTON_PIN      6
static volatile int globalCounter = 0 ;
void myInterrupt (void)
{
  ++globalCounter ;
}
int main (void)
{
  int myCounter = 0 ;
  if (wiringPiSetup () < 0)
  {
    fprintf (stderr, "Unable to setup wiringPi: %s\n", strerror (errno)) ;
    return 1 ;
  }
  if (wiringPiISR (BUTTON_PIN, INT_EDGE_FALLING, &myInterrupt) < 0)
  {
    fprintf (stderr, "Unable to setup ISR: %s\n", strerror (errno)) ;
    return 1 ;
  }
  for (;;)
  {
    printf ("Waiting ... ") ; fflush (stdout) ;
    while (myCounter == globalCounter)
      delay (100) ;
    printf (" Done. counter: %5d\n", globalCounter) ;
    myCounter = globalCounter ;
  }
  return 0 ;
}

RPi.GPIO

Python的PRI.GPIO库也对中断进行了封装,以下是使用例子,差别在于wiringPi支持多线程,允许在等待中断的时候干点别的。

#!/usr/bin/env python2.7  
# script by Alex Eames http://RasPi.tv/  
# http://raspi.tv/2013/how-to-use-interrupts-with-python-on-the-raspberry-pi-and-rpi-gpio  
import RPi.GPIO as GPIO  
GPIO.setmode(GPIO.BCM)  
  
# GPIO 25 set up as input. It is pulled up to stop false signals  
GPIO.setup(16, GPIO.IN, pull_up_down=GPIO.PUD_UP)  
  
print "Make sure you have a button connected so that when pressed"  
print "it will connect GPIO port 23 (pin 16) to GND (pin 6)\n"  
raw_input("Press Enter when ready\n>")  
  
print "Waiting for falling edge on port 23"  
# now the program will do nothing until the signal on port 23   
# starts to fall towards zero. This is why we used the pullup  
# to keep the signal high and prevent a false interrupt  
  
print "During this waiting time, your computer is not"   
print "wasting resources by polling for a button press.\n"  
print "Press your button when ready to initiate a falling edge interrupt."  
while 1:
    try:  
        GPIO.wait_for_edge(16, GPIO.FALLING)  
        print "\nFalling edge detected. Now your program can continue with"  
        print "whatever was waiting for a button press."  
    except KeyboardInterrupt:  
        GPIO.cleanup()       # clean up GPIO on CTRL+C exit  
GPIO.cleanup()           # clean up GPIO on normal exit  

参考链接

  1. https://projects.drogon.net/raspberry-pi/wiringpi/functions/
  2. http://raspi.tv/2013/how-to-use-interrupts-with-python-on-the-raspberry-pi-and-rpi-gpio

 

http://hugozhu.myalert.info/2013/04/08/27-interrupts-with-gpio-pins.html

 

 


目前有几种途径可以在您的程序中获得 GPIO 的输入信息。第一种也是最简易的一种为在某个时间点检查输入值。这即是所谓的“轮询(polling)”,而且如果您的程序在错误的时间里进行了读取,可能会错过某个输入值。在循环中运用轮询,有可能使处理器资源紧张。另一种对 GPIO 输入进行响应的方式可以使用“中断(interruots)”(边缘检测(edge detection))。边缘可以是从 HIGH 到 LOW 的过度(下降临界值(falling edge))或从 LOW 到 HIGH 的过度(上升临界值(rising edge))。

上拉/下拉电阻
如果您在输入针脚上没有连接任何元件,那么它将是“浮动(float)”的。换句话说,因为您没有连接任何元件,在按下按钮或开关之前,读取的值是没有意义的。由于电源的波动,获取到的值可能会有很大的变化。
为了解决这个问题,我们需要使用上拉/下拉电阻。这样,我们就可设定输入的默认值了。在这里,可以使用硬件或软件对电阻进行上拉/下拉。使用硬件方式,将一个 10K 的电阻连接在输入通道与 3.3V(上拉)或 0V(下拉)之间是常用的做法。而 RPi.GPIO 也允许您通过软件的方式对配置 Broadcom SOC 来达到目的:
 GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_UP)
或者
 GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
(通道编号是基于您所使用的编号系统所指定的(BOARD 或 BCM)。)

输入测试(轮询(polling))
您可以在某个时间点获得一次输入的快照:
if GPIO.input(channel):
    print(‘Input was HIGH’)
else:
    print(‘Input was LOW’)
在循环中等待按钮被按下后进行轮询:
 while GPIO.input(channel) ==GPIO.LOW:
 time.sleep(0.01) # 为 CPU 留出 10 毫秒,供其处理其它事物
(这里假设为当按下按钮时,输入状态从 LOW 到 HIGH)

中断和边检检测
边缘的定义为电信号从 LOW 到 HIGH(上升临界值)或从 HIGH 到 LOW(下降临界值)状态的改变。正常情况下,对于输入的值来说,我们更关心的是输入的状态是否发生了改变。这种状态上的改变是很重要的。
为了避免您的程序在忙于处理其它的事物时而错过了您按下按钮的操作,这里有两种方法可以解决:
wait_for_edge() 函数
event_detected() 函数
在检测到边缘时执行线程回调函数

WAIT_FOR_EDGE() 函数
wait_for_edge() 函数被设计用于在检测到边缘之前阻止程序的运行。换句话说,上面的示例中,等待按钮被按下的语句可以改写为:

GPIO.wait_for_edge(channel, GPIO.RISING)
注意,您可以输入 GPIO.RISING、GPIO.FALLING、GPIO.BOTH 对边缘进行检测。这种方式的优点是占用 CPU 资源很少,因此系统可以有充裕的资源处理其它事物。

EVENT_DETECTED() 函数
event_detected() 函数被设计用于循环中有其它东西时使用,但不同于轮询的是,它不会错过当 CPU 忙于处理其它事物时输入状态的改变。这在类似使用 Pygame 或 PyQt 时主循环实时监听和响应 GUI 的事件是很有用的。
GPIO.add_event_detect(channel, GPIO.RISING) # 在通道上添加上升临界值检测
do_something()
if GPIO.event_detected(channel):
    print(‘Button pressed’)
注意,您可以输入 GPIO.RISING、GPIO.FALLING、GPIO.BOTH 对边缘进行检测。

线程回调
RPi.GPIO 在第二条线程中执行回调函数。这意味着回调函数可以同您的主程序同时运行,并且可以立即对边缘进行响应。例如:
def my_callback(channel):
    print(‘这是一个边缘事件回调函数!’)
    print(‘在通道 %s 上进行边缘检测’%channel)
    print(‘该程序与您的主程序运行在不同的进程中’)

GPIO.add_event_detect(channel, GPIO.RISING, callback=my_callback) # 在通道上添加上升临界值检测
7 … 其它程序代码 …
如果您需要多个回调函数:
def my_callback_one(channel):
    print(‘回调 1’)

def my_callback_two(channel):
    print(‘回调 2’)

GPIO.add_event_detect(channel, GPIO.RISING)
GPIO.add_event_callback(channel, my_callback_one)
GPIO.add_event_callback(channel, my_callback_two)
注意,在该示例中,回调函数为顺序运行而不是同时运行。这是因为当前只有一个进程供回调使用,而回调的运行顺序是依据它们被定义的顺序。

开关防抖
您可能会注意到,每次按钮按下时,回调操作被调用不止一次。这种现象被称作“开关抖动(switch bounce)”。这里有两种方法解决开关抖动问题:
将一个 0.1uF 的电容连接到开关上。
软件防止抖动
两种方式一起用
使用软件方式抖动,可以在您指定的回调函数中添加 bouncetime= 参数。
抖动时间需要使用毫秒为单位进行书写。例如:
# 在通道上添加上升临界值检测,忽略由于开关抖动引起的小于 200ms 的边缘操作
GPIO.add_event_detect(channel, GPIO.RISING, callback=my_callback, bouncetime=200)
或者
GPIO.add_event_callback(channel, my_callback, bouncetime=200)

remove_event_detect()
由于某种原因,您不希望您的程序检测边缘事件,您可以将它停止:
GPIO.remove_event_detect(channel)

One Response so far.

  1. Laticia says:
    Too many colmtipenms too little space, thanks!

Leave a Reply to Laticia Cancel reply