USB de nivel incluso más bajo (avr-vusb) en registros: punto final masivo usando el ejemplo de almacenamiento masivo
USB en registros: punto final de interrupción usando el ejemplo de HID
USB en registros: punto final isócrono usando el ejemplo de dispositivo de audio
Ya conocemos el software USB usando el ejemplo de AVR, es hora de tomar piedras más pesadas - stm32. Nuestros sujetos experimentales serán el clásico STM32F103C8T6, así como un representante de la serie STM32L151RCT6 de bajo consumo. Como antes, no usaremos tableros de depuración comprados y HAL, prefiriendo una bicicleta.
Dado que hay dos controladores en el título, vale la pena hablar sobre las principales diferencias. En primer lugar, esta es una resistencia pull-up que le dice al host USB que algo se ha atascado en él. En L151 está integrado y controlado por el bit SYSCFG_PMC_USB_PU, pero en F103 no lo está; tendrás que soldarlo a la placa desde el exterior y conectarlo a VCC o al tramo del controlador. En mi caso, la pierna PA10 vino debajo de mi brazo. En el que cuelga UART1 ... Y el otro pin de UART1 entra en conflicto con el botón ... Tiré un tablero maravilloso, ¿no crees? La segunda diferencia es la cantidad de memoria flash: en el F103 tiene 64 kB, y en el L151 hasta 256 kB, que usaremos algún día cuando estudiemos los endpoints Bulk. También tienen configuraciones de reloj ligeramente diferentes, y pueden colgarse de diferentes patas con bombillas con botones, pero estas ya son bastante insignificantes. Ejemplo para F103está disponible en el repositorio, por lo que no será difícil adaptar el resto de experimentos con el L151 para ello. Los códigos fuente están disponibles aquí: github.com/COKPOWEHEU/usb
Principio general de trabajar con USB
El funcionamiento con USB en este controlador se asume mediante un módulo de hardware. Es decir, le decimos qué hacer, él lo hace y al final tira la interrupción “¡Estoy listo!”. En consecuencia, no necesitamos llamar casi nada desde el main main (aunque he proporcionado la función usb_class_poll por si acaso). El ciclo normal de trabajo se limita a un solo evento: el intercambio de datos. El resto (reinicio, suspensión y otros) son eventos excepcionales y únicos.
Esta vez no entraré en detalles de bajo nivel del intercambio. Cualquiera interesado puede leer sobre vusb. Pero permítame recordarle que el intercambio de datos ordinarios no se realiza por un byte, sino por paquete, y la dirección de transmisión la establece el host. Y también dicta los nombres de estas direcciones: transmisión IN significa que el host recibe datos (y el dispositivo transmite), y OUT significa que el host transmite datos (y nosotros recibimos). Además, cada paquete tiene su propia dirección: el número del punto final con el que el host desea comunicarse. Por ahora, tendremos un único punto final 0, responsable del dispositivo en su conjunto (para abreviar, también lo llamaré ep0). Para qué sirve el resto, te lo contaré en otros artículos. Según el estándar, el tamaño de ep0 es estrictamente de 8 bytes para dispositivos de baja velocidad (a los que pertenece el mismo vusb) y una opción de 8, 16, 32,64 bytes para los de máxima velocidad como los nuestros.
¿Qué pasa si los datos son demasiado pequeños y no llenan el búfer por completo? Aquí todo es simple: además de los datos en el paquete, también se transmite su tamaño (este puede ser el campo wLength o una combinación de bajo nivel de señales SE0, que indica el final de la transmisión), por lo que incluso si necesitamos transferir tres bytes a través de ep0 de 64 bytes, luego se transferirán exactamente tres bytes ... Como resultado, no desperdiciaremos ancho de banda generando ceros innecesarios. Así que no sea demasiado pequeño: si podemos permitirnos gastar 64 bytes, lo gastamos sin dudarlo. Entre otras cosas, esto reducirá algo la carga del bus, porque es más fácil transferir una pieza de 64 bytes (más todos los encabezados y colas) a la vez que 8 veces por 8 bytes (a cada uno de los cuales, nuevamente, encabezados y colas ).
¿Y si hay demasiados datos al contrario? Aquí es más complicado. Los datos deben dividirse según el tamaño del punto final y transferirse en trozos. Digamos que el tamaño de ep0 es de 8 bytes y el host está intentando transmitir 20 bytes. En la primera interrupción nos llegarán los bytes 0-7, en la segunda 8-15, en la tercera 16-20. Es decir, para recopilar todo el paquete, debe recibir hasta tres interrupciones. Para esto, en el mismo HAL, se inventó un búfer engañoso, con el que intenté descifrarlo, pero luego del cuarto nivel de transferir lo mismo entre funciones, escupí. Como resultado, en mi implementación, el almacenamiento en búfer recae sobre los hombros del programador.
Pero el host al menos siempre dice cuántos datos está tratando de transferir. Cuando transferimos datos, de alguna manera debemos engañar a los estados de bajo nivel de las piernas para dejar en claro que los datos terminaron. Más precisamente, para dejarle claro al módulo USB que los datos se terminaron y que debes tirar de las piernas. Esto se hace de una manera obvia: escribiendo solo una parte del búfer. Por ejemplo, si tenemos 8 bytes de búfer y hemos escrito 4, entonces obviamente solo tenemos 4 bytes de datos, después de lo cual el módulo enviará la combinación mágica SE0 y todos estarán felices. Y si escribimos 8 bytes, ¿significa que tenemos solo 8 bytes, o que esto es solo una parte de los datos que caben en el búfer? El módulo usb cree que el. Por lo tanto, si queremos detener la transferencia, luego de escribir el búfer de 8 bytes, debemos escribir el siguiente de 0 bytes. Esto se denomina ZLP, paquete de longitud cero. Cómo se ve en el código,Te lo cuento un poquito más tarde.
Organización de la memoria
Según el estándar, el tamaño del punto final 0 puede ser de hasta 64 bytes. Cualquier otro tamaño: hasta 1024 bytes. El número de puntos también puede diferir de un dispositivo a otro. El mismo STM32L1 admite hasta 7 puntos en la entrada y 7 en la salida (sin contar ep0), es decir, hasta 14 kB de búferes solo. Lo que en tal volumen probablemente nunca será necesario para nadie. ¡Consumo inaceptable de memoria! En cambio, el módulo usb mastica una parte de la memoria del kernel compartida y la usa. Esta área se llama PMA (área de memoria de paquetes) y comienza con USB_PMAADDR. Y para indicar dónde se ubican los búferes de cada punto final dentro de él, se asigna al principio una matriz de 8 elementos, cada uno con la siguiente estructura, y solo entonces el área real para los datos:
typedef struct{
volatile uint32_t usb_tx_addr;
volatile uint32_t usb_tx_count;
volatile uint32_t usb_rx_addr;
volatile union{
uint32_t usb_rx_count;
struct{
uint32_t rx_count:10;
uint32_t rx_num_blocks:5;
uint32_t rx_blocksize:1;
};
};
}usb_epdata_t;
Aquí establece el comienzo del búfer de transmisión, su tamaño, luego el comienzo del búfer de recepción y su tamaño. En primer lugar, tenga en cuenta que usb_tx_count no establece el tamaño real del búfer, sino la cantidad de datos a transferir. Es decir, nuestro código debe escribir datos en la dirección usb_tx_addr, luego escribir su tamaño en usb_tx_count y solo luego extraer el registro del módulo usb que los datos están escritos, transferirlos. Preste aún más atención al extraño formato del tamaño del búfer de recepción: es una estructura en la que 10 rx_count bits son responsables de la cantidad real de datos leídos, mientras que el resto son realmente del tamaño del búfer. Es necesario conocer el trozo de hierro hasta donde se puede escribir, y donde comienzan los datos de otras personas. El formato de esta configuración también es bastante interesante: la bandera rx_block_size indica en qué unidades se establece el tamaño. Si se restablece a 0,luego, en palabras de 2 bytes, el tamaño del búfer es 2 * rx_num_blocks, es decir, de 0 a 62. Y si se establece en 1, entonces en bloques de 32 bytes, respectivamente, el tamaño del búfer resulta ser 32 * rx_num_blocks y se encuentra en el rango de 32 a 512 (sí, no hasta 1024, tal es la limitación del controlador).
Para colocar búferes en esta área, usaremos un enfoque semidinámico. Es decir, asignar memoria a pedido, pero no liberarla (¡malloc / free aún no era suficiente para inventar!). El comienzo del espacio no asignado será señalado por la variable lastaddr, que inicialmente apunta al comienzo del PMA menos la tabla de estructuras discutida anteriormente. Bueno, cada vez que se llame a la función para configurar el siguiente punto final usb_ep_init (), se cambiará según el tamaño de búfer especificado allí. Y el valor deseado se ingresará en la celda correspondiente de la tabla, por supuesto. El valor de esta variable se restablece tras un evento de restablecimiento, seguido de una llamada a usb_class_init (), en la que los puntos se reconfiguran de acuerdo con la tarea del usuario.
Trabajar con registros de transmisión-recepción
Como se acaba de decir, en la recepción leemos cuántos datos se recibieron realmente (el campo usb_rx_count), luego leemos los datos en sí, luego tiramos del módulo usb para que el búfer esté libre, pueda recibir el siguiente paquete. Para la transmisión, al revés: escribimos los datos en el búfer, luego establecemos cuánto se ha escrito en usb_tx_count, y finalmente tiramos del módulo para que el búfer esté lleno y podamos transferirlo.
El primer rastrilloEmpiece por trabajar con el búfer en sí: no está organizado en 32 bits, como el resto del controlador, y no en 8 bits, como cabría esperar. ¡Y 16 bits cada uno! Como resultado, se escribe y se lee en 2 bytes, alineados con 4 bytes. ¡Gracias ST por hacer tal perversión! ¡Qué aburrida sería la vida sin él! Ahora la memcpy ordinaria es indispensable, tienes que vallar funciones especiales. Por cierto, si alguien ama el DMA, entonces parece que puede hacer tal transformación por sí solo, aunque no lo he probado.
Y luego el segundo rastrillocon escritura en los registros del módulo. El hecho es que para la configuración de cada punto final, por su tipo (control, masivo, etc.) y estado, un registro USB_EPnR es responsable, es decir, simplemente no puede cambiar un poco en él, debe tener cuidado para no estropear el resto. Y en segundo lugar, ¡ya hay cuatro tipos de bits en este registro! Algunos están disponibles solo para lectura (esto es genial), otros para leer y escribir (también normal), otros ignoran el registro 0, pero al escribir 1, cambian el estado al contrario (comienza la diversión), y el cuarto, en el al contrario, ignore el registro 1, pero el registro 0 los restablece a 0. Dígame, ¿qué adicto pensó en hacer bits en un registro que ignoran 0 e ignoran 1? No, estoy listo para asumir que esto se hizo para preservar la integridad del registro, cuando se accede a él desde el código y el hardware. Pero que quieres,¿Fue demasiado perezoso poner el inversor para que los bits se reiniciaran escribiendo 1? ¿O un inversor para que otros bits se inviertan escribiendo 0? Como resultado, la configuración de dos bits de registro se ve así (gracias de nuevo a ST por tal perversión):
#define ENDP_STAT_RX(num, stat) do{USB_EPx(num) = ((USB_EPx(num) & ~(USB_EP_DTOG_RX | USB_EP_DTOG_TX | USB_EPTX_STAT)) | USB_EP_CTR_RX | USB_EP_CTR_TX) ^ stat; }while(0)
Oh sí, casi lo olvido: tampoco tienen acceso al registro por número. Es decir, macros USB_EP0R, USB_EP1R, etc. lo han hecho, pero si el número vino en una variable, entonces, por desgracia. Tuve que inventar mi propio USB_EPx () y qué hacer.
Bueno, para cumplir con las formalidades, señalaré que la bandera de preparación (es decir, que ya leímos los datos anteriores) está configurada por la máscara de bits USB_EP_RX_VALID, y para escritura (es decir, hemos escrito los datos en completo y se puede transferir) - mediante la máscara USB_EP_TX_VALID.
Procesamiento de solicitudes IN y OUT
La ocurrencia de una interrupción de USB puede indicar diferentes cosas, pero por ahora nos centraremos en las solicitudes de comunicación. La bandera para tal evento será el bit USB_ISTR_CTR. Si lo vimos, podemos averiguar con qué punto quiere comunicarse el anfitrión. El número de punto está oculto debajo de la máscara de bits USB_ISTR_EP_ID, y la dirección IN o OUT está oculta debajo de los bits USB_EP_CTR_TX y USB_EP_CTR_RX, respectivamente.
Dado que podemos tener muchos puntos, y cada uno con su propio algoritmo de procesamiento, crearemos funciones de devolución de llamada para todos ellos, que serían invocados en los eventos correspondientes. Por ejemplo, el host envió datos al endpoint3, leemos USB-> ISTR, sacó de allí que la solicitud es OUT y que el número de punto es 3. Entonces llamamos epfunc_out [3] (3). El número de punto entre paréntesis se transmite si de repente el código de usuario quiere colgar un controlador en varios puntos. Oh, sí, incluso en el estándar USB, es habitual marcar los puntos de entrada IN con un séptimo bit amartillado. Es decir, endpoint3 en la salida tendrá el número 0x03, y en la entrada - 0x83. Además, estos son puntos diferentes, se pueden usar simultáneamente, no interfieren entre sí. Bueno, casi: en stm32 tienen una configuración del tipo (bulk, interrupt, ...) tanto para la recepción como para la transmisión. Entonces, el mismo punto 0x83th IN coincidirá con la devolución de llamada 'en epfunc_in [3] (3 | 0x80).
El mismo principio se aplica a ep0. La única diferencia es que su procesamiento se realiza dentro de la biblioteca y no dentro del código de usuario. Pero, ¿qué sucede si necesita procesar solicitudes específicas como algunas HID? ¿No se moleste en elegir el código de la biblioteca? Para esto, hay devoluciones de llamada especiales usb_class_ep0_out y usb_class_ep0_in, que se llaman en lugares especiales y tienen un formato especial, del que hablaré más cerca del final.
Vale la pena mencionar otro punto no muy obvio relacionado con la ocurrencia de interrupciones en el procesamiento de paquetes. Con las solicitudes OUT, todo es simple: llegaron los datos, aquí están. Pero la interrupción IN se genera no cuando el host ha enviado una solicitud IN, sino cuando el búfer de transmisión está vacío. Es decir, en principio, esta interrupción es similar a la interrupción por insuficiencia de datos del búfer UART. Por lo tanto, cuando queremos transferir algo al host, simplemente escribimos los datos en el búfer de transferencia, esperamos la interrupción IN y agregamos lo que no encaja (no se olvide del ZLP). Y está bien, incluso con los puntos finales "habituales", están controlados por el programador, puede ignorarlos por ahora. Pero a través de ep0, el intercambio siempre continúa. Por lo tanto, trabajar con él debe estar integrado en la biblioteca.
Como consecuencia, el comienzo de la transferencia lo realiza la función ep0_send, que escribe la dirección del comienzo del búfer y la cantidad de datos a transferir a la variable global, después de lo cual, tenga en cuenta, él mismo extrae el evento IN handler por primera vez. En el futuro, se llamará a este controlador en eventos de hardware, pero aún necesita dar un empujón.
Bueno, el controlador en sí es bastante simple: escribe la siguiente pieza de datos en el búfer de transferencia, cambia la dirección del comienzo del búfer y reduce el número de bytes que quedan para la transferencia. Una muleta separada está asociada con el mismo ZLP y la necesidad de responder a algunas solicitudes con un paquete vacío. En este caso, el final de la transferencia se indica por el hecho de que la dirección de datos se ha convertido en NULL. Y un paquete vacío, que es igual a la constante ZLPP. Ambos ocurren cuando el tamaño es igual a cero, por lo que no se produce una grabación real.
Deberá implementarse un algoritmo similar al trabajar con otros puntos finales. Pero esta es la preocupación del usuario. Y la lógica de su trabajo es a menudo diferente de trabajar con ep0, por lo que en algunos casos esta opción será más conveniente que almacenar en búfer a nivel de biblioteca.
Lógica de comunicación USB
El anfitrión determina el hecho mismo de la conexión por la presencia de una resistencia pull-up entre alguna línea de datos y la fuente de alimentación. Reinicia el dispositivo, le asigna una dirección en el bus e intenta determinar qué estaba exactamente atascado en él. Para ello, lee descriptores de dispositivo y configuración (y, si es necesario, específicos). También puede leer los descriptores de cadena para comprender cómo se llama el dispositivo (aunque si el par VID: PID le resulta familiar, preferiría extraer las líneas de su base de datos). Después de eso, el anfitrión puede cargar el controlador apropiado y trabajar con el dispositivo en un idioma que comprenda. El lenguaje que comprende incluye solicitudes y llamadas específicas a interfaces y puntos finales específicos. También llegaremos a eso, pero primero necesitamos que el dispositivo se muestre al menos en el sistema.
Procesamiento de solicitudes de CONFIGURACIÓN: DeviceDescriptor
Una persona que ha manipulado USB al menos un poco debería haber sido cauteloso durante mucho tiempo: COKPOWEHEU, estás hablando de solicitudes IN y OUT, pero SETUP también se detalla en el estándar. Sí, lo es, pero es más bien una especie de solicitud OUT, especialmente estructurada y destinada exclusivamente al endpoint 0. Hablemos de su estructura y características de trabajo.
La estructura en sí tiene este aspecto:
typedef struct{
uint8_t bmRequestType;
uint8_t bRequest;
uint16_t wValue;
uint16_t wIndex;
uint16_t wLength;
}config_pack_t;
Los campos de esta estructura se consideran en muchas fuentes, pero les recordaré de todos modos.
bmRequestType es una máscara de bits, cuyos bits significan lo siguiente:
7: dirección de transmisión. 0: de host a dispositivo, 1: de dispositivo a host. De hecho, es el tipo de la siguiente transmisión, OUT o IN.
6-5: clase de solicitud
0x00 (USB_REQ_STANDARD) - estándar (solo los procesaremos por ahora)
0x20 (USB_REQ_CLASS) - específico de la clase (los veremos en los próximos artículos)
0x40 (USB_REQ_VENDOR) - específico del fabricante ( Espero que no tengamos que tocarlos)
4-0: interlocutor
0x00 (USB_REQ_DEVICE) - dispositivo como un todo
0x01 (USB_REQ_INTERFACE) - interfaz separada
0x02 (USB_REQ_ENDPOINT) -
Punto final
bRequest - Solicitud wValue en sí - pequeño campo de datos de 16 bits. En caso de solicitudes simples, para no impulsar transferencias en toda regla.
wIndex es el número del destinatario. Por ejemplo, la interfaz con la que el anfitrión desea comunicarse.
wLength: el tamaño de los datos adicionales si 16 bits de wValue no son suficientes.
En primer lugar, al conectar un dispositivo, el host intenta averiguar qué estaba exactamente atascado en él. Para ello, envía una solicitud con los siguientes datos:
bmRequestType = 0x80 (solicitud de lectura) + USB_REQ_STANDARD (estándar) + USB_REQ_DEVICE (al dispositivo en su conjunto)
bRequest = 0x06 (GET_DESCRIPTOR) - solicitud de descriptor
wValue = 0x0100 (DEVICE_DESCRIPTOR) - descriptor de dispositivo completo
wIndex = 0 - no utilizado
wLength = 0 - sin datos adicionales
Luego envía una solicitud IN, donde el dispositivo debe poner la respuesta. Como recordamos, la solicitud IN del host y la interrupción del controlador están débilmente acopladas, por lo que escribiremos la respuesta inmediatamente en el búfer del transmisor ep0. En teoría, los datos de este y de todos los demás descriptores están vinculados a un dispositivo específico, por lo que no tiene sentido colocarlos en el núcleo de la biblioteca. Las solicitudes correspondientes se pasan a la función usb_class_get_std_descr, que devuelve al kernel un puntero al comienzo de los datos y su tamaño. La cuestión es que algunos descriptores pueden tener un tamaño variable. Pero DEVICE_DESCRIPTOR no es uno de ellos. Su tamaño y estructura están estandarizados y tienen este aspecto:
uint8_t bLength; //
uint8_t bDescriptorType; // . USB_DESCR_DEVICE (0x01)
uint16_t bcdUSB; // 0x0110 usb-1.1, 0x0200 2.0.
uint8_t bDeviceClass; //
uint8_t bDeviceSubClass; //
uint8_t bDeviceProtocol; //
uint8_t bMaxPacketSize0; // ep0
uint16_t idVendor; // VID
uint16_t idProduct; // PID
uint16_t bcdDevice_Ver; // BCD-
uint8_t iManufacturer; //
uint8_t iProduct; //
uint8_t iSerialNumber; //
uint8_t bNumConfigurations; // ( 1)
En primer lugar, preste atención a los dos primeros campos: el tamaño del descriptor y su tipo. Son típicos para casi todos los descriptores USB (excepto para HID, quizás). Además, si bDescriptorType es una constante, bLength tiene que contarse casi manualmente para cada descriptor. En algún momento, me cansé de esto y se escribió una macro.
#define ARRLEN1(ign, x...) (1+sizeof((uint8_t[]){x})), x
Calcula el tamaño de los argumentos que se le pasan y lo sustituye en lugar del primero. El hecho es que a veces los descriptores están anidados, por lo que uno, digamos, requiere un tamaño en el primer byte, otro en 3 y 4 (número de 16 bits) y el tercero en 6 y 7 (nuevamente un número de 16 bits). . A las macros no les importan los valores exactos de los argumentos, pero al menos el número debería ser el mismo. En realidad, las macros para sustitución en 1, en 3 y 4, así como en 6 y 7 bytes también están ahí, pero mostraré su aplicación con un ejemplo más típico.
Por ahora, veamos campos de 16 bits como VID y PID. Está claro que mezclar constantes de 8 bits y 16 bits en una matriz no funcionará, además de endiannes ... en general, las macros vuelven al rescate: USB_U16 (x).
En términos de selección de VID: PID es una pregunta delicada. Si planea producir productos producidos en masa, aún vale la pena comprar un par personal. Para uso personal, puede recoger el de otra persona desde un dispositivo similar. Digamos que tengo pares de AVR LUFA y STM en mis ejemplos. De todos modos, el host determina errores de implementación específicos en lugar de la asignación de este par. Porque el propósito del dispositivo se describe en detalle en un descriptor especial.
¡Atención, rastrillo!Resultó que Windows vincula los controladores a este par, es decir, por ejemplo, ensambló el dispositivo HID, mostró el sistema e instaló los controladores. Y luego volvimos a flashear el dispositivo en MSD (unidad flash) sin cambiar VID: PID, entonces los controladores permanecerán viejos y, naturalmente, el dispositivo no funcionará. Tendremos que ir a "administración de hardware", eliminar controladores y obligar al sistema a buscar nuevos. Creo que no sorprenderá a nadie que Linux no tenga este problema: los dispositivos simplemente se enchufan y funcionan.
StringDescriptor
Otra característica interesante de los descriptores USB es el amor por las cadenas. En la plantilla de descriptor, se indican con el prefijo i, como iSerialNumber
¡Atención, rastrillo! No importa cuán grande sea la tentación de insertar solo una cadena en iSerialNumber, incluso una cadena con una versión honesta como u''1.2.3 '', ¡no lo haga! Algunos sistemas operativos creen que solo debe haber dígitos hexadecimales, es decir, '0' - '9', 'A' - 'Z' y eso es todo. Ni siquiera puedes puntos. Probablemente, de alguna manera cuentan el hash de este "número" para identificarlo cuando se reconectan, no lo sé. Pero noté tal problema al probar en una máquina virtual con Windows 7, ella consideró el dispositivo defectuoso. Curiosamente, Windows XP y 10 no notaron el problema.
ConfigurationDescriptor
Desde el punto de vista del host, un dispositivo representa un conjunto de interfaces separadas, cada una de las cuales está diseñada para resolver algún problema. Un descriptor de interfaz describe su dispositivo y los puntos finales asociados. Sí, los puntos finales no se describen por sí mismos, sino solo como parte de la interfaz. Normalmente, las interfaces con una arquitectura compleja se controlan mediante solicitudes de CONFIGURACIÓN (es decir, a través de ep0), en las que el campo wIndex corresponde al número de interfaz. El máximo está permitido para guardar el punto final por interrupciones. Y desde las interfaces de datos, el host solo necesita descripciones de los puntos finales y el intercambio pasará por ellos.
Puede haber muchas interfaces en un dispositivo y muy diferentes. Por lo tanto, para no confundirse donde termina una interfaz y comienza otra, el descriptor indica no solo el tamaño del "encabezado", sino también por separado (generalmente 3-4 bytes) el tamaño completo de la interfaz. Por lo tanto, la interfaz se pliega como una muñeca anidada: dentro de un contenedor común (que almacena el tamaño del "título", bDescriptorType y el tamaño completo del contenido, incluido el título) puede haber un par de contenedores más pequeños, pero dispuestos en de la misma manera. Y por dentro cada vez más. Aquí hay un ejemplo de un descriptor para un dispositivo HID primitivo:
static const uint8_t USB_ConfigDescriptor[] = {
ARRLEN34(
ARRLEN1(
bLENGTH, // bLength: Configuration Descriptor size
USB_DESCR_CONFIG, //bDescriptorType: Configuration
wTOTALLENGTH, //wTotalLength
1, // bNumInterfaces
1, // bConfigurationValue: Configuration value
0, // iConfiguration: Index of string descriptor describing the configuration
0x80, // bmAttributes: bus powered
0x32, // MaxPower 100 mA
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_INTERFACE, //bDescriptorType
0, //bInterfaceNumber
0, // bAlternateSetting
0, // bNumEndpoints
HIDCLASS_HID, // bInterfaceClass:
HIDSUBCLASS_NONE, // bInterfaceSubClass:
HIDPROTOCOL_NONE, // bInterfaceProtocol:
0x00, // iInterface
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_HID, //bDescriptorType
USB_U16(0x0101), //bcdHID
0, //bCountryCode
1, //bNumDescriptors
USB_DESCR_HID_REPORT, //bDescriptorType
USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
)
)
};
Aquí el nivel de anidamiento es pequeño, además de que no se describe un solo punto final, bueno, así que intenté elegir un dispositivo más simple. Aquí puede haber cierta confusión debido a que las constantes bLENGTH y wTOTALLENGTH equivalen a ceros de ocho y dieciséis bits. Dado que en este caso se utilizan macros para calcular el tamaño, sería extraño duplicar su trabajo y contar bytes a mano. Qué extraño es escribir ceros. Y las constantes son algo notable, lo que contribuye a la claridad del código.
Como puede ver, este descriptor consta del "encabezado" USB_DESCR_CONFIG (que almacena el tamaño completo del contenido, incluido él mismo), la interfaz USB_DESCR_INTERFACE (que describe los detalles del dispositivo) y USB_DESCR_HID, que en términos generales dice qué tipo de HID estamos rindiendo. Y exactamente qué en términos generales: una estructura HID específica se describe en un descriptor especial HID_REPORT_DESCRIPTOR, que no consideraré aquí, simplemente porque lo conozco demasiado mal. Así que nos restringiremos a copiar y pegar de algún ejemplo .
Volvamos a las interfaces. Teniendo en cuenta que tienen números, es lógico suponer que puede haber muchas interfaces en un dispositivo. Además, pueden ser responsables tanto de una tarea común (por ejemplo, la interfaz de control USB-CDC y la interfaz de datos) como de otras fundamentalmente no relacionadas. Digamos, nada nos impide (excepto por la falta de conocimiento hasta ahora) en un controlador para implementar dos adaptadores USB-CDC más una unidad flash USB más, digamos, un teclado. Obviamente, la interfaz de la unidad flash no conoce el puerto COM. Sin embargo, aquí hay trampas que, espero, algún día consideremos. También vale la pena señalar que una interfaz puede tener varias configuraciones alternativas (bAlternateSetting) que difieren, por ejemplo, en el número de puntos finales o la frecuencia de su sondeo. En realidad, por eso se hizo: si el anfitrión cree que es mejor ahorrar ancho de banda,puede cambiar la interfaz a cualquier modo alternativo que más le guste.
Comunicación con HID
En términos generales, los dispositivos HID simulan objetos del mundo real, que no tienen tanto datos como un conjunto de ciertos parámetros que se pueden medir o configurar (solicitudes SET_REPORT / GET_REPORT) y que pueden notificar al host sobre un evento externo repentino (INTERRUPCIÓN). Por lo tanto, de hecho, estos dispositivos no están destinados al intercambio de datos ... ¡pero quién lo detuvo y cuándo!
No tocaremos las interrupciones por ahora, ya que necesitan un punto final especial. Pero consideraremos leer y configurar parámetros. En este caso, solo hay un parámetro, que es una estructura de dos bytes, que, por diseño, son responsables de dos LED, o de un botón y un contador.
Comencemos con uno más simple: leer a pedido HIDREQ_GET_REPORT. De hecho, esta es la misma solicitud que cualquier DEVICE_DESCRIPTOR, solo que es específica para HID. Además, esta solicitud no está dirigida al dispositivo en su conjunto, sino a la interfaz. Es decir, si hemos implementado varios dispositivos HID independientes en un dispositivo, se pueden distinguir por el campo wIndex de la solicitud. Es cierto que este no es el mejor enfoque específicamente para HID: es más fácil hacer que el descriptor sea compuesto. En cualquier caso, estamos lejos de tales perversiones, por lo que ni siquiera analizaremos qué y dónde intentó enviar el host: para cualquier solicitud a la interfaz y con el campo bRequest igual a HIDREQ_GET_REPORT, devolveremos los datos reales. En teoría, este enfoque está destinado a devolver descriptores (con todo bLength y bDescriptorType), pero en el caso de HID, los desarrolladores decidieron simplificar todo e intercambiar solo datos.Entonces devolvemos un puntero a nuestra estructura y su tamaño. Bueno, un poco de lógica adicional como botones de procesamiento y un contador de solicitudes.
Un caso más complejo es una solicitud de escritura. Esta es la primera vez que encontramos datos adicionales en una solicitud de CONFIGURACIÓN. Es decir, el núcleo de nuestra biblioteca debe leer primero la solicitud en sí, y solo luego los datos. Y transferirlos a la función de usuario. Y les recuerdo que no tenemos parachoques. Como resultado de algo de magia de bajo nivel, se desarrolló el siguiente algoritmo. Siempre se llamará a la devolución de llamada, pero le diremos de qué byte están los datos en el búfer de recepción del punto final (desplazamiento) y el tamaño de estos datos (tamaño). Es decir, cuando se recibe la solicitud en sí, los valores de desplazamiento y tamaño son cero (no hay datos). Cuando se recibe el primer paquete, el desplazamiento sigue siendo cero y el tamaño es el tamaño de los datos recibidos. Para el segundo, el offset será igual al tamaño de ep0 (porque si los datos tuvieran que dividirse, lo hacen según el tamaño del punto final), y el tamaño será igual al tamaño de los datos recibidos.Etc. ¡Importante! Si los datos son aceptados, deben leerse. Esto lo puede hacer el controlador llamando a usb_ep_read () y devolviendo 1 (dicen "Yo mismo pensé, no te molestes"), o simplemente devolviendo 0 ("No necesito estos datos") sin leer - entonces el núcleo de la biblioteca se ocupará de la limpieza. La función se basa en este principio: comprueba si los datos están disponibles y, de ser así, los lee y enciende los LED.
Software de intercambio de datos
Aquí no reinventé la rueda, sino que tomé un programa listo para usar del artículo anterior .
Conclusión
Eso, de hecho, es todo. Dije los conceptos básicos de trabajar con USB usando un módulo de hardware en STM32, también toqué un poco de rastrillo. Teniendo en cuenta la cantidad mucho menor de código que el horror que genera STMCube, será más fácil resolverlo. De hecho, todavía no lo he descubierto en los fideos Cube, hay demasiadas llamadas de la misma cosa en diferentes combinaciones. Mucho mejor para entender la opción de EddyEm , de la que partí . Por supuesto, no lo hay sin jambas, pero al menos es adecuado para la comprensión. También me jacto de que el tamaño de mi versión es casi 5 veces más pequeño que el de ST (~ 2.7 kB frente a 14), a pesar de que no he estado involucrado en la optimización y, seguro, aún puede reducirlo.
También me gustaría señalar la diferencia en el comportamiento de varios sistemas operativos al conectar equipos cuestionables. Linux simplemente funciona incluso si hay errores en los descriptores. Windows XP, 7, 10, ante el menor error, juran que "el dispositivo está roto, me niego a trabajar con él". Y XP a veces, incluso en BSOD, se indignó. Oh, sí, también muestran constantemente "el dispositivo puede funcionar más rápido", no sé qué hacer al respecto. En general, no importa lo bueno que sea Linux para el desarrollo, perdona demasiado, es necesario probarlo en sistemas menos fáciles de usar.
Planes adicionales: considere otros tipos de puntos finales (hasta ahora solo había un ejemplo con Control); considere otros controladores (digamos, todavía tengo at90usb162 (AVR) y gd32vf103 (RISC_V) por ahí), pero estos son planes muy lejanos. También sería bueno echar un vistazo más de cerca a los dispositivos USB individuales como los mismos HID, pero tampoco es una tarea prioritaria.