Driver de Botão para Linux em Sistemas Embarcados

Princípios do Driver de Botão no Linux

O desenvolvimento de drivers para botões em Linux segue uma abordagem semelhante a drivers de LEDs, mas com foco em entrada de dados em vez de saída. A principal diferença reside na leitura dos estados de nível lógico (alto/baixo) dos pinos GPIO, em contraste com a escrita para controlar LEDs.

No driver, uma variável inteira armazena o valor do botão, que é lido pela aplicação usando a função read. Como o driver escreve nessa variável e a aplicação lê, ela constitui um recurso compartilhado que requer proteção. Para variáveis inteiras, operações atômicas são ideais, garantindo acesso seguro durante leituras e escritas. Este exemplo demonstra um driver de entrada GPIO básico, mas em contextos práticos, o subsistema Input do Linux é frequentemente empregado para lidar com dispositivos de entrada de forma mais eficiente.

Análise do Esquema de Hadrware

A placa de desenvolvimento considerada possui quatro botões: um RESET especial e três botões genéricos: BTN0, BTN1 e BTN_WAKE. O esquemático mostra que BTN0 está conectado a um resistor de pull-up de 10kΩ. Quando BTN0 não está pressionado, o pino PG3 mantém nível alto; ao pressionar, o nível torna-se baixo.

Escrita do Programa Experimental

Modificação da Árvore de Dispositivos

Adicione um nó para o botão na raiz da árvore de dispositivos, como mostrado abaixo:

<node_name> {
    compatible = "example,button";
    status = "okay";
    button-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
};

Após modificar, compile a árvore de dispositivos e copie o arquivo .dtb resultante para o diretório de boot, por exemplo, usando comandos como cd e cp no terminal.

Código do Driver (button_driver.c)

Crie um diretório para o driver, como 12_button, e implemente o driver conforme abaixo. As alterações incluem nomes de variáveis e funções modificados para refletir um código mais genérico.

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define BUTTON_COUNT        1
#define BUTTON_NAME         "button"
#define BUTTON_PRESSED      0xF0
#define BUTTON_IDLE         0x00

struct button_device {
    dev_t dev_num;
    struct cdev cdev;
    struct class *cls;
    struct device *dev;
    int major;
    int minor;
    struct device_node *node;
    int gpio_num;
    atomic_t state;
};

static struct button_device btn_dev;

static int configure_button_gpio(void)
{
    int ret;
    const char *prop;

    btn_dev.node = of_find_node_by_path("/node_name");
    if (!btn_dev.node) {
        printk("Button device node not found!\n");
        return -EINVAL;
    }

    ret = of_property_read_string(btn_dev.node, "status", &prop);
    if (ret < 0 || strcmp(prop, "okay") != 0)
        return -EINVAL;

    ret = of_property_read_string(btn_dev.node, "compatible", &prop);
    if (ret < 0 || strcmp(prop, "example,button") != 0) {
        printk("Compatible property mismatch\n");
        return -EINVAL;
    }

    btn_dev.gpio_num = of_get_named_gpio(btn_dev.node, "button-gpio", 0);
    if (btn_dev.gpio_num < 0) {
        printk("Failed to obtain GPIO number\n");
        return -EINVAL;
    }

    ret = gpio_request(btn_dev.gpio_num, "BTN0");
    if (ret) {
        printk("GPIO request failed\n");
        return ret;
    }

    ret = gpio_direction_input(btn_dev.gpio_num);
    if (ret < 0) {
        printk("GPIO direction setting failed\n");
        return ret;
    }
    return 0;
}

static int btn_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &btn_dev;
    return configure_button_gpio();
}

static ssize_t btn_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int ret;
    int val;
    struct button_device *dev = filp->private_data;

    if (gpio_get_value(dev->gpio_num) == 0) {
        while (!gpio_get_value(dev->gpio_num));
        atomic_set(&dev->state, BUTTON_PRESSED);
    } else {
        atomic_set(&dev->state, BUTTON_IDLE);
    }

    val = atomic_read(&dev->state);
    ret = copy_to_user(buf, &val, sizeof(val));
    return ret;
}

static ssize_t btn_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

static int btn_release(struct inode *inode, struct file *filp)
{
    struct button_device *dev = filp->private_data;
    gpio_free(dev->gpio_num);
    return 0;
}

