USB en registros: interrumpa el punto final usando HID como ejemplo





Un nivel aún más bajo (avr-vusb)

USB en registros: STM32L1 / STM32F1

USB en registros: punto final masivo en el ejemplo de almacenamiento masivo



USB en registros: punto final isócrono en el ejemplo de dispositivo de audio



Continuamos tratando con USB en controladores STM32L151. Como en la parte anterior, aquí no habrá nada que dependa de la plataforma, pero dependerá del USB. Más precisamente, consideraremos el tercer tipo de punto final: interrupción. Y lo haremos usando el ejemplo de un dispositivo compuesto "teclado + tableta" ( enlace a la fuente ).

Por si acaso, te lo advierto: este artículo (como todos los demás) es más bien una sinopsis de lo que entendí mientras entendía este tema. Muchas cosas han quedado "mágicas" y agradeceré si hay un especialista que pueda explicarlas.



En primer lugar, permítame recordarle que el protocolo HID (Dispositivo de interfaz humana) no está diseñado para intercambiar grandes cantidades de datos. Todo el intercambio se basa en dos conceptos: un evento y un estado . Un evento es un mensaje único que ocurre en respuesta a un impacto externo o interno. Por ejemplo, el usuario presionó un botón o movió el mouse. O en un teclado deshabilité NumLock, después de lo cual el host es forzado y el segundo envía el comando apropiado para arreglarlo, también enviando la señal de pulsación de tecla NumLock y habilitándolo nuevamentelo muestra en el indicador. Los puntos de interrupción se utilizan para señalar eventos. Un estado es algún tipo de característica que no cambia así. Bueno, digamos la temperatura. O ajusta el nivel de volumen. Es decir, algo por lo que el anfitrión controla el comportamiento del dispositivo. La necesidad de esto rara vez surge, por lo tanto, la interacción es la más primitiva, a través de ep0.



Por lo tanto, el propósito de un punto de interrupción es el mismo que el de una interrupción en un controlador: reportar rápidamente un evento raro. Pero el USB es una cosa centrada en el host, por lo que el dispositivo no tiene derecho a iniciar la transferencia por sí solo. Para evitar esto, a los desarrolladores de USB se les ocurrió una muleta: el host envía periódicamente solicitudes para leer todos los puntos de interrupción. La frecuencia de la solicitud se configura mediante el último parámetro del EndpointDescriptor (esto es parte del ConfigurationDescriptor). Ya hemos visto el campo bInterval en los capítulos anteriores, pero su valor fue ignorado. Ahora finalmente ha encontrado un uso. El valor tiene un tamaño de 1 byte y se establece en milisegundos, por lo que seremos sondeados a intervalos de 1 ms a 2,55 segundos. Para dispositivos de baja velocidad, el intervalo mínimo es de 10 ms. La presencia de una muleta con puntos de interrupción sondeando para nosotros significaque incluso en ausencia de intercambio, estarán desperdiciando ancho de banda del bus.



La conclusión lógica: los puntos de interrupción son solo para transacciones IN. En particular, se utilizan para transmitir eventos desde el teclado o el mouse, para notificar sobre cambios en las líneas de servicio del puerto COM, para sincronizar el flujo de audio y similares. Pero para todo esto, tendrás que sumar otro tipo de puntos. Por tanto, para no complicar el ejemplo, nos limitaremos a la implementación del dispositivo HID. De hecho, ya hicimos un dispositivo de este tipo en la primera parte, pero no se utilizaron puntos adicionales en absoluto y no se consideró la estructura del protocolo HID.



ConfigurationDescriptor



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
    2, // bNumEndpoints
    HIDCLASS_HID, // bInterfaceClass: 
    HIDSUBCLASS_BOOT, // bInterfaceSubClass: 
    HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol: 
    0x00, // iInterface
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_HID, //bDescriptorType
    USB_U16(0x0110), //bcdHID
    0, //bCountryCode
    1, //bNumDescriptors
    USB_DESCR_HID_REPORT, //bDescriptorType
    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_ENDPOINT, //bDescriptorType
    INTR_NUM, //bEdnpointAddress
    USB_ENDP_INTR, //bmAttributes
    USB_U16( INTR_SIZE ), //MaxPacketSize
    10, //bInterval
  )
  ARRLEN1(
    bLENGTH, //bLength
    USB_DESCR_ENDPOINT, //bDescriptorType
    INTR_NUM | 0x80, //bEdnpointAddress
    USB_ENDP_INTR, //bmAttributes
    USB_U16( INTR_SIZE ), //MaxPacketSize
    10, //bInterval
  )
  )
};
      
      





