Desenvolvimento

14 set, 2017

O Guia do iniciante para o módulo Kernel Linux, Raspberry Pi e matriz LED

Publicidade

Quando encontrei um antigo alto-falante bluetooth quebrado com matriz LED RGB, eu decidi fazer uso dele. Será o grande desafio conectar LEDs no Raspberry e exibir algumas informações sofisticadas.

Invertendo a matriz

Quero conectar esta matriz ao Raspberry Pi de alguma forma. Mas como isso funciona? Aprendi como as matrizes LED funcionam em geral. E comecei a buscar no google alguns nomes e números de chip para essa coisa particular. Não encontrei nada. Mas tive sorte, depois de cerca de 10 páginas, havia um link para arquivo PDF. Todas as informações estavam em chinês, mas o chip correspondia. O tradutor do Google realmente me ajudou. Depois de algumas medições, descobri de que serve cada pin. Havia 3 pins para escolher a linha, relógio, trava, dados em série e habilitação de saída. Então, eu preciso de 7 portas GPIO para gerir essa matriz. O protocolo estava descrito em PDF. Este foi o primeiro sucesso. Finalmente, a matriz LED começou a trabalhar com o meu Raspberry Pi. Mas havia alguns scripts simples.

Módulo Kernel Linux

Tenho algumas ideias sobre como implementar o driver de tela. Mas, finalmente, eu decidi escolher um módulo kernel com interface sysfs. Há algumas vantagens sobre isso:

  • Atualização praticamente em tempo real
  • Carregado quando o sistema inicia
  • Uso independente da linguagem

 

Implementando o módulo Kernel

Precisaremos de 3 coisas:

  • Usar GPIO a partir do espaço do kernel
  • Criar um segmento do módulo kernel para gerir a matriz led
  • Interface Sysfs

 

Então, vamos começar. Nós o desenvolveremos diretamente no Raspberry Pi com o Rapsbian. Vamos chamar o projeto de pix_mod.

Makefile

A coisa mais importante é Makefile. Ele irá construir o nosso módulo simplesmente digitando make. Não se preocupe, é muito simples:

obj-m := pix_mod.oobj-m := pix_mod.o
all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

O módulo

Nós temos que criar um arquivo pix_mod.c ao lado do nosso arquivo make.

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
static int __init pix_init(void){
 printk(KERN_INFO "PIX: staring...");
 // stuff to do
 printk(KERN_INFO "PIX: staring done.");
 return 0;
}
static void __exit pix_exit(void){
 printk(KERN_INFO "PIX: stopping...");
 // stuff to do
 printk(KERN_INFO "PIX: stopping done.");
}
module_init(pix_init);
module_exit(pix_exit);

Este é um modelo simples do módulo kernel. Temos duas funções que irão invocar quando o módulo irá carregar ou descarregar. Agora podemos criar isso e tentar carregá-lo. Para carregar um módulo kernel, você deve digitar sudo insmod pix_mod.ko. Se funcionar, vamos verificar dmesg. Uau, aí está a nossa mensagem. Ótimo. Podemos avançar.

Use GPIO a partir do espaço do kernel

Não precisamos escrever o driver GPIO. Rapsbian tem um. Precisamos apenas usá-lo. Vamos adicionar mais duas funções para inicializar e lançar nossos GPIOs.

#include <linux/gpio.h>
#define A1 17 // 0
#define A2 18 // 1
#define A3 27 // 2
#define OE 22 // 3
#define LE 23 // 4
#define SDI 24 // 5
#define CLK 25 // 6
void pix_gpio_init(void){
 printk(KERN_INFO "PIX: starting gpio...");
 gpio_request(A1, "A1");
 gpio_request(A2, "A2");
 gpio_request(A3, "A3");
gpio_request(OE, "OE");
 gpio_request(LE, "LE");
 gpio_request(SDI, "SDI");
 gpio_request(CLK, "CLK");
gpio_direction_output(A1, 0);
 gpio_direction_output(A2, 0);
 gpio_direction_output(A3, 0);
gpio_direction_output(OE, 1);
 gpio_direction_output(LE, 0);
 gpio_direction_output(SDI, 0);
 gpio_direction_output(CLK, 0);
 printk(KERN_INFO "PIX: starting gpio done.");
}
void pix_gpio_exit(void){
 printk(KERN_INFO "PIX: stopping gpio...");
 gpio_free(A1);
 gpio_free(A2);
 gpio_free(A3);
gpio_free(OE);
 gpio_free(LE);
 gpio_free(SDI);
 gpio_free(CLK);
 printk(KERN_INFO "PIX: stopping gpio done.");
}

Essas linhas irão definir a que porta GPIO cada linha da matriz está conectada. A1, A2, A3 são linhas para escolher a linha que está sendo exibida. CLK é um pin de relógio, SDI é um pin de dados em série, LE, OE, irão controlar a comunicação serial. Vamos inicializar cada pin e definir o valor inicial como 0. Como podemos alterar o valor do pin GPIO? Vamos implementar funções que definem qual linha está exibindo atualmente.

v

oid pix_line(u8 row){
 gpio_set_value(A1, !(row & 0b00000001));
 gpio_set_value(A2, !(row & 0b00000010));
 gpio_set_value(A3, !(row & 0b00000100));
}

