DEV Community

Guilherme Giácomo Simões
Guilherme Giácomo Simões

Posted on

Introdução aos drivers USB

Introdução

Nesse quarto texto da série "introdução ao desenvolvimento de driver para linux" vamos introduzir um pouco do conceito das conexões USB, como se comunicam e como iniciar um "hello world" para um driver de usb.
Se você caiu de paraquedas nesse texto, aqui esta o link com todos os codigos e textos dessa serie

Protocolo USB

Nessa introdução ao protocoloco USB, vou falar brevemente sobre o assunto.

Pinagem
Se você pegar um cabo USB e abri-lo, vai perceber que ele tem 4 cabos dentro dele envolto na "capa". Eles tem cada um suas funções. O cabo vermelho é um VCC e tem tensão de 5v, o cabo preto é um GND, o cabo verde (chamado de DATA+ ou simplesmente D+) e o cabo branco (chamado de DATA- ou simplesmente D-) são usados para envio e recebimento de dados. Os dados são transferidos através dos conectores D+ e D- enquanto os conectores VCC e GND fornecem energia ao dispositivo USB.

Imagem de exemplo de um cabo USB sem a "capa"

Comunicação
Toda transferencia de dados, independente da direção é iniciada pelo host. O host controla o dispositivo USB. O host também controla o tempo de comunicação com o dispositivo mantendo intervalos de tempo chamados frames.
Para inicio do frames, o host emite uma sequência de início de frame (SOF - Start Of Frame ) nas linhas de dados USB no início de cada frame.
O host USB conduz transações de comunicação com os dispositivos durante o frame.

USB Frame

O mecanismo de transferência de dados envolve o host gravar e ler de um determinado espaço de memória. Esses locais de memória são chamados de endpoints. O tamanho de um endpoint pode variar muito dependendo do dispositivos.
O número de endpoints começa em 0 e podem chegar até 32. Os endpoints do dispositivo são encontrados em pares numerados, então, cada índice do endpoint tem dois endereços, chamados de IN e OUT. Então o endpoint 1 tem o endereço 1_IN e 1_OUT, o endpoint 2 tem o 2_IN e o 2_OUT e assim por diante.

Os endpoints OUT transportam dados que saem do host para o device, enquanto os endpoints IN contêm dados enviados do device para o host.
O software do dispositivo fica responsável por ir até o endpoint OUT e verificar se tem algo para ler. Caso durante o monitoramento o software do dispositivo identifique uma mensagem no OUT, ele copia essa mensagem e executa alguma ação. Se o software do dispositivo deseja enviar uma mensagem ele coloca essa mensagem no endpoint IN. A mensagem permanecerá no endpoint IN até que o host decida ler ela.

Imagem ilustrativa demonstrando o processo de comunicação

As informações sobre todos os parâmetros do endpoint e requisitos de comunicação são definidas pelo device e fornecidas ao host quando o dispositivo é conectado ao host e enumerado com sucesso.
Todo dispositivo USB na verdade reserva o endpoint 0 (0_in e 0_out) como um endpoint de controle. O endpoint 0_in contém uma descrição do dispositivo USB, o código do fabricante, o código do dispositivo, entre várias outras informações que são lidas pelo host durante a enumeração. O 0_out fornece ao host a capacidade de enviar comandos ao dispositivo como Redefinir, Suspender ou Restaurar um dispositivo suspenso.

Non Return To Zero
No seguimento de comunicação, um código de linha Sem Retorno a Zero (Non Return To Zero ou simplesmente NRZ) é um código binário no qual, o nível lógico alto é geralmente representado por uma tensão positiva, e o nível lógico baixo é representado por uma tensão negativa. Por isso então no USB temos o D+ e o D- para envio e recebimento de dados. D+ é para representar nível lógico alto e D- é para representar nível lógico baixo.
O NRZ, tem esse nome justamente porque ele nunca é zero, não tem um sinal de repouso nem nada do tipo. A tensão ou é positiva ou negativa. Como na imagem abaixo:

NRZ

Non Return To Zero Inverted
Bom, o USB não se comunica através do NRZ, mas sim através de uma variante do NRZ conhecida como NRZI (Non Return to Zero Inverted). Ela é como o NRZ, nunca é zero, porém nesse tipo de encodamento, o nível lógico 1 significa que nenhuma mudança no bit ocorreu enquanto que o nível lógico 0 significa que o bit mudou. Parece um pouco confuso, mas não é tanto assim. Vamos pensar no seguinte binário: 0011. Veja que os dois primeiros dígitos são 0s. Logo ele vai emitir 11, porque não há mudança no nível lógico. Porém entre os bits 01, ele emitirá um bit 0 (ou uma tensão negativa através do D-) para indicar que houve uma mudança no nível lógico. E a sequência 0011, encodada para NRZI fica representada por 1101.
Agora usando um exemplo mais complexo, dado a sequência binária a seguir: 111010000100. Ela poderá ser representada pelo NRZI da seguinte forma: 111000111001. Então quando tem uma alteração do nível lógico da informação, o nível lógico do D+ e D- mudam conforme mencionado anteriormente.

