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