El lector atento puede notar inmediatamente las descripciones de los puntos finales. Con el segundo todo está en orden - el punto IN (ya que la suma con 0x80) es de tipo interrupción, se especifican el tamaño y el intervalo. Pero el primero parece estar declarado como OUT, pero al mismo tiempo interrumpe, lo que contradice lo dicho anteriormente. Y también sentido común: el anfitrión no necesita muletas para transferir nada al dispositivo en ningún momento. Pero de esta manera, se omiten otros rakes: el tipo de punto final en STM32 no se establece para un punto, sino solo para el par IN / OUT, por lo que no funcionará establecer el punto 0x81st en el tipo de interrupción, sino en el 0x01st control. Sin embargo, esto no es un problema para el host, probablemente también enviaría los mismos datos en el punto masivo ... que, sin embargo, no verificaré.



Descriptor HID



La estructura del descriptor HID es muy similar al archivo de configuración "nombre = valor", pero a diferencia de él, "nombre" es una constante numérica de la lista específica de USB, y "valor" es también una constante o una variable de tamaño 0 hasta 3 bytes.



Importante:para algunos "nombres", la longitud del "valor" se especifica en los 2 bits menos significativos del campo "nombre". Por ejemplo, tomemos LOGICAL_MINIMUM (el valor mínimo que esta variable puede tomar en modo normal). El código para esta constante es 0x14. En consecuencia, si no hay "valor" (parece que esto no sucede, pero no discutiré, por alguna razón se ingresó este caso), entonces el descriptor contendrá un solo número 0x14. Si el "valor" es 1 (un byte), se escribirá 0x15, 0x01. Para un valor de dos bytes se escribirá 0x1234, 0x16, 0x34, 0x12; el valor se escribe de menor a mayor. Bueno, antes del montón, el número 0x123456 será 0x17, 0x56, 0x34, 0x12.



Naturalmente, soy demasiado vago para memorizar todas estas constantes numéricas, así que usaremos macros. Desafortunadamente, nunca encontré una manera de hacer que averiguaran el tamaño del valor pasado y se expandieran a 1, 2, 3 o 4 bytes. Por lo tanto, tuve que hacer una muleta: una macro sin sufijo es responsable de los valores de 8 bits más comunes, con un sufijo 16 para valores de 16 bits y con 24 para valores de 24 bits. También se han escrito macros para valores "compuestos" como el rango LOGICAL_MINMAX24 (min, max), que se expanden a 4, 6 u 8 bytes.



Al igual que con los archivos de configuración, hay "secciones" llamadas páginas (página_uso) que agrupan los dispositivos por propósito. Por ejemplo, hay una página con periféricos básicos como teclados, ratones y solo botones, hay joysticks y gamepads (¡sinceramente recomiendo mirar cuáles! También los hay para tanques, y para naves espaciales, y para submarinos y para cualquier otra cosa. ), incluso hay pantallas ... Es cierto que no tengo ni idea de dónde buscar software que pueda funcionar con todo esto.



Dentro de cada página, se selecciona un dispositivo específico. Por ejemplo, para un mouse es un puntero y botones, y para una tableta, un lápiz o el dedo de un usuario (¡¿qué?!). También designan los componentes del dispositivo. Entonces, parte del puntero son sus coordenadas X e Y. Algunas características se pueden agrupar en una "colección", pero no entiendo realmente por qué se hace esto. En la documentación, los campos a veces se marcan con un par de letras sobre el propósito del campo y cómo trabajar con él:



California Colección (aplicación) Información de servicio, que no corresponde a ninguna variable
CL Colección (lógica) - / -
CP Colección (física) - / -
Dv Valor dinámico valor de entrada o salida (variable)
MC Control momentáneo bandera de estado (1 bandera amartillada, 0 borrada)
OSC Control de un disparo Evento de una vez. Solo se procesa la transición 0-> 1




Hay otros, por supuesto, pero no se utilizan en mi ejemplo. Si, por ejemplo, el campo X está marcado como DV, entonces se considera una variable de longitud distinta de cero y se incluirá en la estructura del informe. Los campos MC u OSC también se incluyen en el informe, pero tienen un tamaño de 1 bit.



