Introdução aos Sistemas Operacionais/Exemplo de Drivers USB em Linux: Dream Cheeky USB Missile Launcher

Fonte: Wikiversidade

Objetivo geral[editar | editar código-fonte]

O objetivo do estudo de caso, através da análise do funcionamento do driver USB Linux para o dispositibo DreamCheeky USB Missile Launcher é o de complementar os estudos feitos em sala de aula, de forma a observar na prática os processos feito pelo computador para reconhecer um dispositivo USB e tratá-lo de acordo com as suas características.

Introdução[editar | editar código-fonte]

Com o objetivo de criar um driver USB Linux para o dispositivo Dream Cheeky USB Missile Launcher, este estudo de caso explica o processo de criação de um driver de dispositivo USB para o kernel Linux. Depois de ter a engenharia reversa do protocolo de comunicação USB, é apresentado a arquitetura do driver de dispositivo USB. Embora o autor tenha tido que mergulhar em especificidades do dispositivo para o qual o driver foi desenvolvido, o processo pode ser aplicado a outros dispositivos USB também.

Informações gerais[editar | editar código-fonte]

  • Autores do grupo
    • Gabriel Henrique
    • Diego Fraga
    • Lauro Henrique M Ribeiro
  • Versão do kernel
    • Linux 2.6.31 kernel
  • Identificação do driver analisado
  • lista de arquivos de códigos-fontes analisados
  • outras informações relevantes
    • O driver analisado não é disponibilizado pelo fabricante para sistemas Linux.
    • O driver foi produzido a partir da estrutura básica do Skeleton_driver, sugerido no livro Linux Device Driver 3rd
    • As mensagens de entrada e saída do dispositivo foram capturadas com programas de snoopy a partir do driver fornecido pelo fabricante para sistemas Windows.

Missilelauncher

Carga (load ou init)[editar | editar código-fonte]

  • Processo de carga do módulo
649 static int __init usb_ml_init(void)
650 {
651 	int result;
652 	result = usb_register(&ml_driver);
653 	if (result) {
654 		DBG_ERR("registering driver failed");
655 	} else {
656 		DBG_INFO("driver registered successfully");
657 	}
658 
659 	return result;
660 }

A carga do módulo é feita através do comando insmod ou modprobe. No entanto o comando modprobe é desnecessário pois o driver analisado não possui dependências de outros módulos.

  • Parâmetros que podem ser passados na carga do módulo
35  #define DEBUG_LEVEL_DEBUG		0
36  #define DEBUG_LEVEL_INFO		1
37  #define DEBUG_LEVEL_WARN		2
38  #define DEBUG_LEVEL_ERROR		3
39  #define DEBUG_LEVEL_CRITICAL	4
...
...
...
130    static int debug_level = DEBUG_LEVEL_INFO;
131    static int debug_trace = 0;
132    module_param(debug_level, int, S_IRUGO | S_IWUSR);
133    module_param(debug_trace, int, S_IRUGO | S_IWUSR);
134    MODULE_PARM_DESC(debug_level, "debug level (bitmask)");
135    MODULE_PARM_DESC(debug_trace, "enable function tracing");
    • O driver aceita apenas parametros de debug durante a carga.
    • As linhas 132 e 133 indicam nome, tipo e permissão dos parametros que podem ser passados.
      • S_IRUGO indica que o parâmetro ficará disponível em modo somente leitura para qualquer usuário.
      • S_IWUSR indica que o usario root poderá alterar o parametro.
    • O parametro debug_level é inicializado, por padrão, com 1, indicando que deverão ser exibidas mensagens informativas. O valor podeŕa ser alterado para 0,2,3,4, para indicar quais mensagens serão exibidas.
  • Como passar um parâmetro para o módulo, etc;
    • A passagem de parametros é feita durante a carregamento de modulo com o comando insmod:
    • # insmod ml_driver.ko debug_level=1.
      • Esse comando gera a seguinte mensagem para o kernel
      • usbcore: registered new interface driver missile_launcher
        
      • [info]  usb_ml_init(657): driver registered successfully
        

Reconhecimento ou detecção (probe)[editar | editar código-fonte]

  • Quais dispositivos podem ser reconhecidos
    • O driver analisado foi desenvolvido para um equipamento especifico, o USB missile Launcher, VendorID: 0x0a81, ProductID 0x0701.
    • Os dispositivos reconheciveis sao definidos na struct struct usb_device_id ml_table [].
 
   static struct usb_device_id ml_table [] = {
	{ USB_DEVICE(0x0a81, 0x0701 ) },
	{ }
   };
  • Como se processa a identificação do dispositivo