Está bem. Podemos alterar o estado de pin específicos de GPIO. O que vem a seguir?

Kernel Thread

Exibir uma imagem na matriz LED é um trabalho um pouco mais complexo. Como nem todas as linhas são exibidas ao mesmo tempo, você precisa exibir constantemente cada linha muito rapidamente. É para isso que serve o nosso segmento. Nós precisaremos de um buffer de tela que irá armazenar a nossa imagem e do loop que irá ler dados de imagem desse buffer e exibi-los em LEDs. Esta é uma parte segmentar do nosso módulo kernel:

#include <linux/kthread.h>
#include <linux/delay.h>
#define THREAD_NAME "pix"
struct task_struct *task;
int pix_thread(void *data){
 u8 line, pos, bit;
 struct task_struct *TSK;
 struct sched_param PARAM = { .sched_priority = MAX_RT_PRIO };
 TSK = current;
PARAM.sched_priority = THREAD_PRIORITY;
 sched_setscheduler(TSK, SCHED_FIFO, &PARAM);
while(1) {
 // display line
 usleep_range(2000, 2000);
 if (kthread_should_stop()) break;
 }
 return 0;
}
void pix_thread_init(void){
 printk(KERN_INFO "PIX: starting thread...");
 task = kthread_run(pix_thread, NULL, THREAD_NAME);
 printk(KERN_INFO "PIX: starting thread done.");
}
void pix_thread_exit(void){
 printk(KERN_INFO "PIX: stopping thread...");
 kthread_stop(task);
 printk(KERN_INFO "PIX: stopping thread done.");
}

Há duas funções para inicializar e lançar nosso segmento. Vamos usá-las em callbacks linux principais. E uma função de segmento que irá loop durante todo o tempo exibindo imagens na matriz LED. Para tornar este artigo curto, irei ignorar a implementação do algoritmo de exibição. Toda a fonte estará disponível no final.

Agora podemos exibir imagens em nossa matriz. Nos esquecemos de mais uma coisa, como outros programas definirão pixels?

Interface Sysfs

A última coisa que temos que implementar é uma interface sysfs. É um arquivo simples para o qual você pode escrever. Por exemplo, para acender o 5º pixel na 10ª fila em branco, você tem de sequenciar 5 10 1 1 1 para esse arquivo. Existem 5 valores. X, Y, R, G, B. Os valores RGB são booleanos, mas depois tentarei implementar algum tipo de PWM para arquivar mais cores.

static struct kobject *pix_kobject;
static ssize_t set_pix(struct kobject *kobj, struct kobj_attribute *attr, const char *buff, size_t count) {
 u8 x = 0;
 u8 y = 0;
 u8 r = 0;
 u8 g = 0;
 u8 b = 0;
 sscanf(buff, "%hhd %hhd %hhd %hhd %hhd", &x, &y, &r, &g, &b);
 pix_dot(x,y,r,g,b);
 return count;
}
static struct kobj_attribute pix_attribute =__ATTR(dot, (S_IWUSR | S_IRUGO), NULL, set_pix);
void pix_sysfs_init(void){
 printk(KERN_INFO "PIX: starting sysfs...");
 pix_kobject = kobject_create_and_add("pix", NULL);
 if (sysfs_create_file(pix_kobject, &pix_attribute.attr)) {
 pr_debug("failed to create pix sysfs!\n");
 }
 printk(KERN_INFO "PIX: starting sysfs done.");
}
void pix_sysfs_exit(void){
 printk(KERN_INFO "PIX: stopping sysfs...");
 kobject_put(pix_kobject);
 printk(KERN_INFO "PIX: stopping sysfs done.");
}

Este pedaço de código criará um arquivo /sys/pix/dot. Como sempre, temos 2 callbacks init e exit. Para cada escrita em /sys/pix/dot kernel irá invocar a função set_pix. Esta função irá analisar a entrada e invocar outra função que irá alterar um buffer de imagem. Lembre-se do segmento, o segmento está desenhando nossa imagem o tempo todo, então as mudanças devem aparecer imediatamente.

O Módulo Final

Aqui está a fonte do módulo:

https://gist.github.com/fazibear/f1fe97c9799501e0e0955859e12ef4f8

Se você quiser testá-lo, aqui vai um pequeno script de rubi que irá desenhar linhas de cores:

while true
 r = rand(2)
 g = rand(2)
 b = rand(2)
 (0..15).each do |y|
 (0..15).each do |x|
 File.write('/sys/pix/dot', "#{x} #{y} #{r} #{g} #{b}")
 end
 end
end

Não é perfeito, mas funciona muito bem. No Raspberry Pi Zero, onde há apenas um núcleo, vejo algumas falhas. Então, algum trabalho a mais é necessário para torná-lo perfeito. Estou muito feliz com isso. Aprendo muito sobre matrizes LED e kernel linux.

Espero que tenha sido útil para você. Obrigado por ler!

***

Michał Kalbarczyk faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://blog.fazibear.me/the-beginners-guide-to-linux-kernel-module-raspberry-pi-and-led-matrix-790e8236e8e9