DEV Community

Guilherme Giácomo Simões
Guilherme Giácomo Simões

Posted on • Updated on

Driver de display de 7seg

Introdução
Começando agora o quarto episódio da série "introdução ao desenvolvimento de drivers para o kernel linux".
Se você caiu de paraquedas nesse texto, o link com todos os episódios dessa série, está aqui
Hoje finalmente escreveremos nosso primeiro driver para linux. Mais especificamente para o raspberry pi (no meu caso 3). A versão do kernel do meu raspberry é a 6.1-rc7.
Eu vou começar pelo raspberry por alguns motivos. Primeiro que vamos mapear os pinos do GPIO do raspberry para ser usado por nosso driver e para isso precisamos introduzir o conceito de device-tree que é importante para os desenvolvedores de driver, e segundo porque é uma comunicação simples de fazer e me parece um bom caminho para começar.
Nos próximos textos faremos drivers para USB utilizando classes existentes.

Queria salientar que esse é um texto maior e mais denso do que os anteriores, então é preciso que você leia, releia e pratique para conseguir entender os conceitos.

Circuito
O circuito é muito simples. Somente um display de 7 segmentos em uma protoboard com resistires de +/- 220Ω. O display de 7segmentos espera uma corrente entre 6mA e 20mA. A tensão emitida pela GPIO do raspberry é 3,3v. Então 3,3v/220Ω = 15mA. Estamos dentro do range de 6mA a 20mA. O esquema do circuito fica como abaixo:

7 segmento circuito

Da pra perceber que não sou a melhor pessoa do mundo para fazer esquemas de circuito, mas acho que da para ter uma noção. Para ajudar, vou deixar também uma tabela com a descrição das conexões:

Segmento GPIO
A 2
B 3
C 4
D 5
E 6
F 7
G 9

Aqui a foto de como ficou meu circuito montado na protoboard:
Imagem protoboard 1

Imagem protoboard 2

O que sao device-tree
No kernel linux temos uma estrutura de dados chamado device-tree que fica responsável por informar a descrição dos periféricos de IO para o kernel.

Em varias situações do nosso dia-a-dia temos que usar documentos ou ferramentas que nos ajudam a descrever certas funcionalidades de algo. Manuais de usuario nos auxiliam a entender as funcionalidades de um produto. Device-tree em sistemas Linux Embarcados compartilham dessa mesma particularidade, e servem para descrever com precisão como sera configurado e utilizado o hardware.

O que faremos aqui é o chamado overlay do device-tree. Um overlay do device-tree consiste em um arquivo de dados que altera a estrutura atual do device-tree do hardware em questão. Assim como os módulos ele pode ser acoplado dinamicamente ao kernel.

Explicando a estrutura do device-tree
Segue abaixo nosso device-tree para acoplar ao kernel chamado overlay.dts:

/dts-v1/;
/plugin/;
/{   
  compatible = "brcm,bcm2835";
  fragment@0 {         
    target-path = "/";         
    __overlay__{             
      my_device{
        compatible = "ggs-prd,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>;
    };         
    };      
  }; 
};

Enter fullscreen mode Exit fullscreen mode

A primeira linha /dts-v1/; é usada para informar a versão do dts.

A segunda linha /plugin/ é usada para informar que esse overlay de device-tree é um plugin

A linha compatible = "brcm,bcm2835"; descreve para qual plataforma esse device-tree foi feito para funcionar. Aqui existe uma regra super importante, ele começa sempre na mais compativel e vai para a menos compativel. Então nesse caso a plataforma mais compativel para a qual esse device-tree foi feito é a brcm, e a segunda é a bcm2835.
É importante sempre mencionar todas as plataformas para a qual você quer que o overlay funcione porque vão acontecer erros nas plataformas que não forem mencionadas.
Nesse caso, brcm e bcm2835 fazem referencia a fabricante Broadcom, responsavel por fabricar os chips do raspberry pi.

