Inventando a biblioteca vusb

1. Introdução


Depois de ler o nome, uma pergunta lógica pode surgir: por que hoje em dia estudamos a implementação de software de USB de baixa velocidade quando existem vários controladores baratos com um módulo de hardware? O fato é que o módulo de hardware, ocultando o nível de troca de níveis lógicos, transforma o protocolo USB em uma espécie de mágica. Para sentir como essa “mágica” funciona, não há nada melhor do que reproduzi-la do zero, começando no nível mais baixo.


Para esse fim, tentaremos criar um dispositivo fingindo ser USB-HID com base no controlador ATmega8. Diferentemente da literatura difundida, não iremos da teoria à prática, do nível mais baixo ao mais alto, das tensões lógicas às conclusões e terminamos com a "invenção" do mesmo vusb, após cada etapa, verificando se o código funciona como esperado. Separadamente, observo que não invento uma alternativa a essa biblioteca, mas reproduzi consistentemente seu código-fonte, preservando a estrutura e os nomes originais o máximo possível, explicando por que essa ou aquela seção serve. No entanto, meu estilo usual de escrever código é diferente do estilo dos autores de vusb. Imediatamente, admito sinceramente que, além do interesse altruísta (contar um tópico difícil para os outros), também tenho um interesse egoísta - estudar o tópico por conta própria e captar no máximo um número de pontos sutis para mim. Segue-se também que algum ponto importante pode ser esquecido ou algum tópico não é totalmente divulgado.


Para uma melhor compreensão do código, tentei destacar as seções alteradas com comentários e removê-las das seções discutidas anteriormente. Na verdade, o código fonte será a principal fonte de informação, e o texto explicará o que foi feito e por que, além de qual resultado é esperado.


Também observo que apenas o USB de baixa velocidade é considerado, mesmo sem mencionar, o que distingue mais variedades de alta velocidade.


Etapa 0. Ferro e outra preparação


Como teste, vamos fazer uma placa de depuração caseira baseada no ATmega8 com quartzo de 12 MHz. Não vou dar o esquema, é bastante padrão (consulte o site oficial da vusb), a única coisa que vale a pena mencionar são as conclusões utilizadas. No meu caso, a saída D + corresponde ao PD2, a saída D-PD3 e o suspensor fica no PD4. Em princípio, um resistor pull-up pode ser conectado à energia, mas o controle manual parece um pouco mais consistente com o padrão.


A alimentação de 5 V é fornecida pelo conector USB; no entanto, não são esperados mais de 3,6 V nas linhas de sinal (por que isso foi um mistério para mim). Portanto, você precisa diminuir a potência do controlador ou colocar os diodos zener nas linhas de sinal. Eu escolhi a segunda opção, mas em geral isso não importa.


Como estamos “inventando” a implementação, seria bom ver o que acontece no cérebro do controlador, ou seja, pelo menos algum tipo de informação de depuração é necessária. No meu caso, esses são dois LEDs no PD6, PD7 e, o mais importante, UART no PD0, PD1, configurado no 115200, para que você possa ouvir a conversa do controlador através de uma tela regular ou de outro programa para trabalhar com a porta COM:


$ screen /dev/ttyUSB0 115200 

Além disso, um wireshark com o módulo apropriado se tornará um utilitário útil para a depuração USB (nem sempre começa da caixa, mas a solução desses problemas está localizada com sucesso na Internet e não é a tarefa deste artigo).


Aqui seria possível gastar outro kilobyte de texto na descrição do programador, makefiles e outras coisas, mas isso dificilmente faz sentido. Da mesma forma, não vou focar nas configurações periféricas que não estão relacionadas ao USB. Se alguém não consegue entender isso, é muito cedo para entrar nas entranhas do software USB?


O código fonte de todas as etapas está disponível no Github.


Etapa 1. Aceite pelo menos algo


