24 pontos por GN⁺ 21 일 전 | 1 comentários | Compartilhar no WhatsApp
  • 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) e PID (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, e 4ee0 é o PID do bootloader Nexus/Pixel
  • 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
  • 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 de VID:PID for 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
  • Depois disso, é possível obter o descritor do dispositivo com a requisição GET_DESCRIPTOR
    • Os dados retornados incluem informações como idVendor, idProduct e bDeviceClass
  • Com o comando lsusb -v, é possível verificar em detalhes todos os descritores (dispositivo, configuração, interface, endpoint etc.)
    • Exemplo: a interface Android Fastboot possui endpoints Bulk IN(0x81) e Bulk OUT(0x02)

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
  • 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 dados
    • OUT: direção em que o host envia dados
    • Se o bit mais significativo (MSB) do endereço do endpoint for 1, ele é IN; se for 0, é 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"
  • 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
      
    • OKAY indica estado de sucesso, e 0.4 é a versão do Fastboot

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

 
GN⁺ 21 일 전
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

    • O período de espera da Guitar Center não é só para verificar se o item é roubado. Pela lei, eles têm a obrigação de não vender por um certo tempo, como uma casa de penhores (pawn shop), e só podem vender depois que passar o prazo em que o dono original poderia recuperá-lo
    • Eu também uso esse equipamento e empacotei esse driver no AUR. O blob binário não funcionou, mas para usar como roteador MIDI simples já basta
  • 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

    • Em Rust, recomendo nusb
  • 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

    • Dá para amenizar isso, porque o override de driver exige ajustar só uma camada
  • 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

    • Dispositivos padronizados normalmente usam USB/CDC/ECM ou RNDIS, então são reconhecidos automaticamente. O acesso em espaço de usuário é mais útil justamente para dispositivos fora do padrão. No Windows, dá para implementar isso de forma portátil com libusb sem assinatura de driver
    • No Linux, você pode criar um dispositivo tun/tap para se comunicar com o kernel a partir do espaço de usuário, ou então precisa rodar outros subsistemas também em espaço de usuário
  • 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

    • Isso é uma ligadura de fonte de programação. Se você copiar, vai ver que na verdade é ->. É a sintaxe de trailing return type do C++ moderno
    • Alguns desenvolvedores preferem fontes com ligaduras. Elas juntam dois caracteres em um único glifo
    • Se você configurar uma tecla Compose, dá para digitar “→” em qualquer teclado
    • No fim, é só "->". A fonte apenas renderiza isso como uma seta
  • Fiquei 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

    • Dispositivos USB não acessam diretamente a memória do host como PCIe ou FireWire. Em vez disso, o controlador XHCI é que faz DMA, e a maioria dos controladores de dispositivo suporta DMA entre a própria RAM e o USB
    • Toda transferência é iniciada pelo host. Mesmo quando parece que o dispositivo está enviando dados primeiro, na prática o host é que faz a solicitação. DMA direto seria um grande risco de segurança
  • 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

    • Eu também achava descriptors algo misterioso, mas no fim percebi que são apenas structs binárias fixas. Se você preencher corretamente os campos e endpoints definidos por cada classe USB, o dispositivo é reconhecido
    • USB é ok, mas do ponto de vista elétrico o USB 1/2 não é sinal diferencial de verdade
    • Quase não há material em formato de tutorial, mas para um padrão de grandes empresas ele é até bem razoável. Só que há opções demais, então você precisa ler muitas especificações relacionadas
  • 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