Introduccion
Después de leer el título, puede surgir una pregunta lógica: ¿por qué hoy en día estudiar la implementación de software de USB de baja velocidad cuando hay un montón de controladores baratos con un módulo de hardware? El hecho es que el módulo de hardware, al ocultar el nivel de intercambio de niveles lógicos, convierte el protocolo USB en una especie de magia. Para sentir cómo funciona esta "magia", no hay nada mejor que reproducirla desde cero, comenzando desde el nivel más bajo.
Con este fin, intentaremos hacer un dispositivo que finja ser USB-HID basado en el controlador ATmega8. A diferencia de la literatura generalizada, no pasaremos de la teoría a la práctica, del nivel más bajo al más alto, de los voltajes lógicos a las conclusiones, y terminaremos con la "invención" del mismo vusb, después de cada paso verificando si el código funciona como se esperaba. Por separado, noto que no invento una alternativa a esta biblioteca, sino que reproduzco constantemente su código fuente, preservando la estructura y los nombres originales tanto como sea posible, explicando por qué sirve esta o aquella sección. Sin embargo, el estilo de escribir código familiar para mí es diferente del estilo de los autores vusb. Inmediatamente, honestamente admito que, además del interés altruista (para contar un tema difícil a otros), también tengo un interés egoísta: estudiar el tema por mi cuenta y captar un máximo de puntos sutiles para mí. También se deduce que se puede pasar por alto algún punto importante o que algún tema no se revela completamente.
Para una mejor percepción del código, traté de resaltar las secciones modificadas con comentarios y eliminarlas de las secciones discutidas anteriormente. En realidad, el código fuente será la principal fuente de información, y el texto explicará qué se hizo y por qué, así como qué resultado se espera.
También noto que solo se considera USB de baja velocidad, incluso sin mencionar, lo que distingue a las variedades más de alta velocidad.
Paso 0. Hierro y otra preparación
Como prueba, tomemos una placa de depuración casera basada en ATmega8 con cuarzo de 12 MHz. No daré el esquema, es bastante estándar (vea el sitio web oficial de vusb), lo único que vale la pena mencionar son las conclusiones utilizadas. En mi caso, la salida D + corresponde a PD2, la salida D-PD3 y el suspensor se cuelga en PD4. En principio, una resistencia pull-up podría conectarse a la alimentación, pero el control manual parece un poco más consistente con el estándar.
La alimentación de 5 V se suministra desde el conector USB, sin embargo, no se esperan más de 3,6 V en las líneas de señal (¿por qué esto fue un misterio para mí?). Por lo tanto, debe reducir la potencia del controlador o colocar los diodos zener en las líneas de señal. Elegí la segunda opción, pero en general no importa.
Como estamos "inventando" la implementación, sería bueno ver qué sucede en el cerebro del controlador, es decir, se necesita al menos algún tipo de información de depuración. En mi caso, estos son dos LED en PD6, PD7 y, lo más importante, UART en PD0, PD1, configurados en 115200, para que pueda escuchar la conversación del controlador a través de una pantalla normal u otro programa para trabajar con el puerto COM:
$ screen /dev/ttyUSB0 115200
Además, un wirehark con el módulo apropiado resultará ser una utilidad útil para la depuración de USB (no siempre comienza desde la caja, pero resolver estos problemas se encuentra con bastante éxito en Internet y no es la tarea de este artículo).
Aquí sería posible gastar otro kilobyte de texto en la descripción del programador, los archivos MAKE y otras cosas, pero esto apenas tiene sentido. Del mismo modo, no me enfocaré en configuraciones periféricas que no estén relacionadas con USB. Si alguien ni siquiera puede resolver esto, ¿es demasiado pronto para meterse en las entrañas del software USB?
El código fuente de todos los pasos está disponible en Github.
Paso 1. Acepta al menos algo
De acuerdo con la documentación, USB admite varias velocidades fijas, de las cuales AVR extraerá solo la más baja: 1.5 megabits por segundo. Está determinado por la resistencia pull-up y la comunicación posterior. Para nuestra frecuencia elegida, la resistencia debe conectar D- con una fuente de alimentación de 3.3 V y tener un valor nominal de 1.5 kOhm, pero en la práctica se puede conectar con +5 V, y el valor nominal puede variar ligeramente. Con una frecuencia de controlador de 12 MHz, solo 8 ciclos de reloj por bit. Está claro que tal precisión y velocidad solo se pueden lograr en el ensamblador, por lo que abriremos el archivo drvasm.S Esto también implica la necesidad de usar una interrupción para captar el comienzo de un byte. Me alegra que el primer byte transmitido por USB sea siempre el mismo, SYNC, así que si llegas al principio, está bien. Como resultado, desde el comienzo del byte hasta su final, solo pasan 64 ciclos de controlador (de hecho, el margen es aún más pequeño), por lo que no debe usar otras interrupciones que no sean USB.
Inmediatamente ponga la configuración en un archivo usbconfig.h separado. Es allí donde se establecerán los pines responsables del USB, así como los bits, constantes y registros utilizados.
Inserto teórico
La transferencia a través de USB se realiza en paquetes de varios bytes en cada uno. El primer byte es siempre el byte de sincronización SYNC, igual a 0b10000000, el segundo es el identificador de byte del paquete PID. La transferencia de cada byte va del bit menos significativo al más significativo (esto no es del todo cierto, pero en vusb esta sutileza se ignora, dada en otra parte) usando la codificación NRZI. Este método consiste en el hecho de que un cero lógico se transmite cambiando el nivel lógico al opuesto, y una unidad lógica se transmite sin cambio. Además, se introduce protección desde la desincronización (que no usaremos, pero debemos tener en cuenta) de la fuente de señal y el receptor: si hay seis unidades seguidas en la secuencia transmitida, es decir, el estado de los terminales no cambia durante seis períodos consecutivos, se agrega una inversión forzada a la transmisión, como si cero se transmite. Por lo tanto, el tamaño del byte puede ser de 8 o 9 bits.
También vale la pena mencionar que las líneas de datos en USB son diferenciales, es decir, cuando D + es alto, D- es bajo (esto se llama estado K) y viceversa (estado J). Esto se hace para una mejor inmunidad al ruido a alta frecuencia. Es cierto que hay una excepción: la señal al final del paquete (se llama SE0) se transmite tirando ambas líneas de señal al suelo (D + = D- = 0). Hay dos señales más transmitidas al mantener un voltaje bajo en la línea D + y un alto voltaje en la línea D + para diferentes tiempos. Si el tiempo es pequeño (un byte de longitud o un poco más largo), entonces es inactivo, una pausa entre paquetes y, si es grande, una señal de reinicio.
Entonces, la transmisión está en un par diferencial, sin contar el caso exótico de SE0, pero aún no lo consideraremos. Entonces, para determinar el estado del bus USB, solo necesitamos una línea, D + o D-. En general, no hay diferencia cuál elegir, pero para ser definitivos, dejemos que D-sea.
El comienzo del paquete se puede determinar al recibir el byte SYNC después de un largo inactivo. El estado inactivo corresponde al log.1 en la línea D (también es el estado J), y el byte SYNC es 0b100000, pero se transmite del bit menos significativo al más significativo, además, está codificado en NRZI, es decir, cada cero significa inversión de señal, y uno significa manteniendo el mismo nivel. Entonces la secuencia de estados D- será la siguiente:
El comienzo del paquete es más fácil de detectar en un flanco descendente, y configuraremos una interrupción en él. Pero, ¿qué sucede si el controlador está ocupado durante el inicio de la recepción y no puede ingresar a la interrupción de inmediato? Para evitar perder el recuento de pistas en tal situación, utilizamos el byte SYNC para el propósito previsto. Consiste completamente en frentes en los límites de los bits, para que podamos esperar uno de ellos, luego otro medio bit, y pasar directamente al medio del siguiente. Sin embargo, esperar un frente de "algo" no es una buena idea, porque no solo necesitamos meternos en la mitad del bit, sino también saber en qué parte nos metimos en el marcador. Y para esto, SYNC también es adecuado: tiene dos bits cero seguidos al final (son estados K). Aquí los atraparemos. Entonces, en el archivo drvasm.S, aparece un fragmento de código desde la entrada de interrupción hasta foundK. Además, debido al tiempo para verificar el estado del puerto, para una transición incondicional, etc., llegamos a la marca no al comienzo del bit, sino solo en el medio. Pero no tiene sentido comprobar el mismo bit, porque ya sabemos su significado. Por lo tanto, esperamos 8 ciclos de reloj (hasta ahora vacío nop'ami) y verificamos el siguiente bit. Si también es cero, entonces hemos encontrado el final de SYNC y podemos proceder a la recepción de bits significativos.
En realidad, todo el código adicional está destinado a leer dos bytes más con salida posterior a UART. Bueno, esperando el estado de SE0 para no entrar accidentalmente en el siguiente paquete.
Ahora puede compilar el código resultante y ver qué bytes acepta nuestro dispositivo. Personalmente, tengo la siguiente secuencia:
4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00
Recuerde, estamos generando datos sin procesar, excluyendo ceros incrementales y decodificación NRZI. Intentemos decodificar manualmente, comenzando con el bit bajo:
No tiene sentido decodificar ceros, ya que 16 valores idénticos seguidos no se pueden incluir en un paquete.
Por lo tanto, pudimos escribir firmware que acepta los primeros dos bytes del paquete, aunque hasta ahora sin decodificar.
Paso 2. Versión demo de NRZI
Para no volver a codificar manualmente, puede confiar esto al controlador en sí: la operación XOR hace exactamente lo que necesita, aunque el resultado se invierte, así que agregue otra inversión después:
mov temp, shift lsl shift eor temp, shift com temp rcall uart_hex
El resultado es bastante esperado:
2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF
Paso 3. Deshágase del ciclo de recepción de bytes
Vamos a dar un pequeño paso más y expandir el ciclo de recibir el primer byte en un código lineal. Por lo tanto, se obtienen muchos nops, solo se necesita esperar al comienzo del siguiente bit. En lugar de algunos de ellos, puede usar el decodificador NRZI, otros serán útiles más adelante.
El resultado de la opción anterior no es diferente.
Paso 4. Leer al búfer
Leer en registros separados es, por supuesto, rápido y hermoso, pero cuando hay demasiados datos, es mejor usar una entrada de búfer ubicada en algún lugar de la RAM. Para hacer esto, declararemos una matriz de tamaño suficiente en el principal, y en la interrupción escribiremos allí.
Inserto teórico
La estructura de paquetes en USB está estandarizada y consta de las siguientes partes: byte SYNC, byte PID + CHECK (2 campos de 4 bits cada uno), campo de datos (a veces 11 bits, pero más a menudo un número arbitrario de bytes de 8 bits) y una suma de verificación CRC de 5 ( para un campo de datos de 11 bits), o 16 (para el resto) bits. Finalmente, la indicación de fin de paquete (EOP) es de dos bits de pausa, pero ya no se trata de datos.
Antes de trabajar con la matriz, aún necesita configurar los registros, y liberar nop antes del primer bit no es suficiente para esto. Por lo tanto, tendrá que colocar la lectura de los primeros dos bits en la sección lineal del código, entre los comandos de los cuales insertaremos el código de inicialización, y luego saltaremos a la mitad del ciclo de lectura, a la etiqueta rxbit2. Hablando del tamaño del búfer. Según la documentación, no se pueden transferir más de 8 bytes de datos en un paquete. Agregamos los bytes de servicio PID y CRC16, obtenemos un tamaño de búfer de 11 bytes. El byte SYNC y el estado EOP no se escribirán. No podremos controlar el intervalo de solicitudes del host, pero tampoco queremos perderlas, por lo que tomaremos un doble margen para la lectura. Por ahora, no usaremos todo el búfer, pero para no regresar en el futuro, es mejor asignar inmediatamente el volumen requerido.
Paso 5. Trabajando con el buffer humanamente
En lugar de leer directamente los primeros bytes de la matriz, escribimos un fragmento de código que lee exactamente tantos bytes como realmente se escribió en la matriz. Y al mismo tiempo agregue un separador entre paquetes.
Ahora la salida se ve así:
>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
Paso 6. Agregar un Aditivo Aditivo Cero
Finalmente, es hora de terminar de leer el flujo de bits al estándar. El último elemento sin el que logramos administrar con éxito fue un cero falso, agregado después de cada seis unidades consecutivas. Como tenemos la recepción de bytes desplegada en el cuerpo lineal del bucle, debe verificar después de cada bit, en los ocho lugares. Considere los dos primeros bits como ejemplo:
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 la conveniencia de la navegación, las etiquetas de la derecha contarán las direcciones de los comandos descritos. Tenga en cuenta que se introdujeron para contar los ciclos de reloj del controlador, por lo que no están en orden. El siguiente byte se lee en la etiqueta rxLoop, el byte anterior se invierte y se escribe en el búfer [0, 3]. A continuación, en la etiqueta [1], se lee el estado de la línea D, de acuerdo con XOR con el estado aceptado previamente, decodificamos NRZI (recuerdo que XOR ordinario agrega su inversión, para fijar cuál ingresamos al registro de máscara x3, inicializado con unidades 0xFF) y escribimos a 0- con un poco del registro de desplazamiento [7,8]. Entonces comienza la diversión: verificamos si el bit recibido fue el sexto sin cambios. El bit constante recibido con D- corresponde a escribir cero (¡no uno! Cambiaremos a uno al final, XOR) en el registro. Por lo tanto, debe verificar si los bits 0, 7, 6, 5, 4, 3 son ceros. Los dos bits restantes no importan, permanecieron del byte anterior y se verificaron antes. Para deshacernos de ellos, cortamos el registro mediante la máscara [12], donde todos los bits de interés para nosotros se establecen en 1: 0b11111001 = 0xF9. Si después de aplicar la máscara todos los bits resultan ser ceros, la situación de agregar un bit es fija y hay una transición a la etiqueta unstuff0. Un bit más [17] se lee allí en lugar de lo que se leyó previamente, en el intervalo entre otras operaciones, de un exceso [9]. También intercambiamos los registros de los valores actuales y anteriores x1, x2. El hecho es que en cada bit el valor se lee en un registro, y luego XOR está con otro, después de lo cual se intercambian los registros. En consecuencia, al leer el registro incremental, esta operación también debe realizarse. Pero lo más interesante es que en el registro de datos de turno escribimos no el cero, que recibimos honestamente, sino la unidad que el host intentó transferir [18]. Esto se debe al hecho de que al recibir los siguientes bits, también se tendrá que tener en cuenta el valor cero, y si registramos cero, la comprobación de máscara no podría descubrir que el bit adicional ya se ha tenido en cuenta. Por lo tanto, en el registro de desplazamiento, todos los bits se invierten (en relación con los transmitidos por el host) y el cero no. Para evitar tal desorden en el búfer, realizaremos una inversión inversa de acuerdo con XOR no con 0xFF [0], sino con 0xFE, es decir, un registro en el que el bit correspondiente se restablecerá a 0 y, en consecuencia, no conducirá a la inversión. Para hacer esto, en la muestra [15] y restablezca el bit cero.
Una situación similar ocurre con los bits 1-5. Digamos, el primer bit corresponde a la verificación 1, 0, 7, 6, 5, 4, mientras que los bits 2, 3 se ignoran. Esto corresponde a la máscara 0xF3.
Pero el procesamiento de 6 y 7 bits es 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:
La máscara para el sexto bit es el número 0b01111110 (0x7E), pero no puede superponerlo en el registro de desplazamiento, ya que restablecerá el bit 0, que debe escribirse en la matriz. Además, en la cuenta regresiva [45], una máscara ya estaba superpuesta, restableciendo 7 bits. Por lo tanto, es necesario procesar el bit extra si los bits 1-6 son iguales a cero, y el 0º no importa. Es decir, el valor del registro debe ser 0 o 1, que se verifica perfectamente comparando "menos de 2" [53, 54].
Se usó el mismo principio para el séptimo bit: en lugar de aplicar la máscara 0xFC, se realiza una comprobación para "menos de 4" [61, 63].
Paso 7. Ordena los paquetes
Como podemos recibir un paquete con el primer byte (PID) igual a 0x2D (SETUP), intentaremos ordenar el recibido. Por cierto, ¿por qué llamé al paquete 0x2D SETUP cuando parece ser ACK? El hecho es que la transmisión USB del bit menos significativo al más significativo se lleva a cabo dentro de cada campo, y no byte, mientras aceptamos byte por byte. El primer campo significativo, PID, ocupa solo 4 bits, seguido de 4 bits más de COMPROBACIÓN, que representan una inversión bit a bit del campo PID. Por lo tanto, el primer byte recibido no será PID + CHECK, sino CHECK + PID. Sin embargo, no hay mucha diferencia, ya que todos los valores se conocen de antemano, y es fácil reorganizar los mordiscos en algunos lugares. Inmediatamente, escribiremos los códigos principales que pueden sernos útiles en el archivo usbconfig.h.
Todavía no hemos comenzado a agregar el código de procesamiento PID, tenga en cuenta que debe ser rápido (es decir, en el ensamblador), pero no se requiere la alineación por relojes, porque ya hemos aceptado el paquete. Por lo tanto, posteriormente esta sección se transferirá al archivo asmcommon.inc, que contendrá el código del ensamblador que no está vinculado a la frecuencia. Mientras tanto, solo resalte el comentario.
Ahora pasemos a ordenar los paquetes recibidos.
Inserto teórico
Los paquetes de datos en el bus USB se combinan en transacciones. Cada transacción comienza con el envío por parte del host de un paquete marcador especial que contiene información sobre lo que el host desea hacer con el dispositivo: configurar (CONFIGURAR), transmitir datos (OUT) o recibirlos (IN). Después de que se transmite el paquete marcador, sigue una pausa de dos bits. Esto es seguido por un paquete de datos (DATA0 o DATA1), que pueden ser enviados tanto por el host como por el dispositivo, dependiendo del paquete marcador. A continuación, otra pausa de dos bits de longitud y la respuesta es HANDSHAKE, un paquete de confirmación (ACK, NAK, STALL, los consideraremos en otro momento).
Como el intercambio continúa en las mismas líneas, el host y el dispositivo tienen que cambiar constantemente entre transmisión y recepción. Obviamente, el retraso de dos bits es precisamente para este propósito y se hace para que no comiencen a jugar push-push, mientras intentan transferir simultáneamente algunos datos al bus.
Entonces, conocemos todos los tipos de paquetes necesarios para el intercambio. Agregamos una verificación del byte PID recibido para el cumplimiento de cada uno. Por el momento, el dispositivo aún no puede escribir paquetes tan primitivos como ACK en el bus, lo que significa que no puede decirle al host qué es. Por lo tanto, no se pueden esperar comandos como IN. Por lo tanto, solo verificaremos la recepción de los comandos SETUP y OUT, para lo cual indicaremos la inclusión de los LED correspondientes en las ramas correspondientes.
Además, vale la pena enviar los registros más allá de la interrupción, en algún lugar principal.
Destellamos el dispositivo con lo que sucedió después de hacer estos cambios y observamos la siguiente secuencia de bytes recibidos:
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
Y además, ambos LED encendidos. Entonces, atrapamos SETUP y OUT.
Paso 8. Lea la dirección en el sobre
Inserto teórico
Los paquetes de marcadores (SETUP, IN, OUT) sirven no solo para mostrar al dispositivo lo que quieren de él, sino también para dirigirse a un dispositivo específico en el bus y a un punto final específico dentro de él. Los puntos finales son necesarios para resaltar funcionalmente una subfunción particular de un dispositivo. Pueden variar en frecuencia de sondeo, tipo de cambio y otros parámetros. Digamos, si el dispositivo parece ser un adaptador USB-COM, su tarea principal es recibir datos del bus y transferirlos al puerto (primer punto final) y recibir datos del puerto y enviarlos al bus (segundo). En términos de significado, estos puntos están destinados a un gran flujo de datos no estructurados. Pero además de esto, de vez en cuando, el dispositivo debe intercambiar con el host el estado de las líneas de control (todo tipo de RTS, DTR, etc.) y la configuración de intercambio (velocidad, paridad). Y aquí, no se esperan grandes cantidades de datos. Además, es conveniente cuando la información del servicio no se mezcla con los datos. Por lo tanto, resulta conveniente usar al menos 3 puntos finales para el adaptador USB-COM. En la práctica, por supuesto, sucede de diferentes maneras ...
Una pregunta igualmente interesante es por qué se envía su dirección al dispositivo, porque aparte de eso, todavía no puede pegar nada en este puerto en particular. Esto se hace para simplificar el desarrollo de los concentradores USB. Pueden ser bastante "tontos" y simplemente transmitir señales desde el host a todos los dispositivos sin preocuparse por la clasificación. Y el dispositivo lo resolverá, procesará el paquete o lo ignorará.
Entonces, tanto la dirección del dispositivo como la dirección del punto final están contenidas en los paquetes de marcadores. La estructura de dichos paquetes se da a continuación:
el campo
, - ( - 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.
Conclusión
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 - (, ) ,