Este artigo tem como objetivo exemplificar a criação de um módulo de driver que pode ser utilizado para mais de um dispositivo através da adição de overlays na device-tree. O código do módulo demonstrado no decorrer do artigo refere-se a um driver para uma Orange Pi (armbian 22.05) que monitora as saídas de múltiplos circuitos com push buttons e pode ser acessado na íntegra aqui.Para mais detalhes do que são drivers de dispositivos carregados no kernel via módulos é recomendada a leitura deste artigo.
Esquemático circuitos
Abaixo está representado o exemplo do circuito com push button para o qual o módulo foi feito, neste caso se optou pelo resistor na configuração pull up e a saída Vout foi conectada a uma GPIO da Orange Pi. Portanto, quando o push button está em repouso, na saída Vout mede-se a tensão 3,3V (nível lógico alto ou 1) e quando o botão é pressionado mede-se uma tensão próxima de 0V (nível lógico baixo ou 0).
Como o driver funciona?
Este driver irá funcionar para um número indeterminado, porém limitado, de circuitos com push buttons. Toda vez que desejar monitorar mais um desses circuitos é necessário executar o shell script overlayGen e passar o número da GPIO da Orange Pi como parâmetro (a pinagem completa desse computador pode ser acessado aqui), o sistema, então, será reiniciado e a overlay criada será inserida no device tree do armbian.
Com todas as overlays adicionadas e com o módulo já carregado no kernel será possível acessar um diretório no caminho /sys/class para cada um dos circuitos previamente adicionados, o formato do nome desses diretórios é push_button_x , sendo x um número que identifica sequencialmente cada um dos circuitos. Portanto, caso você esteja monitorando três circuitos será possível acessar os seguintes diretórios: push_button_0, push_button_1 e push_button_2. Nesses diretórios será possível encontrar o arquivo pressNum que irá conter a quantidade de vezes que o botão em questão foi pressionado.
Passo a Passo
Headers do kernel
Para escrever um módulo de driver para uma distribuição Linux são necessários alguns headers em C que contém funções, structs e variáveis indispensáveis para o desenvolvimento do kernel. Para instalar esses headers via terminal para qualquer distribuição baseada em Debian (como o armbian) e qualquer versão de kernel basta usar o seguinte comando:
sudo apt-get install linux-headers-$(uname -r)Makefile
Para gerar o compilado do módulo do driver a partir do código C é necessário um arquivo Makefile. O ideal é criar um diretório, neste caso foi criado um chamado multi-devices-driver e lá um arquivo chamado Makefile com o seguinte conteúdo:
obj-m += pshBtns.o
all : modulo
modulo:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Por padrão este Makefile irá compilar o arquivo C do módulo, que foi chamado de pshBtns.c e gerar o arquivo pshBtns.ko necessário para carregar o módulo no kernel.
Funções do driver
Usualmente quando opta-se por carregar um driver no kernel via módulo cria-se no código principal, neste caso chamado pshBtns.c, duas funções para o módulo, uma de inicialização e uma de remoção, e as chama através das macros module_init e module_exit definidas no header module.h. Essa abordagem que foi utilizada neste artigo aqui não é tão trivial quando se deseja utilizar um mesmo módulo de driver para um número imprevisível de dispositivos, portanto para isso utilizamos o método de inicialização e remoção de drivers implementado no header platform_device.h.
O trecho de código abaixo exemplifica melhor essa abordagem, primeiro definimos duas structs, a primeira, of_device_id, está declarada no header of_device.h e leva como parâmetro o compatible do driver, que é uma label que identifica todos os dispositivos na device-tree que utilizam esse driver. A segunda, platform_driver é uma struct para a definição de um driver genérico de um dispositivo, ela recebe como parâmeteros a struct of_device_id e as funções de inicialização e remoção desse driver.A macro module_platform_driver garante que quando o módulo for carregado/removido no kernel as funções de inicialização e remoção, respectivamente pbtn_init_probe e pbtn_exit_remove, sejam chamadas sempre que identificado algum dispositivo no device-tree com o compatible definido. Então, por exemplo, se no device-tree existirem dois dispositivos com o compatible“emc-logic, pshBtns” será impressa duas vezes a mensagem Inicialização! no log do kernel (acessado via comando dmesg) quando o módulo for carregado. O análogo ocorre quando o módulo for removido.
#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of_device.h>
/* declaração de funções */
static int pbtn_init_probe(struct platform_device *pdev);
static int pbtn_exit_remove(struct platform_device *pdev);
/* Strcuts */
static struct of_device_id pshBtns_ids[] = {
{.compatible = "emc-logic,pshBtns"},
{/* end node */}
};
static struct platform_driver pshBtns_driver = {
.probe = pbtn_init_probe,
.remove = pbtn_exit_remove,
.driver = {
.name = "pshBtns_driver",
.owner = THIS_MODULE,
.of_match_table = pshBtns_ids,
}
};
/* Funções de inicialização e remoção do driver*/
static int pbtn_init_probe(struct platform_device *pdev){
printk(KERN_ALERT"Inicialização!");
return 0;
}
static int pbtn_exit_remove(struct platform_device *pdev){
printk("Remoção!\n");
return 0;
}
/* Módulo*/
MODULE_LICENSE("GPL");
module_platform_driver(pshBtns_driver);
Para carregar o módulo no kernel deve-se compilar o arquivo C através do Makefile, para isso basta utilizar o comando make e depois que o arquivo .ko for gerado deve-se proceder como indicado a seguir:
sudo insmod pshBtns.koJá para remoção do módulo pode ser realizada no terminal por:
sudo rmmod pshBtnsOverlays no Device-tree
Nos sistemas operacionais baseados em Linux temos uma estrutura de dados chamada device-tree que é responsável por informar a descrição dos periféricos e componentes do hardware ao kernel. As portas de entrada e saída de dados (GPIOs) são mapeadas nessa estrutura, por isso podemos acessá-las em um módulo de driver fazendo uma overlay no device-tree.
Uma overlay no device-tree consiste em um arquivo de dados que altera a atual composição da device-tree do hardware em questão, ele é compilado e acoplado assim como um módulo do kernel. Aqui queremos uma overlay para o armbian com a seguinte estrutura:
/dts-v1/;
/plugin/;
/{
compatible = "allwinner,sun8i-h3";
fragment@0 {
target-path = "/";
__overlay__{
my_device_0{
dev_num = <0>;
compatible = "emc-logic,pshBtns";
status = "okay";
pBtn_label = "rpi-gpio-12";
pBtn_gpio = <12>;
};
};
};
};
Note, que além do compatible indicado nessa overlay existem mais três variáveis importantes que devem ser identificadas pelo módulo para garantir o funcionamento do driver: dev_num, que indica qual o número do dispositivo, pBtn_label e pBtn_gpio que armazenam as informações da GPIO que está sendo utilizada pelo dispositivo. Supondo que esse arquivo de overlay receba o nome de overlay.dts,para carregá-la no armbian deve-se utilizar o seguinte comando e depois reiniciar o sistema:
armbian-add-overlay overlay.dtsComo o intuito aqui é utilizar esse módulo para múltiplos dispositivo o processo de criação de overlays foi automatizado, o código a seguir refere-se ao shell script chamado overlayGenque cria overlays já no padrão do armbian e desse módulo, as carrega no kernel e reinicia o sistema para que as modificações sejam feitas de forma correta.
#!/bin/bash
declare -i i=0
while [[ $i -lt 6 ]]
do
if [[ -f "overlay-$i.dts" ]]
then
echo "File overlay-$i.dts exists"
else
echo "File overlay-$i.dts does not exist"
echo "/dts-v1/;" > overlay-$i.dts
echo "/plugin/;" >> overlay-$i.dts
echo "/{" >> overlay-$i.dts
echo " compatible = \"allwinner,sun8i-h3\";" >> overlay-$i.dts
echo " fragment@0 {" >> overlay-$i.dts
echo " target-path = \"/\";" >> overlay-$i.dts
echo " __overlay__{" >> overlay-$i.dts
echo " my_device_$i{" >> overlay-$i.dts
echo " dev_num = <$i>;" >> overlay-$i.dts
echo " compatible = \"emc-logic,pshBtns\";" >> overlay-$i.dts
echo " status = \"okay\";" >> overlay-$i.dts
echo " pBtn_label = \"rpi-gpio-$1\";" >> overlay-$i.dts
echo " pBtn_gpio = <$1>;" >> overlay-$i.dts
echo " };" >> overlay-$i.dts
echo " };" >> overlay-$i.dts
echo " };" >> overlay-$i.dts
echo "};" >> overlay-$i.dts
armbian-add-overlay overlay-$i.dts
sudo reboot -h now
break
fi
((i++))
done
O script é bem simples, os nomes dos arquivos das overlays geradas por ele são padronizados como overlay-x.dts, sendo x o número do dispositivo que não pode ser repetido e que é sempre inicializado no 0. No caso exemplificado neste artigo limitamos a seis o número máximo de circuitos de push button que o driver pode monitorar, mas esse limite pode variar dependendo da disponibilidade de GPIOs do computador de placa única utilizado. Então, quando o script é executado ele verifica se já existem todos os arquivos overlay-x.dts, com x de 0 a 5, e caso não ele os cria sequencialmente levando em conta o número da GPIO passado por parâmetro. Para executar esse script com a GPIO 12 da Orange Pi, por exemplo, deve-se executar no terminal:
./overlayGenerator.sh 12Integração módulo com device-tree
A integração do módulo com os dados dos dispositivos armazenados na device-tree é feita na função de inicialização do driver e é necessária para que seja possível identificar qual push button foi pressionado via interrupções das GPIOs. O primeiro passo é declarar algumas variáveis:
/* Defines */
#define MAX_DEV_NUM 6
/* Variábeis globais */
int device_num = 0;
int times_onProbe = 0;
int times_onRemove = 0;
struct device_info{
int pBtn_gpio;
int dev_num;
const char *pBtn_label;
char buffer[15];
uint numOf_presses;
int irq_num;
};
A constante MAX_DEV_NUM vai determinar qual o limite de circuitos com push button que esse driver atende, apesar desse número poder ser adaptado para atender melhor às necessidades do hardware é IMPORTANTÍSSIMO que ele seja o mesmo utilizado como limite no script overlayGen para que não haja problemas com a alocação de memória. A variável device_num armazena o número do dispositivo que estamos tratando no momento, a times_onProbe e a times_onRemove contam respectivamente quantas vezes as funções de inicialização e remoção foram chamadas. A struct device_info armazena todos os dados importantes de cada dispositivo: seu número de identificação, os dados da GPIO e IRQ, o número de vezes que ele foi pressionado e o nome do seu diretório (buffer).
Assim, é possível alocar memória (usando o kmalloc do header slab.h) para o número máximo de dispositivos que podem ser usados e armazenar as informações de todos os dispositivos que estão sendo monitorados na função inicialização através das funções device_property_read_u32 e device_property_read_string (mais detalhes no header property.h):
struct device_info *pBtn_info = NULL;
static int pbtn_init_probe(struct platform_device *pdev){
int ret;
struct device *dev = &pdev->dev;
printk(KERN_ALERT"Inicialização!");
/* Caso seja a primeira vez na função: alocar memória */
if(times_onProbe == 0){
/* alocação device_info */
pBtn_info = (struct device_info *)kmalloc(MAX_DEV_NUM*sizeof(struct device_info), GFP_ATOMIC);
if(!pBtn_info){
return -ENOMEM;
printk("device_info erro de alocação");
}
printk("Primeira vez na inicialização!");
}else{
printk("Não é a primeira vez na inicialização!");
}
/* Qual o nº do dispositivo sendo inicializado? */
ret = device_property_read_u32(dev,"dev_num",&device_num);
(pBtn_info+device_num)->dev_num = device_num;
/* Armazenando os dados na struct */
ret = device_property_read_string(dev,"pBtn_label",&(pBtn_info+device_num)->pBtn_label);
ret = device_property_read_u32(dev,"pBtn_gpio",&(pBtn_info+device_num)->pBtn_gpio);
sprintf((pBtn_info+device_num)->buffer, "%s_%d", "push_button", device_num);
printk("Device number is: %d", device_num);
printk("GPIO pin is: %d", (pBtn_info+device_num)->pBtn_gpio);
times_onProbe = times_onProbe + 1;
return 0;
}
Na remoção do módulo a memória deve ser liberada:
static int pbtn_exit_remove(struct platform_device *pdev){
/* Caso seja a primeira vez na função: desalocar memória */
if(times_onRemove == 0){
printk("Primeira vez na função de remoção!");
kfree(pBtn_info);
}
times_onRemove = times_onRemove + 1;
printk("Remoção!\n");
return 0;
}
Classes e arquivos
Como citado anteriormente, cada dispositivo adicionado para ser monitorado pelo driver tem um diretório no caminho /sys/class e um arquivo chamado pressNum que contém o número de vezes que o push button em questão foi pressionado. Por padrão, o nome deste diretório é push_button_(número do dispositivo), então, por exemplo, se o dispositivo que está sendo inicializado tem na overlay a variável dev_num indicando 0, seu diretório será /sys/class/push_button_0.
Para um dispositivo ter um diretório no /sys/class ele precisa de uma classe, as informações de uma classe são sempre registradas em uma struct chamada class definida no header device.h. Cada um dos dispositivos possui um classe diferente, portanto sempre que a função de inicialização for chamada deve-se registrar uma nova classe com o nome armazenado na variável buffer da struct pBTn_info, na primeira vez que a função de inicialização for chamada, no entanto, deve-se por segurança alocar o espaço na memória para as classes do número máximo de dispositivos que podem ser monitorados:
struct device_info *pBtn_info = NULL;
static struct class *device_class = NULL;
static int pbtn_init_probe(struct platform_device *pdev){
int ret;
struct device *dev = &pdev->dev;
static struct lock_class_key __key;
printk(KERN_ALERT"Inicialização!");
/* Caso seja a primeira vez na função: alocar memória */
if(times_onProbe == 0){
/* class allocation */
device_class = (struct class *)kmalloc(MAX_DEV_NUM*sizeof(struct class),GFP_ATOMIC);
if(!device_class){
printk("classe erro de alocação");
return -ENOMEM;
}
/* alocação device_info */
pBtn_info = (struct device_info *)kmalloc(MAX_DEV_NUM*sizeof(struct device_info), GFP_ATOMIC);
if(!pBtn_info){
printk("device_info erro de alocação");
return -ENOMEM;
}
printk("Primeira vez na inicialização!");
}else{
printk("Não é a primeira vez na inicialização!");
}
/* Qual o nº do dispositivo sendo inicializado? */
ret = device_property_read_u32(dev,"dev_num",&device_num);
(pBtn_info+device_num)->dev_num = device_num;
/* Armazenando os dados na struct */
ret = device_property_read_string(dev,"pBtn_label",&(pBtn_info+device_num)->pBtn_label);
ret = device_property_read_u32(dev,"pBtn_gpio",&(pBtn_info+device_num)->pBtn_gpio);
sprintf((pBtn_info+device_num)->buffer, "%s_%d", "push_button", device_num);
printk("Device number is: %d", device_num);
printk("GPIO pin is: %d", (pBtn_info+device_num)->pBtn_gpio);
/* Registro da classe de cada um dos dispositivos */
(device_class+device_num)->name = (pBtn_info+device_num)->buffer;
(device_class+device_num)->owner = THIS_MODULE;
ret = __class_register((device_class+device_num),&__key);
times_onProbe = times_onProbe + 1;
return 0;
}Como em cada um dos diretórios encontra-se o arquivo pressNum, também é necessário alocar memória para eles e registrá-los. Assim, para cada um dos arquivos é preciso atribuir uma struct chamada class_attribute, que também encontra-se no header device.h, e duas funções: uma que é chamada quando o arquivo é aberto e outra quando ele é fechado.
Neste caso, iremos usar as mesmas funções de arquivos para todos os dispositivos, portanto, toda vez que a função de abertura de arquivo for chamada precisamos identificar de qual dispositivo o usuário quer ler o número de vezes que o push button foi pressionado. Para isso lemos o nome da classe que o arquivo pertence, seu décimo terceiro caractere refere-se ao número do dispositivo, que aqui não ultrapassa uma dezena, transformamos isso para um inteiro e fazemos a conversão da tabela ASCII (o caractere 0 corresponde ao inteiro 48, por isso sempre retira-se 48 do valor obtido):
static ssize_t show_pressNum( struct class *class, struct class_attribute *attr, char *buf );
static ssize_t store_pressNum( struct class *class, struct class_attribute *attr, const char *buf, size_t count );
/* Funções de arquivos: ler e escrever */
static ssize_t show_pressNum( struct class *class, struct class_attribute *attr, char *buf ){
uint value = 0;
int num = 0;
num = (int)(*class).name[12];
num = num - 48;
printk("Numero do push button: %d, Arquivo lido!", num);
value = (pBtn_info+num)->numOf_presses;
return sprintf(buf, "%d", value);
}
static ssize_t store_pressNum( struct class *class, struct class_attribute *attr, const char *buf, size_t count ){
printk("Não é possível escrever nesse arquivo!\n");
return count;
}A partir disso, basta alocar memória para os arquivos e registrá-los na função de inicialização, de maneira análoga ao que fizemos para as classes:
struct device_info *pBtn_info = NULL;
static struct class *device_class = NULL;
struct class_attribute *class_attr = NULL;
static int pbtn_init_probe(struct platform_device *pdev){
int ret;
struct device *dev = &pdev->dev;
static struct lock_class_key __key;
printk(KERN_ALERT"Inicialização!");
/* Caso seja a primeira vez na função: alocar memória */
if(times_onProbe == 0){
/* alocação classe */
device_class = (struct class *)kmalloc(MAX_DEV_NUM*sizeof(struct class),GFP_ATOMIC);
if(!device_class){
printk("classe erro de alocação");
return -ENOMEM;
}
/* alocação atributo de arquivo */
class_attr = (struct class_attribute *)kmalloc(MAX_DEV_NUM*sizeof(struct class_attribute), GFP_ATOMIC);
if(!class_attr){
printk("atributo de arquivo erro de alocação");
return -ENOMEM;
}
/* alocação device_info */
pBtn_info = (struct device_info *)kmalloc(MAX_DEV_NUM*sizeof(struct device_info), GFP_ATOMIC);
if(!pBtn_info){
printk("device_info erro de alocação");
return -ENOMEM;
}
printk("Primeira vez na inicialização!");
}else{
printk("Não é a primeira vez na inicialização!");
}
/* Qual o nº do dispositivo sendo inicializado? */
ret = device_property_read_u32(dev,"dev_num",&device_num);
(pBtn_info+device_num)->dev_num = device_num;
/* Armazenando os dados na struct */
ret = device_property_read_string(dev,"pBtn_label",&(pBtn_info+device_num)->pBtn_label);
ret = device_property_read_u32(dev,"pBtn_gpio",&(pBtn_info+device_num)->pBtn_gpio);
sprintf((pBtn_info+device_num)->buffer, "%s_%d", "push_button", device_num);
printk("Device number is: %d", device_num);
printk("GPIO pin is: %d", (pBtn_info+device_num)->pBtn_gpio);
/* Registro da classe de cada um dos dispositivos */
(device_class+device_num)->name = (pBtn_info+device_num)->buffer;
(device_class+device_num)->owner = THIS_MODULE;
ret = __class_register((device_class+device_num),&__key);
/* Registro atributo de arquivos */
(*(class_attr + device_num)).show = show_pressNum;
(*(class_attr + device_num)).store = store_pressNum;
(*(class_attr + device_num)).attr.name = "pressNum";
(*(class_attr + device_num)).attr.mode = 0777 ;
ret = class_create_file((device_class+device_num), &(*(class_attr+device_num)));
times_onProbe = times_onProbe + 1;
return 0;
}
Lembrando que toda memória alocada na inicialização deve ser liberada na remoção:
static int pbtn_exit_remove(struct platform_device *pdev){
if(times_onRemove == 0){
printk("Primeira vez na função de remoção!\n");
class_unregister(device_class);
class_destroy(device_class);
kfree(class_attr);
kfree(pBtn_info);
}else{
printk("Não é a primeira vez na função de remoção!");
class_unregister((device_class+times_onRemove));
class_destroy((device_class+times_onRemove));
}
times_onRemove = times_onRemove + 1;
printk("Remoção!\n");
return 0;
}
GPIO e Interrupção
Por fim, como o intuito é registrar quantas vezes o push button de cada dispositivo foi pressionado, precisamos alocar um pino de entrada digital para os circuitos e também habilitar a interrupção para cada um deles. Já vimos como as overlays são inseridas na device tree passando o pino GPIO por parâmetro e, na inicialização de cada um dos dispositivos, já vimos como armazenar o número desse pino na struct pBtn_info. Agora, utilizaremos funções definidas nos headers consumer.h, gpio.h e interrupt.h para registrar as GPIOs e as interrupções dos dispositivos na função de inicialização, para isso deve-se acrescentar na função os seguintes trechos de código:
/* Alocando a GPIO para o dispositivo*/
if(gpio_request((pBtn_info+device_num)->pBtn_gpio, (pBtn_info+device_num)->pBtn_label)){
printk("Erro!\n");
return -1;
}
/* Definindo a GPIO como entrada */
if(gpio_direction_input((pBtn_info+device_num)->pBtn_gpio)) {
printk("Erro!\n");
gpio_free((pBtn_info+device_num)->pBtn_gpio);
return -1;
}
/* Salvando o número da IRQ correspondente a GPIO */
(pBtn_info+device_num)->irq_num = gpio_to_irq((pBtn_info+device_num)->pBtn_gpio);
printk("IRQ num:%d", (pBtn_info+device_num)->irq_num);
/* Habilitando a interrupção - trigger de 1 para 0 */
if(request_irq((pBtn_info+device_num)->irq_num, (irq_handler_t) gpio_irq_handler, IRQF_TRIGGER_FALLING, "my_gpio_irq", NULL) != 0){
printk("Interrupt error!\n: %d\n", (pBtn_info+device_num)->irq_num);
gpio_free((pBtn_info+device_num)->pBtn_gpio);
return -1;
}
/* Habilitando debounce de 100ms */
if(gpio_set_debounce((pBtn_info+device_num)->pBtn_gpio,100)){
printk("Debounce!\n");
}
Logo, define-se a função de interrupção que quando chamada, neste contexto, deve identificar em qual pino houve o trigger e adicionar 1 ao número de quantas vezes o push button em questão foi pressionado:
static irq_handler_t gpio_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs);
/* Callback da interrupção */
static irq_handler_t gpio_irq_handler(unsigned int irq, void *dev_id, struct pt_regs *regs) {
int irq_count;
printk(KERN_ALERT "Interrupção!\n");
/*Identificando o pino*/
for(irq_count=0; irq_count<=times_onProbe;irq_count++){
if((pBtn_info+irq_count)->irq_num == irq){
break;
}
}
printk("O push button %d foi pressionado", (pBtn_info+irq_count)->dev_num);
/* Evitando estourar a memória do inteiro */
if((pBtn_info+irq_count)->numOf_presses <= 1000000){
(pBtn_info+irq_count)->numOf_presses = (pBtn_info+irq_count)->numOf_presses + 1;
}
return (irq_handler_t) IRQ_HANDLED;
}Na função de remoção do módulo as GPIOs devem ser liberadas e a interrupção desabilitada:
static int pbtn_exit_remove(struct platform_device *pdev){
if(times_onRemove == 0){
printk("Primeira vez na função de remoção!\n");
class_unregister(device_class);
class_destroy(device_class);
kfree(class_attr);
kfree(pBtn_info);
free_irq(pBtn_info->irq_num,NULL);
gpio_free(pBtn_info->pBtn_gpio);
}else{
printk("Não é a primeira vez na função de remoção!");
class_unregister((device_class+times_onRemove));
class_destroy((device_class+times_onRemove));
free_irq((pBtn_info+times_onRemove)->irq_num,NULL);
gpio_free((pBtn_info+times_onRemove)->pBtn_gpio);
}
times_onRemove = times_onRemove + 1;
printk("Remoção!\n");
return 0;
}
Conclusão
No decorrer deste artigo foi demonstrada uma maneira de fazer um módulo de driver que possa ser utilizado para mais de um dispositivo semelhante, aqui fizemos esse módulo especificamente para uma Orange Pi (rodando armbian) e para monitorar a saída digital de um circuito simples com push button, mas os conceitos podem ser replicados para qualquer plataforma que rode qualquer distribuição Linux e para qualquer dispositivo: monitorando via GPIO, I2C, SPI, UART e etc. O interessante do circuito com push button é que ele simula eletricamente o comportamento de alguns sensores digitais como, por exemplo, os sensores de presença. Uma pequena demonstração do resultado deste driver sendo utilizado para monitorar quatro dispositivos pode ser visualizado no vídeo abaixo:
Referências
Linux Device Drivers, 3rd Edition – Jonathan Corbet, Alessandro
Rubini e Greg Kroah-Hartman – O’Reilly Media.









Ótimo artigo.
Me ajudou muito.
Parabéns!
Obrigada, Matheus!
Fico feliz que tenha ajudado 🙂