Bit stiffing
Quando longas séries de 0s são transmitidas usando NRZI, causa uma transição nos níveis. Mas quando longas séries de 1s são transmitidas, nenhuma transição ocorre de acordo com o esquema de codificação NRZI. O bit sempre permanece o mesmo. Nenhuma transição de níveis por muito tempo pode confundir o receptor e dessincroniza-lo.
O Bit stiffing é um processo no qual um 0 é inserido nos dados brutos a cada seis 1s consecutivos.
A inserção de zero provoca transição de nível. O receptor deve reconhecer os bits preenchidos e descartá-los após decodificar os dados NRZI.
Caso nenhuma transição ocorra no sinal NRZI após seis 1s consecutivos, o receptor decide que o bit stiffing não foi feito e descarta os dados recebidos.

Mãos no código

Vamos começar uma breve introdução aos drivers USBs, e nos próximos textos faremos algo mais complexo.
Para começar vou criar um hello_world_usb.c bem simples com os imports e funções básicas padrões de todo modulo de driver

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_AUTHOR("SEU NOME <seu_email@email.com>");
MODULE_DESCRIPTION("Introducao a criacao de driver pra linux");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.0");

static int __init hello_world_init(void)
{
    return 0;
}

static void __exit hello_world_exit(void)
{
    pr_info("Adeus mundo cruel\n");
}

module_init(hello_world_init);
module_exit(hello_world_exit);
Enter fullscreen mode Exit fullscreen mode

Bom, para começar falando sobre a implementação do driver, preciso dizer que praticamente tudo de baixo nivel relacionado a USB é abstraído pelo kernel. Então ele facilita muito nossa comunicação de driver USB com o dispositivo.
Inclusive existe uma interface chamada usb_driver para que possamos implementar o nosso driver USB com facilidade. Essa lib fica dentro do path include/linux/usb.h da árvore de código do linux. Então já podemos importar ela no inicio do nosso módulo:

///...
#include <linux/usb.h>
Enter fullscreen mode Exit fullscreen mode

Vamos usar a struct usb_driver para começar nosso driver. Mas antes vamos comentar um pouco sobre ela.
Ela tem vários parâmetros, vou comentar todos. Aqui esta ela:

static struct usb_driver skel_driver = {
    .name = "skeleton",
    .probe = skel_probe,
    .disconnect = skel_disconnect,
    .suspend = skel_suspend,
    .resume = skel_resume,
    .pre_reset = skel_pre_reset,
    .post_reset = skel_post_reset,
    .id_table = skel_table,
    .supports_autosuspend = 1
};
Enter fullscreen mode Exit fullscreen mode
  • .name: Esse parâmetro deixa bem explicito para que ele serve. Ele é uma descrição do driver e normalmente é o mesmo nome do modulo.
  • .probe: Chamado na inicialização do driver para verificar se o driver está disposto ou precisa gerenciar uma interface de algum dispositivo em particular. Por ex, no nosso caso, usaremos esse cara para gerenciar um usb e controlar um display de 7segmentos, então ele vai precisar gerenciar um dispositivo em particular. Se precisasse gerenciar um mouse, não precisaria informar uma interface específica.
  • .disconnect: Função chamada quando o dispositivo usb é desconectado ou o modulo é removido.
  • .suspend: Chamado quando o dispositivo será suspenso pelo sistema no contexto de suspensão do sistema ou de suspensão do tempo de execução.
  • .resume: Chamado quando o dispositivo volta da suspensao do sistema operacional.
  • .pre_reset: Chamado por usb_reset_device() quando o dispositivo esta prestes a ser reiniciado
  • .post_reset: Chamado por usb_reset_device() após o dispositivo ser reiniciado.
  • .id_table: Os drivers USB usam uma tabela de ID para suportar hotplugging (conectar e usar). Exporte esse id_table com MODULE_DEVICE_TABLE(). Se você não exportar sua função probe jamais sera chamada.
  • .supports_autosuspend: Se for 0 o nucleo USB do kernel jamais permitira a suspensão automatica para interfaces vinculadas a esse driver.

No usb_driver somente é obrigatório fornecer o campo name, probe, disconnect e id_table. O resto é opcional.
Os métodos probe() e disconnect() são chamados em um contexto onde podem suspender o device. Mas não abuse desse privilégio. A maior parte do trabalho de conexão a um device deve ser realizada quando o device for inicializado e desfeito no último fechamento. O código de desconexão precisa resolver problemas de simultaneidade com relação aos métodos open() e close(), bem como forcar a conclusão de todas as solicitações de E/S pendentes.

