Device Drivers para Linux Embarcado - Blog do Software Livre
por Vinícius Maciel | jun 12, 2015 | Linguagem C, Linux, Linux Embarcado, Sistemas Embarcados | 0 Comentários
Neste artigo eu irei falar sobre desenvolvimento Device Drivers para Linux Embarcado. Há certa carência de documentação ou tutoriais sobre o assunto. Espero com este artigo preencher um pouco essa lacuna.
Aplicações em espaço de usúario não podem se comunicar diretamente com o hardware porque o Linux não permite. O Linux divide a memória RAM em duas regiões: espaço de kernel (kernel space) e espaço de usúario (user space). O kernel space é onde o Linux executa e provêm seus serviços, e onde os device drivers residem. Já o user space é a região de memória onde os processos de usuários são executados. O kernel space pode ser acessado por processos de usuário somente através do uso de system calls (chamadas de sistema).
Desta forma, preferencialmente processos em espaço de kernel podem se comunicar com o hardware e acessar os periféricos. Você também pode usar drivers em espaço de usuário para acessar o hardware. Esse mecanismo permite aos desenvolvedores escreverem o código do software sem ter que se preocupar com detalhes de hardware. E protege o usúario de, inadvertidamente, acessar os dispositivos e de alguma maneira danificá-los. Isso tem funcionando bem até agora.
O que é Device Driver?
É um programa ou processo que é executado em uma região especial de memória e através do qual o usuário pode acessar um dispositivo ou recurso de um processador ou sistema computacional. Ele serve como um mediador entre o software e o hardware.
Por exemplo, quando você tira uma foto em seu celular Android, o driver da câmera interage com o software e passa informações a ele referentes às imagens capturadas e outras informações. O resultado final é a foto. Ou quando você quer jogar seu jogo preferido, mas não é possível sem o driver da placa de vídeo, certo? Por meio do driver o jogo acessa recursos da placa de vídeo que permitem a experiência incrível que os jogos proporcionam.
Então, a ideia é justamente essa. Servir como mediador entre o usuário e o hardware ou entre o software e o hardware.
Drivers como módulos ou embutidos no kernel
Os drivers no kernel Linux podem ser compilados como módulos que podem ser carregados em tempo de execução ou embutidos no próprio kernel.
Os drivers em módulos são conhecidos como Loadable Kernel Module (Módulo de Kernel Carregável) e se comportam de forma semelhante às DLLs do Windows. Um LKM (Loadable Kernel Module) se constitui de um único arquivo de objeto ELF, normalmente nomeado como "serial.o" para o kernel 2.4.x ou "serial.ko" para o kernel 2.6.x e versões posteriores. Uma grande vantagem é que ele pode ser carregado na memória em tempo de execução, bastando para isso um simples comando. E outra vantagem é que quando você altera o código do LKM, não é necessário compilar todo o kernel, basta compilar somente o LKM.
Os drivers embutidos ou monolíticos são módulos que são construídos como parte do kernel. Eles formam, juntamente com subsistemas do Linux e outros componentes, uma imagem única, cujo resultado final é o kernel propriamente dito. Vantagens de usar drivers monolíticos:
uma vez aceito no kernel Linux oficial, ele será mantido pelos desenvolvedores;
com custo de manutenção grátis, conserto de falhas de segurança e melhorias;
fácil acesso ao código fonte pelos usuários.
Device Tree é uma estrutura de dados para descrever hardware. Em vez de incluir código de hardware no sistema operacional, muitos aspectos do hardware podem ser descritos em uma estrutura de dados, que é passada ao kernel na hora do boot.
O Device Tree era usado até pouco tempo apenas em sistemas com processadores PowerPC e SPARC. Mas foi recentemente portado para processadores ARM no kernel 3.1 e seu uso é agora obrigatório no desenvolvimento de novos drivers. É uma grande vantagem para sistemas embarcados, porque podemos facilmente descrever diferentes tipos de placas que usam o mesmo processador ou ainda processadores diferentes.
A estrutura de dados em si é uma simples árvore de nós e propriedades com nomes. Os nós contêm propriedades e nós filhos. Propriedades são um par de valor-nome. Exemplo de descrição de um controlador SPI:
spi@10115000 { compatible = "arm,pl022"; reg = ; };
Com o Device Tree, é possível dar boot em qualquer placa com o mesmo kernel! Basta que os drivers do processador escolhido tenham suporte a Device Tree.
Caso você não saiba, da mesma forma que é necessário um compilador cruzado (cross compiler) ou toolchain para gerar executáveis para uma dada plataforma, você precisa de um compilador cruzado para gerar device drivers. Hoje em dia diversas distribuições Linux disponibilizam binários pré-compilados de compiladores cruzados e ferramentas de fácil instalação para uma variedade de arquiteturas. Você pode também compilar um manualmente se preferir.
O compilador usado vai depender da arquitetura do SoC (System-on-a-chip) escolhido. As plataformas ou arquiteturas mais utilizadas em Linux Embarcado atualmente são ARM, PowerPC e MIPS. Também é usado às vezes o x86 da Intel. Você precisa compilar ou baixar uma variante do gcc para o alvo especificado. É o que chamamos de compilador cruzado ou toolchain. Então para ARM nós teríamos algo como arm-linux-gcc. Para MIPS, mips-linux-gcc. E assim por diante.
"The 2.5 kernel implements a unified device driver model that will make driver development for 2.6 easier."
A versão do kernel é muito importante quando se programa Linux device drivers. A API do kernel Linux muda constamente com o tempo. Um driver I2C escrito para a versão 2.6.30 do kernel não funciona para a versão 2.6.36 ou maior. E as diferenças entre a versão 2.4 e 2.6 são ainda maiores.
Na versão 2.5 do Linux os desenvolvedores criaram um Modelo Unificado de Device Drivers (Unified Device Driver Model) e que passou a vigorar na versão 2.6 do kernel. Ele consiste de um número de estruturas e funções comuns a todos os device drivers. E inclui suporte a gerenciamento de energia, comunicação com o espaço de usuário, dispositivos hotplug, classes de dispositivos, e outros mais. O Linux Device Driver Model é uma complexa estrutura de dados que reside na pasta /sys.
Fonte: Livro "Linux Device Drivers, 3º Edição" – Figura 14-1 (A small piece of the device model)
E sempre há mudanças na API do kernel ao longo do tempo. Por que a API do kernel muda constamente? Leia este documento para mais detalhes: https://www.kernel.org/doc/Documentation/stable_api_nonsense.txt
Uma coisa interessante sobre a evolução do kernel Linux é que as novas versões sempre adicionam novos atributos e nos anos recentes muitos atributos especifícos para sistemas embarcados tem sido adicionados. Um bom exemplo é o subsistema IIO (Industrial I/O). Ele foi designado para dar suporte a conversores A/D e D/A, acelerômetro, sensor de luz, sensor de proximidade, magnetrômetro, etc.
Para quem escreve código para software não importa a versão do kernel. Uma aplicação escrita para a versão 2.4.x roda nas versões 2.6.x, 3.x e possivelmente posteriores. Isso porque o Linux sempre mantém compatibilidade com versões anteriores em nível de aplicação.
Periféricos em Sistemas Embarcados
De uma perspectiva de device driver, desenvolvedores de software embarcado (firmware) frequentemente lidam com dispositivos que não são comumente encontrados em computadores convencionais. Exemplos de tais dispositivos: modem GSM, SPI, I2C, ADC, memória NAND, rádio, GPS.
Sob o Linux, existem essencialmente três tipos de dispositivos: dispositivos de rede, dispositivos de bloco e dispositivos de caractere. A maioria dos dispositivos se enquadram na categoria de dispositivos de caractere. Porém, hoje em dia, muitos device drivers não são implementados diretamente como dispositivos de caractere. Eles são desenvolvidos sob um framework, específico para um dado dispositivo. Exemplos de frameworks: framebuffer (gráficos), V4L2 (captura de vídeo), IIO (Industrial I/O), etc.
Por exemplo, se você deseja usar uma câmera para a qual não há suporte no Linux, você precisa escrever um device driver para essa câmera usando o framework Video4Linux2.
Se você já tem algum conhecimento ou experiência com sistemas embarcados, provavelmente sabe que interrupções é uma parte importante no desenvolvimento de firmwares. No entanto, infelizmente não é tão simples usar interrupções em Linux Embarcado. Só é possível usar interruções em drivers, ou seja, em espaço de kernel ou através de mecanismos de poll/select em espaço de usuário. Porém o uso de poll/select oferece poucos recursos.
A forma de interrupção que estamos acostumados a usar em sistemas embarcados só é possível em device drivers. Não se pode criar uma rotina ou função de tratamento de interrupção em um software. Outra alternativa é usar drivers em espaço de usuário.
Como em muitos outros sistemas operacionais, as interrupções são associadas a números e intervalos de endereços de entrada e saída. A programação de interrupções em Linux embarcado é semelhante a como é feito para Linux Desktop.
Escrevendo Device Drivers Portáveis
"Follow the kernel team's rules to make your drivers work on all architectures."
Quando você trabalha com sistemas embarcados há uma boa chance de que use múltiplos tipos de processadores ao longo de sua carreira. É importante manter em mente que você deveria sempre que possível escrever device drivers portáveis. Quase todos os device drivers do Linux funcionam em mais de um tipo de processador. Para alcançar este objetivo, é necessario conhecer alguns conceitos como tipos de variavéis apropriadas, tamanhos de página de memória, problemas com ordem de byte, alinhamento de dados apropriado, e muitos outros.
Por exemplo, os processadores ARM dispõem de páginas de memória de 4K, 16K ou 32K, mas o i386 da Intel dispõe apenas de páginas de memória de 4K.
Alguns processadores usam o método de Port-Mapped I/O (Entrada e Saída mapeados em porta) e tem instruções especiais para acessar portas e periféricos, como o x86 da Intel. Outros processadores usam o metodo de Memory-Mapped I/O (Entrada e Saída mapeados em memória), como o ARM, onde o acesso a portas e periféricos é feito diretamente através da memória RAM.
Para qualquer um dos métodos, MMIO ou PMIO, para poder acessar a memória RAM ou as portas de E/S no Linux, é necessário mapear a memória física para uma memória virtual. Para acessar a memória ou portas de maneira portável, você precisa chamar ioremap() para acessar uma região de memória e iounmap() para liberar uma região alocada anteriormente. No entanto essas funções estão sendo substituídas por devm_ioremap_resource() e futuramente se tornarão obsoletas. Se a alocação da região de memória não retornar um erro, pode-se então usar a família de funções read()/write() para ler e escrever a memória mapeada. Esse é o método típico para acessar os registradores de um processador que usa o método de MMIO.
Neste artigo procurei abordar os tópicos mais relevantes no desenvolvimento de drivers para Linux Embarcado e que apresentam diferenças entre a programação de drivers para Linux Desktop e para Linux Embarcado. Essas diferenças são mais perceptíveis quando se programa drivers de plataforma.