A linha com fragment@0 é o inicio dos fragmentos do device-tree. Aqui descreveremos qual dispositivo sera sobreposto.

A linha que contem o segundo "compatible" compatible = "ggs-prd,7segment"; é super importante, ele é o identificador do nosso driver em questão. Ele indica o nome do driver e qual a empresa ou dev responsavel pela manutenção dele. Esse segundo compatible é indispensável para nossos próximos passos para que o modulo reconheça qual overlay contém as alterações necessárias para que o driver funcione corretamente.

Nas linhas a-gpio = <&gpio 2 0>;, b-gpio = <&gpio 3 0>; e assim por diante, apesar de nao parecer é um conceito muito simples. Estamos mapeando as portas gpio para serem uma porta de saida de dados. Por isso o 0 no fim.

Criando um novo driver
Para começar, vamos criar dois arquivos 7segment.c e um 7segment.h. E criar uma classe e um atributo de classe como ensinamos nos textos anteriores:

7segment.h

#ifndef __7SEGMENT_H__
#define __7SEGMENT_H__

static struct class *device_class = NULL;
static struct class_attribute *attr = NULL;

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

volatile int value_display;

#endif 
Enter fullscreen mode Exit fullscreen mode

7segment.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/device.h>

#include "7segment.h" 

MODULE_AUTHOR("SEU NOME <seu_email@email.com>");
MODULE_DESCRIPTION("7segment");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.0");

static ssize_t show_value(struct class *class, 
        struct class_attribute *attr, char* buf)
{
    pr_info("valor do display %s - LEITURA", value_display);
    return sprintf(buf, "%d", value_display);
}

static ssize_t store_value(struct class *class, 
        struct class_attribute *attr, const char* buf, size_t count)
{
    sscanf(buf, "%d", &value_display);
    pr_info("valor do display %d - ESCRITA", value_display);
    return count;
}

static int __init class_init(void)
{
    device_class = (struct class *) kzalloc(sizeof(struct class), GFP_ATOMIC);
    if(!device_class) 
      pr_err("ERRO NA ALOCACAO DA CLASSE");

    device_class->name = "7segment";
    class_register(device_class);

    attr = (struct class_attribute *) kzalloc(sizeof(struct class_attribute), GFP_ATOMIC);
    attr->show = show_value;
    attr->store = store_value;
    attr->attr.name = "value";
    attr->attr.mode = 0777;
    class_create_file(device_class, attr);

    pr_info("class registrada");

    return 0;
}

static void __exit class_exit(void)
{
    class_unregister(device_class);
    class_destroy(device_class);
    pr_info("Modulo removido");

}

module_init(class_init);
module_exit(class_exit);
Enter fullscreen mode Exit fullscreen mode

Pronto, nada de diferente do que fizemos nos textos anteriores. É exatamente o mesmo codigo do texto anterior.

Agora precisamos realizar a integracao do nosso novo device-tree com o nosso driver para conseguirmos alterar o estado das GPIOs do raspberry.
Para isso, primeiro vou criar uma struct do tipo of_device_id que está declarado em /include/linux/of_device.h e passar como parametro para ela o compatible do driver explicado anteriormente. Ele vai vincular o nosso driver atual com esse "identificador" e ela conseguirá identificar todos os devices na device-tree que utilizam esse driver.
Então vamos declarar e atribuir o parametro na struct dentro do nosso 7segment.h

//...
static struct of_device_id driver_ids[] = {
    {.compatible = "ggs-prd,7segment"},
    {}
};
Enter fullscreen mode Exit fullscreen mode

Agora, precisamos criar uma struct do tipo platform_driver que fica declarada em /include/linux/platform_device.h que é uma interface para criação de um driver genérico, ela vai receber como parâmetro nossa struct declarada anteriormente (of_device_id driver_ids[]) e as funções de inicialização e remoção do driver que não serão as mesmas das funções de inicialização e remoção do módulo.

