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:
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:
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>;
};
};
};
};
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
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);
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"},
{}
};
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,
}
};
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;
}
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;
Não podemos nos esquecer de importar o consumer.h
no nosso 7segment.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
:
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;
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;
.
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
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:
dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts
E para fazer o overlay dele no kernel precisamos do seguinte comando:
sudo dtoverlay overlay.dtbo
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
E na rotina all:
vamos chamar além do nosso run tambem o nosso dt.
all: run dt
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
Feito, agora vamos rodar o make
e inserir nosso modulo no kernel e ver o resultado.
make
sudo insmod 7segment.ko
Nosso resultado:
[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
- Livro linux kernel programming comprehensive guide de Kaiwan N Billimoria
- Artigo driver-linux-para-display-de-7-segmentos de Brenda Jacomelli
Top comments (0)