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

Top comments (0)