Lendo um pino de I/O por interrupção no Linux

- por Sergio Prado

Categorias: Linguagem C, Linux Tags: , ,

O acesso a pinos de I/O (GPIO) no Linux é feito através de arquivos exportados no diretório /sys/class/gpio/. Para acessar um pino de I/O, o primeiro passo é exportá-lo escrevendo o número do pino de I/O no arquivo export. Depois é necessário configurar a direção do pino de I/O no arquivo direction e por fim ler ou alterar o estado do pino de I/O no arquivo value.

Por exemplo, para acionar o GPIO 53:

# echo 53 > /sys/class/gpio/export
# echo out > /sys/class/gpio/gpio53/direction
# echo 1 > /sys/class/gpio/gpio53/value

Da mesma forma, podemos ler o estado do GPIO 25 com os comandos abaixo:

# echo 25 > /sys/class/gpio/export
# echo in > /sys/class/gpio/gpio25/direction
# cat /sys/class/gpio/gpio25/value
0

Podemos também escrever um programa em linguagem C para ler o GPIO, usando as funções de acesso a arquivos da biblioteca do sistema:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, const char *argv[])
{
    char buf[16];
    int fd, qtd;
 
    fd = open("/sys/class/gpio/export", O_WRONLY);
    write(fd, "25", 2);
    close(fd);
 
    fd = open("/sys/class/gpio/gpio25/direction", O_WRONLY);
    write(fd, "in", 2);
    close(fd);
 
    fd = open("/sys/class/gpio/gpio25/value", O_RDONLY);
    qtd = read(fd, buf, sizeof(buf) - 1);
    close(fd);
 
    buf[qtd] = '\0';
    printf("GPIO status is %s\n", buf);
 
    return 0;
}

E se quisermos monitorar o estado de um botão conectado a um GPIO para tomar alguma decisão quando o botão for pressionado? Podemos colocar a leitura do pino de I/O em um loop infinito, conforme exemplo abaixo:

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
int main(int argc, const char *argv[])
{
    char buf[16];
    int fd, qtd;
 
    fd = open("/sys/class/gpio/export", O_WRONLY);
    write(fd, "25", 2);
    close(fd);
 
    fd = open("/sys/class/gpio/gpio25/direction", O_WRONLY);
    write(fd, "in", 2);
    close(fd);
 
    for (;;) {
 
        fd = open("/sys/class/gpio/gpio25/value", O_RDONLY);
        qtd = read(fd, buf, sizeof(buf) - 1);
        close(fd);
 
        buf[qtd] = '\0';
 
        if (!strcmp(buf, '1'))
            printf("Botão pressionado!\n");
    }
 
    return 0;
}

O grande problema é que o código vai rodar em um loop infinito, lendo o pino de I/O indefinidamente e consumindo 100% de CPU!

Uma forma de minimizar este problema é ler o botão periodicamente, por exemplo a cada 100ms, utilizando as rotinas de espera da biblioteca do sistema (sleep(), usleep(), etc). Mas mesmo assim iremos desperdiçar um pouco de CPU a cada 100ms, além da possibilidade de perder algum evento se ele acontecer mais rápido que a periodicidade definida.

A melhor forma de resolver este problema é trabalhando por interrupção.

Para isso, precisamos habilitar a interrupção do pino de I/O no arquivo edge, que permite configurar como a interrupção será gerada. São suportados os valores “none” (interrupção desabilitada), “rising” (interrupção na transição para nível lógico alto), “falling” (interrupção na transição para nível lógico baixo) ou “both” (interrupção na transição para os níveis lógicos alto ou baixo).

Com o arquivo edge configurado, as interrupções no pino de I/O podem ser capturadas com a chamada de sistema poll(). Veja o exemplo completo abaixo:

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
int main(int argc, const char *argv[])
{
    struct pollfd fds[1];
    char aux[16];
    int fd;
 
    fd = open("/sys/class/gpio/export", O_WRONLY);
    write(fd, "25", 2);
    close(fd);
 
    fd = open("/sys/class/gpio/gpio25/direction", O_WRONLY);
    write(fd, "in", 2);
    close(fd);
 
    fd = open("/sys/class/gpio/gpio25/edge", O_WRONLY);
    write(fd, "rising", 6);
    close(fd);
 
    for (;;) {
 
        fds[0].fd = open("/sys/class/gpio/gpio25/value", O_RDONLY);
        fds[0].events = POLLPRI | POLLERR;
 
        /* need to read before polling */
        read(fds[0].fd, aux, sizeof(aux));
 
        /* block waiting for GPIO interrupt */
        poll(fds, 1, -1);
 
        if (fds[0].revents & POLLPRI)
            printf("Botão pressionado!\n");
 
        close(fds[0].fd);
    }
 
    return 0;
}

Não espere uma resposta determinística neste exemplo, afinal estamos trabalhando na camada de usuário com muitas chamadas de sistema e trocas de contexto envolvidas. De qualquer forma, é um código com baixa carga de processamento e 100% baseado em eventos, como todo código elegante deve ser. :-)

Happy coding!

Sergio Prado

  • Francisco Helder Candido

    Parabéns Sergio Prado pelo artigo sobre interrupção em GPIO, ficou muito bom…

  • Helton Marques

    Olá Sérgio, ao invés de ler os “arquivos”, eu uso um outro método através de mmap. O código pode ser visto aqui: https://github.com/heltonmarx/libgpio

    • Olá Helton! É verdade. A grande vantagem deste método é que a latência é muito menor. Por outro lado, ele tem algumas desvantagens. É um método inseguro e não portável, além de você não conseguir facilmente tratar a interrupção de um pino de I/O. Um abraço!

  • Daniel_Alexandre

    Bom dia Sérgio,

    Estou desenvolvendo um projeto e preciso fazer exatamente isso, monitorar um pino para disparar um evento quando ele for acionado. Pesquisei bastante na internet e parece que essa abordagem que você descreveu é a mais “correta” mesmo.

    O problema é que eu preciso fazer isso com mais de um pino ao mesmo tempo. Tenho que monitorar 5 GPIOs e por isso estava pensando em usar algo mais parecido com um callback para cada um dos pinos. Você tem alguma dica de como implementar isso? Caso seja mais fácil utilizar poll é necessário multithread? Muito obrigado e grande abraço

    • Olá Daniel,

      A idéia é esta do post mesmo.

      Não é necessário uma aplicação multithread, já que você pode passar para a função poll e monitorar diversos descritores de arquivo ao mesmo tempo.

      Já a lógica das callbacks você pode implementar na aplicação.

      Um abraço!

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.