Un informe (paquete de datos enviado o recibido por el dispositivo) contiene los valores de todas las variables descritas en él. La descripción del botón habla de solo un bit ocupado, pero para coordenadas relativas (cuánto se ha movido el mouse, por ejemplo), se requiere al menos un byte, y para coordenadas absolutas (como para una pantalla táctil), al menos 2 bytes. Se necesitan. Además, muchos controles tienen sus propias limitaciones físicas. Por ejemplo, un ADC de la misma pantalla táctil puede tener una resolución de solo 10 bits, es decir, dar valores de 0 a 1023, que el host deberá escalar a resolución de pantalla completa. Por lo tanto, además del propósito de cada campo, el descriptor también especifica el rango de sus valores permisibles (LOGICAL_MINMAX), más a veces el rango de valores físicos (en milimatrios allí, o en grados) y la presentación en el el informe es obligatorio.La representación se establece mediante dos números: el tamaño de una variable (en bits) y su número. Por ejemplo, las coordenadas de tocar la pantalla táctil en el dispositivo que estamos creando se establecen de la siguiente manera:



USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00,   0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
      
      





Aquí puede ver que se declaran dos variables, que varían en el rango de 0 a 10000 y ocupan dos secciones de 16 bits en el informe.



El último campo dice que el host (IN) leerá las variables anteriores y explica exactamente cómo. No describiré sus banderas en detalle, me detendré solo en algunas. La bandera HID_ABS indica que el valor es absoluto, es decir, ningún historial lo afecta. El valor alternativo HID_REL indica que el valor es un desplazamiento del anterior. La bandera HID_VAR dice que cada campo es responsable de su propia variable. El valor alternativo HID_ARR dice que no se transmitirán los estados de todos los botones de la lista, sino solo los números de los activos. Esta bandera se aplica solo a campos de un solo bit. En lugar de transmitir 101/102 estados de todos los botones del teclado, puede limitarse a unos pocos bytes con una lista de teclas presionadas. Entonces, el primer parámetro REPORT_FMT será responsable del tamaño del número y el segundo del número.



Dado que el tamaño de todas las variables se establece en bits, es lógico preguntar: ¿qué pasa con los botones, porque su número puede no ser un múltiplo de 8, y esto conducirá a dificultades de alineación al leer y escribir? Sería posible asignar un byte a cada botón, pero luego el volumen del informe aumentaría considerablemente, lo que es desagradable para programas de alta velocidad como interrupción. En su lugar, se intenta colocar los botones más cerca entre sí, y el espacio restante se llena con bits con la bandera HID_CONST.



Ahora podemos, si no escribir un descriptor desde cero, al menos intentar leerlo, es decir, determinar a qué bits corresponde este o aquel campo. Basta con contar los INPUT_HID y los REPORT_FMT correspondientes. Solo tenga en cuenta que se me ocurrieron tales macros, nadie más las usa. En los descriptores de otras personas, tendrá que buscar input, report_size, report_count o incluso constantes numéricas.



Ahora puedes traer el descriptor completo:



static const uint8_t USB_HIDDescriptor[] = {
  //keyboard
  USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,
  USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,
  COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,
    REPORT_ID( 1 ), // 0x85, 0x01,
    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
    USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,    
    LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,
    REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08     
    INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,
     //reserved
    REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,
    INPUT_HID(HID_CONST), // 0x81, 0x01,
              
    REPORT_FMT(1, 5),  // 0x75, 0x01, 0x95, 0x05,
    USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,
    USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,  
    OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,
    //  1 
    REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,
    OUTPUT_HID( HID_CONST ), // 0x91, 0x01,
    REPORT_FMT(8, 6),  // 0x75, 0x08, 0x95, 0x06,
    LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,         
    USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
    USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,
    INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,           
  )
  //touchscreen
  USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,
  USAGE( USAGE_PEN ), // 0x09, 0x02,
  COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,
    REPORT_ID( 2 ), //0x85, 0x02,
    USAGE( USAGE_FINGER ), // 0x09, 0x22,
    COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
      USAGE( USAGE_TOUCH ), // 0x09, 0x42,
      USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,
      LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,
      REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,
      INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,
      REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,
      INPUT_HID( HID_CONST ), // 0x81, 0x01,
                
      USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,
      USAGE( USAGE_POINTER ), // 0x09, 0x01,
      COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,         
        USAGE( USAGE_X ), // 0x09, 0x30,
        USAGE( USAGE_Y ), // 0x09, 0x31,
        LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
        REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
        INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
      )
    )
  )
};
      
      