De acordo com a documentação, o USB suporta várias velocidades fixas, das quais o AVR obterá apenas as mais baixas: 1,5 megabits por segundo. É determinado pelo resistor de pull-up e comunicação subsequente. Para a frequência escolhida, o resistor deve conectar D- a uma fonte de alimentação de 3,3 V e ter um valor nominal de 1,5 kOhm, mas, na prática, pode ser conectado com +5 V e o valor nominal pode variar ligeiramente. Com uma frequência de controlador de 12 MHz, apenas 8 ciclos de clock por bit. É claro que essa precisão e velocidade são alcançáveis ​​apenas no assembler, por isso abriremos o arquivo drvasm.S Isso também implica a necessidade de usar uma interrupção para capturar o início de um byte. Fico feliz que o primeiro byte transmitido via USB seja sempre o mesmo, SYNC, por isso, se você começar, tudo bem. Como resultado, desde o início do byte até o final, apenas 64 ciclos do controlador ocorrem (na verdade, a margem é ainda menor); portanto, você não deve usar outras interrupções que não sejam USB.


Coloque imediatamente a configuração em um arquivo usbconfig.h separado. É aí que serão definidos os pinos responsáveis ​​pelo USB, bem como os bits, constantes e registradores utilizados.


Inserção teórica
A transferência via USB é realizada em pacotes de vários bytes em cada um. O primeiro byte é sempre o byte de sincronização SYNC, igual a 0b10000000, o segundo é o identificador de byte do pacote PID. A transferência de cada byte vai do bit menos significativo para o mais significativo (isso não é inteiramente verdade, mas no vusb essa sutileza é ignorada, dada em outro lugar) usando a codificação NRZI. Esse método consiste no fato de que um zero lógico é transmitido alterando o nível lógico para o oposto, e uma unidade lógica é transmitida por não alteração. Além disso, a proteção é introduzida contra a dessincronização (que não usaremos, mas deve ser levada em consideração) da fonte e do receptor do sinal: se houver seis unidades consecutivas na sequência transmitida, ou seja, por seis relógios consecutivos o estado dos terminais não muda, uma inversão forçada é adicionada à transmissão, como se zero é transmitido. Assim, o tamanho do byte pode ser 8 ou 9 bits.
Também vale ressaltar que as linhas de dados em USB são diferenciais, ou seja, quando D + é alto, D- é baixo (isso é chamado de estado K) e vice-versa (estado J). Isso é feito para melhorar a imunidade a ruídos em alta frequência. É verdade que há uma exceção: o sinal no final do pacote (chamado SE0) é transmitido puxando as duas linhas de sinal para o solo (D + = D- = 0). Há mais dois sinais transmitidos mantendo uma baixa tensão na linha D + e uma alta tensão na linha D + por diferentes momentos. Se o tempo for pequeno (comprimento de um byte ou um pouco mais), isso será Inativo, uma pausa entre pacotes e, se for grande, um sinal de redefinição.

Portanto, a transmissão é feita em um par diferencial, sem contar o caso exótico do SE0, mas ainda não o consideraremos. Portanto, para determinar o status do barramento USB, precisamos de apenas uma linha, D + ou D-. De um modo geral, não há diferença qual escolher, mas por definição deixe D- ser.


O início do pacote pode ser determinado pelo recebimento do byte SYNC após um longo tempo inativo. O estado Idle corresponde ao log.1 na linha D (também é o estado J) e o byte SYNC é 0b100000, mas é transmitido do bit menos significativo para o mais significativo, além disso, é codificado em NRZI, ou seja, cada zero significa inversão de sinal e um significa mantendo o mesmo nível. Portanto, a sequência dos estados D- será a seguinte:


byteInativoSYNCPID
USB1..100000001????????
D-1..101010100????????

