8

I know the linux kernel has a language agnostic GPIO interface through /sys/class/gpio which can be manipulated with echo, cat, etc. How can this be used to create event driven callbacks programmatically?


Part of the reason for this question is that someone recently pointed out that the ruby ruby-gpio gem, which is built on the linux kernel's sysfs interface, makes the odd choice to busy loop in callbacks. This is something people occasionally do here in their own code, but it is a very bad approach. Better but also unnecessary are loops with timed sleeps.

The kernel interface can be used to fire interrupt based callbacks that do not require any spurious processor time or commitments to an arbitrarily timed granularity. Although this is already documented, the official documentation presumes a knowledge of C and its use value may not be clear to people working in other languages.

Note that interface is generic and cannot (or should not) be used for any of the special purposes (PWM, I2C, SPI, UART, etc.). There are other general purpose interfaces for I2C, SPI, and UART. For hardware PWM you should use one of the pi libraries: pigpio, wiringPi, or libbcm2835. These do require language specific bindings. You should be able to mix and match them with sysfs access if you are sensible.

goldilocks
  • 58,859
  • 17
  • 112
  • 227
  • 1
    You should be aware that Linux interrupts are limited to circa 20 k per second per GPIO on the Pi (tested under C). Of course this won't be a problem for most uses. Also that limit may be further reduced depending on the programming language being used. – joan Jan 13 '16 at 13:35

1 Answers1

7

The sysfs GPIO interface is fairly straightforward. I'll demonstrate it using perl to respond to events from three buttons connected to GPIOS 17,27, and 22 (by the Broadcom numbering). You don't need to have any experience with perl to follow the methodology. Although all of the working code is provided at the end, I only refer to a small portion of it in the conceptual explanation.

Recent versions of Raspbian give permission to anyone in the gpio group for this but on other distros this may not be the case. Note /sys files aren't regular files, which is why they have a size of zero. They are not stored anywhere because they do not really have content. When you write to a sysfs file, you are sending the kernel a message. When you read one, you are asking it for information.

As per the documentation linked in the question (which you should look at, since I will not regurgitate all of it, and it is assumed below you are referring to this), the first thing that needs to be done is to open or export a specific pin by writing the number to /sys/class/gpio/export. I'm creating an object oriented API, so I do that in the constructor for the GPIO pin class, if necessary.

The sysfs interface is text based and line buffered, meaning it will output newlines at the end of each value, and "0" and "1" refer to characters, not integers. I.e., looking at this byte for byte those would be 0x30 ('0') or 0x31 ('1'), 0x0A ('\n'), 0x00 (NUL). Of course, in most languages you might as well just use strings. You don't have to include the newline in input, but for simplicity with using and comparing constants in perl I do.

use constant {                 
    INPUT      => "in\n",      
    OUTPUT     => "out\n",     
    HIGH       => "1\n",       
    LOW        => "0\n",       
    POSEDGE    => "rising\n",  
    NEGEDGE    => "falling\n", 
    BOTH_EDGES => "both\n",    
    NO_EDGE    => "none\n"     
};

The use for these will become clear. An example of creating a pin object for monitoring a button, like that used in the demo, might be:

my $pin = SysfsGPIO->new (         
    number => 17,             
    direction => SysfsGPIO::INPUT, 
    edge => SysfsGPIO::BOTH_EDGES, 
    active => SysfsGPIO::LOW,      
    bouncetime => 0.01             
); 

In the code implementing the class, the direction is set by writing to the direction file in (for this case) /sysfs/class/gpio/gpio17, once that directory exists (by writing to export as mentioned above). The rest of this requires further explanation.

The event triggers make use of the select() or poll() system calls. These are not perl, although perl like every other language has a wrapper for them. They are most commonly used in networking and IPC; they allow a number of file descriptors (presumably, network sockets) to be polled for readiness to read or write. Basically, you submit a list of file handles, and the call blocks until one or more of them is ready. This is not any kind of busy or half-busy loop. Depending on what the handles refer to, the kernel implements this via genuine hardware interrupts.

Such calls are often used as the opening condition of a program's "main loop", and that is the model I will follow. Like a server, we are just going to wait for and respond to events when they happen. I'm using three buttons to demonstrate that you are not limited to just one at a time. This is also a single-threaded exercise.

