Introdução
O kernel é responsável por executar diferentes tarefas que podem ser divididas em gerenciamento de processos, gerenciamento de memória, comunicação em rede, sistema de arquivos e controle de dispositivos. Nesse último, encontramos os device-drivers, que são códigos para controle de dispositivos específicos. E o kernel precisa ter os devidos device-drivers para cada um de seus periféricos: HD, teclado, impressora, monitor, etc. Os device-drivers fazem parte do processo de entrada e saída do sistema operacional. Quando um usuário imprime um documento, ele está utilizando entre outras coisas um device-driver que transmite para a impressora o que ela deve imprimir de uma forma que ela entenda. Quando ele está digitando algo no teclado, é um device-driver que traduz o sinal do teclado para o computador. Os device-drivers são o canal de comunicação entre o kernel e o dispositivo. Através dele, o usuário consegue acionar funcionalidades do dispositivo sem precisar conhecer especificamente o funcionamento do dispositivo. No Linux, os device-drivers seguem uma interface padronizada que facilita ainda mais o uso dos drivers. Devido a essa interface, o sistema pode disponibilizar algumas chamadas padronizadas e independentes dos drivers para o usuário. Para ilustra melhor o papel dos device-drivers, a figura abaixo apresenta o posicionamento lógico dos device drivers.
Na arquitetura dos devices-drivers no Linux, eles se enquadram na categoria de módulos. Isto permite que drivers sejam adicionados e removidos do kernel dinamicamente, isto é, sem a necessidade de reiniciar o sistema, por exemplo. Dentro dos módulos, eles ainda podem ser classificados em módulos de caractere, de bloco e de rede. Módulos de caractere disponibilizam uma interface que podem ser acessada como uma stream de bytes e disponibiliza as funções de open, close, read e write, e a diferença deles para arquivos é que geralmente eles devem ser acessados sequencialmente. Módulos de bloco suportam sistemas de arquivos e podem manusear operações de transferência de blocos de dados. No Linux, os módulos de blocos podem ser utilizados como módulos de caractere, sendo que a modulação é interna ao kernal e, portanto, transparente ao usuário. Módulos de rede, diferentemente dos outros, tratam do envio e recebimento de pacotes de dados, dentro do subsistema de comunicação em rede do kernel. Assim, o módulo de rede cuida apenas de enviar e receber os pacotes, sendo que o endereçamento dos dados e protocolo utilizado é tratado acima do driver.
É importante voltar a ressaltar que o papel dos drivers é traduzir chamadas de funções para operações de entrada e saída. Portanto não é o driver que vai implementar o sistema de arquivo de um disco rígido, por exemplo. Haverá um processo tratando especificamente disso e repassando chamadas simples de leitura e escrita para o driver executar a devida operação no disco.
O que é desejável de um device-driver?
Outra característica de design dos device-drivers no Linux é a separação de mecanismo e política, ou seja, o que o driver oferece e o que pode ser utilizado. No caso, a camada dos device-drivers é responsável pelo mecanismo, de modo que não deve haver nenhum tratamento do de permissões na implementação de um driver, a não ser que seja alguma limitação do dispositivo.
Desse modo, os drivers são flexíveis para diferentes utilizações. Como algumas vezes o excesso de opções dificulta mais do que facilita a utilização, alguns drivers vêm acompanhados de aplicativos que tratam da política e facilitam a configuração e utilização dos drivers (deixando o usuário livre para utilizar ou não esse auxílio).
Além disso, é essencial que os drivers sejam desenhados pensando na segurança do kernel, uma vez que serão “inseridos” como parte do código dele. É imprescindível que haja, por exemplo, verificação de que o que está sendo colocado no buffer não vai estourar sua capacidade, ou que os dados que o usuário insere serão transferidos temporariamente a uma área protegida, para serem verificados. Tomando o devido cuidado, garante-se que o driver não irá inserir falhas de segurança ao kernel.
O que é necessário para poder desenvolver um device-driver no Linux?
É preciso ter um bom conhecimento da arquitetura de drivers do Linux, além de saber programar em C e ter conhecimento das bibliotecas específicas que serão utilizadas. Como ferramentas de programação, é preciso que o usuário possua um compilador e as bibliotecas de desenvolvimento para Linux.
Também é desejável que o usuário tenha acesso a uma máquina de testes (ou no mínimo um backup completo do sistema), uma vez que existe o risco de que device-drivers problemáticos causem dano ao sistema operacional, podendo ocasionar até perda de dados.
Desenvolvimento de uma implementação de driver USB
O próprio Linux facilita e muito o trabalho com dispositivos USB através do subsistema ‘USB core’, que cuida de boa parte da complexa interação com os dispositivos. O ‘USB core’ disponibiliza uma API mais simplificada para ser acessada pelo driver. O driver, então, interage com o USB core ao invés de interagir diretamente com o controlador do dispositivo.
Do lado do dispositivo, o USB pode disponibilizar diversas interfaces para o usuário, cada interface com diversas saídas, sendo que cada interface é responsável por uma conexão lógica (comunicação, interface com usuário, áudio, etc) e cada saída possui uma função (controle, interrupção, troca de dados). Para se comunicar com as saídas, um dos mecanismos que o kernel utiliza é o urb (USB request block – bloco de pedido de USB). O urb é um bloco de comunicação unidirecional que, portanto, pode ser utilizado ou para enviar ou para transmitir dados, nunca os dois.
Para programar um driver, é preciso primeiramente registrar o driver no USB core. Para isso, o módulo deve conter uma estrutura que representa o driver como:
static struct usb_driver skel_driver = {Cada uma dos campos da estrutura contém uma explicação mais aprofundada na página 348 do livro Linux Device Drivers da O’Reilly (disponível em http://oreilly.com/catalog/linuxdrive3/book/ch13.pdf). Elas são utilizadas para que o USB core possa acessar dados/funções do driver. Ou seja, o core possui uma listagem de estruturas USB_driver e acessa as funcionalidades do driver através dessa estrutura.
.owner = THIS_MODULE,
.name = "skeleton",
.id_table = skel_table,
.probe = skel_probe,
.disconnect = skel_disconnect,
};
Também devem ser fornecidas pelo menos as funções probe e disconnect. A função probe de cada driver USB é chamada pelo USB core para verificar se o driver é adequado para manusear o dispositivo. A função disconnect é chamada quando o dispositivo é desconectado ou removido da listagem do USB core.
Como foi explicado na "Introdução" os drivers são módulos que podem ser carregados no kernel do Linux. Portanto, o registro do driver USB no core ocorre na inicialização do módulo, como por exemplo em:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/usb.h>
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;
}
module_init(usb_skel_init)
Analogamente, ao remover o módulo do kernel, deve-se desregistrar o driver do USB core:
static void __exit usb_skel_exit(void)Sendo assim, temos o suficiente para que, quando um dispositivo USB for instalado no computador, o USB core irá tentar atribuir algum driver para o manuseio deste dispositivo. Através da função probe, o USB core irá verificar se o driver se adéqua e, em caso positivo, ele deve inicializar estruturas para gerenciar o dispositivo. No exemplo abaixo, é ilustrado uma forma de registrar os primeiros terminais bulk de entrada e saída:
{
/* deregister this driver with the USB subsystem */
usb_deregister(&skel_driver);
}
module_exit(usb_skel_exit)
/* set up the endpoint information */Abaixo, é apresentada uma maneira de recuperar esses dados guardados na interface de dispositivo:
/* use only the first bulk-in and bulk-out endpoints */
iface_desc = interface->cur_altsetting;
for (i = 0; i <>desc.bNumEndpoints; ++i) {
endpoint = &iface_desc->endpoint[i].desc;
if (!dev->bulk_in_endpointAddr &&
(endpoint->bEndpointAddress & USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)
= = USB_ENDPOINT_XFER_BULK)) {
/* we found a bulk in endpoint */
buffer_size = endpoint->wMaxPacketSize;
dev->bulk_in_size = buffer_size;
dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);
if (!dev->bulk_in_buffer) {
err("Could not allocate bulk_in_buffer");
goto error;
}
}
if (!dev->bulk_out_endpointAddr &&
!(endpoint->bEndpointAddress & USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)
= = USB_ENDPOINT_XFER_BULK)) {
/* we found a bulk out endpoint */
dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
}
}
if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {
err("Could not find both bulk-in and bulk-out endpoints");
goto error;
}
/* save our data pointer in this interface device */
usb_set_intfdata(interface, dev);
struct usb_skel *dev;Para registrar o dispositivo, deve ser utilizada a função USB_register_dev que passa a interface e uma USB_class_driver que será atrelada à interface. A USB_class_driver contém diversos parâmetros para registrar um “minor number”, que se refere ao dispositivo. Nela haverá informações como o nome, as operações para acessar o dispositivo, entre outros.
struct usb_interface *interface;
int subminor;
int retval = 0;
subminor = iminor(inode);
interface = usb_find_interface(&skel_driver, subminor);
if (!interface) {
err ("%s - error, can't find device for minor %d",
__FUNCTION__, subminor);
retval = -ENODEV;
goto exit;
}
dev = usb_get_intfdata(interface);
if (!dev) {
retval = -ENODEV;
goto exit;
}
Para a desconexão do dispositivo, deve ser removido o USB_class_driver associado à interface e o dispositivo deve ser removido do registro no core:
static void skel_disconnect(struct usb_interface *interface)Para a transferência de dados, será abordado como utilizar os URBs, embora seja possível se comunicar com o dispositivo através de mensagens bulk e control. Para se comunicar através das urbs é preciso alocá-las, o que pode ser feito da seguinte forma:
{
struct usb_skel *dev;
int minor = interface->minor;
/* prevent skel_open( ) from racing skel_disconnect( ) */
lock_kernel( );
dev = usb_get_intfdata(interface);
usb_set_intfdata(interface, NULL);
/* give back our minor */
usb_deregister_dev(interface, &skel_class);
unlock_kernel( );
/* decrement our usage count */
kref_put(&dev->kref, skel_delete);
info("USB Skeleton #%d now disconnected", minor);
}
urb = usb_alloc_urb(0, GFP_KERNEL);Após isso, é bom criar um buffer DMA para realizar a transferência de maneira mais eficiente:
if (!urb) {
retval = -ENOMEM;
goto error;
}
buf = usb_buffer_alloc(dev->udev, count, GFP_KERNEL, &urb->transfer_dma);Com a urb alocada e o buffer local carregado é preciso inicializar a urb com esses dados:
if (!buf) {
retval = -ENOMEM;
goto error;
}
if (copy_from_user(buf, user_buffer, count)) {
retval = -EFAULT;
goto error;
}
/* initialize the urb properly */Somente após isso é realizada a transferência de dados para o dispositivo:
usb_fill_bulk_urb(urb, dev->udev,
usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
buf, count, skel_write_bulk_callback, dev);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
/* send the data out the bulk port */Como no envio foi enviado um ponteiro para skel_write_bulk_callback, o USB core irá chamar essa função quando a transmissão de dados for concluída. Um exemplo de implementação pode ser:
retval = usb_submit_urb(urb, GFP_KERNEL);
if (retval) {
err("%s - failed submitting write urb,
error %d", __FUNCTION__, retval);
goto error;
}
static void skel_write_bulk_callback(struct urb *urb, struct pt_regs *regs) { /* sync/async unlink faults aren't errors */ if (urb->status && !(urb->status = = -ENOENT ||
urb->status = = -ECONNRESET ||
urb->status = = -ESHUTDOWN)) {
dbg("%s - nonzero write bulk status received: %d",
__FUNCTION__, urb->status); } /* free up our allocated buffer */ usb_buffer_free(urb->dev, urb->transfer_buffer_length,
urb->transfer_buffer, urb->transfer_dma); }
Instalação
Como os device-drivers são módulos do Linux, sua instalação é feita através da inserção de módulos ao kernel. Como foi visto, isto é realizável através da função insmod após compilar o módulo:
>> cd (endereço do driver)Isso irá fazer com que seja adicionado um /dev/(arquivo do driver) para o Ubuntu, por exemplo.
>> make
>> insmod (endereço do driver)/(arquivo do driver).o
Como testar o device-driver?
Para testar um device-driver, uma idéia é colocar arquivo do driver na pasta /tmp que é apagada sempre que o sistema reinicia e colocar na pasta de drivers um link para o arquivo. Desse modo, caso o driver dê crash no kernel e este não consiga iniciar, o problema será resolvido automaticamente reiniciando o computador.
Enquanto isso, é possível verificar as rotinas de _init(), _info() e _attach() e verificar se o comportamento do dispositivo está dentro do esperado, com menos riscos para o sistema.
Referências.
http://www.faqs.org/qa/qa-16964.html
http://www.lrr.in.tum.de/Par/arch/usb/download/usbdoc/usbdoc-1.32.pdf
http://oreilly.com/catalog/9780596005900/book/index.csp
Nenhum comentário:
Postar um comentário