Projeto de um USB device driver para Linux

Alunos: Misael Alexandre e Tiago Porto

INTRODUÇÃO

Importância dos device drivers

O sistema operacional necessita se comunicar com dispositivos de hardware. Com exceção do processador, memória e alguns outros poucos dispositivos, a comunicação é feita por pedaços de código específicos para este dispositivo, que são chamados device drivers.

No Linux, device drivers podem ser considerados caixas-pretas que fazem um determinado hardware responder a uma interface do sistema operacional, ou seja, eles escondem completamente como o dispositivo funciona. As atividades do usuário são realizadas por um conjunto de chamadas padronizadas que são independentes do driver específico. Assim, o sistema operacional especifica uma interface que estabelece como é que o controle de um dispositivo é realizado. Mapear estas chamadas padrão para chamadas físicas específicas do dispositivo é o papel do device driver.

Um aspecto importante de um device driver é que o mesmo indica quais capacidades devem ser providas, ou seja, seu mecanismo, mas não como usar estas capacidades. O sistema operacional, assim, fica responsável pela política, ou seja, como usar as capacidades que o device driver provê. Essa separação faz com que o pacote de software final (Sistema Operacional mais drivers) seja mais fácil de desenvolver e de se adaptar.

Um exemplo de como esse desacoplamento é útil no Linux pode ser visto em como o mesmo gerencia a exibição gráfica. A parte que “conhece” o hardware e se comunica com o mesmo é o Servidor X, que dá as capacidades gráficas que o sistema operacional pode usar. Os gerenciadores de janelas e sessões são os responsáveis pela política, ou seja, como as capacidades que o Servidor X dá serão usadas e implementadas. Isso provê duas grandes flexibilidade: as pessoas podem usar um mesmo gerenciador de janelas (KDE, por exemplo) em diferentes máquinas, já que o mesmo não “sabe” nada sobre o hardware; e pode-se ter mais de um gerenciador num mesmo sistema operacional, alternando-se entre os mesmos à escolha do usuário.

Em resumo, a importância principal do device driver num sistema operacional é fazer que o mesmo reconheça novos dispositivos físicos sem ter conhecimento prévio do funcionamento dos mesmos. O conhecimento do hardware é papel do device driver. No caso específico do Linux, os drivers são acoplados ao kernel e passam a fazer parte do mesmo quando instalados.

Arquitetura de device drivers no Linux

A interface do kernel do Linux é tal que drivers podem ser feitos separadamente do resto do kernel e plugados em tempo de execução. Cada pedaço de código que é adicionado ao kernel em tempo de execução é chamado de módulo. O sistema operacional oferece suporte para diversos tipos de módulos, incluindo device drivers. Ou seja, um device driver é um tipo de módulo e pode ser acoplado ao kernel em tempo de execução. Esta modularidade faz que drivers em Linux seja fáceis de escrever. A divisão do kernel em módulos (entre eles device drivers) pode ser vista na Figura 1.

Figura_1.jpg
Figura 1 – Arquitetura do Kernel do Linux

A comunicação entre os módulos é feita através de chamadas de funções. Em tempo de carregamento, um módulo exporta todas as funções que serão públicas para uma tabela de símbolos que o Linux mantém. Estas funções então são visíveis para todos os módulos. Acesso aos dispositivos é feito através da camada de abstração de hardware, na qual sua implementação depende da plataforma de hardware em que o kernel é compilado, como por exemplo no x86 ou SPARC.

Device drivers podem ser divididos em três classes principais, que estão ligadas diretamente ao comportamento de três tipos de dispositivos. Estas classes são:

  • Char drivers: são responsáveis por controlar dispositivos que são acessados por um stream de bytes, como um arquivo. Exemplos de dispositivos deste tipo são o console de texto e os ports seriais.
  • Block drivers: são responsáveis por controlar dispositivos que podem conter um sistema de arquivos. O exemplo mais comum de um block device é o HD dos PCs.
  • Network interface drivers: controlam dispositivos que são responsáveis por enviar e receber pacotes de dados em uma rede.

Requisitos de um device driver e Ferramentas necessárias para construí-lo

Os principais requisitos desejáveis em um device driver são:

  • Realiza gerenciamento de input/output para o dispositivo;
  • Provê um gerenciamento de dispositivo transparente, evitando programação de baixo-nível;
  • Aumenta a velocidade de I/O;
  • Inclui gerenciamento de erros para software e hardware;
  • Permite acesso simultâneo ao dispositivo (hardware) por diversos processos.

Para se construir um device driver no Linux, são necessários:

  • Conhecimento de programação de baixo-nível, por exemplo gerenciamento direto de ports e tratadores de interrupção. A linguagem mais comumente usada é C.
  • Conhecimento da arquitetura do PC.
  • Conhecimento sobre pás partes internas do sistema operacional. No caso do Linux, conhecimento do kernel.
  • Conhecimento básico sobre procedimentos de I/O.

Implementação de um USB device driver

