Introdução aos drivers USB
Published: 2023-10-03 02:36:28

## 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](https://github.com/GuilhermeGiacomoSimoes/introducao_dev_driver_linux)

## 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"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gpqk67pub42ghyf6655u.png)

**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/edbj53huzeohksapl3e5.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4z3dlwzqssveu7wruu0.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0e37hhecf0tg8seyd8d7.png)



**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
```C
#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);
``` 

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:
```C
///...
#include <linux/usb.h>
```
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:
```C
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
};
```
* `.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:
```C
//...
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,
};
//....
```

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:
```C
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);
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4obppn9o5r84upbdy15.png)

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:
```C
//...
#define USB_VENDOR_ID 0x2341
#define USB_PRODUCT_ID 0x0043
//...
``` 

Então quando eu conectar o meu Arduino, a minha função probe deve ser executada. Vou declarar minha função probe e disconnect:
```C
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);
}
```
 
E também precisamos montar nossa função `__init` e nela precisamos chamar a função `usb_register` para registrar nosso novo driver USB.

```C
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;
}
```

E na minha função `__exit` preciso remover o meu driver:
```C
static void __exit hello_world_exit(void)
{
	usb_deregister(&hello_world_usb);
	pr_info("Adeus mundo cruel\n");
}
```

Vamos declarar nosso Makefile
```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
```

Agora podemos compilar e instalar nosso modulo 
```Bash
$ make
$ sudo insmod hello_world_usb.ko
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hsyqppkjadikz6qryt29.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/btmdbni09u4sl4iz26pr.png)
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**
- [Documentação oficial do linux](https://www.kernel.org/doc/html/next/driver-api/usb/writing_usb_driver.html)
- [USB Pinout](https://www.electroschematics.com/usb-how-things-work/)
- [USB in a NutShell](https://www.beyondlogic.org/usbnutshell/usb1.shtml)
- [How USB communication work](https://microchipdeveloper.com/usb:how-it-works)