Drivers Linux: O passo a passo do driver para um display de 7 segmentos

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:

SegmentoPino HS-3161GPIO Raspberry
a10PA2
b9PA3
c8PA4
d5PA5
e4PA6
f2PA7
g3PA9
dp7PA10
Tabela 1 – Pinagem utilizada no projeto

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):

Driver Linux para Display de 7 segmentos

A seguir pode-se observar o esquemático do circuito montado:

Driver Linux para Display de 7 segmentos
Figura 1 – Esquemático do circuito do HS-3161

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

Device Drivers para Linux Embarcado – Introdução

BeagleBone – Carregando Device Tree Overlay pelo U-Boot

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Linux Embarcado » Drivers Linux: O passo a passo do driver para um display de 7 segmentos

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: