Device-driver USB para linux

Cícero David Leite
Ítalo Fernandes Aguiar

Introdução

Um device-driver consiste em um programa computacional que possibilita programadores de software de níveis mais altos interagirem com os dispositivos de hardware. O driver se comunica com o hardware através de um barramento ou subsistemas de comunicação, possibilitando que os programas de níveis mais altos chamem rotinas do driver, o qual realiza uma série de comandos e retorna para o programa as requisições solicitadas.

Os drivers de dispositivo são importantes porque oferece aos programadores de subsistemas uma abstração das formas de comunicação entre o software sendo desenvolvido e os dispositos de hardware.

Arquitetura

A arquitetura dos drivers em Linux é representada em módulos, que consistem em pedaços de código que extendem as funcionalidades do kernel. Os módulos são divididos em camadas como mostra a figura abaixo.

arquitetura.bmp

Figura 1 - Arquiteruta de módulos no Linux

A comunicação entre os programas e os módulos são feitas através de chamadas de funções (function calls) . Ao carregar o sistema os módulos todas as funções públicas para uma tabela de símbolos mantida pelo kernel do Linux, assim todas essas funções ficam visíveis para os demais módulos. O acesso aos dispositivos de hardware é feito então através de uma camada de abstração de hardware, hardware abstraction layer - HAL, que depende da paltaforma para o qual o sistema operacional foi compilado.

Requerimentos

No desenvolvimento de drivrers de dispositivos deve-se preocupar principalmente com segurança e confiabilidade. Um driver não pode inesperadamente falhar ou elevar privilégios para um usuário indesejado. Um desenvolvedor de drivers deve ter consciência das situações em que alguns tipos de acesso a dispositivos pode afetar o sistema de forma indesejada, como por exemplo operações que afetam recursos globais. Logo, um controle para esse tipo de situação deve ser provido pelo driver.

Deve-se manter uma preocupação em evitar a inserção de bugs de segurança. Muitas linguagens de programação, como a linguagem C, facilitam a criação indesejada de erros, como por exemplo um problema de buffer overrun, no qual o programador esquece de checar o tamanho de um dado escrito em um buffer, possibilitando a inserção de código em áreas não correlacionadas.

Outro ponto importante em relação a segurança em relação a confiabilidade dos dados provenientes de programas do usuário. Qualquer entrada proveniente de processos do usuário deve ser tratada com suspeita, sendo sempre necessária uma verificação. Além disso, qualquer memória obtida do kernel deve ser zerada ou inicializada antes que seja disponibilizada para os processos dos usuário dispositivo, evitando assim vazamento de informações e envio de mensagens maliciosas para os dipositivos de hardware.

Ferramentas necessárias para o desenvolvimento de um device driver para Linux

Para desenvolver drivers para Linux é necessário possuir um conhecimento de alto nível em linguagem de programação C, programação de microprocessadores, gerenciamento de memória, interrupções, entrada e saída de dados, bem como outros assuntos relacionados ao funcionamento geral de um microcomputador.

Outro ponto de extrema importância é conhecimento sobre o dispositivo de hardware para o qual está sendo desenvolvido o driver. Saber forma de interfaceamento, os tipos tipos de interrupções, os dispositivos periféricos, dentre outras características, é essencial para a criação do driver. Por esse motivo, a maior parte dos drivers são desenvolvidos pelo próprio fabricante do hardware.

No Linux existe um subsistema chamado "USB core" que possui uma API específica para suportar dispositivos USB, que objetiva abstrair todas as partes dependente do dispositivo definindo um conjunto de estrutura de dados, macros e funções. Uma ilustração do USB Core é mostrado na figura abaixo.

usbcore2.bmp

Figura 2 - USB core

Antes de iniciar o desenvolvimento de um driver pro Linux é necessário conhecer ao menos as funções básicas e o conjunto de estrutura de dados oferecidas pelo USB core, que serão descritos mais detalhadamente no próximo tópico.

Desenvolvendo um USB Driver

O primeiro passo para construir um USB driver é determinar como controlar o dispositivo. Dependendo do fabricante, uma documentação é disponibilizada. Caso contrário, o desenvolvedor é forçado a aplicar uma engenharia resersa no dispositivo.

Nesse trabalho será estudada a implementação de um driver USB para um dispositivo que consiste basicamente de um série de LEDs com cores deferentes (Delcom's USB Visual Signal Indicator).

A segudo passo seria codificação das estruturas de dados necessárias ao USB Core do Linux. Abaixo segue o código da estrutura "usb_driver":

static struct usb_driver led_driver = {
    .owner =    THIS_MODULE,
    .name =        "usbled",
    .probe =    led_probe,
    .disconnect =    led_disconnect,
    .id_table =    id_table,
};

A estrutura acima define quatro pontos importantes do driver:

- Um ponteiro para o módulo proprietário do driver.
- O nome do driver USB
- Uma lista de USB Ids que o driver deve oferecer, que permite ao USC Core mapear que driver está relacionado com qual dispositivo.
- Uma função probe() que é executada quando um dispositivo da tabela de Ids é encontrado.
- Uma função disconnect() que é chamada quando o dispositivo é desconectado.

O código do id_table é definido abaixo:

static struct usb_device_id id_table [] = {
    { USB_DEVICE(VENDOR_ID, PRODUCT_ID) },
    { },
};
MODULE_DEVICE_TABLE (usb, id_table);

A função led_probe() necessita basicamente inicializar o dispositivo e criar os arquivos sysfs no diretório correto. Esses arquivos são arquivos de sistema utilizado pelo sistema operacional para realizar o uso do driver. Segue abaixo, o código da função led_probe().

/* Inicializa a estrutura do driver*/
dev = kmalloc(sizeof(struct usb_led), GFP_KERNEL);
memset (dev, 0x00, sizeof (*dev));
 
dev->udev = usb_get_dev(udev);
usb_set_intfdata (interface, dev);
 
/* Cria os 3 arquivos sysfs */
device_create_file(&interface->dev, &dev_attr_blue);
device_create_file(&interface->dev, &dev_attr_red);
device_create_file(&interface->dev, &dev_attr_green);
 
dev_info(&interface->dev,
    "USB LED device now attached\n");
return 0;

A função disconect_led também é bastante simples, tudo que ela tem que fazer é liberar a memória alocada e apagar os arquivos criados, como é mostrado no código abaixo.

dev = usb_get_intfdata (interface);
usb_set_intfdata (interface, NULL);
 
device_remove_file(&interface->dev, &dev_attr_blue);
device_remove_file(&interface->dev, &dev_attr_red);
device_remove_file(&interface->dev, &dev_attr_green);
 
usb_put_dev(dev->udev);
kfree(dev);
 
dev_info(&interface->dev,
         "USB LED now disconnected\n");

Quando um dos arquivos sysfs são lidos é desejado que seja mostrado o valor atual do LED referente ao arquivo. E quando o arquivo é modificado, deseja-se atualizar o estado do LED no dispositivo. O seguinte macro é utilizado para implementar essa funcionalidade:

#define show_set(value)                             
static ssize_t                                    
show_##value(struct device *dev, char *buf)        
{                                                  
   struct usb_interface *intf =                    
      to_usb_interface(dev);                     
   struct usb_led *led = usb_get_intfdata(intf);  
 
   return sprintf(buf, "%d\n", led->value);       
}                                                
 
static ssize_t                                    
set_##value(struct device *dev, const char *buf,   
            size_t count)                        
{                                                 
   struct usb_interface *intf =                   
      to_usb_interface(dev);                     
   struct usb_led *led = usb_get_intfdata(intf);   
   int temp = simple_strtoul(buf, NULL, 10);       
 
   led->value = temp;                          
   change_color(led);                           
   return count;                                 
}                                                 
 
static DEVICE_ATTR(value, S_IWUGO | S_IRUGO,
                   show_##value, set_##value);
show_set(blue);
show_set(red);
show_set(green);

A macro acima cria seis funções, uma "show" e outra "set" para uma das três cores; e três estruturas de atributos: dev_attr_blue, dev_attr_red and dev_attr_green.
Observe que o código acima faz uso da função change_color(). Assim, quando usuário escreve 1 em algum dos sysfs, por exemplo arquivo relativo a cor vermelha, a função set_red() , a qual faz uma chamada à função change_color() descrita abaixo:

#define BLUE    0x04
#define RED    0x02
#define GREEN    0x01
   buffer = kmalloc(8, GFP_KERNEL);
 
   color = 0x07;
   if (led->blue)
      color &= ~(BLUE);
   if (led->red)
      color &= ~(RED);
   if (led->green)
      color &= ~(GREEN);
   retval =
      usb_control_msg(led->udev,
                      usb_sndctrlpipe(led->udev, 0),
                      0x12,
                      0xc8,
                      (0x02 * 0x100) + 0x0a,
                      (0x00 * 0x100) + color,
                      buffer,
                      8,
                      2 * HZ);
   kfree(buffer);

A função verifica se algum LED está acesso em então zera o bit relativo à esse LED e então envia uma mensagem de controle para o dispositivo para que seja escrito o novo valor no dispositivo.

Instalando um Device Driver

A instação de drivers varia de acordo com a distribuição do Linux. Como os drivers consistem em módulos, o método para instalar um driver recai na instalação de um módulo. Todos os drivers são instalados e mantidos no diretório "/lib/modules/kernel_x.x.xx". Existe ainda um arquivo de configuração localizado em /etc/modules.conf que utilizado pelo kernel para carregar e descarregar módulos. Existem um conjunto de comandos que auxilia, a instalação e desinstalação de módulos no Linux. O comando "insmod" instala o módulo a partir da compilação do código. O comando "remmod" é utilizado para remover um módulo.

Testando um Device Driver

Os testes em drivers de dispostivos podem ser feitos com ou sem o dispositivo a que se refere o driver. Testar o driver sem o dispositivo para ser uma tarefa difícil e pouco conclusiva, entretanto algumas estratégias podem ser aplicadas, possibilitando realizar testes efetivos em todos os "entry points" e nas funções auxiliares. Para realizar essa tarefa pode-se aplicar uma das duas estratégias:
- Introduzir diretivas de pré-processamento para as funções no arquivo de cabeçalho.
- Introduzir arquivos e módulos separados que possuem as funções stubs.

Para ambos os casos funções stub poderão ser utilizadas para realizar um log das mensagens supostamente trocadas entre o sistema e o dispositivo.

É importante testar o driver usanda uma das técnicas descritas anteriormente mesmo que o dispositivo esteja disponível. Um dos razões para isso é que bugs no kernel ou no dispositivo poderão ser identificados antes de começar os testes com o dispositivo real. Caso não fossem realizados testes com funções stubs não seria possível identificar a origem de um comportamento indesejado.

Após testar o driver usando uma das estratégias descritas anteriormente, pode-se testar o driver com o dispositivo real. Todas as modificações feitas para ser possível realizar os testes inciais sem o driver devem ser desfeitas progressivamente substituindo gradativamente os entry points e a funções stubs por suas funções reais, sendo realizados testes e validações à cada substituição.

Um caso de teste básico consiste no teste de acesso. Deve ser possível ler e escrever no hardware. Esse teste deve ser realizado antes que os demais testes envolvendo a lógica de controle do driver sejam realizados.

Os principais casos de teste envolvem os pontos descritos abaixo:

  • Liberação de variáveis de acesso exclusivo

Vereficar se semáforos, mutexes e "read and write locks" estão sendo liberados adequadamente.

  • Carregamento/descarregamento

Deve-se verificar se o carregamento e descarregamento, automático ou não, do driver ocorre de forma apropriada

  • Desligamento adequado

Deve-se checar se ocorre um desligamento apropriado depois que o driver foi carregado e descarregado algumas vezes, observando sempre o vazamento ou acesso inválido a memória.
Proper shutdown

  • Open/close/read/write

Checar se todas as combinações para uma sequência de comandos open, close, read e write funcionam adequadamente.

  • Vazamento de memória

Dado que módulos no kernel tem acesso à todo o espaço de memória, pode ser difícil identificar quando um acesso à memória é legal ou ilegal;

A idéia principal que deve-se manter em mente é que deve-se percorrer todo o código fonte, criando casos de teste que sejam capazes de cobrir todo os caminhos no código fonte.

Referências

CALBET, Xavier. Writing device drivers in Linux: A brief tutorial. Free Software Magazine. Disponível em http://www.freesoftwaremagazine.com/articles/drivers_linux. Acessado em: 09/07/2009

ANÔNIMO. Universal Serial Bus. Wikipedia, Internet, n. , p.1-1, 01 abr. 2002. Disponível em: <http://en.wikipedia.org/wiki/Usb>. Acesso em: 07 jul. 2009.

CORBET, Jonathan; RUBINI, Alessandro; KROAH-HARTMAN, Greg. Linux Device Drivers: Third Edition, Sebastopol, n. , p.1-14;327-361, 27 jan. 2005.

KROAH-HARTMAN, Greg. Writing a Simple USB Driver. Linux Journal, 1 de abril de 2004. Disponível em: http://www.linuxjournal.com/article/7353. Acesso em: 09/07/2009.

FLIEGL, Detlef. Programming Guide for Linux USB Device Drivers, 25 de dezembro de 2000. Disponível em: http://usb.cs.tum.edu/usbdoc. Acessado em: 09/07/2009

TSAGAYE, Melekam; FOSS, Richard. A Comparison of the Linux and Windows Device Driver Architectures, 10 de julho de 2000.

VELU, Arun. Debugging simulated hardware on Linux, 2 de novembro de 2005. Disponível em http://www.ibm.com/developerworks/linux/library/l-devdebug.html. Acessado em: 09/07/2009

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