Os valores de VendorID e ProductID na lista de dispositivos reconheciveis sao utilizados pela funcao static int ml_probe(struct usb_interface *interface, const struct usb_device_id *id) para verificar se o dispositivo conectado é suportado pelo driver.
A função ml_probe é 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 ml_table.
A função probe recebe como parametros a interface utilizada pelo novo dispositivo e uma entrada da tabela ml_table, indicando qual dispositivofoi detectado.
        struct usb_device *udev = interface_to_usbdev(interface);
	struct usb_ml *dev = NULL;
	struct usb_host_interface *iface_desc;
	struct usb_endpoint_descriptor *endpoint;
	int i, int_end_size;
	int retval = -ENODEV;

	if (! udev) {
		DBG_ERR("udev is NULL");
		goto exit;
	}

	dev = kzalloc(sizeof(struct usb_ml), GFP_KERNEL);
	if (! dev) {
		DBG_ERR("cannot allocate memory for struct usb_ml");
		retval = -ENOMEM;
		goto exit;
	}


  • O que ocorre quando o driver não é reconhecido
Quando um dispositivo é reconhecido e todas as estruturas são alocadas com sucesso a função probe() retorna 0, indicando que o dispositivo esta pronto para ser utilizado.
Porém, sempre que durante o processo de reconhecimento ou de alocação ocorre algum erro, é utilizada um comando goto exit ou goto error;, indicando que não o dispositivo nao foi reconhecido ou que o driver não é capaz de controlar este dispositivo.

Inicialização de estruturas[editar | editar código-fonte]

  • quais estruturas são inicializadas
Apos o reconhecimento do dispositivo a a função probe aloca memória para a estrutura interna do dispositivo (struct usb_ml),que contém informações sobre o status do dispositivo conectado, inicializa semáforos e spin-locks.
        dev = kzalloc(sizeof(struct usb_ml), GFP_KERNEL);
	if (! dev) {
		DBG_ERR("cannot allocate memory for struct usb_ml");
		retval = -ENOMEM;
		goto exit;
	}

	dev->command = ML_STOP;

	init_MUTEX(&dev->sem);
	spin_lock_init(&dev->cmd_spinlock);
Alem disso a função ml_probe() também faz registro de estruturas de buffer e de URB(USB request block).
// Set up interrupt endpoint information. 
	for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
		endpoint = &iface_desc->endpoint[i].desc;

		if (((endpoint->bEndpointAddress & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN)
				&& ((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK) ==
					USB_ENDPOINT_XFER_INT)) 
			dev->int_in_endpoint = endpoint;

	}
	if (! dev->int_in_endpoint) {
		DBG_ERR("could not find interrupt in endpoint");
		goto error;
	}

	int_end_size = le16_to_cpu(dev->int_in_endpoint->wMaxPacketSize);

	dev->int_in_buffer = kmalloc(int_end_size, GFP_KERNEL);
	if (! dev->int_in_buffer) {
		DBG_ERR("could not allocate int_in_buffer");
		retval = -ENOMEM;
		goto error;
	}

	dev->int_in_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (! dev->int_in_urb) {
		DBG_ERR("could not allocate int_in_urb");
		retval = -ENOMEM;
		goto error;
	}

	// Set up the control URB. 
	dev->ctrl_urb = usb_alloc_urb(0, GFP_KERNEL);
	if (! dev->ctrl_urb) {
		DBG_ERR("could not allocate ctrl_urb");
		retval = -ENOMEM;
		goto error;
	}

	dev->ctrl_buffer = kzalloc(ML_CTRL_BUFFER_SIZE, GFP_KERNEL);
	if (! dev->ctrl_buffer) {
		DBG_ERR("could not allocate ctrl_buffer");
		retval = -ENOMEM;
		goto error;
	}

	dev->ctrl_dr = kmalloc(sizeof(struct usb_ctrlrequest), GFP_KERNEL);
	if (! dev->ctrl_dr) {
		DBG_ERR("could not allocate usb_ctrlrequest");
		retval = -ENOMEM;
		goto error;
	}
O dispositivo estudado possui dois Endpoints: Endpoint 0 e Endpoint de interrupção. Portanto deve ser alocadas estruturas para armazenar informaçes sobre esses dois endpoints.

Registro do driver (register)[editar | editar código-fonte]

Quando o módulo é carregado, a estrutura led_driver precisa ser registrada com o USB core. Isto é realizado com uma chamda a função usb_register().

A função usb_register registra um driver USB com o USB_core. A lista de interfaces acopladas serão examinados novamente sempre que um novo driver é adicionado, permitindo que o novo driver para prender a todos os dispositivos reconhecidos. Retorna um código de erro negativo em caso de falha e 0 em caso de sucesso
       int result;
result = usb_register(&ml_driver);
if (result) {
DBG_ERR("registering driver failed");
} else {
DBG_INFO("driver registered successfully");
}
return result;


Do mesmo modo, quando o driver é descarregado do sistema, ele deve "desregistrar" ele mesmo do USB core:

  usb_deregister(&ml_driver);
DBG_INFO("module deregistered");


  • Como o driver é reconhecido
  • Quais procedimentos (comandos) para verificar qual dispositivo específico que foi reconhecido
  • Quais drivers que foram carregados para o dispositivo, etc;

Comunicação[editar | editar código-fonte]

A comunicação via USB se dá através dos chamados endpoints. Um endpoint pode transportar dados em uma única direção, seja do computador para o dispositivo (chamado OUT endpoint) ou do dispositivo para o computador (chamado IN endpoint). Os endpoints são descritos no kernel por um ponteiro que aponta para uma estrutura chamada usb_endpoint_descriptor, que contém todos os dados específicos da USB nos mesmos formatos especificados pelo próprio driver

