DEV Community

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

Posted on • Edited 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

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay