Este artigo mostra o passo a passo do desenvolvimento de um driver baseado em device-tree para um display de 7 segmentos de cátodo comum. Neste caso estamos utilizando o computador de placa única Raspberry Pi Zero W (com a distro Raspbian 10 LTS) e o display HS-3161, mas é possível replicar para outros computadores de placa única e outros displays trocando a pinagem e fazendo as devidas mudanças no arquivo de overlay do device-tree.
O repositório com o código completo pode ser acessado aqui.
Módulos de driver
Uma característica muito interessante das distribuições Linux é a possibilidade de adicionar ou remover algumas funcionalidades do sistema operacional enquanto ele já está operando. Os compilados de códigos que podem ser carregados a qualquer momento ao kernel para adicionar essas funcionalidades são chamados de módulos e podem ser classificados em diversos grupos de acordo com a sua aplicabilidade no sistema. Neste artigo estaremos tratando especificamente de um driver carregado ao kernel via módulo, ou seja, um código que permite a comunicação do kernel com um hardware (neste caso o display HS-3161) e que pode ser carregado ao sistema dinamicamente.
Esquemático e pinagem
Para facilitar o entendimento do projeto, a tabela a seguir mostra quais GPIOs da Raspberry Pi foram conectadas a cada pino do display:
| Segmento | Pino HS-3161 | GPIO Raspberry |
| a | 10 | PA2 |
| b | 9 | PA3 |
| c | 8 | PA4 |
| d | 5 | PA5 |
| e | 4 | PA6 |
| f | 2 | PA7 |
| g | 3 | PA9 |
| dp | 7 | PA10 |
O componente HS-3161 é um display de LEDs de 7 segmentos com configuração de cátodo comum, ou seja, os cátodos de todos os LEDs são conectados e devem ser aterrados. Para acender cada um dos segmentos precisamos fornecer pelo menos a mínima corrente de operação deste LED.
Como os segmentos devem ser acionados separadamente, cada um precisou ser conectado a uma GPIO diferente da Raspberry Pi, a qual fornece uma tensão de 3.3V nesses pinos quando a saída lógica é 1. O modelo de display usado foi HS-3161AS, que possui segmentos de LED vermelho, assim, considerando que a queda em cada LED seria de aproximadamente 2V a utilização de um resistor de 100Ω resultaria em uma corrente de 13mA, que é o suficiente para um LED operar (normalmente a corrente de operação fica entre 6mA e 20mA):
A seguir pode-se observar o esquemático do circuito montado:
Como o driver funciona?
Depois de carregar o módulo será possível encontrar os arquivos do driver na pasta /sys/class/7segment. Para acender e desligar o LED do ponto deve-se escrever, respectivamente, 1 e 0 no arquivo enableDP. O valor escrito no arquivo value será o número exibido no display, caso seja escrito um valor superior a 9 será exibido o caractere E de erro. A leitura dos arquivos irá retornar o último valor escrito.
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 em qualquer versão de kernel basta usar o seguinte comando:
sudo apt-get install linux-headers-$(uname -r)
No caso específico da Raspberry Pi também é possível fazer a instalação através do comando:
sudo apt-get install raspberrypi-kernel-headers
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 LinuxDeviceDrivers e lá um arquivo chamado Makefile com o seguinte conteúdo:
obj-m += 7segment.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 7segment.c, e gerar o arquivo 7segment.ko necessário para carregar o módulo no kernel.
Funções do Módulo
No arquivo 7segment.c devem estar definidas duas funções referentes ao módulo, uma que será chamada quando o módulo é carregado no kernel e outra que é chamada quando ele é removido do kernel. A seguir pode-se observar um exemplo de declaração dessas funções:
#include <linux/module.h>
#include <linux/kernel.h>
/***************Funções de Módulo****************/
MODULE_LICENSE("GPL");
static int segmentsDisplay_init(void)
{
printk(KERN_ALERT "MÓDULO INICIALIZADO!");
return 0;
}
static void segmentsDisplay_exit(void)
{
printk(KERN_ALERT "MÓDULO REMOVIDO!");
}
module_init(segmentsDisplay_init);
module_exit(segmentsDisplay_exit);Para essa implementação foram necessários dois headers. No header module.h consta a definição das macros MODULE_LICENSE(), module_init() e module_exit(), através delas atribui-se, respectivamente, a licença do módulo e as funções de inicialização e remoção do módulo. A função de inicialização por padrão retorna um static int e, neste caso, quando chamada, emite a mensagem “MÓDULO INICIALIZADO” no log do kernel (acessado via terminal pelo comando dmesg). Já a função de remoção do módulo não retorna nada por definição e aqui emite a mensagem “MÓDULO REMOVIDO” no log do kernel.
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 7segment.ko
Já para remoção do módulo pode ser realizada no terminal por:
sudo rmmod 7segment
Diretório no /sys/class
Para acessar os arquivos do drive do display é necessário criar uma classe e consequentemente um diretório no caminho /sys/class. As informações da classe são registradas em uma struct chamada class definida no header device.h, a seguir consta como esta struct foi declarada globalmente no código:
static struct class *device_class = NULL;
Depois é preciso alocar um espaço da memória para esta classe, aqui utilizamos a função kzalloc definida no header slab.h. Abaixo está indicado o trecho de código que foi incrementado na função de inicialização do módulo para que a classe fosse devidamente criada e nomeada como 7segment:
/*Criando a classe no diretório /sys/class*/
device_class = (struct class *)kzalloc(sizeof(struct class),GFP_ATOMIC);
if(!device_class){
printk("Erro na alocação da classe!");
}
device_class->name = "7segment";
device_class->owner = THIS_MODULE;
ret = __class_register(device_class,&key);O procedimento correto quando o módulo é removido é limpar a memória alocada para essa classe e destruir essa classe no /sys/class, portanto a função de remoção do módulo também deve ser modificada, com o acréscimo das seguintes linhas de código:
/*Destruindo a classe no /sys/class*/
class_unregister(device_class);
class_destroy(device_class);
Arquivos do driver
Após a criação da classe deve-se criar os arquivos que permitem a integração do usuário com display e que estarão localizados dentro do diretório /sys/class/7segment. Para cada arquivo desta classe é preciso atribuir uma struct chamada class_attribute, que encontra-se no header device.h, e também duas funções: uma que é chamada quando o arquivo é aberto e outra quando ele é fechado. Como explicitado acima, existem dois arquivos para este driver: um para acender/desligar o led do ponto e outro para o usuário definir qual número será mostrado no display. O trecho seguinte exemplifica a declaração e definição das funções para um deles, o arquivo value:
/********Declaração das funções dos arquivos********/
static ssize_t show_value( struct class *class, struct class_attribute *attr, char *buf );
static ssize_t store_value( struct class *class, struct class_attribute *attr, const char *buf, size_t count );
/***************Variaveis Globais*****************/
volatile int value_display;
/********Funções de escrita e leitura arquivo value*********/
static ssize_t show_value( struct class *class, struct class_attribute *attr, char *buf ){
printk("Valor do display - LEITURA!");
return sprintf(buf, "%d", value_display);
}
static ssize_t store_value( struct class *class, struct class_attribute *attr, const char *buf, size_t count ){
printk("Valor do display - ESCRITA!");
sscanf(buf,"%d",&value_display);
return count;
}Quando o arquivo value for lido, por exemplo, a função show_value irá exibir o último número escrito no arquivo, que é sempre registrado na variável value_display, e exibirá a frase “Valor do display – ESCRITA!” no log do kernel. Mas para isso acontecer deve-se relacionar um atributo de cada arquivos com as funções de arquivo e com classe criada no /sys/class. A seguir temos a exemplificação da definição de um atributo de arquivo:
struct class_attribute *attr_value = NULL;
E, então, como deve-se registrar esse atributo, criando o arquivo, na função de inicialização do módulo:
/*Criando o arquivo /sys/class/7segment/value*/
attr_value = (struct class_attribute *)kzalloc(sizeof(struct class_attribute ),GFP_ATOMIC);
attr_value->show = show_value;
attr_value->store = store_value;
attr_value->attr.name = "value";
attr_value->attr.mode = 0777 ;
ret = class_create_file(device_class, attr_value);
return 0;
}
Um ponto muito interessante é que uma das variáveis que atributos de arquivos recebem é a mode, ela define qual a permissão de leitura e escrita destes arquivos. A permissão 0777 permite que qualquer usuário escreva ou leia o arquivo, na maioria das aplicações ela é indesejada, é recomendado que o desenvolvedor atente-se para qual permissão é mais adequada para o seu projeto.
Para destruir um arquivo na remoção do módulo deve-se liberar a memória alocada para seu atributo na função de remoção do módulo:
/*Destruindo o arquivo do /sys/class*/
kfree(attr_value);
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 dinamicamente ao kernel. A estrutura da overlay para o Raspbian, que reserva os pinos para este projeto, foi salva em um arquivo chamado overlay.dts, seu conteúdo pode ser observado abaixo:
/dts-v1/;
/plugin/;
/{
compatible = "brcm,bcm2835";
fragment@0 {
target-path = "/";
__overlay__{
my_device{
compatible = "emc-logic,7segment";
status = "okay";
a-gpio = <&gpio 2 0>;
b-gpio = <&gpio 3 0>;
c-gpio = <&gpio 4 0>;
d-gpio = <&gpio 5 0>;
e-gpio = <&gpio 6 0>;
f-gpio = <&gpio 7 0>;
g-gpio = <&gpio 9 0>;
dp-gpio = <&gpio 10 0>; };
};
};
};
O formato da overlay pode mudar para cada distro de Linux, mas parte da formatação do arquivo sempre se mantém a mesma. Em geral, é importante atentar-se às variáveis compatible, a primeira refere-se a compatibilidade do processador (o bcm2835 é o SoC utilizado na Raspberry Pi Zero W) e a segunda, a compatibilidade do driver, indica o nome do driver e qual a empresa ou desenvolvedor responsável pela manutenção dele. Este segundo compatible será indispensável nos próximos passos para que o módulo reconheça qual overlay contém as alterações necessárias para que o driver funcione corretamente.
A overlay, então, precisa ser compilada e depois carregada no device-tree. Esse procedimento também varia com cada distro, o Makefile com a adição do comando para a compilação da overlay no Raspbian segue abaixo:
obj-m += 7segment.o
all : modulo dt
dt: overlay.dts
dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
modulo:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm -rf overlay.dtbo
Para carregar o overlay no Raspbian basta executar o comando no terminal:
sudo dtoverlay overlay.dtbo
Driver
Por fim, temos que realizar a integração da overlay de device-tree com o módulo do driver para que seja possível alterar o estado das GPIOs da Raspberry Pi de acordo com o que é escrito nos arquivos localizados no /sys/class/7segment. Para isso precisamos definir duas structs, a primeira, of_device_id, está declarada no header of_device.h e leva como parâmetro o compatible do driver explicado no tópico acima, ela é usada para identificar todos os dispositivos na device-tree que utilizam esse driver. A segunda, platform_driver, declarada no header platform_device.h, é uma struct para a definição de um driver genérico de um dispositivo, ela recebe como parâmetros a struct of_device_id (caso utilize uma overlay com determinado compatible) e as funções de inicialização e remoção desse driver:
/***************Structs Globais******************/
static struct of_device_id driver_ids[] = {
{.compatible = "emc-logic,7segment"},
{/* end node */}
};
static struct platform_driver display_driver = {
.probe = gpio_init_probe,
.remove = gpio_exit_remove,
.driver = { .name = "display_driver",
.owner = THIS_MODULE,
.of_match_table = driver_ids, }
};
É preciso, então, definir e declarar as funções de inicialização (conhecida como probe) e de remoção do driver:
/***************Variaveis Globais*****************/
struct gpio_desc *a, *b, *c, *d;
struct gpio_desc *e, *f, *g, *dp;
/******Declaração funções do Driver********/
static int gpio_init_probe(struct platform_device *pdev);
static int gpio_exit_remove(struct platform_device *pdev);
/********Funções do Driver*********/
static int gpio_init_probe(struct platform_device *pdev){
printk("DRIVER INICIALIZADO!");
a = devm_gpiod_get(&pdev->dev, "a", GPIOD_OUT_LOW);
b = devm_gpiod_get(&pdev->dev, "b", GPIOD_OUT_LOW);
c = devm_gpiod_get(&pdev->dev, "c", GPIOD_OUT_LOW);
d = devm_gpiod_get(&pdev->dev, "d", GPIOD_OUT_LOW);
e = devm_gpiod_get(&pdev->dev, "e", GPIOD_OUT_LOW);
f = devm_gpiod_get(&pdev->dev, "f", GPIOD_OUT_LOW);
g = devm_gpiod_get(&pdev->dev, "g", GPIOD_OUT_LOW);
dp = devm_gpiod_get(&pdev->dev, "dp", GPIOD_OUT_LOW);
return 0;
}
static int gpio_exit_remove(struct platform_device *pdev){
printk("DRIVER REMOVIDO!");
return 0;
}
Note que na função de inicialização, explicitada acima, é utilizada a função devm_gpiod_get para inicializar todas as GPIOs, referentes a cada um dos segmentos definidos na overlay, em nível lógico baixo e atribuí-las às structs gpio_desc, de modo que seja possível alterar seus estados posteriormente. Para mais detalhes dessa função e struct é interessante verificar o header gpio/consumer.h. Com a declaração e definição das funções de inicialização e remoção do driver é preciso registrá-lo no módulo, para isso os seguintes trecho foram, respectivamente, acrescentados nas funções de inicialização e remoção do módulo:
/*Inicializando o driver*/ if(platform_driver_register(&display_driver)){
printk("ERRO! Não foi possível carregar o driver! \n"); return -1;
}
/*Remoção do driver*/
platform_driver_unregister(&display_driver);
As últimas alterações para garantir o funcionamento pleno deste módulo de driver devem ser feitas nas funções de escrita e leitura dos arquivos enableDP e value. Para escrever qualquer nível lógico nas GPIOs dos segmentos do display utilizamos a função gpiod_set_value, o exato segmento que terá o nível lógico alterado é reconhecido pela struct gpio_desc que lhe foi atribuída na função de inicialização do driver. Assim, no caso do ponto, o nível lógico do segmento dp deve receber o último valor escrito pelo usuário no arquivo enableDP. Já o valor escrito pelo usuário no arquivo value (armazenado na variável value_display) deve modificar o nível lógico dos segmentos de a,b,c,d,e,f e g para que o número correto seja exibido no display. O trecho de código a seguir mostra, por exemplo, como exibir o número 3 no display:
gpiod_set_value(a, 1);
gpiod_set_value(b, 1);
gpiod_set_value(c, 0);
gpiod_set_value(d, 1);
gpiod_set_value(e, 1);
gpiod_set_value(f, 0);
gpiod_set_value(g, 1);
Conclusão
Existem diversas maneiras de se fazer um driver de um dispositivo em uma distro Linux, este artigo, apesar de demonstrar o passo a passo de um driver específico, esclarece alguns pontos que podem ser muito úteis em diversas aplicações como, por exemplo, criar uma classe no /sys/class, acessar GPIOs via uma overlay no device-tree e registrar atributos e funções de arquivos. Uma pequena demonstração do resultado pode ser visualizado no video abaixo:
Referências
Linux Device Drivers, 3rd Edition – Jonathan Corbet, Alessandro Rubini e Greg Kroah-Hartman – O’Reilly Media.
https://github.com/Johannes4Linux/Linux_Driver_Tutorial/tree/main/21_dt_gpio
Saiba mais
Exemplo de Device Driver I2C para Linux Embarcado