Como somente é obrigatório o campo name, probe, disconnect e id_table, não vamos inventar moda aqui em um texto introdutório sobre o assunto, vamos usar somente o obrigatório. Iremos declarar a struct no nosso hello_world_usb.c no inicio do arquivo:

//...
static struct usb_driver hello_world_usb = {
    .name = "hello world usb",
    .probe = hello_world_usb_probe,
    .disconnect = hello_world_usb_disconnect,
    .id_table = hello_world_usb_table,
};
//....
Enter fullscreen mode Exit fullscreen mode

Para permitir que o sistema linux-hotplug carregue o driver automaticamente quando o dispositivo for conectado, você precisa criar um MODULE_DEVICE_TABLE. O código a seguir informa aos scripts hotplug que este modulo oferece suporte a um único dispositivo com um ID de fornecedor e ID de produto específicos:

static struct usb_device_id hello_world_usb_table[] = {
    { USB_DEVICE(USB_VENDOR_ID, USB_PRODUCT_ID) },
    { }
};
MODULE_DEVICE_TABLE(usb, hello_world_usb_table);
Enter fullscreen mode Exit fullscreen mode

Lembra que dissemos acima sobre o endpoint 0 do USB? Então, essas informações de código de fornecedor (USB_VENDOR_ID) e código do produto (USB_PRODUCT_ID) estarão armazenadas la. Não vamos aprender a ler elas nesse texto por ser um conteúdo introdutório, mas nos próximos textos com certeza iremos aprender sobre isso.
Bom, ele precisa ter o VENDOR_ID e o PRODUCT_ID no endppoint 0, para ler, e achar dentro da tabela de dispositivos o VENDOR_ID e PRODUCT_ID correspondente ao que ele leu no endpoint 0, para então chamar o modulo de driver correto.

Nesse caso, vou fazer o seguinte: Tenho um Arduino aqui em casa e vou conecta-lo ao meu computador, e rodar o comando lsusb. Dessa forma ele vai listar os dispositivos conectados ao meu computador e vai me dar algumas breves informações sobre eles. Esse é o resultado do meu lsusb:

Image description

Repare na linha onde tem a descrição Arduino SA Uno R3 que temos várias informações com relação ao dispositivo. Na sexta coluna onde tem a informação 2341:0043 esses são o VENDOR ID e o PRODUCT ID do meu Arduino. Então 2341 é o vendor_id e o 0043 é o product_id. Isso é um hexadecimal. Vou declarar meu VENDOR_ID e PRODUCT_ID no código:

//...
#define USB_VENDOR_ID 0x2341
#define USB_PRODUCT_ID 0x0043
//...
Enter fullscreen mode Exit fullscreen mode

Então quando eu conectar o meu Arduino, a minha função probe deve ser executada. Vou declarar minha função probe e disconnect:

static int hello_world_usb_probe(struct usb_interface *intf, 
        const struct usb_device_id *id) 
{
    pr_info("Dispositivo %s foi conectado", USB_PRODUCT_ID);
    return 0;
}

static void hello_world_usb_disconnect(struct usb_interface *intf)
{
    pr_info("Dispositivo %s desconectado", USB_PRODUCT_ID);
}
Enter fullscreen mode Exit fullscreen mode

E também precisamos montar nossa função __init e nela precisamos chamar a função usb_register para registrar nosso novo driver USB.

static int __init hello_world_init(void)
{
    int result = usb_register(&hello_world_usb);
    if(result) {
        pr_err("usb_register falhou para o driver %s. Erro numero %s"
                , hello_world_usb.name, result);
        return -1;
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

E na minha função __exit preciso remover o meu driver:

static void __exit hello_world_exit(void)
{
    usb_deregister(&hello_world_usb);
    pr_info("Adeus mundo cruel\n");
}
Enter fullscreen mode Exit fullscreen mode

Vamos declarar nosso Makefile

obj-m += hello_world_usb.o

all: run

run:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Enter fullscreen mode Exit fullscreen mode

Agora podemos compilar e instalar nosso modulo

$ make
$ sudo insmod hello_world_usb.ko
Enter fullscreen mode Exit fullscreen mode

Apos a insercao do modulo de driver, quando eu insiro meu dispositivo USB o meu dmesg tem a seguinte saida:

Saida dmesg apos a insercao do modulo de driver

Ele reconheceu a conexão e printou a mensagem que esperávamos. Agora quando eu remover o dispositivo USB:

Saida dmesg após remover o device
Obtemos a mensagem esperada que colocamos na nossa função de disconnect.

Revisão

  • Aprendemos como funciona a pinagem da conexão USB.
  • Aprendemos como funciona a comunicação USB.
  • Aprendemos a criar um driver introdutório para um dispositivo USB existente.

Referencias

Top comments (0)