Então vamos declarar a struct platform_driver e tambem declarar as assinaturas das funções de inicialização e remoção do driver no nosso 7segment.h:

static int gpio_init_probe(struct platform_device *pdev);
static int gpio_exit_remove(struct platform_device *pdev);

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,
    }
};
Enter fullscreen mode Exit fullscreen mode

E agora no nosso 7segment.c vamos dar inicio a implementação das funções probe e remove do driver e tambem importar nossas bibliotecas of_device.h e platform_device.h:

//...
#include <linux/of_device.h>
#include <linux/platform_device.h>

//
//...
//

static int gpio_init_probe(struct platform_device *pdev) 
{
    pr_info("Driver inicializado");
    return 0;
}

static int gpio_exit_remove(struct platform_device *pdev) 
{
    pr_info("Driver removido");
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Feito isso, vamos criar no nosso 7segment.h as variaveis referentes a cada gpio em uso no display no nosso raspberry. Essas variaveis serão do tipo gpio_desc, struct declarada em include/linux/gpio/consumer.h elas serão mapeadas para cada porta do raspberry que configuramos como saida no nosso overlay de device-tree e poderemos manipular o valor logico delas (1 ou 0):

//...
struct gpio_desc *a, *b, *c, *d,
    *e, *f, *g;
Enter fullscreen mode Exit fullscreen mode

Não podemos nos esquecer de importar o consumer.h
no nosso 7segment.c:

//...
#include <linux/gpio/consumer.h>
//...
Enter fullscreen mode Exit fullscreen mode

Agora, vamos mapear nossas variáveis para as portas gpio do rasp e setá-las com valor lógico 0 por padrão usando a função devm_gpiod_get da lib gpio/consumer.h dentro da nossa função gpio_init_probe:

static int gpio_init_probe(struct platform_device *pdev) 
{
    pr_info("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);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Agora, com as funções de inicialização e remoção do driver prontos, e preciso registrá-lo no módulo, pra isso os seguintes trechos foram, respectivamente, acrescentados nas funções de inicialização e remoção do módulo.

Na função de inicialização deve ser registrado o driver no fim da função, antes do return 0;

static int __init class_init(void)
{
    //...

    if(platform_driver_register(&display_driver)) {
        pr_err("ERRO! nao foi possivel carregar o driver");
        return -1;
    }

    return 0;
}

static void segments_display_exit(void)
{
    //...    
   platform_driver_unregister(&display_driver);

}
Enter fullscreen mode Exit fullscreen mode

Pronto registramos nosso driver. Agora, precisamos criar na nossa função store_value que a função que é executada quando o arquivo de atributo de classe e fechado algumas condições para mudar as portas GPIOs do raspberry de acordo com o número que esta dentro da variável value_display antes do nosso return count;.

    if(value_display == 0) {
        gpiod_set_value(a, 1);                 
        gpiod_set_value(b, 1);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 1);      
        gpiod_set_value(e, 1);                 
        gpiod_set_value(f, 1);                 
        gpiod_set_value(g, 0);
    }
    else if(value_display == 1) {
        gpiod_set_value(a, 0);                 
        gpiod_set_value(b, 1);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 0);      
        gpiod_set_value(e, 0);                 
        gpiod_set_value(f, 0);                 
        gpiod_set_value(g, 0);
    }
    else if(value_display == 2) {
        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);
    }
    else if(value_display == 3) {
        gpiod_set_value(a, 1);                 
        gpiod_set_value(b, 1);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 1);      
        gpiod_set_value(e, 0);                 
        gpiod_set_value(f, 0);                 
        gpiod_set_value(g, 1);
    }
    else if(value_display == 4) {
        gpiod_set_value(a, 0);                 
        gpiod_set_value(b, 1);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 0);      
        gpiod_set_value(e, 0);                 
        gpiod_set_value(f, 1);                 
        gpiod_set_value(g, 1);
    }
    else if(value_display == 5) {
        gpiod_set_value(a, 1);                 
        gpiod_set_value(b, 0);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 1);      
        gpiod_set_value(e, 0);                 
        gpiod_set_value(f, 1);                 
        gpiod_set_value(g, 1);
    }
    else if(value_display == 6) {
        gpiod_set_value(a, 0);                 
        gpiod_set_value(b, 0);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 1);      
        gpiod_set_value(e, 1);                 
        gpiod_set_value(f, 1);                 
        gpiod_set_value(g, 1);
    }
    else if(value_display == 7) {
        gpiod_set_value(a, 1);                 
        gpiod_set_value(b, 1);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 0);      
        gpiod_set_value(e, 0);                 
        gpiod_set_value(f, 0);                 
        gpiod_set_value(g, 0);
    }

    else if(value_display == 8) {
        gpiod_set_value(a, 1);                 
        gpiod_set_value(b, 1);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 1);      
        gpiod_set_value(e, 1);                 
        gpiod_set_value(f, 1);                 
        gpiod_set_value(g, 1);
    }
    else if(value_display == 9) {
        gpiod_set_value(a, 1);                 
        gpiod_set_value(b, 1);                
        gpiod_set_value(c, 1);                 
        gpiod_set_value(d, 0);      
        gpiod_set_value(e, 0);                 
        gpiod_set_value(f, 1);                 
        gpiod_set_value(g, 1);
    }
    else {
        gpiod_set_value(a, 1);                 
        gpiod_set_value(b, 0);                
        gpiod_set_value(c, 0);                 
        gpiod_set_value(d, 1);      
        gpiod_set_value(e, 1);                 
        gpiod_set_value(f, 1);                 
        gpiod_set_value(g, 1);
    }