Además de los campos discutidos anteriormente, también hay un campo tan interesante como REPORT_ID. Dado que, como se desprende de los comentarios, nuestro dispositivo es compuesto, el host necesita determinar de alguna manera qué datos recibe. Para ello, se necesita este campo.



Y un campo más al que me gustaría llamar su atención es OUTPUT_HID. Como su nombre lo indica, no es responsable de recibir un informe (IN), sino de transmitir (OUT). Se encuentra en la sección del teclado y describe los indicadores CapsLock, NumLock, ScrollLock y dos exóticos: Componer (una bandera para ingresar algunos caracteres que no tienen sus propios botones como á, µ o) y Kana (ingresar jeroglíficos) . En realidad, por el bien de este campo, comenzamos el punto OUT. En su controlador, comprobaremos si los indicadores CapsLock y NumLock necesitan estar encendidos: solo hay dos diodos en la placa y están cableados.



Hay un tercer campo relacionado con el intercambio de datos: FEATURE_HID, lo usamos en el primer ejemplo. Si INPUT y OUTPUT están destinados a transmitir eventos, entonces FEATURE es un estado que se puede leer o escribir. Es cierto que esto no se hace a través de puntos finales dedicados, sino a través del ep0 habitual mediante las solicitudes adecuadas.



Si observa detenidamente el descriptor, puede restaurar la estructura del informe. Más precisamente, dos informes:



struct{
  uint8_t report_id; //1
  union{
    uint8_t modifiers;
    struct{
      uint8_t lctrl:1; //left control
      uint8_t lshift:1;//left shift
      uint8_t lalt:1;  //left alt
      uint8_t lgui:1;  //left gui.   hyper,   winkey
      uint8_t rctrl:1; //right control
      uint8_t rshift:1;//right shift
      uint8_t ralt:1;  //right alt
      uint8_t rgui:1;  //right gui
    };
  };
  uint8_t reserved; //        
  uint8_t keys[6]; //   
}__attribute__((packed)) report_kbd;

struct{
  uint8_t report_id; //2
  union{
    uint8_t buttons;
    struct{
      uint8_t touch:1;   //  
      uint8_t inrange:1; //   
      uint8_t reserved:6;//  1 
    };
  };
  uint16_t x;
  uint16_t y;
}__attribute__((packed)) report_tablet;
      
      





Los enviaremos pulsando los botones del tablero, además. ya que estamos escribiendo solo un ejemplo de implementación, y no un dispositivo completo, lo haremos de manera bárbara - enviando dos informes, en el primero de los cuales "presionando" las teclas, y en el segundo - "soltando". Además, con un enorme retraso "estúpido" entre los mensajes. Si no envía un informe con las teclas "liberadas", el sistema considerará que la tecla aún está presionada y lo repetirá. Naturalmente, no hay duda de eficiencia, seguridad también, pero servirá para la prueba. ¡Oh sí, donde sin otro rastrillo! El tamaño de la estructura debe coincidir con lo que se describe en el descriptor; de lo contrario, Windows hará como si no entiende lo que quieren de ella. Como de costumbre, Linux ignora estos errores y funciona como si nada.



Durante las pruebas, me encontré con un efecto secundario divertido: en Windows7, cuando haces clic en la "pantalla táctil", aparece la ventana de escritura a mano. No conocía esta función.



Si tiene un dispositivo terminado



... y quiero mirarlo desde adentro. En primer lugar, por supuesto, miramos, incluso puede hacerlo desde un usuario común, ConfigurationDescriptor:



lsusb -v -d <VID:PID>
      
      





Para el descriptor HID, no encontré (y no busqué) una mejor manera que desde la raíz:



cat /sys/kernel/debug/hid/<address>/rdes
      
      





En aras de la integridad, valdría la pena agregar aquí cómo mirar cosas similares en otros sistemas operativos. Pero no tengo conocimientos relevantes, tal vez te lo cuenten en los comentarios. Es deseable, por supuesto, sin instalar software de terceros.



Conclusión



Esto es, de hecho, todo lo que desenterré en HID. El plan mínimo, para aprender a leer descriptores listos para usar, emular varios dispositivos al mismo tiempo e implementar la entrada de la tableta, está completo. Bueno, al mismo tiempo se consideró la filosofía de los puntos de interrupción.



Como en el mal momento, dejé un poco de documentación en el repositorio por si los diseñadores de USB-IF deciden volver a arruinar el sitio.



All Articles