A implementação do driver USB Linux utiliza um bloco de requisição USB (URB) como "suporte de dados" para se comunicar com dispositivos USB. URBS são como mensagens de dados que são enviadas de forma assíncrona a partir de e para os Endpoints. Lembrando que o padrão USB inclui quatro tipos de Endpoints. Da mesma forma, quatro tipos diferentes de URBS existem, ou seja, o controle, bulk, de interrupção e isócronos URBS. Uma vez que um URB é alocado e inicializado pelo driver, ele será submetido ao núcleo USB, que encaminha para o dispositivo. Se a URB foi entregue com sucesso para o núcleo USB, um manipulador de conclusão é executado. Em seguida, o núcleo USB de controle retorna para o driver de dispositivo.

Como o nosso lançador de mísseis possui dois Endpoints (Endpoint 0 e o Endpoint de interrupção), temos que lidar com o controle e a interrupção de URBS. Os comandos de engenharia reversa são, basicamente, embalado em um controle URB e depois enviado para o dispositivo. Além disso, estamos continuamente recebendo informações sobre o estado das interrupções periódicas URBs. Por exemplo, para enviar dados simples para o lançador de mísseis, a função usb_control_msg função é utilizada:

   
   memset(&buf, 0, sizeof(buf));
   buf[0] = cmd;

   /* The interrupt-in-endpoint handler also modifies dev->command. */
   spin_lock(&dev->cmd_spinlock);
   dev->command = cmd;
   spin_unlock(&dev->cmd_spinlock);

   retval = usb_control_msg(dev->udev,
           usb_sndctrlpipe(dev->udev, 0),
           ML_CTRL_REQUEST,
           ML_CTRL_REQEUST_TYPE,
           ML_CTRL_VALUE,
           ML_CTRL_INDEX,
           &buf,
           sizeof(buf),
           HZ*5);

   if (retval < 0) {
       DBG_ERR("usb_control_msg failed (%d)", retval);
       goto unlock_exit;
   }

O comando cmd é inserido no buffer buf contendo os dados a serem enviados para o dispositivo. Se o URB é concluída com êxito, o manipulador correspondente é executado. Ele executa nada demais, exceto dizer ao driver que lançou um (ainda não corrigido) comando através da write syscall:

static void ml_ctrl_callback(struct urb *urb, struct pt_regs *regs)
{
   struct usb_ml *dev = urb->context;
   dev->correction_required = 0;
}
  • arquivos especiais de dispositivo (/proc, /dev e /sys)
A funcção ml_probe() cria o arquivo especial /dev/mlx, após o resgistro do dispositivo, e é através deste arquivo que o dispositivo sera acessado.
  • tipos de comunicação utilizados pelo driver (control, bulk, etc);
Durante o processo de inicialização de estruturas são criados dois endpoints; de controle e interrupt.

Finalização[editar | editar código-fonte]

  • Mensagens de remoção do dispositivo:
A fase de desconexão do dispositivo é invocada quando o USB core detecta que o dispositivo não está mais presente no sistema. No caso do driver ml_driver, a função associada a este evento é a função ml_disconnect(struct usb_interface *interface)
Essa função tem o objetivo de desalocar as estruturas alocadas durante o processo de probe e de interromper as URBs que ainda estiverem ativas.


  usb 6-1: new low speed USB device using uhci_hcd and address 16
[62801.208173] usb 6-1: USB disconnect, address 19e
[info] ml_disconnect(657): USB missile launcher /dev/ml0
  • desalocação/liberação de estruturas;
     mutex_lock(&disconnect_mutex);	/* Not interruptible */

dev = usb_get_intfdata(interface);
usb_set_intfdata(interface, NULL);

down(&dev->sem); /* Not interruptible */

minor = dev->minor;

usb_deregister_dev(interface, &ml_class);

/* If the device is not opened, then we clean up right now. */
if (! dev->open_count) {
up(&dev->sem);
ml_delete(dev);
} else {
dev->udev = NULL;
up(&dev->sem);
}

mutex_unlock(&disconnect_mutex);

Considerações finais[editar | editar código-fonte]

A comunicação dos dispositivos com o espaço do usuário - e conseqüentemente com as aplicações - se dá por meio de entradas nos diretórios /proc, /sys e /dev do Linux. Para o dispositivo missile laucher é criada uma entrada no diretorio /dev/ml.
O criador do driver desenvolveu uma aplicação para o usuário se comunicar com o dispositivo, que é responsavel por receber as solicitações do usuário e enviar para o dispositivo. Além do projeto de trabalho futuro o desenvolvimento de um driver Linux para um lançador de mísseis com uma web cam acoplada.

Bibliografia/Referências[editar | editar código-fonte]

Documentação Linux

/usr/src/linux-source-2.6.31/Documentation/usb/hiddev.txt /usr/src/linux-source-2.6.31/Documentation/usb/URB.txt