static const struct file_operations btn_fops = {
    .owner = THIS_MODULE,
    .open = btn_open,
    .read = btn_read,
    .write = btn_write,
    .release = btn_release,
};

static int __init btn_driver_init(void)
{
    int ret;

    atomic_set(&btn_dev.state, BUTTON_IDLE);

    if (btn_dev.major) {
        btn_dev.dev_num = MKDEV(btn_dev.major, 0);
        ret = register_chrdev_region(btn_dev.dev_num, BUTTON_COUNT, BUTTON_NAME);
    } else {
        ret = alloc_chrdev_region(&btn_dev.dev_num, 0, BUTTON_COUNT, BUTTON_NAME);
        if (ret == 0) {
            btn_dev.major = MAJOR(btn_dev.dev_num);
            btn_dev.minor = MINOR(btn_dev.dev_num);
        }
    }
    if (ret < 0) {
        printk("Device number registration failed\n");
        return ret;
    }

    cdev_init(&btn_dev.cdev, &btn_fops);
    btn_dev.cdev.owner = THIS_MODULE;
    ret = cdev_add(&btn_dev.cdev, btn_dev.dev_num, BUTTON_COUNT);
    if (ret < 0)
        goto unregister;

    btn_dev.cls = class_create(THIS_MODULE, BUTTON_NAME);
    if (IS_ERR(btn_dev.cls))
        goto del_cdev;

    btn_dev.dev = device_create(btn_dev.cls, NULL, btn_dev.dev_num, NULL, BUTTON_NAME);
    if (IS_ERR(btn_dev.dev))
        goto destroy_class;

    return 0;

destroy_class:
    device_destroy(btn_dev.cls, btn_dev.dev_num);
del_cdev:
    cdev_del(&btn_dev.cdev);
unregister:
    unregister_chrdev_region(btn_dev.dev_num, BUTTON_COUNT);
    return -EIO;
}

static void __exit btn_driver_exit(void)
{
    device_destroy(btn_dev.cls, btn_dev.dev_num);
    class_destroy(btn_dev.cls);
    cdev_del(&btn_dev.cdev);
    unregister_chrdev_region(btn_dev.dev_num, BUTTON_COUNT);
}

module_init(btn_driver_init);
module_exit(btn_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_INFO(intree, "Y");

Aplicação de Teste (button_test.c)

Implemente um programa simples para ler o estado do botão via interface de arquivo.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

#define BUTTON_PRESSED  0xF0
#define BUTTON_IDLE     0x00

int main(int argc, char *argv[])
{
    int fd, ret;
    int state;

    if (argc != 2) {
        printf("Uso: %s <device_path>\n", argv[0]);
        return -1;
    }

    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        printf("Falha ao abrir %s\n", argv[1]);
        return -1;
    }

    while (1) {
        read(fd, &state, sizeof(state));
        if (state == BUTTON_PRESSED)
            printf("Botão pressionado, valor: 0x%X\n", state);
    }

    ret = close(fd);
    if (ret < 0) {
        printf("Falha ao fechar %s\n", argv[1]);
        return -1;
    }
    return 0;
}

Testes de Execução

Criação do Makefile

Elabore um Makefile para compilar o módulo do kernel. Ajuste o caminho para o diretório do código-fonte do kernel conforme necessário.

KERNELDIR ?= /path/to/linux/kernel/source
CURRENT_PATH := $(shell pwd)

obj-m := button_driver.o

all:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

Compilação e Implantação

Compile o driver e a aplicação de teste usando ferramentas apropriadas, como:

make
arm-none-linux-gnueabihf-gcc button_test.c -o button_test

Copie os arquivos gerados para o sistema de arquivos raiz da placa de desenvolvimento, por exemplo, para /lib/modules/.

Carregamento e Uso

Na placa, navegue até o diretório dos módulos e execute comandos para carregar o driver:

cd /lib/modules
depmod
modprobe button_driver.ko

Em seguida, execute a aplicação de teste:

./button_test /dev/button

Ao pressionar o botão BTN0, a mensagem correspondente será exibida no terminal. Note que a falta de tratamento de debouncing pode causar múltiplas leituras por pressionamento.

Para descarregar o driver:

rmmod button_driver

Tags: Linux GPIO Device Drivers atomic operations Embedded Systems

Publicado em 6-14 18:46 por Thomas