Para construir um device driver USB, devem ser seguidos os seguintes passos:

  • Programar os arquivos fonte do driver, dando atenção especial à interface com o kernel.
  • Integrar o driver com o kernel, incluindo no kernel chamadas para as funções do driver.
  • Configurar e compilar o novo kernel.
  • Testar o driver, escrevendo um programa de usuário.

A primeira tarefa, ao programar os arquivos fonte do driver, é selecionar um nome que o identifique unicamente. No exemplo de código abaixo, que apresenta um exemplo de estrutura para um device driver USB, a variável name é esta variável.

static struct usb_driver skel_driver = {
     name:        "skeleton",
     probe:       skel_probe,
     disconnect:  skel_disconnect,
     fops:        &skel_fops,
     minor:       USB_SKEL_MINOR_BASE,
     id_table:    skel_table,
};

A variável name é uma string que descreve o driver. Ela é usada em mensagens informativas gravadas no log do sistema.
Em seguida, devem ser programadas as funções I/O do driver. Os cuidados a serem tomados devem ser:

  • Bibliotecas padrão não estão disponíveis, já que o driver não tem acesso a elas;
  • Algumas operações de ponto flutuante não são disponíveis;
  • O tamanho da pilha é limitado;
  • Não é possível a espera de eventos, já que o kernel está parado.

Para entender como devem ser programadas as funções do driver, é necessário o entendimento básico de como funciona a comunicação USB.

Um dispositivo USB é algo bem complexo. Felizmente, o kernel do Linux provê uma subsistema chamado USB Core que lida com toda a complexidade. O device driver se conecta diretamente ao USB Core através de uma API, como mostrado na Figura 2. O esquema após o acoplamento do device driver pode ser visto na Figura 3.

Figura_2.jpg
Figure 2: USB Core API Layers

Figura_3.jpg
Figura 3 - USB driver overview

A forma de comunicação USB mais básica é feita através dos chamados endpoints. Um endpoint USB carrega dados em somente uma direção, do computador pro dispositivo ou o contrário. Os tipos de endpoints estão diretamente relacionados com os tipos de transmissão de dados, que são:

  • Control: Endpoints geralmente pequenos usados para configuração do dispositivo e troca de informações sobre o dispositivo;
  • Interrupt: Endpoints que transmitem pequenas quantidades de dados a uma velocidade fixa sempre que o USB host pede;
  • Bulk: Endpoints para transmitir grandes quantidades de dados. Geralmente são grandes e garante a não perda de dados;
  • Isochronous: Endpoints que também transmitem grandes quantidades de dados em intervalos fixos, sem garantir a não perda dos mesmos.

Os endpoints, por sua vez, são encapsulados em interfaces. Interfaces USB lidam somente com um tipo de conexão lógica, tal como um teclado ou mouse. Um dispositivo, assim, pode ter múltiplas interfaces (por exemplo uma câmera de vídeo com microfone). Como uma interface USB representa um tipo de conexão lógica, cada driver USB controla uma interface. Assim, dispositivos que contém mais de uma interface devem ter um drive para cada interface. Este é um ponto muito importante, já que na implementação das funções do driver devem ser feitas referencias à interface.

Por fim, as interfaces são encapsuladas em configurações. Um dispositivo então pode ter múltiplas configurações e pode mudar entre elas para mudar o estado do dispositivo.

O esquema final do dispositivo pode ser visto na Figura 4.

Figura_4.jpg
Figura 4 - USB device overview

Um ponto que deve ser considerado ao programar o driver é saber se o dispositivo tem alimentação própria ou não. Se a alimentação depender da porta USB, devem ser programadas configurações sobre, por exemplo, o máximo de potencia a ser dada ao dispositivo.

Das funções a serem implementadas, vale destacar duas. A primeira é a probe function, que é chamada sempre que, através do ponteiro probe da estrutura do USB driver, um novo dispositivo é acoplado ao barramento, ou seja, sempre que um dispositivo que bate com a informação provida pela id_table (uma tabela de identificação de tipos de dispositivos). A disconnect function é chamada quando um dispositivo é removido do barramento através do ponteiro disconnect.

Uma estrutura que também deve ser destacada é a struct usb_device_id, que provê uma lista dos diferentes tipos de dispositivos USB que o drive suporta.

A tarefa de integrar o driver ao kernel, que no Linux é a forma de instalação do driver, é feita pelos seguintes passos:

  • Inserir chamadas no kernel para novo driver;
  • Adicionar o driver à lista de drivers do kernel;
  • Modificar os scripts de compilação;
  • Recompilar o driver.

Para registrar a estrutura do driver no USB Core, uma chamada para usb_register_driver é feita passando o ponteiro da estrutura do driver como parâmetro. Isto é tradicionalmente feito no código de inicialização do driver USB, como mostrado no código abaixo:

static int __init usb_skel_init(void)
{
int result;
/* register this driver with the USB subsystem */
result = usb_register(&skel_driver);
if (result)
err("usb_register failed. Error number %d", result);
return result;
}

