Linux Device Drivers – Parte 1

- por Sergio Prado

Categorias: Linux, Mini2440 Tags: , ,

Desenvolvimento de drivers de dispositivo para Linux embarcado é um tema que sempre tive dificuldades de encontrar informações na internet. É também um tópico muito requisitado pelos leitores do blog. Então resolvi escrever uma série de artigos para tentar desmistificar um pouco o tema, e mostrar que desenvolvimento de drivers para Linux não é tão complicado quanto parece.

Neste artigo introdutório, tenham paciência, porque ainda não escreveremos uma linha de código. Nosso objetivo aqui é criar uma base para que possamos, nos próximos artigos, botar a mão na massa.

Usaremos como máquina host qualquer PC com um sistema GNU/Linux, e como target nosso já rodado kit FriendlyARM mini2440. Mas nada impede que você use outro hardware como target, apenas tomando a devida atenção aos trechos de código dependente de hardware.

Nosso objeto de estudo será o kernel 2.6, e temos realmente muita coisa para ver. Precisamos entender a arquitetura básica do kernel do Linux, e faremos isso neste artigo. Depois começaremos desenvolvendo drivers de dispositivo simples interfaceando leds, botões, buzzer, RTC, display, porta serial, etc. Vamos aprender a trabalhar com interrupções, kernel threads, softirqs, tasklets e mais algumas APIs do kernel. Iremos também ler e analisar muito código de drivers já desenvolvidos para o mini2440. No fim, uma surpresa… Mas só falarei quando estivermos chegando lá!

POR QUE APRENDER SOBRE DRIVERS DE DISPOSITIVO PARA LINUX?

Não vou ficar aqui "chovendo no molhado". Se você esta lendo este artigo, sabe muito bem o porquê. Você encontra Linux em todo lugar, de relógios e mainframes, passando por roteadores, celulares, videogames, set-top-boxes, etc. Com a popularização do sistema operacional Android, a lista aumentou ainda mais. E toda essa popularização da plataforma Linux em sistemas embarcados é consequência de vários fatores, dentre eles o fato do código ser open-source e livre de royalties, muito bem documentado e com muitos fóruns e comunidades disponíveis, com suporte a mais de 15 arquiteturas nativas no código, e aplicações open-source disponíveis para fazer virtualmente o que você quiser, bla, bla, bla. Isso sem falar da segurança, da robustez e da flexibilidade do kernel. Mas claro que tudo isso você também já sabe.

O que você talvez não saiba, é que desenvolver drivers para Linux não é muito mais complicado do que desenvolver drivers para outras plataformas. Afinal, o objetivo é o mesmo: prover uma camada de interface entre sua aplicação e o hardware. O que muda basicamente são as ferramentas utilizadas e a API do sistema operacional.

COMPONENTES DE UM LINUX EMBARCADO

Antes de falar do kernel, vamos revisar os componentes presentes em um sistema Linux:

  1. Bootloader
  2. Kernel
  3. Rootfs

Um sistema Linux embarcado não é muito diferente de um Linux convencional para desktop. O kernel é exatamente o mesmo, sendo apenas configurado e compilado de maneira customizada para a plataforma em questão. O rootfs é composto basicamente por bibliotecas e pelas aplicações dos usuários. Normalmente usa-se a uclibc (mais enxuta) como biblioteca do sistema em substituição à glibc (mais pesada). Usa-se também o busybox, um pacote enxuto com os principais comandos do sistema e aplicações gerais. Já o bootloader é uma aplicação bem específica, pois é muito dependente do hardware. O U-Boot e o Redboot são bootloaders comuns para sistemas embarcados, além dos conhecidos LILO e GRUB no universo desktop, que são mais usados em plataformas x86 embarcadas.

Para montar todo o sistema, você precisará de um toolchain, que é um conjunto de ferramentas para compilar o kernel, as bibliotecas e as aplicações. Esse toolchain utiliza um conceito de compilação cruzada, pois ele pode ser executado em um PC (x86, por exemplo), mas vai gerar um binário que será executado em uma outra plataforma (ARM, por exemplo).

Portanto, para gerar um sistema linux embarcado completo, você precisará basicamente de:

  1. Gerar o toolchain
  2. Compilar o bootloader
  3. Configurar e compilar o kernel
  4. Compilar as bibliotecas e as aplicações
  5. Gerar o rootfs
  6. Gerar a imagem final para gravar na flash

É claro que fazer tudo isso "na raça" vai te tomar alguns dias, ou até semanas (há quem diga meses). É por isso que existem ferramentas que automatizam todo esse processo, como por exemplo o Buildroot, ScratchBox e o OpenEmbedded.

Escrevi um artigo sobre como usar o Buildroot para gerar uma imagem Linux disponível aqui.

A partir de agora, nosso foco será o kernel do Linux.

ARQUITETURA DO KERNEL

