Neste artigo daremos um pequeno e simples exemplo de como codificar um device driver para Linux Embarcado. Na verdade esse exemplo funciona no Linux Desktop também, a única diferença será o compilador usado e arquivo para compilação Makefile. Então vamos lá!
Preparando o ambiente para desenvolvimento
Para que possamos executar o driver numa determinada placa cujo processador é diferente daquele onde o código será desenvolvido, nós precisamos usar um toolchain cruzado ou cross-toolchain. Mas se você for executar esse exemplo no PC, você pode usar o compilador gcc nativo. E se você já tem um toolchain instalado em seu sistema, pode pular esta parte do artigo e ir direto para a compilação do kernel.
Das plataformas utilizadas em Linux Embarcado, a plataforma ARM é a mais usada e difundida atualmente. Assim as instruções a seguir visam apenas placas com processadores ARM. Mas você poderia rodar esse exemplo em qualquer placa ARM ou mesmo placas com processadores diferentes como MIPS e PowerPC, fazendo as devidas adaptações. Para processadores MIPS, por exemplo, você deve usar algo como gcc-mips-linux-gnueabi.
Será utilizado para o exemplo o cross-toolchain da Linaro gcc-linaro-arm-linux-gnueabihf-4.7, por ser estável e por rodar em praticamente qualquer distribuição de sua escolha. E se o Linux que roda em sua placa não suporta ponto flutuante por hardware, use a versão gcc-linaro-arm-linux-gnueabi-4.7, ou seja, sem o hf no final.
1 – Digite o seguinte comando em seu Linux Desktop para baixá-lo:
$ wget https://releases.linaro.org/13.04/components/toolchain/binaries/gcc-linaro-arm-linux-gnueabihf-4.7-2013.04-20130415_linux.tar.xz
2 – Descompacte o compilador:
$ tar -xJf gcc-linaro-arm-linux-gnueabihf-4.7-2013.04-20130415_linux.tar.xz
3 – Crie a pasta toolchains/gnueabi sob /opt:
# mkdir -p /opt/toolchains/gnueabi
4 – Mova a pasta do compilador para a pasta /opt/toolchains/gnueabi:
# mv gcc-linaro-arm-linux-gnueabihf-4.7-2013.04-20130415_linux /opt/toolchains/gnueabi/
5 – Adicione o diretório do compilador ao PATH de seu usuário:
$ mcedit ~/.bashrc (use seu editor de texto preferido)
E adicione à última linha:
export PATH=”/opt/toolchains/gnueabi/gcc-linaro-arm-linux-gnueabihf-4.7-2013.04-20130415_linux/bin/:$PATH”
6 – Teste o funcionamento básico (antes de fazer o teste, feche e abra o terminal novamente):
$ arm-linux-gnueabihf-gcc -v
Você deverá ver na última linha: gcc version 4.7.3 20130328 (prerelease) (crosstool-NG linaro-1.13.1-4.7-2013.04-20130415 – Linaro GCC 2013.04)
Para seguirmos com o resto do tutorial, é necessário que o leitor tenha instalado em sua máquina o git, além de ferramentas de desenvolvimento básicas. Instale-as através do comando:
# apt-get install git git-email build-essential lzop u-boot-tools
O git-email será útil se você pretende enviar patches para algum repositório que utiliza o git, como o kernel Linux.
Gerando uma imagem de kernel para processadores da Allwinner
O kernel Linux usado nas placas com processadores da Allwinner como A13-Olinuxino e Cubieboard é um kernel derivado do trabalho de uma comunidade na internet conhecida como comunidade sunxi. O seu mantenedor é um desenvolvedor chamado Alejandro Mery. Existe também uma versão especial para o u-boot, o u-boot-sunxi. O nome sunxi é pelo fato de o fabricante do processador, Allwinner, ser chinês. Por isso ele foi batizado como linux-sunxi.
Existe também um trabalho que está em andamento para incluir o suporte aos processadores da Allwinner no kernel oficial. Mas ainda muitos periféricos estão sem driver, como o audio e o vídeo. Dessa forma, nós iremos usar o kernel da comunidade sunxi. Para baixá-lo, digite:
$ git clone https://github.com/linux-sunxi/linux-sunxi.git
Isso pode demorar um pouco! Depois de concluído, configure e compile o kernel com os seguintes comandos:
$ cd linux-sunxi $ make ARCH=arm sun4i_defconfig $ make ARCH=arm menuconfig $ make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- uImage modules $ make ARCH=arm INSTALL_MOD_PATH=output modules_install
A opção sun4i_defconfig é usada para processadores da Allwinner modelo A10. Mude para a opção adequada de acordo com seu processador. Se tudo deu certo, dentro da pasta do código fonte do kernel, no caminho arch/arm/boot, deverá ter o arquivo de imagem do kernel uImage. E em output/ os módulos do kernel. Agora transfira o kernel e módulos para a placa.
Gerando uma imagem de kernel para processadores da Texas
O kernel Linux mantido pela Texas Instruments está entre um dos mais estáveis entre processadores ARM. Desde de muito tempo, eles têm funcionários dedicados à manutenção e suporte ao kernel Linux. As instruções abordadas aqui se aplicam à placa BeagleBone Black rodando a distribuição Debian. Se o leitor for usar o driver na distribuição Ångström Linux, que já vem gravada na memória da Beaglebone Black na revisão B, deve usar um compilador sem ponto flutuante, ou seja, sem o hf no final. Na BeagleBone Black revisão C, a distribuição gravada na memória flash é o Debian.
Para baixar o kernel para a placa BeagleBone Black execute os seguintes comandos:
$ git clone https://github.com/beagleboard/linux.git $ cd linux $ git checkout 3.14
Agora compile o kernel com os seguintes comandos:
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bb.org_defconfig $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 zImage dtbs LOADADDR=0x82000000 $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 modules $ make ARCH=arm INSTALL_MOD_PATH=output modules_install
A opção bb.org_defconfig é a configuração de kernel para a placa BeagleBone. E dtbs se refere ao arquivo de Device Tree, explicado no artigo Linux Device Drivers – Diferenças entre Drivers de Plataforma e de Dispositivo. zImage e modules referem-se ao kernel e módulos do kernel respectivamente. Você deve ter percebido alguns comandos em comum no processo de compilação dos dois processadores. Os mesmos comandos se aplicam para a compilação do kernel para outros processadores ARM, com ou sem suporte a Device Tree.
Se tudo deu certo, deverá existir um arquivo zImage em arch/arm/boot, assim como no caso da Allwinner. Além disso, haverá o arquivo de Device Tree, am335x-boneblack.dtb, em arch/arm/boot/dts e os módulos do kernel em output/. Agora transfira o kernel, o arquivo de Device Tree e os módulos para a placa.
Driver Hello World
O leitor pode estar se perguntando que relação tem baixar ou compilar o kernel com a programação de um device driver. Para compilar o código do driver, é necessário que se tenha o código fonte do kernel. E se você for compilar um driver embutido no kernel, deverá necessariamente recompilar o código fonte do kernel! Além disso, não é possível carregar um driver num kernel que não foi gerado por você.
O driver exemplo será compilado como módulo (loadable module), ou seja, separado do kernel Linux. Mas não apenas isso, o código estará fora da árvore do kernel. Por esse motivo, o módulo precisa ser carregado por meio do comando insmod. Você não poderá usar o modprobe!
Salve o código abaixo como hello.c num diretório qualquer. Pode ser fora da pasta do código fonte do kernel.
/* hello.c */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
pr_alert("Hello World!\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_alert("Good bye cruel world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Exemplo simples");
Vamos agora ao arquivo Makefile. Salve o conteúdo abaixo com o nome Makefile (com M maiúsculo mesmo) na mesma pasta do código fonte do driver.
ifneq ($(KERNELRELEASE),) obj-m := hello.o else KDIR := /caminho/para/kernel all: $(MAKE) -C $(KDIR) M=$$PWD clean: $(MAKE) -C $(KDIR) M=$(PWD) clean endif
Altere o valor de KDIR de acordo com o caminho onde o código fonte do kernel se encontra, o qual foi baixado e compilado anteriormente. Para compilar o driver simplesmente digite:
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
O compilador referenciado nesse comando, arm-linux-gnueabihf-gcc, é o mesmo que baixamos nas instruções para preparação do ambiente. Se você usa outro, altere para o nome correto. Essa é a forma como os drivers de código fechado, feitos por certos fabricantes, como Nvidia, são feitos. É bastante prático quando é necessário fazer testes e alterações no código do driver, pelo fato de que não é necessário recompilar o kernel.
O leitor verificará que o driver foi compilado corretamente se o arquivo hello.ko for criado na pasta.
Explicação do código
1. __init
O kernel toma isso como sinal de que a função é usada apenas durante a fase de inicialização e libera recursos de memória utilizados, após a inicialização do kernel, quando o kernel imprime: Freeing unused kernel memory: 236k freed
Definido em: include/linux/init.h
2. __exit
Alguns drivers podem ser configurados como módulos (como o nosso exemplo acima). Nesses casos eles usam o exit. Entretanto, se eles são compilados embutidos no kernel, eles não necessitam do exit. Assim como o __init, indica que a função será colocada numa região do binário para ser descartada depois.
3. Cabeçalhos específicos do kernel Linux: linux/xxx.h
Não existe acesso à biblioteca C padrão na API do kernel Linux. Ele tem sua própria API. Para imprimir mensagens, por exemplo, usa-se printk() em vez de printf().
4. Uma função de inicialização – hello_init()
Chamada quando o módulo é carregado, retornando o código 0 em caso de sucesso e um valor negativo em caso de falha. Declarada pela macro module_init().
5. Uma função de cleanup – hello_exit()
Chamada quando o módulo é descarregado e declarada pela macro module_exit().
6. module_init()/module_exit()
A macro module_init() indica que função será chamada quando o módulo for carregado no kernel. O nome da função não importa, embora que <nome_do_módulo>_init() é uma convenção. Já a macro module_exit(), indica a função que será chamada quando o módulo é removido do sistema via rmmod.
7. Declarações de metadados
Declarações de metadados são feitas através das seguintes macros: MODULE_LICENSE(), MODULE_DESCRIPTION() and MODULE_AUTHOR(). Eles indicam a licença, a descrição do que módulo faz e o autor, respectivamente.
Alguns observações importantes:
- A partir do módulo do kernel, apenas um limitado número de funções do kernel pode ser chamado;
- Funções e variáveis devem ser explicitamente exportadas pelo kernel para serem visivéis para outros módulos do kernel;
- Duas macros são utilizadas no kernel para exportar funções e variáveis:
- EXPORT_SYMBOL(nome_do_simbolo), exporta uma função ou variável para todos os módulos;
- EXPORT_SYMBOL_GPL (nome_do_simbolo), exporta uma função ou variável só para módulos GPL.
Um detalhe interessante é que em drivers modernos as macros module_init() e module_exit() não são mais usadas, embora ainda funcionem. Em vez delas, usa-se module_i2c_driver() para drivers de I2C, module_spi_driver para drivers de SPI, e assim por diante. Mas como nosso exemplo é apenas para ilustrar a adição e remoção do driver no sistema, não faria sentido usar module_i2c_driver(), por exemplo.
Transferindo o driver para a placa
Provavelmente, a forma mais fácil de transferir o driver ou um arquivo qualquer para a placa de desenvolvimento é através de ssh. Exemplo:
$ scp hello.ko usuario@192.168.1.5:/home/usuario
Inserindo e removendo o driver
Para carregar o módulo no Linux da placa, mude para pasta onde foi transferido o arquivo e digite como usuário root:
# insmod hello.ko
Para remover o módulo do sistema digite:
# rmmod hello
Os mesmos comandos se aplicam ao Linux Desktop para testar o driver. Ao inserir o driver no sistema o leitor deverá ver a mensagem “Hello World!” e quando removê-lo via rmmod, “Good bye cruel world!”. Caso não veja, simplesmente digite o comando:
# dmesg | tail
O printk é utilizado na programação do kernel para imprimir mensagens para os logs do kernel. E pr_alert é definido como:
#define pr_alert(fmt, ...) \ printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
A sintaxe de printk é: printk (“log level” “message”, );
Os níveis de log indicam a importância da mensagem que está sendo impressa. No kernel são definidos 8 níveis de log no arquivo printk.h:
#define KERN_EMERG "" /* system is unusable*/ #define KERN_ALERT "" /* action must be taken immediately*/ #define KERN_CRIT "" /* critical conditions*/ #define KERN_ERR "" /* error conditions*/ #define KERN_WARNING "" /* warning conditions*/ #define KERN_NOTICE "" /* normal but significant condition*/ #define KERN_INFO "" /* informational*/ #define KERN_DEBUG "" /* debug-level messages*/
Como normalmente o nível de log no sistema é configurado para 4, o leitor deverá ver as mensagens do driver sem precisar alterar o nível do log do seu sistema. No próximo artigo veremos um exemplo de driver I2C.
Para saber mais
– Linux Device Drivers – Diferenças entre Drivers de Plataforma e de Dispositivo
– Device Drivers para Linux Embarcado – Introdução
– Como se tornar um especialista em Linux embarcado
– Seminário Linux Embarcado 2011
– Anatomia de um Sistema Linux embarcado
– Embedded Linux Build Systems
– Controlador Industrial com Linux embarcado
– https://elinux.org/Building_BBB_Kernel#Downloading_and_building_the_Linux_Kernel
– https://www.ti.com/lsds/ti/tools-software/linux.page
– https://free-electrons.com/doc/training/linux-kernel/
– https://tuxthink.blogspot.com.br/2012/07/printk-and-console-log-level.html
Referências







Precisei fazer algumas alterações para o kernel para a beaglebone..
– Para baixar o kernel para a beaglebone não seria o comando
git clone https://github.com/beagleboard/kernel.git
– e após de devemos fazer um ?
Agora gostaria de gerar uma zImage e um device tree, poderia me ajudar?
Rodrigo,
Utilize o repo https://github.com/beagleboard/linux.git, assim como mencionado no post.
Ele está sendo utilizado como o principal pelos integrantes do projeto.
Para gerar o zImage e o dtb siga os passos do tutorial e nas linhas onde se encontra uImage, troque por zImage.
Olá Diego,
Obrigado pela rápida resposta, meu kernel está rodando na Beaglebone black!