Enter fullscreen mode Exit fullscreen mode

Repare que não nos atentamos a legibilidade do código aqui, você tem a liberdade de depois abstrair isso em funções e melhorar a qualidade desse trecho de código

Caso o valor não esteja no range de 0..9 vamos exibir a letra E de erro.

Agora vamos criar nosso Makefile como todos os outros que ja criamos

obj-m += 7segment.o

all: run

run:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Enter fullscreen mode Exit fullscreen mode

Porém, faremos algumas alterações, porque precisamos compilar o nosso device-tree e fazer o overlay dele no kernel.
Para compilar precisamos do seguinte comando:

dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
Enter fullscreen mode Exit fullscreen mode

E para fazer o overlay dele no kernel precisamos do seguinte comando:

sudo dtoverlay overlay.dtbo
Enter fullscreen mode Exit fullscreen mode

Então vamos criar uma rotina no Makefile chamada dt

#...
dt: overlay.dts
    dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
    sudo dtoverlay overlay.dtbo
Enter fullscreen mode Exit fullscreen mode

E na rotina all: vamos chamar além do nosso run tambem o nosso dt.

all: run dt
Enter fullscreen mode Exit fullscreen mode

O nosso Makefile fica assim

obj-m += 7segment.o

all: run dt

run:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

dt: overlay.dts
    dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
    sudo dtoverlay overlay.dtbo
Enter fullscreen mode Exit fullscreen mode

Feito, agora vamos rodar o make e inserir nosso modulo no kernel e ver o resultado.

make
sudo insmod 7segment.ko
Enter fullscreen mode Exit fullscreen mode

Nosso resultado:

[99897.183680] 7segment: loading out-of-tree module taints kernel.
[99897.185331] class registrada
Enter fullscreen mode Exit fullscreen mode

Agora quando rodarmos o comando sudo echo "8" > /sys/class/7segment/value a letra 8 será exibida no nosso display.

Revisão

  • Aprendemos o que é um device-tree.
  • Aprendemos como substituir o atual device-tree do kernel por um que nós mesmo criamos.
  • Aprendemos como registrar um novo driver de dispositivo.

Referencias

Top comments (0)