O início do pacote é mais fácil de detectar em uma borda descendente e configuraremos uma interrupção nele. Mas e se o controlador estiver ocupado durante o início da recepção e não puder entrar na interrupção imediatamente? Para evitar a perda de contagens de faixa em tal situação, usamos o byte SYNC para a finalidade a que se destina. Ele consiste inteiramente de frentes nos limites dos bits, para que possamos esperar por um deles, depois outro meio-bit, e ir direto para o meio do próximo. No entanto, esperar por uma frente de "alguns" não é uma boa idéia, porque precisamos não apenas entrar no meio da briga, mas também saber que brecha chegamos à pontuação. E para este SYNC também é adequado: ele tem dois bits zero seguidos no final (são estados K). Aqui nós vamos pegá-los. Portanto, no arquivo drvasm.S, um trecho de código aparece na entrada de interrupção para foundK. Além disso, devido ao tempo para verificar o status da porta, para uma transição incondicional e assim por diante, chegamos à marca não no início do bit, mas apenas no meio. Mas não faz sentido verificar a mesma parte, porque já sabemos o seu significado. Portanto, esperamos 8 ciclos de relógio (até agora nop'ami vazio) e verificamos o próximo bit. Se também for zero, encontramos o final de SYNC e podemos prosseguir para a recepção de bits significativos.


Na verdade, todo o código adicional destina-se à leitura de mais dois bytes com saída subsequente para o UART. Bem, aguardando o estado de SE0 para não entrar acidentalmente no próximo pacote.


Agora você pode compilar o código resultante e ver quais bytes nosso dispositivo aceita. Pessoalmente, tenho a seguinte sequência:


 4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00 

Lembre-se de que estamos produzindo dados brutos, excluindo zeros incrementais e decodificação NRZI. Vamos tentar decodificar manualmente, começando com o bit baixo:


4E
NRZI010011100 (bit anterior)
byte00101101
2D

55
NRZI010101010 (bit anterior)
byte00000000
00

Não faz sentido decodificar zeros, pois 16 valores idênticos em uma linha não podem ser incluídos em um pacote.


Assim, conseguimos escrever um firmware que aceita os dois primeiros bytes do pacote, embora até agora sem decodificação.


Etapa 2. Versão demo do NRZI


Para não recodificar manualmente, você pode confiar isso no próprio controlador: a operação XOR faz exatamente o que você precisa, embora o resultado seja invertido; portanto, adicione outra inversão depois:


 mov temp, shift lsl shift eor temp, shift com temp rcall uart_hex 

O resultado é bastante esperado:


 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 

Etapa 3. Livre-se do ciclo de recebimento de bytes


Vamos dar mais um pequeno passo e expandir o ciclo de recebimento do primeiro byte em um código linear. Assim, acontece muitos nops, necessários apenas para aguardar o início do próximo bit. Em vez de alguns deles, você pode usar o decodificador NRZI, outros serão úteis mais tarde.


O resultado da opção anterior não é diferente.


Etapa 4. Leia para o buffer


É claro que ler em registros separados é rápido e bonito, mas quando há muitos dados, é melhor usar uma entrada de buffer localizada em algum lugar da RAM. Para fazer isso, declararemos uma matriz de tamanho suficiente no main e, na interrupção, escreveremos lá.
Inserção teórica


A estrutura de pacotes no USB é padronizada e consiste nas seguintes partes: byte SYNC, byte PID + CHECK (2 campos de 4 bits cada), campo de dados (às vezes 11 bits, mas mais frequentemente um número arbitrário de bytes de 8 bits) e uma soma de verificação CRC de 5 ou 5 ( para um campo de dados de 11 bits) ou 16 (para o restante) bits. Finalmente, o fim da indicação de pacote (EOP) é ​​de dois bits de pausa, mas isso não é mais dados.


Antes de trabalhar com a matriz, você ainda precisa configurar os registradores e liberar nop antes que o primeiro bit não seja suficiente para isso. Portanto, você terá que colocar a leitura dos dois primeiros bits na seção linear do código, entre os comandos dos quais inseriremos o código de inicialização e, em seguida, pularemos para o meio do ciclo de leitura, no rótulo rxbit2. Falando no tamanho do buffer. De acordo com a documentação, em um pacote é impossível transferir mais de 8 bytes de dados. Adicionamos os bytes de serviço PID e CRC16, obtemos um tamanho de buffer de 11 bytes. O byte SYNC e o estado EOP não serão gravados. Não poderemos controlar o intervalo de solicitações do host, mas também não queremos perdê-las, por isso teremos uma margem dupla para leitura. Por enquanto, não usaremos o buffer inteiro, mas para não retornar no futuro, é melhor alocar imediatamente o volume necessário.


Etapa 5. Trabalhando com o Buffer Humanamente


Em vez de ler diretamente os primeiros bytes da matriz, escrevemos um pedaço de código que lê exatamente quantos bytes foram realmente gravados na matriz. E, ao mesmo tempo, adicione um separador entre pacotes.
Agora a saída fica assim:


 >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF 

Etapa 6. Adicionando um aditivo zero aditivo


Finalmente, é hora de terminar de ler o fluxo de bits para o padrão. O último item sem o qual gerenciamos com êxito foi um zero falso, adicionado a cada seis unidades consecutivas. Como temos a recepção de bytes implantada no corpo linear do loop, você deve verificar após cada bit, nos oito locais. Considere os dois primeiros bits como um exemplo:


 unstuff0: ;1 (  breq) andi x3, ~(1<<0) ;1 [15]  0-  .     mov x1, x2 ;1 [16]      () in x2, USBIN ;1 [17] <-- 1-   .     ori shift, (1<<0) ;1 [18]  0-   .1      rjmp didUnstuff0 ;2 [20] ;<---//---> rxLoop: eor shift, x3 ;1 [0] in x1, USBIN ;1 [1] st y+, shift ;2 [3] ldi x3, 0xFF ;1 [4] nop ;1 [5] eor x2, x1 ;1 [6] bst x2, USBMINUS ;1 [7]     0-   shift bld shift, 0 ;1 [8] in x2, USBIN ;1 [9] <--  1- (, ) andi x2, USBMASK ;1 [10] breq se0 ;1 [11] andi shift, 0xF9 ;1 [12] didUnstuff0: breq unstuff0 ;1 [13] eor x1, x2 ;1 [14]; bst x1, USBMINUS ;1 [15]     1-   shift bld shift, 1 ;1 [16] rxbit2: in x1, USBIN ;1 [17] <--  2-  (, ) andi shift, 0xF3 ;1 [18] breq unstuff1 ;1 [19] didUnstuff1: 

Para facilitar a navegação, os endereços dos comandos descritos serão contados pelas etiquetas à direita. Observe que eles foram introduzidos para contar os ciclos de clock do controlador, portanto não estão em ordem. O próximo byte é lido no rótulo rxLoop, o byte anterior é invertido e gravado no buffer [0, 3]. Em seguida, no rótulo [1], o status da linha D é lido, de acordo com XOR com o estado aceito anteriormente, decodificamos NRZI (lembro que o XOR comum adiciona sua inversão, para corrigir o que inserimos no registro de máscara x3, inicializado com as unidades 0xFF) e escrevemos para 0- com o bit do registro de deslocamento [7,8]. Então começa a diversão - verificamos se o bit recebido foi o sexto inalterado. O bit constante recebido com D- corresponde a escrever zero (não um! Vamos mudar para um no final, XOR) no registro. Portanto, você precisa verificar se os bits 0, 7, 6, 5, 4, 3 são zeros. Os dois bits restantes não importam, eles permaneceram no byte anterior e foram verificados anteriormente. Para se livrar deles, cortamos o registro pela máscara [12], onde todos os bits de interesse para nós são definidos como 1: 0b11111001 = 0xF9. Se, após aplicar a máscara, todos os bits forem zeros, a situação de adicionar um bit será corrigida e haverá uma transição para o rótulo unstuff0. Mais um bit [17] é lido lá, em vez do que foi lido anteriormente, no intervalo entre outras operações, de um excesso [9]. Também trocamos os registros dos valores atuais e anteriores x1, x2. O fato é que, em cada bit, o valor é lido em um registro e, em seguida, o XOR está com outro, após o qual os registros são trocados. Portanto, ao ler o registro incremental, essa operação também precisa ser realizada. Mas o mais interessante é que, no registro de dados de turno, escrevemos não o zero, que recebemos honestamente, mas a unidade que o host tentou transferir [18]. Isso se deve ao fato de que, ao receber os próximos bits, o valor de zero também precisará ser levado em consideração e, se registrarmos zero, a verificação da máscara não poderá descobrir que o bit extra já foi levado em consideração. Assim, no registro de deslocamento, todos os bits são invertidos (em relação ao transmitido pelo host) e o zero não. Para evitar tal confusão no buffer, realizaremos uma inversão reversa de acordo com XOR, não com 0xFF [0], mas com 0xFE, ou seja, um registro no qual o bit correspondente será redefinido para 0 e, consequentemente, não levará à inversão. Para fazer isso, na amostra [15] e redefina o bit zero.


Uma situação semelhante ocorre com os bits 1 a 5. Digamos, o 1º bit corresponde à verificação 1, 0, 7, 6, 5, 4, enquanto os bits 2, 3 são ignorados. Isso corresponde à máscara 0xF3.
Mas o processamento de 6 e 7 bits é diferente:


 didUnstuff5: andi shift, 0x3F ;1 [45]   5-0 breq unstuff5 ;1 [46] ;<---//---> bld shift, 6 ;1 [52] didUnstuff6: cpi shift, 0x02 ;1 [53]   6-1 brlo unstuff6 ;1 [54] ;<---//---> bld shift, 7 ;1 [60] didUnstuff7: cpi shift, 0x04 ;1 [61]   7-2 brsh rxLoop ;3 [63] unstuff7: 

A máscara do sexto bit é o número 0b01111110 (0x7E), mas você não pode sobrepor-se ao registrador de deslocamento, pois redefinirá o 0º bit, que deve ser gravado no array. Além disso, na contagem regressiva [45], uma máscara já estava sobreposta, redefinindo 7 bits. Portanto, é necessário processar o bit extra se os bits 1 a 6 forem iguais a zero, e o 0º não importa. Ou seja, o valor do registro deve ser 0 ou 1, o que é perfeitamente verificado pela comparação de "menor que 2" [53, 54].


O mesmo princípio foi usado para o 7º bit: em vez de aplicar a máscara 0xFC, uma verificação é realizada para "menos de 4" [61, 63].


Etapa 7. Classifique os pacotes


Como podemos receber um pacote com o primeiro byte (PID) igual a 0x2D (SETUP), tentaremos classificar o pacote recebido. A propósito, por que chamei o pacote 0x2D SETUP quando parece ser ACK? O fato é que a transmissão USB do bit menos significativo para o mais significativo é realizada em cada campo, e não em byte, enquanto aceitamos byte a byte. O primeiro campo significativo, PID, ocupa apenas 4 bits, seguidos por mais 4 bits CHECK, representando uma inversão bit a bit do campo PID. Portanto, o primeiro byte recebido não será PID + CHECK, mas sim CHECK + PID. No entanto, não há muita diferença, pois todos os valores são conhecidos antecipadamente e é fácil reorganizar os petiscos em alguns lugares. Imediatamente, escreveremos os principais códigos que podem ser úteis para nós no arquivo usbconfig.h.


Ainda não começamos a adicionar o código de processamento PID, observe que ele deve ser rápido (ou seja, no assembler), mas o alinhamento por relógios não é necessário, porque já aceitamos o pacote. Portanto, posteriormente, esta seção será transferida para o arquivo asmcommon.inc, que conterá o código do assembler que não está vinculado à frequência. Enquanto isso, apenas destaque o comentário.
Agora, vamos classificar os pacotes recebidos.


Inserção teórica
Pacotes de dados no barramento USB são combinados em transações. Cada transação começa com o envio pelo host de um pacote de marcador especial que carrega informações sobre o que o host deseja fazer com o dispositivo: configurar (SETUP), transmitir dados (OUT) ou recebê-lo (IN). Depois que o pacote do marcador é transmitido, uma pausa de dois bits segue. Isso é seguido por um pacote de dados (DATA0 ou DATA1), que pode ser enviado pelo host e pelo dispositivo, dependendo do pacote do marcador. Em seguida, outra pausa de dois bits de comprimento e a resposta é HANDSHAKE, um pacote de confirmação (ACK, NAK, STALL, nós os consideraremos outra vez).
CONFIGURAÇÃODATA0Aperto de mão
host-> dispositivopausarhost-> dispositivopausardispositivo-> host

OUTDATA0 / DATA1Aperto de mão
host-> dispositivopausarhost-> dispositivopausardispositivo-> host

INDATA0 / DATA1Aperto de mão
host-> dispositivopausardispositivo-> hostpausarhost-> dispositivo


Como a troca segue na mesma linha, o host e o dispositivo precisam alternar constantemente entre transmissão e recepção. Obviamente, o atraso de dois bits é precisamente para esse fim e é feito para que eles não comecem a tocar push-push, enquanto tentam transferir simultaneamente alguns dados para o barramento.

Portanto, conhecemos todos os tipos de pacotes necessários para a troca. Adicionamos uma verificação do byte PID recebido para conformidade com cada um. No momento, o dispositivo ainda não consegue gravar pacotes primitivos como ACK no barramento, o que significa que é incapaz de dizer ao host o que é. Portanto, comandos como IN não podem ser esperados. Portanto, apenas verificaremos a recepção dos comandos SETUP e OUT, para os quais indicaremos a inclusão dos LEDs correspondentes nos ramos correspondentes.


Além disso, vale a pena levar o envio de logs além da interrupção, em algum lugar principal.


Nós atualizamos o dispositivo com o que aconteceu depois de fazer essas alterações e observamos a seguinte sequência de bytes recebidos:


 2D|80|06|00|01|00|00|40|00 C3|80|06|00|01|00|00|40|00 2D|80|06|00|01|00|00|40|00 C3|80|06|00|01|00|00|40|00 

E além disso - os dois LEDs em chamas. Então, pegamos SETUP e OUT.


Etapa 8. Leia o endereço no envelope


Inserção teórica
Os pacotes de marcadores (SETUP, IN, OUT) servem não apenas para mostrar ao dispositivo o que eles querem dele, mas também para endereçar um dispositivo específico no barramento e para um ponto de extremidade específico dentro dele. Os pontos de extremidade são necessários para destacar funcionalmente uma subfunção específica de um dispositivo. Eles podem variar em frequência de pesquisa, taxa de câmbio e outros parâmetros. Digamos, se o dispositivo parece ser um adaptador USB-COM, sua principal tarefa é receber dados do barramento e transferi-los para a porta (primeiro ponto final) e receber dados da porta e enviá-los para o barramento (segundo). Em termos de significado, esses pontos destinam-se a um grande fluxo de dados não estruturados. Além disso, de tempos em tempos, o dispositivo deve trocar com o host o status das linhas de controle (todos os tipos de RTS, DTR, etc.) e trocar configurações (velocidade, paridade). E aqui, grandes quantidades de dados não são esperadas. Além disso, é conveniente quando as informações de serviço não são misturadas com os dados. Portanto, é conveniente usar pelo menos três pontos de extremidade para o adaptador USB-COM. Na prática, é claro, isso acontece de maneiras diferentes ...
Uma pergunta igualmente interessante é por que o dispositivo recebe seu endereço, porque, além disso, você ainda não pode colocar nada nessa porta em particular. Isso é feito para simplificar o desenvolvimento de hubs USB. Eles podem ser bastante "burros" e simplesmente transmitir sinais do host para todos os dispositivos sem se preocupar com a classificação. E o próprio dispositivo descobrirá, processará o pacote ou o ignorará.
Portanto, o endereço do dispositivo e o endereço do terminal estão contidos nos pacotes de marcadores. A estrutura desses pacotes é apresentada abaixo:
o campo
o campoSYNCaddrponto finalCRCEop
Bits USB0-7012345601230123401
bits recebidos0123456701234567


, - ( - PID = SETUP OUT) (IN) , .

, (-) (Handshake) :


  • : , , NAK
  • -: SETUP OUT, , IN — ,
  • . , , ,

« — » . PID', , . «PID» . usbCurrentTok. PID' (DATA0, DATA1) , . , ? : , ( 0 usbCurrentTok ), , . ( SE0) , - , D+, D- . , SYNC, . , , . «» , . .


, . x3, (, , , ).


, USB , , . , , , CRC ( ). , [21]. 0- . , [26]. , CRC, .


9.


, , « », ACK. NAK', ( cnt — ). USB , , SYNC PID. Y, cnt ( ). , — ACK. x3 — 1 , . x3 ( r20) 20.


( SETUP, ), ACK' , , , . , .


, D+, D- ( ), — . XOR , , , , - .


, , , , . , , , . . vusb : txBitloop 2 ([00], [08]). 3 , 6 . , . 1 3 : 171. ( 171, 11 , ), — , . cnt=4:


4 — 171 = -167 = ( ) 89 (+ )
89 — 171 = -82 = ( ) 174 (+ )
174 — 171 = 3. ,
, .


, 3 , 1. 6 , , x4. D+, D- , . .
:


 2D|80|06|00|01|00|00|40|00 69|00|10|00|01|00|00|40|00 

C3 . , , UART . , , IN , . , .


10. NAK


NAK , . , . , - .


, . , , - , . usbRxBuf, . , — , USB_BUFSIZE. usbInputBufOffset, . .


NAK handleData , [22]. (usbRxLen), - . ( — ), usbRxLen, , — usbRxToken, SETUP OUT - . : , , ACK .


. , , - , -, . ? , , , , - .


,


 2D|80|06|00|01|00|00|40|00 

, NAK`, , .


11.


, , . — . , , , , , . . . , USB, usbPoll. — , . — . SETUP , PID CRC, SETUP 5- , 16-. 3 «» . «» PID usbRxToken, CRC , , . usbProcessRx, , .


, , — , SE0. , USB .


. SETUP, . . SETUP usbRequest_t 8 . : ( USB-) , - . , . .
, , , .


12. SETUP'


, , . . usbDriverSetup, . , . , ( , , ) . , : ACK NAK, .


13.


, SETUP + DATAx, DATAx 8 . IN DATAx, . , . , ACK NAK. , . — usbTxBuf, , usbTxLen . low-speed USB 8 ( PID, CRC), usbTxLen 11. PID, , . , 16, , 0x0F, . PID , . IN, , (handshake , ).


:
SETUP + DATAx, ACK NAK . , , usbPoll, , ( PID=DATA1 ( DATA0 DATA1 , , DATA1). CRC . , , - . — 4 . , 3 , 4. , SYNC . « IN NAK?» NAK. , , DATA1 .


, — USBRQ_SET_ADDRESS ( , ). . (drvsdm.S, make SE0). , , , DATA1 , , . , , , , , . , , .


14.


, . , USBRQ_GET_DESCRIPTOR USBRQ_SET_ADDRESS, , . usbDriverDescriptor, . , USBRQ_GET_DESCRIPTOR. , , :


USBDESCR_DEVICE — : USB (1.1 ), , , . .
USBDESCR_CONFIG — , , . .
USBDESCR_STRING — , .
, , USBDESCR_DEVICE, , .


15.


. -, . , - - , , HID, , . Vendor ID Product ID, USB, . , vusb .


, , - . , , , (, ) usbMsgPtr, — len, usbMsgLen. ( ) 18 , 8. , , 3 . - , STALL.


usbDeviceRead. , memcpy_P, , , .


, , , . , , .


, , .



PID' DATA0 DATA1 . PID' , , - .

, DATA0 / DATA1 ( ), , , 3 , . XOR PID', . , , XOR' . PID DATA1, XOR PID , XOR DATA0 .


, , USBDESCR_CONFIG.


16. - !


USBDESCR_CONFIG USBDESCR_DEVICE. ( , ) . , - USB-, , D+, D-.


, : , , . , ( , ). , UTF-16, . USB UTF-8 .


vusb , lsusb . VID, PID , . , VID, PID, — .


, , ( ). SETUP: , , . 0, , — . , , , .
.


17. (HID)



HID — human interface device, , , . HID , . , , , , , . «» . HID ( low-speed 800 ), .

HID , USBDESCR_HID_REPORT. vusb, . , usbDriverSetup ( ) usbFunctionSetup ( ). , SETUP, OUT. , , , usbFunctionWrite.


, usbDeviceRead usbFunctionRead, . , , usbFunctionSetup ( , ) USB_FLG_USE_USER_RW, usbDriverSetup .


— — usbFunctionWrite usbFunctionRead. . — , .


usbDriverSetup.


18.


, , . HID, , , ( udev - ). , , . , , , .
UPD: ramzes2 , HIDAPI


.


19. vusb


vusb , .


drvasm.S - usbdrvasm.S asmcommon.inc, -, , usbdrvasm12.inc — usbdrvasm20.inc.


main.c main.c ( ) usbdrv.c ( vusb)
usbconfig.h ( ), , , usbconfig.h.


Conclusão


vusb, , , . , , . . , , , USB-HID. , , , vusb, , , , .



https://www.obdev.at/products/vusb/index.html ( vusb)
http://microsin.net/programming/arm-working-with-usb/usb-in-a-nutshell-part1.html
.. USB:
https://radiohlam.ru/tag/usb/
http://we.easyelectronics.ru/electro-and-pc/usb-dlya-avr-chast-1-vvodnaya.html
http://usb.fober.net/cat/teoriya/


PS - (, ) ,

Source: https://habr.com/ru/post/pt460815/


All Articles