O kernel é basicamente o responsável por gerenciar o hardware, controlar a execução dos processos e prover uma interface (API) para as aplicações. Ele pode ser dividido em diversos sub-sistemas, dentre eles a camada de interface com as aplicações (System Call Interface), gerenciamento de processos, gerenciamento de arquivos, gerenciamento de memória, pilha de protocolos de rede e drivers de dispositivo.

Todos esses sub-sistemas rodam em um ambiente chamado de kernel space, enquanto que as aplicações do usuário rodam em user space. É no kernel space que temos permissão para acessar diretamente o hardware, portanto é lá que se encontram os drivers de dispositivo.

Apesar do Linux ser um kernel monolítico (todos os sub-sistemas rodam em kernel space, como se fossem um único binário), ele é modular e possibilita a carga de módulos em tempo de execução, através de comandos do sistema (insmod, rmmod, modprobe, etc). Por módulos entenda-se qualquer "pedaço de código" que você queira executar em kernel space, como por exemplo adicionar uma thread no kernel para executar determinada tarefa, ou mesmo carregar um device driver para gerenciar determinado hardware.

Portanto, isso significa que ao compilar e gerar um novo driver de dispositivo, você não precisa gerar uma nova imagem do kernel para testar. Basta copiá-lo para o rootfs do equipamento e carregá-lo dinamicamente. Veremos esse processo com mais detalhes nos próximos artigos.

INTERFACEANDO COM DRIVERS DE DISPOSITIVO

Um driver de dispositivo possui 3 "lados": um deles conversa diretamente com o hardware, outro conversa com o kernel, e outro conversa com as aplicações dos usuários através de chamadas do sistema.

Mas como as aplicações interfaceiam com os drivers de dispositivo? Através de arquivos! O Linux segue o modelo UNIX. Em um sistema UNIX, tudo é arquivo. Por exemplo, através do diretório "/proc" é possível acessar as estruturas de dados do kernel, status dos processos em execução e suas opções de configuração.

Drivers de dispositivo usam os diretórios "/dev" para prover o acesso ao hardware e "/sys" para exportar informações do hardware. Cada arquivo dentro de "/dev" provê acesso à determinado hardware. Por exemplo, segue abaixo a listagem do arquivo que representa a porta serial no kit mini2440:

crw-rw-rw- 1 root root 204, 64 Oct 17 2010 /dev/ttyAMA0

Portanto, para a aplicação, o acesso ao hardware é feito através da manipulação de arquivos. Escrever um byte neste arquivo significa enviar um byte pela serial, assim como ler um byte deste arquivo significa receber um byte pela serial. Operações especiais como configurações do hardware podem ser realizadas através de funções do tipo ioctl(). Esta camada de abstração do hardware em arquivos realmente funciona muito bem (veremos isso em mais detalhes nos próximos artigos).


Mas quando acessamos um arquivo, como o kernel sabe para qual device driver ele esta mapeado? Na listagem do arquivo /dev/ttyAMA0 acima, veja que existem dois números "mágicos": 204 e 64. É o primeiro número (204), também chamado de major number, que associa o arquivo ao device driver. Assim o kernel sabe que qualquer acesso à este arquivo deverá ser mapeado para o device driver registrado com o número 204. Você verá que ao desenvolver um device driver, será necessário registrar o major number que estará associado ao seu driver. Ou então você pode deixar que o kernel associe um major number dinamicamente para seu driver.

O segundo número (64), também chamado de minor number, indica o número do seu dispositivo. Por exemplo, se você tiver um driver que trata 4 portas seriais, o major number continuaria sendo 204, mas cada porta serial teria seu minor number (64, 65, 66, 67), conforme abaixo:

crw-rw-rw- 1 root root 204, 64 Oct 17 2010 /dev/ttyAMA0
crw-rw-rw- 1 root root 204, 65 Oct 17 2010 /dev/ttyAMA1
crw-rw-rw- 1 root root 204, 66 Oct 17 2010 /dev/ttyAMA2
crw-rw-rw- 1 root root 204, 67 Oct 17 2010 /dev/ttyAMA3

Existem basicamente dois tipos de drivers de dispositivo em linux: char devices e block devices.

Char devices trabalham com acesso sequencial, e os bytes são transmitidos/recebidos um de cada vez (Ex: mouse, teclado, porta serial, etc). 

Já os Block Devices trabalham com transferências em bloco, são normalmente endereçáveis e possibilitam acesso aleatório ao seu conteúdo (Ex: discos, cd-rom, regiões de memória, etc).

Veremos exemplos dos dois tipos nos próximos artigos.

E AGORA?

Tentei passar neste artigo pelo menos o básico da arquitetura do kernel. Muito do que foi visto aqui será estudado com mais detalhes nos próximos artigos. A partir de agora, nosso foco será o código!

Um abraço,

Sergio Prado

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.