Note again that ALL general purpose languages will have their own version of poll() and/or select(), which is why this is a language agnostic methodology. You should be able to find out what they are easily. If you haven't used whatever it is before, you will want to read the documentation and perhaps find some examples or tutorials. I prefer poll(), so I'm using perl's core IO::Poll module.

The file you actually want to hold open for the descriptor/handle is value. However, there are a couple of special things to note:

  • Unlike normal use of select() or poll(), what's triggered isn't a read or write flag/descriptor set. For poll() style functions you need the "error" and "priority" flags set, for select() you would use the "exception" (not the "read") set.

    use constant POLL_EVENTS => POLLPRI | POLLERR;
    
  • When a specific handle fires, you must read the value and then either close & reopen the file or rewind the handle. If you don't it will keep firing immediately. Rewinding makes more sense so the descriptor does not change for the poll/select set.

  • The handle is triggered when value changes, but there this can be from 0 to 1 or from 1 to 0. The former is called the "rising" or positive edge and the latter the "falling" or negative edge (notice I defined constants for these). You can choose to respond to either or both of these events (this is the purpose of edge).

  • The active_low file allows you to invert value, such that 1 will indicate a physically low signal and 0 a high one. I use it for style since the button is on a pull-up -- the signal will be low when the button is down. By setting the active low inversion that will be read as a 1 from value. This is unnecessary but I use in the demo, so do not be confused by the inverted values below.

There are a few ways of reacting to both the rising and falling edge. The obvious and easy one is to write both to the edge file. An apparently common pitfall with buttons is "bounce", whereby the signal may oscillate when the button is depressed or released. A programmatic solution is to "debounce" by discarding multiple changes within a brief window. This doesn't require a sleep, it just requires you timestamp events.1

So, once the GPIOs are configured (as inputs, set active low with a trigger on both edges) the value file handles are set up with a POLLERR | POLLPRI mask and handed to poll(). Since poll() returns with a list of handles that actually fired, I created a hash table (%buttons) corresponding handles to SysfsGPIO objects. Thus the main loop begins:

