USB para desenvolvedores de software: introdução à criação de drivers USB em espaço de usuário
(werwolv.net)- O desenvolvimento de drivers USB costuma ser visto como um trabalho de nível de kernel, mas na prática também pode ser implementado em espaço de usuário com uma dificuldade semelhante à programação com sockets
- Com libusb, é possível fazer enumeração de dispositivos, transferências de controle e envio/recebimento de dados sem escrever código de kernel
- A comunicação USB é composta por quatro tipos de transferência — Control, Bulk, Interrupt, Isochronous — e pelas direções IN/OUT, e cada endpoint funciona como um canal unidirecional
- Usando o protocolo Fastboot de dispositivos Android como exemplo, o texto demonstra em código o processo de trocar comandos e respostas por endpoints Bulk
- Mesmo em espaço de usuário, é possível implementar um driver USB completo, e todos os protocolos USB compartilham a mesma estrutura básica
Introdução
- Drivers para dispositivos USB podem parecer difíceis por causa da ideia de que exigem lidar com código de kernel, mas na prática têm uma complexidade de nível de aplicação semelhante ao uso de sockets
- Mesmo desenvolvedores sem muita experiência com hardware podem aprender a lidar com USB em espaço de usuário
- Existem materiais que tratam do funcionamento detalhado do USB, mas eles são difíceis de abordar para iniciantes
- O uso de USB não exige conhecimento de nível de sistemas embarcados e pode ser abordado como sockets de rede
Dispositivo USB
- O exemplo usa um smartphone Android em modo bootloader
- É fácil de obter, o protocolo é simples e, como o SO não traz driver padrão para ele, é adequado para experimentação
- A forma de entrar no modo bootloader varia conforme o dispositivo, mas em geral é possível usando uma combinação do botão de energia com os botões de volume
Enumeração manual do dispositivo
- Enumeração (Enumeration) é o processo em que o host solicita informações do dispositivo para identificá-lo, e isso é feito automaticamente quando o dispositivo é conectado
- Dispositivos padrão carregam drivers automaticamente com base em sua classe USB, enquanto dispositivos específicos do fabricante usam
VID(Vendor ID) ePID(Product ID) - No Linux, é possível verificar informações do dispositivo com o comando
lsusb- Exemplo:
ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot) 18d1é o VID do Google, e4ee0é o PID do bootloader Nexus/Pixel
- Exemplo:
- Com o comando
lsusb -t, é possível verificar a classe e o estado do driver- Se aparecer
Class=Vendor Specific Class,Driver=[none], isso indica que o SO não carregou um driver
- Se aparecer
- No Windows, as mesmas informações podem ser verificadas no Device Manager ou no USB Device Tree Viewer
Enumeração de dispositivos com libusb
- Com a biblioteca libusb, é possível se comunicar com dispositivos USB em espaço de usuário sem escrever código de kernel
- Com
libusb_hotplug_register_callback(), é possível configurar a execução de um callback quando um dispositivo com uma combinação específica deVID:PIDfor conectado - Ao conectar o dispositivo depois de executar o programa, a mensagem
"Device plugged in!"é exibida - No Linux, isso funciona por padrão e, se necessário, é possível separar o driver de kernel com
libusb_detach_kernel_driver() - No Windows, o driver
Winusb.sysé necessário e, se ele não estiver presente, pode ser substituído manualmente com a ferramenta Zadig
Comunicação com o dispositivo
- A primeira comunicação com um dispositivo USB é feita pelo endpoint Control (endereço 0x00)
- Com
libusb_control_transfer(), é possível enviar uma requisição padrão (GET_STATUS) para ler o estado do dispositivo- Exemplo de resposta:
01 00→ o primeiro byte indica Self-Powered, e o segundo indica sem suporte a Remote Wakeup
- Exemplo de resposta:
- Depois disso, é possível obter o descritor do dispositivo com a requisição GET_DESCRIPTOR
- Os dados retornados incluem informações como
idVendor,idProductebDeviceClass
- Os dados retornados incluem informações como
- Com o comando
lsusb -v, é possível verificar em detalhes todos os descritores (dispositivo, configuração, interface, endpoint etc.)- Exemplo: a interface
Android Fastbootpossui endpoints Bulk IN(0x81) e Bulk OUT(0x02)
- Exemplo: a interface
Endpoints
- Endpoints são um conceito semelhante a portas de rede: canais pelos quais o dispositivo envia e recebe dados
- O tipo e a direção de cada endpoint são definidos nos descritores
-
Tipo de transferência Control
- Todo dispositivo possui um, e o endereço é sempre
0x00 - É usado para configuração inicial e para solicitar informações do dispositivo
- Não pertence a uma interface e existe como parte do próprio dispositivo
- Todo dispositivo possui um, e o endereço é sempre
-
Tipo de transferência Bulk
- Usado para transferência de grandes volumes de dados não em tempo real
- Ex.: Mass Storage, CDC-ACM (serial), RNDIS (Ethernet)
- A largura de banda é alta, mas a prioridade é baixa
-
Tipo de transferência Interrupt
- Usado para transferência de pequenas quantidades de dados com baixa latência
- Teclados, mouses e afins fazem polling rápido de entradas como botões
- Não é uma interrupção real de hardware; o host faz as requisições periodicamente
-
Tipo de transferência Isochronous
- Usado para grandes volumes de dados sensíveis ao tempo (streaming de áudio e vídeo)
- Se houver atraso, a perda de qualidade aparece imediatamente
- No libusb, isso é tratado de forma assíncrona
-
Direções IN / OUT
- O USB tem uma estrutura centrada no host, então o dispositivo não transmite dados antes de receber uma solicitação
IN: direção em que o host recebe dadosOUT: direção em que o host envia dados- Se o bit mais significativo (MSB) do endereço do endpoint for
1, ele é IN; se for0, é OUT - É possível usar até 127 endpoints definidos pelo usuário (
0x00é exclusivo para Control) - Endpoints são unidirecionais e normalmente aparecem em pares IN/OUT, como na interface Fastboot
Protocolo Fastboot
- Fastboot é um protocolo de comunicação do bootloader Android, no qual se envia uma string de comando e se recebe um código de status de 4 bytes e dados
- Ex.:
Host: "getvar:version"→Client: "OKAY0.4"Host: "getvar:nonexistant"→Client: "OKAY"
- Ex.:
- Exemplo de código que envia comandos Fastboot usando libusb
- A interface 0 é assumida com
libusb_claim_interface() - O comando
"getvar:version"é enviado para o endpoint Bulk OUT(0x02) - A resposta é recebida pelo endpoint Bulk IN(0x81)
- Exemplo de saída:
Request: getvar:version Response: OKAY0.4 OKAYindica estado de sucesso, e0.4é a versão do Fastboot
- A interface 0 é assumida com
Encerrando
- É possível implementar um driver USB completo em espaço de usuário sem escrever código de kernel
- Todos os drivers USB seguem os mesmos princípios básicos; o que muda é o protocolo
- Mesmo protocolos complexos (como MTP) têm a mesma estrutura fundamental e podem ser abordados com um conceito semelhante à comunicação por sockets
1 comentários
Comentários no Hacker News
O timing foi perfeito. Em breve vou buscar um MOTU MIDI Express XT numa Guitar Center aqui da região
Como é equipamento usado, eles são obrigados por lei a segurá-lo por um certo período, então estou esperando. O problema é que esse equipamento não usa MIDI-over-USB padrão, e sim um protocolo proprietário, então não dá para usar direto por USB nos meus sistemas, como Linux, OpenBSD e Haiku
Por enquanto tudo bem, porque só preciso fazer roteamento entre módulos de synth e controladores, mas seria ótimo fazê-lo funcionar também no PC
Existe um driver Linux já existente, mas a estabilidade é incerta e não está claro se há suporte ao XT. Disseram que o problema de kernel panic foi resolvido, mas ainda restam issues
Então estou pensando em criar eu mesmo um driver em espaço de usuário baseado em LibUSB. Se eu expuser portas MIDI e adicionar ferramentas de roteamento, pode ficar bem útil
Se você quiser fazer esse tipo de coisa em Go, eu criei a biblioteca go-usb, que permite acessar USB sem cgo
Também desenvolvi com ela o go-uvc para lidar com dispositivos UVC
Eu também estou implementando recentemente um sistema usbip de forma parecida no Macbook M3
Só que há limitações nas versões mais novas do macOS. Para dispositivos USB que o sistema reconhece, não dá para compilar um driver em espaço de usuário baseado em libusb a menos que você desative manualmente recursos de segurança
No fim das contas, essa abordagem faz com que o driver USB também exerça o papel de código de aplicação. Ou seja, fica mais próximo de uma biblioteca + programa do que de um driver
Por exemplo, fiquei curioso sobre como isso funcionaria para conectar um dispositivo USB-Ethernet como adaptador de rede do sistema operacional
Se eu tivesse lido este artigo alguns anos atrás, teria sido muito mais fácil quando fui fazer engenharia reversa de funções do meu notebook. Em especial, o programa de controle dos LEDs do teclado ainda é um dos meus projetos favoritos
Foi uma introdução realmente útil. Lidar com APIs de hardware de baixo nível é difícil, mas recompensador. As camadas de abstração dos sistemas operacionais modernos facilitaram isso, mas ainda é importante entender o que existe por baixo
O código em C++ pareceu estranho. Nunca vi um teclado em que desse para digitar diretamente o caractere de seta
->. É a sintaxe de trailing return type do C++ moderno"->". A fonte apenas renderiza isso como uma setaFiquei curioso se dispositivos USB suportam DMA. Se isso só é possível por meio do host ou se o dispositivo consegue acessar a memória diretamente
Há algum tempo tentei criar um dispositivo USB simples, mas quase não encontrei informação sobre como escrever descriptors. A maioria das orientações era algo como “ache um dispositivo parecido, copie e vá ajustando”. Fiquei em dúvida se USB é mesmo um padrão tão bom assim
Se me pedissem para “escrever você mesmo um driver de dispositivo USB”, eu devolveria o dispositivo e primeiro veria se não dá para resolver com uma porta COM virtual