Quando um driver USB vai ser descarregado, a estrutura do driver necessita ter seu registro excluído do kernel. Isto é feito com uma chamada a usb_deregister_driver. Quando esta chamada acontece, qualquer interface USB que está sendo controlada por este driver é disconectada e a função disconnect é chamada para estas interfaces. O código que exemplifica o descarregamento de um driver USB pode ser visto abaixo.

static void __exit usb_skel_exit(void)
{
/* deregister this driver with the USB subsystem */
usb_deregister(&skel_driver);
}

Teste do driver implementado

No procedimento de testes citado abaixo, vamos supor que existe disposição de tempo, de máquinas e que o dispositivo está disponível para ser usado.

Inicialmente, deve ser levado em conta que testar um novo device driver pode causar problemas no kernel. Seguir os seguintes passos ajuda a evitar estes problemas:

  • Usar um kernel alternativo;
  • Fazer o boot de uma cópia do kernel e seus arquivos .bin associados ao invés de kernel default pode evitar que inadvertidamente torne o sistema se torne inoperável;
  • Usar um módulo de kernel adicional para os experimentos com diferentes configurações de variáveis do kernel.

Antes de integrar o driver ao kernel, deve ser feito o primeiro teste, que é compilar o driver isoladamente. Esse método pode levar o programador a encontrar erros de sintaxe isolados, sem correr o risco de comprometer o kernel por um erro considerado “bobo” em programação.

Após instalar o driver como um módulo do kernel, se este aparentemente estiver funcionando, pode-se passar para a segunda fase de testes, que consiste em verificar se o driver realiza as funções para as quais foi programado.

Na segunda fase de testes, podem ser levados em conta, primeiramente, problemas na conexão USB. Nesta fase são feitos testes de, em tempo de execução, conectar e desconectar o dispositivo na porta USB, testes em múltiplas máquinas, testes de performance do dispositivo em USB 1.0, USB 1.1 e USB 2.0 e testes temporais (deixar o dispositivo conectado ao sistema por 24h, por exemplo) para comprovar que conexão contínua não é problema.

Sendo comprovado que a conexão USB está OK, deve-se debugar o kernel resultante. Infelizmente, não há nenhum debugger oficial. Debugar o kernel é difícil, já que o mesmo não pode ser facilmente rastreado, pois seu conjunto de funcionalidades não está relacionada a um único processo. Assim, pode-se usar algumas técnicas que ajudam a debugar o kernel resultantes após a integração do driver.

Inicialmente, devem ser ativadas opções internas de debugger no kernel. Em kernels comerciais (que vêm junto a distribuições) estas opções vem desativadas, em função de questões de desempenho.

Em seguida, dos procedimentos que podem ser adotados, dois podem ser destacados:

  • Debugging usando o comando printk: esta técnica, conhecida também como monitoramento, é comumente usada. É uma técnica análoga ao uso de printf pontos específicos de uma aplicação a fins de testes. A principal diferença é que este comando é que printk permite classificação das mensagens de acordo com sua severidade associando diferentes prioridades com as mensagens.
  • Escrever programas de usuário: esta técnica, que é a mais usada, consiste em testar o driver escrevendo um ou mais programas a nível de usuário que acessem o dispositivo (passando pelo driver, logicamente) chamando funções do driver. Se o driver fosse para um flash USB drive, por exemplo, o programa poderia conter partes onde há escrita e leitura do drive (testando indiretamente as funções do driver que especificam como ler e escrever do dispositivo).

REFERÊNCIAS

DEVICE driver Disponível em: <http://en.wikipedia.org/wiki/Device_driver>. Acesso em: 07 jul. 2009.

CORBET, Jonathan; RUBINI, Alessandro; KROAH-HARTMAN, Greg. Linux Device Drivers, Third Edition. Disponível em: <http://lwn.net/Kernel/LDD3/>. Acesso em: 07 jul. 2009.

SANTANA, Andrew; REIS, Cassiano; DEMARTINI, Josué. Device Drivers no Windows e Linux: Visão Geral e Boas Práticas. Disponível em: <http://www.inf.pucrs.br/~eduardob/disciplinas/ProgPerif/sem09.1/trabalhos/Seminarios/g3/Device%20Drivers%20no%20Windows%20e%20Linux.doc>. Acesso em: 07 jul. 2009.

FLIEGL, Detlef. Programming Guide for Linux USB Device Drivers. Disponível em: <http://www.lrr.in.tum.de/Par/arch/usb/usbdoc/>. Acesso em: 07 jul. 2009.

MATIA, Fernando. Writing a Linux Driver. Disponível em: <http://www.linuxjournal.com/article/2476>. Acesso em: 07 jul. 2009.

KROAH-HARTMAN, Greg. How to Write a Linux USB Device Driver. Disponível em: <http://www.linuxjournal.com/article/4786>. Acesso em: 07 jul. 2009.

…TEST device drivers,kernels,embedded applications and… Disponível em: <http://www.faqs.org/qa/qa-16964.html>. Acesso em: 07 jul. 2009.

CRITICAL PATH SOFTWARE. Device Driver Testing. Disponível em: <http://www.criticalpath.com/qa_devicedriver.html>. Acesso em: 07 jul. 2009.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License