while ($gpios->poll) {                         
    foreach ($gpios->handles(POLL_EVENTS)) {   
        my $pin = $buttons{$_};                

If this is not clear, we're now checking each of the buttons that poll() says have been triggered.

        my $prev = $pin->{lastValue};          
        my $state = $pin->getValue;            
        next if $pin->checkBounceTime;              
        print "$pin->{color} $state";
        $pin->{output}->toggle if ($state eq SysfsGPIO::HIGH);        

The color attribute was tacked onto the object to reference to the button colors. Note that even if the button going up serves no purpose, you should still watch for it to deal with bounce (see footnote). Here, the button going up will pass checkBounceTime(), which starts the timer again, but then be discarded by the final if ($state...).

Perl is pre-installed by all normal GNU/Linux distros including Raspbian and the complete code below does not require any additional libraries, so if you have some buttons and resistors you can try it out. Pressing all three buttons down then putting them up/down one at a time and finally releasing all three looks like this:

blue 1
green 1
yellow 1
yellow 0
yellow 1
green 0
green 1
blue 0
blue 1
blue 0
green 0
yellow 0

Here's a pic of the connections. There are lots of explanations of button circuits around. These use external pull-ups. In case it isn't obvious the button contacts connect numbered rows when depressed, not the top half of the board to the bottom.

enter image description here


Here's the code. To see this in action, place both files in the same directory, set test.pl executable (chmod 755 test.pl), and run ./test.pl (also note the different extentions, .pl and .pm). You'll have to use ctrl-c to exit and there's no clean-up, so the directories will still exist in /sys/class/gpio. You can restart it without closing those, however.

test.pl

#!/usr/bin/perl                                                      
use strict;                                                          
use warnings FATAL => qw(all);                                       

use IO::Poll qw(POLLPRI POLLERR);                                    
use SysfsGPIO;                                                       

use constant POLL_EVENTS => POLLPRI | POLLERR;                       

my %buttons;                                                         
my $gpios = IO::Poll->new();                                         

foreach (                                                            
    [ 17, 'blue' ],                                                  
    [ 27, 'green' ],                                                 
    [ 22, 'yellow' ]                                                 
) {                                                                  
    my $pin = SysfsGPIO->new (                                       
        number => $_->[0],                                           
        color => $_->[1],                                            
        direction => SysfsGPIO::INPUT,                               
        edge => SysfsGPIO::BOTH_EDGES,                               
        active => SysfsGPIO::LOW,                                    
        bouncetime => 0.01                                           
    );                                                               
    if (!$pin) {                                                     
        print STDERR "Pin $_->[0] ($_->[1]) failed initialization!\n"; 
        next;                                                        
    }                                                                
    $buttons{$pin->{handle}} = $pin;                                 
    $pin->getValue;                                                  
    $gpios->mask($pin->{handle}, POLL_EVENTS);                       
}                                                                    


print "Begin...\n";                                                  


while ($gpios->poll) {                                               
    foreach ($gpios->handles(POLL_EVENTS)) {                         
        my $pin = $buttons{$_};                                      
        my $prev = $pin->{lastValue};                                
        my $state = $pin->getValue;                                  
        next if $pin->checkBounceTime;                                                           

        print "$pin->{color} $state";                                
    }                                                                
}                                                

SysfsGPIO.pm

package SysfsGPIO;                                                      
use strict;                                                             
use warnings FATAL => qw(all);                                          

use Fcntl qw(SEEK_SET);                                                 
use Time::HiRes;                                                        

our $SysfsPath = "/sys/class/gpio";                                     

use constant {                                                          
    INPUT      => "in\n",                                               
    OUTPUT     => "out\n",                                              
    HIGH       => "1\n",                                                
    LOW        => "0\n",                                                
    POSEDGE    => "rising\n",                                           
    NEGEDGE    => "falling\n",                                          
    BOTH_EDGES => "both\n",                                             
    NO_EDGE    => "none\n"                                              
};                                                                      


sub openAndWrite {                                                      
    open(my $fh, '>', shift) or return 0;                               
    print $fh shift;                                                    
    close $fh;                                                          
    return 1;                                                           
}                                                                       


sub openAndRead {                                                       
    open(my $fh, '<', shift) or return undef;                           
    my $x = "";                                                         
    sysread($fh, $x, 1024);                                             
    close $fh;                                                          
    return $x;                                                          
}                                                                       


sub new {                                                               
    my $class = shift;                                                  
    my $self = {                                                        
        number => -1,                                                   
        @_                                                              
    };                                                                  

    $self->{path} = "$SysfsPath/gpio".$self->{number};                  

    return undef if (                                                   
        !-e $self->{path}                                               
        && !openAndWrite (                                              
            "$SysfsPath/export",                                        
            "$self->{number}\n"                                         
        )                                                               
    );                                                                  

    bless $self, $class;                                                

    return undef if (                                                   
        (                                                               
            $self->{direction} &&                                       
            !$self->setDirection($self->{direction})                    
        ) || (                                                          
            !$self->getDirection ||                                     
            !$self->openValue                                           
        )                                                               
    );                                                                  

    return undef if (                                                   
        (                                                               
            $self->{edge} &&                                           
            !$self->setEdge($self->{edge})                               
        ) || !$self->getEdge                                            
    );                                                                  

    return undef if (                                                   
        (                                                               
            $self->{active} &&                                          
            !$self->setActive($self->{active})                          
        ) || !$self->getActive                                          
    );                                                                  

    return $self;                                                       
}                                                                       


sub DESTROY {                                                           
    my $self = shift;                                                   
    if ($self->{handle}) {                                              
        close $self->{handle};                                          
        $self->{handle} = undef;                                        
    }                                                                   
}                                                                       


sub close {                                                             
    my $self = shift;                                                   
    return 0 if !$self->setDirection(INPUT);                            
    if ($self->{handle}) {                                              
        close $self->{handle};                                          
        $self->{handle} = undef;                                        
    }                                                                   
    return 0 if !openAndWrite (                                         
        "$SysfsPath/unexport",                                          
        "$self->{number}\n"                                             
    );                                                                  
    return 1;                                                           
}                                                                       


sub checkBounceTime {                                                   
    my $self = shift;                                                   
    return undef if !$self->{bouncetime};                               
    if ($self->{lastbounce}) {                                          
        my $diff = $self->{bouncetime} -                                
            (Time::HiRes::time() - $self->{lastbounce});                
        return $diff if $diff > 0;                                      
    }                                                                   
    $self->{lastbounce} = Time::HiRes::time();                          
    return 0;                                                           
}                                                                       


sub getActive {                                                         
    my $self = shift;                                                   
    my $alow = openAndRead("$self->{path}/active_low");                 
    return undef if !$alow;                                             
    if ($alow eq LOW) {                                                 
        $self->{active} = HIGH                                          
    } else {                                                            
        $self->{active} = LOW                                           
    }                                                                   
    return $self->{active};                                             
}                                                                       


sub getEdge {                                                           
    my $self = shift;                                                   
    $self->{edge} = openAndRead("$self->{path}/edge");                  
    return $self->{edge};                                               
}                                                                       


sub getDirection {                                                      
    my $self = shift;                                                   
    $self->{direction} = openAndRead("$self->{path}/direction");        
    return $self->{direction};                                          
}                                                                       


sub getValue {                                                          
    my $self = shift;                                                   
    return undef if (                                                   
        sysread($self->{handle}, $self->{lastValue}, 3) <= 0            
    );                                                                  
    seek($self->{handle}, 0, SEEK_SET);                                 
    return $self->{lastValue};                                          
}                                                                       


sub setActive {                                                         
    my $self = shift;                                                   
    $self->{active} = shift;                                            
    my $alow = LOW;                                                     
    $alow = HIGH if $self->{active} eq LOW;                             
    return openAndWrite (                                               
        "$self->{path}/active_low",                                     
        $alow                                                           
    );                                                                  
}                                                                       


sub setEdge {                                                           
    my $self = shift;                                                   
    $self->{edge} = shift;                                              
    return openAndWrite (                                               
        "$self->{path}/edge",                                           
        $self->{edge}                                                   
    );                                                                  
}                                                                       


sub setDirection {                                                      
    my $self = shift;                                                   
    $self->{direction} = shift;                                         
    return openAndWrite (                                               
        "$self->{path}/direction",                                      
        $self->{direction}                                              
    );                                                                  
}                                                                       


sub openValue {                                                         
    my $self = shift;                                                   
    if ($self->{handle}) {                                              
        CORE::close $self->{handle};                                    
        $self->{handle} = undef;                                        
    }                                                                   
    if ($self->{direction} eq OUTPUT) {                                 
        return 0 if !open($self->{handle}, '>', "$self->{path}/value"); 
        return 1;                                                       
    } elsif ($self->{direction} eq INPUT) {                             
        return 0 if !open($self->{handle}, '<', "$self->{path}/value"); 
        return 1;                                                       
    }                                                                   
    return 0;                                                           
}                                             

1;

1. Bounce occurs when the button goes down and goes up, and alternate states evenly. This means even if you are just interested in the "pushed down" edge (in this case, positive/high), you still need to watch both edges so you can apply a bounce time at the end as well. Then when the button goes up, the first event will be the for opposite edge, and any subsequent bounce down again will be properly discarded.

goldilocks
  • 58,859
  • 17
  • 112
  • 227
  • A useful Q&A , and it touches on an area I will have to address in the near future - multiplexing switch arrays - for when you want more buttons then you have discrete lines or they are already so arranged. Consider a membrane switch with the numbers 0-9, #, * and A-D (i.e. 16 switches in a 4x4 array) How might we use poll() to only scan the matrix when any key is pressed (or released) and to decode whether one or more key(s) are pressed and if only one, which it is? – SlySven Jan 13 '16 at 00:32
  • As an exercise for the student: Write an implementation in your favourite language. – Milliways Jan 13 '16 at 00:46
  • These kind of switches are notorious for bounce. This is particularly noticeable when using internal pullup, which also increases the susceptibility to interference. When I implement this kind of circuit I do not rely on the bouncetime but specifically test the state after a short delay. The length of the delay depends on the application; for a reset button I use 0.5 sec much less for normal buttons when the consequence of a false trigger is less. – Milliways Jan 13 '16 at 00:54
  • @SlySven I say for all combinations of "or more" you can't without some EEE cleverness (was that a trick question?) that won't work using digital pins. – goldilocks Jan 13 '16 at 11:30
  • This works like a charm! BUT include lib with use SysfsGPIO; I could NOT use it with require use 'SysfsGPIO.pm'; The gurus may know why - just to avoid some one else catting caught by that pitfall... – Andy16 Jan 12 '17 at 19:08