Driver de display de 7seg
Tags: linux, lkm, drivers
Published: 2023-09-19 16:41:54

**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](https://github.com/GuilhermeGiacomoSimoes/introducao_dev_driver_linux)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsggyefhd4lkqwd3fr87.jpg)


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uol87uy3x8omzkiyxrs7.jpg)

![Imagem protoboard 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qsmpm5z7r6s41ro4anzy.jpg)


**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
/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>;
   	};         
    };      
  }; 
};

```

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
```C
#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 
```

7segment.c
```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);
```

Pronto, nada de diferente do que fizemos nos textos anteriores. É exatamente o mesmo codigo do texto [anterior](https://dev.to/simoes/atributos-de-classes-42d).

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

```C
//...
static struct of_device_id driver_ids[] = {
	{.compatible = "ggs-prd,7segment"},
	{}
};
``` 

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:
```C
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,
	}
};
```

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:

```C
//...
#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;
}
```

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

```C
//...
struct gpio_desc *a, *b, *c, *d,
	*e, *f, *g;
```

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

```c
//...
#include <linux/gpio/consumer.h>
//...
```

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

```c
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;
}
```

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

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

}
```

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;`.

```c
	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);
	}
``` 
_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
```c
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
```

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:
```bash
dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
```

E para fazer o overlay dele no kernel precisamos do seguinte comando:
```bash
sudo dtoverlay overlay.dtbo
```

Então vamos criar uma rotina no Makefile chamada dt
```Makefile
#...
dt: overlay.dts
	dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
	sudo dtoverlay overlay.dtbo
```

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

O nosso Makefile fica assim 
```Makefile
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
```

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

```bash
make
sudo insmod 7segment.ko
```

Nosso resultado:
```bash
[99897.183680] 7segment: loading out-of-tree module taints kernel.
[99897.185331] class registrada
```

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**
- Doc oficial [Linux](https://docs.kernel.org/)
- Livro [linux kernel programming comprehensive guide](https://www.amazon.com.br/Linux-Kernel-Development-Cookbook-programming-ebook/dp/B07RW915K4) de  Kaiwan N Billimoria 
- Artigo [driver-linux-para-display-de-7-segmentos](https://embarcados.com.br/driver-linux-para-display-de-7-segmentos/) de Brenda Jacomelli