EHCI humanamente en ruso

imagen

Introduccion


Doy la bienvenida a todos. Hoy quiero compartir mi experiencia y aún, en mi opinión, explicar claramente sobre, a primera vista, un estándar simple para el controlador de host USB 2.0.

Inicialmente, puede imaginar que un puerto USB 2.0 tiene solo 4 pines, dos de los cuales simplemente transmiten datos (como, por ejemplo, un puerto COM), pero de hecho, no todo es así, e incluso todo lo contrario. El controlador USB, en principio, no nos permite transferir datos como a través de un puerto COM normal. EHCI es un estándar bastante complejo que permite la transferencia de datos confiable y rápida del software al dispositivo en sí, y en la dirección opuesta.

Puede encontrar este artículo útil si, por ejemplo, no tiene suficientes habilidades de escritura para los controladores y la lectura de la documentación de un hardware. Un ejemplo simple: desea escribir su sistema operativo para una mini PC, de modo que alguna distribución de Windows u otra distribución de Linux no descargue hardware, y use toda su potencia exclusivamente para sus propios fines.

¿Qué es el EHCI?


Bueno, empecemos. EHCI - Interfaz de controlador de host mejorada, está diseñada para transferir datos y controlar solicitudes a dispositivos USB, y en la otra dirección, y en el 99% de los casos es un enlace entre cualquier software y un dispositivo físico. EHCI funciona como un dispositivo PCI y, en consecuencia, utiliza MMIO (Memory-Mapped-IO) para controlar el controlador (sí, sé que algunos dispositivos PCI usan puertos, pero aquí generalicé todo). La documentación de Intel describe solo el principio de funcionamiento, y no hay pistas sobre todos los algoritmos escritos al menos en pseudocódigo. EHCI tiene 2 tipos de registros MMIO: Capacidad y Operativo. Los primeros sirven para obtener las características del controlador, mientras que los segundos sirven para controlarlo. En realidad, adjuntaré la esencia misma de la conexión entre el software y el controlador EHCI:

imagen

Cada controlador EHCI tiene varios puertos, cada uno de los cuales se puede conectar a cualquier dispositivo USB. Además, tenga en cuenta que EHCI es una versión mejorada de UHCI, que también fue desarrollada por Intel unos años antes. Por compatibilidad con versiones anteriores, cualquier controlador UHCI / OHCI que tenga una versión inferior a EHCI será un compañero de EHCI. Por ejemplo, tiene un teclado USB (y la mayoría de los teclados del año hasta ahora han sido así) que funciona en USB 1.1 (tenga en cuenta que la velocidad máxima de USB 1.1 es de 12 megabits por segundo, y FullSpeed ​​USB 2.0 tiene ancho de banda tanto como 480 Mbps), y tiene una computadora con un puerto USB 2.0, cuando conecta el teclado a la computadora, el controlador host EHCI funcionará de cualquier manera con USB 1.1. Este modelo se muestra en el siguiente diagrama:

imagen

Además, para el futuro quiero advertirle de inmediato que su controlador puede no funcionar correctamente debido a una situación tan absurda: inicializó UHCI y luego EHCI, mientras agregaba dos dispositivos idénticos, establezca el bit de Control del propietario del puerto en el registro del puerto y UHCI dejó de funcionar, debido a que EHCI arrastra automáticamente el puerto sobre sí mismo, y el puerto en UHCI deja de responder, esta situación necesita ser monitoreada.

Además, veamos un diagrama que muestra la propia arquitectura EHCI:

imagen

A la derecha está escrito sobre la cola, sobre ellos un poco más tarde.

Registros del controlador EHCI


Para empezar, quiero aclarar una vez más que a través de estos registros controlarás tu dispositivo, por lo tanto, son muy importantes, y sin ellos la programación EHCI es imposible.

Primero debe obtener la dirección MMIO que se le da a este controlador, en el desplazamiento + 0x10 será la dirección de nuestros registros tan esperados. Hay una cosa: primero, los registros de capacidad van, y solo después de ellos: operativos, por lo tanto, en el desplazamiento 0 (de la dirección anterior, que recibimos en el desplazamiento 0x10 en relación con el inicio de nuestro MMIO de EHCI), hay un byte: la longitud de los registros de capacidad.

Registros de capacidad


En el desplazamiento 2, se encuentra el registro HCIVERSION : el número de revisión de este HC, que toma 2 bytes y contiene la versión BCD de la revisión (qué BCD se puede encontrar en Wikipedia).
En el desplazamiento +4, se encuentra el registro HCSPARAMS , su tamaño es de 2 palabras, contiene los parámetros estructurales del dispositivo y sus bits muestran lo siguiente:

  • Bit 16 - Indicadores de puerto - LED disponibles para dispositivos USB conectados.
  • Bits 15:12: el número del controlador complementario asignado a este controlador
  • Bits 11: 8: la cantidad de puertos en el controlador complementario
  • Bit 7: reglas de enrutamiento de puertos: muestra cómo estos puertos se asignan a puertos complementarios
  • Bit 4: control de alimentación del puerto: indica si es necesario encender la alimentación de cada puerto; 0: la alimentación se suministra automáticamente
  • Bits 3: 0: el número de puertos para este controlador.
  • En el desplazamiento +8 se encuentra el registro HCCPARAMS: muestra los parámetros de compatibilidad, sus bits significan lo siguiente:
  • Bit 2: disponibilidad de cola asíncrona,
  • Bit 1: disponibilidad de cola periódica (secuencial)
  • Bit 0 - compatibilidad de 64 bits

Registros de operaciones


En el desplazamiento 0, el registro USBCMD es el registro de comando del controlador, sus bits significan lo siguiente:

  • Bits 23:16 - Control de umbral de interrupción: muestra cuántos micro cuadros se usarán para un cuadro normal. Cuanto más grande, más rápido, pero si es más de 8, los micro-cuadros se procesarán a la misma velocidad que para 8.
  • Bit 6: interrupción después de cada transacción en la cola asíncrona,
  • Bit 5: es la cola asincrónica utilizada
  • Bit 4 - uso de cola secuencial,
  • Bits 3: 2: el tamaño de FrameList'a (más sobre eso más adelante). 0 significa 1024 elementos, 1 - 512, 2 - 256, 3 - reservado
  • Bit 1: se establece para restablecer el controlador de host.
  • Bit 0 - Ejecutar / Parar
.
A continuación, en el desplazamiento +4, está el registro USBSTS : el estado del controlador host,

  • El bit 15 indica si se está utilizando una cola asincrónica.
  • El bit 14 indica si se está utilizando una cola secuencial,
  • Bit 13: indica que se ha detectado una cola asíncrona vacía,
  • El bit 12 se establece en 1, si se produjo un error al procesar la transacción, el controlador host detendrá todas las colas.
  • El bit 4 se establece en 1, si se produce un error grave, el controlador host detiene todas las colas.
  • Bit 3 FrameList (Registro) Rollover: se establece en 1 cuando el controlador host procesó toda la FrameList.
  • Bit 1 - Interrupción de error USB - ¿Genero una interrupción de error?
  • Bit 0: interrupción de USB: se establece después del procesamiento exitoso de la transacción, si IOC se instaló en TD

No cansado? Puedes servirte una gaviota fuerte y traer el hígado, ¡estamos al principio!

En el desplazamiento +8, hay un registro USBINTR : el registro de habilitación de interrupción
Para no escribir durante mucho tiempo, y aún más, para que no lea durante mucho tiempo, los valores de los bits de este registro se pueden encontrar en la especificación, se dejará un enlace a continuación. Aquí solo escribo 0, porque No tengo ningún deseo de escribir controladores, interrupciones de mapas, etc., por lo que creo que esto es casi completamente inútil.

En el desplazamiento +12 (0x0C) se encuentra el registro FRINDEX , en el que simplemente se encuentra el número de cuadro actual, y quiero señalar que los últimos 4 bits muestran el número de micro-cuadro, en los 28 bits superiores el número de cuadro (el mismo valor no es necesariamente menor que el tamaño de la lista de cuadros Pero si necesita un índice, es mejor tomarlo con una máscara de 0x3FF (o 0x1FF, etc.).

El registro CTRLDSSEGMENT está en offset + 0x10; muestra al controlador host los 32 bits más significativos de la dirección de la hoja de cuadro.

El registro PERIODICLISTBASE tiene un desplazamiento de + 0x14, puede colocar los 32 bits inferiores de la hoja de marco, tenga en cuenta que la dirección debe estar alineada con el tamaño de la página de memoria (4096).

El registro ASYNCLISTADDR tiene un desplazamiento de + 0x18, puede poner la dirección de la cola asincrónica en él, tenga en cuenta que debe estar alineado en el límite de 32 bytes, mientras que debe estar en los primeros cuatro gigabytes de memoria física.

El registro CONFIGFLAG indica si el dispositivo está configurado. Debe establecer el bit 0 después de completar la configuración del dispositivo, tiene un desplazamiento de + 0x40.

Pasemos a los registros de puertos. Cada puerto tiene su propio registro de estado de comando, cada registro de puerto está desplazado + 0x44 + (PortNumber - 1) * 4 , sus bits significan lo siguiente:

  • Bit 12 - alimentación de puerto, 1 - se suministra alimentación, 0 - no.
  • El Bit 8 - Port Rest - está configurado para restablecer el dispositivo.
  • Bit 3: cambio de activación / desactivación de puerto: se establece al cambiar el estado de la "inclusión" del puerto.
  • Bit 2 - puerto activado / desactivado.
  • Bit 1: cambie el estado de la conexión; se establece en 1, por ejemplo, si conectó o desconectó un dispositivo USB.
  • Bit 0 - estado de conexión, 1 - conectado, 0 - no.

Ahora pasemos al jugo mismo.

Transferencia de datos y estructuras de consulta


Organizar una estructura para procesar solicitudes incluye colas y descriptores de transferencia (TD).

Por el momento, consideraremos solo 3 estructuras.

Lista secuencial


La lista secuencial (periódica, pereódica) se organiza de la siguiente manera:

imagen

Como puede ver en el diagrama, el procesamiento comienza con la obtención del marco deseado del marco de la hoja, cada uno de sus elementos ocupa 4 bytes y tiene la siguiente estructura:

imagen

Como puede ver en la imagen, la transferencia de la dirección / descriptor de la cola está alineada en el límite de 32 bytes, el bit 0 significa que el controlador del host no procesará este elemento, los bits 3: 1 indican el tipo de lo que procesará el controlador del host: 0 - TD isosíncrono (iTD), 1 - turno, 2 y 3 en este artículo que no consideraré.

Cola asincrónica


El controlador de host procesa esta cola solo cuando el marco secuencial está vacío o el controlador de host ha procesado toda la lista de serie.

Una cola asíncrona es un puntero a una cola que contiene otras colas que necesitan procesamiento. Esquema:

imagen

qTD (Descriptor de transferencia de elemento de cola)


Este TD tiene la siguiente estructura:

imagen

Siguiente puntero qTD : un puntero a la continuación de la cola para el procesamiento (para ejecución horizontal), bit 0 Siguiente puntero qTD indica que no hay más colas.
Token qTD: token TD, muestra los parámetros de transferencia de datos:

  • Bit 31 - Alternar datos (más sobre eso más adelante)
  • Bits 30:16: la cantidad de datos a transferir, después de la finalización de la transacción, su valor disminuye en la cantidad de datos transferidos.
  • Bit 15 - IOC - Interrupción al finalizar - causa la interrupción después de completar el procesamiento del descriptor.
  • Los bits 14:12 muestran el número del búfer actual con el que se intercambian datos, más sobre esto más adelante.
  • Bits 11:10: la cantidad de errores permitidos. Esta tabla muestra cuándo disminuye el recuento de errores:

    imagen

    Nota 1: la detección de Babble o Stall detiene automáticamente la ejecución del encabezado de la cola. Nota al pie 3: los errores del búfer de datos son problemas con el host. No tienen en cuenta los reintentos del dispositivo.
  • 9: 8 - Código PID - tipo de token: 0 - token a la entrada (del host al dispositivo), 1 - token a la salida (del dispositivo al host), 2 - token "SETUP"
  • Los bits 7: 0 indican el estado de TD:
    El bit 7 indica que el TD está en un estado activo (es decir, el controlador host procesa este TD)
    Bit 6 - Detenido - indica que ha ocurrido un error y que la ejecución de TD se ha detenido.
    Bit 4: balbuceo detectado: la cantidad de datos que enviamos al dispositivo, o por revolución, es inferior a la que transmitimos, es decir, por ejemplo, el dispositivo nos envió 100 bytes de datos, y leemos solo 50 bytes, y luego otros 50 El bit detenido también se establecerá si este bit se establece en 1.
    Bit 3: error de transacción: se produjo un error durante la transacción.

Lista de punteros de la página de búfer qTD : cualquiera de los 5 búferes. Contiene un enlace a donde en la memoria se debe realizar la transacción (enviar datos al dispositivo / recibir datos del dispositivo), todas las direcciones en los buffers, excepto la primera, deben estar alineadas con el tamaño de la página (4096 bytes).

Jefe de línea


La cola de la cola tiene la siguiente estructura:

imagen

Puntero de enlace horizontal del encabezado de la cola: puntero a la siguiente cola, los bits 2: 1 tienen los siguientes valores según el tipo de cola:

imagen

Capacidades / características de punto final - características de la cola:

  • Los bits 26:16 contienen el tamaño máximo de paquete para la transmisión
  • Bit 14: Control de alternancia de datos: muestra dónde el controlador del host debe tomar el valor de alternancia de datos inicial, 0: ignora el bit DT en qTD, guarda el bit DT para la cabeza de la cola.
  • Bit 13:12 - características de velocidad de transmisión: imagen
  • Bits 11: 8: el número del punto final al que se realiza la solicitud
  • Bits 6: 0 - dirección del dispositivo

Capacidades de punto final: Queue Head DWord 2 - continuación de la palabra doble anterior:

  • Bits 29:23 - Número de concentrador
  • Bits 22:16 - Dirección del concentrador

Puntero de enlace qTD actual : puntero al qTD actual.

Pasamos a lo más interesante.

Conductor EHCI


Comencemos con las consultas que el EHCI puede cumplir. Hay 2 tipos de solicitudes: Control - a la orden, y Bulk - a puntos finales, para el intercambio de datos, por ejemplo, la gran mayoría de las unidades flash USB (USB MassStorage) usan el tipo de transferencia de datos Bulk / Bulk / Bulk. El mouse y el teclado también usan solicitudes masivas para la transferencia de datos.

Inicialice EHCI y configure colas asíncronas y secuenciales:

// Base I/O Address PciBar bar; PciGetBar(&bar, id, 0); EhciController *hc = VMAlloc(sizeof(EhciController)); hc->capRegs = (EhciCapRegs *)(uintptr_t)bar.u.address; hc->opRegs = (EhciOpRegs *)(uintptr_t)(bar.u.address + hc->capRegs->capLength); // Read the Command register //    uint cmd = ROR(usbCmdO); // Write it back, setting bit 2 (the Reset bit) //   ,   2(Reset) // and making sure the two schedule Enable bits are clear. //  ,  2   WOR(usbCmdO, 2 | cmd & ~(CMD_ASE | CMD_PSE)); // A small delay here would be good. You don't want to read //     ,     // the register before it has a chance to actually set the bit //   ,         ROR(usbCmdO); // Now wait for the controller to clear the reset bit. //      Reset while (ROR(usbCmdO) & 2); // Again, a small delay here would be good to allow the // reset to actually become complete. //   ROR(usbCmdO); // wait for the halted bit to become set //    Halted    while (!(ROR(usbStsO) & STS_HCHALTED)); //     ,        // ,           128  hc->frameList = (u32 *)VMAlloc(1024 * sizeof(u32) + 8192 * 4); hc->frameList = (((uint)hc->frameList) / 16384) * 16384 + 16384; hc->qhPool = (EhciQH *)VMAlloc(sizeof(EhciQH) * MAX_QH + 8192 * 4); hc->tdPool = (EhciTD *)VMAlloc(sizeof(EhciTD) * MAX_TD + 8192 * 4); hc->qhPool = (((uint)hc->qhPool) / 16384) * 16384 + 16384; hc->tdPool = (((uint)hc->tdPool) / 16384) * 16384 + 16384; // Asynchronous queue setup //    EhciQH *qh = EhciAllocQH(hc); //     ,      // ,    qh->qhlp = (u32)(uintptr_t)qh | PTR_QH; //  ,  ,     qh->ch = QH_CH_H; qh->caps = 0; qh->curLink = 0; qh->nextLink = PTR_TERMINATE; qh->altLink = 0; qh->token = 0; //    for (uint i = 0; i < 5; ++i) { qh->buffer[i] = 0; qh->extBuffer[i] = 0; } hc->asyncQH = qh; // Periodic list queue setup //    qh = EhciAllocQH(hc); //     qh->qhlp = PTR_TERMINATE; qh->ch = 0; qh->caps = 0; qh->curLink = 0; qh->nextLink = PTR_TERMINATE; qh->altLink = 0; qh->token = 0; //   for (uint i = 0; i < 5; ++i) { qh->buffer[i] = 0; qh->extBuffer[i] = 0; } qh->transfer = 0; qh->qhLink.prev = &qh->qhLink; qh->qhLink.next = &qh->qhLink; hc->periodicQH = qh; //        for (uint i = 0; i < 1024; ++i) hc->frameList[i] = PTR_QH | (u32)(uintptr_t)qh; kprintf("FrameList filled. Turning off Legacy BIOS support..."); // Check extended capabilities //  BIOS Legacy support uint eecp = (RCR(hccParamsO) & HCCPARAMS_EECP_MASK) >> HCCPARAMS_EECP_SHIFT; if (eecp >= 0x40) { // Disable BIOS legacy support uint legsup = PciRead32(id, eecp + USBLEGSUP); kprintf("."); if (legsup & USBLEGSUP_HC_BIOS) { PciWrite32(id, eecp + USBLEGSUP, legsup | USBLEGSUP_HC_OS); kprintf("."); for (;;) { legsup = PciRead32(id, eecp + USBLEGSUP); kprintf("."); if (~legsup & USBLEGSUP_HC_BIOS && legsup & USBLEGSUP_HC_OS) { break; } } } } kprintf("Done\n"); // Disable interrupts //   //hc->opRegs->usbIntr = 0; MWIR(ehcibase, usbIntrO, 0); // Setup frame list //     //hc->opRegs->frameIndex = 0; WOR(frameIndexO, 0); //hc->opRegs->periodicListBase = (u32)(uintptr_t)hc->frameList; WOR(periodicListBaseO, (u32)(uintptr_t)hc->frameList); //       //hc->opRegs->asyncListAddr = (u32)(uintptr_t)hc->asyncQH; WOR(asyncListAddrO, (u32)(uintptr_t)hc->asyncQH); //    0 //hc->opRegs->ctrlDsSegment = 0; WOR(ctrlDsSegmentO, 0); // Clear status //   //hc->opRegs->usbSts = ~0; WOR(usbStsO, ~0); // Enable controller //  , 8 -,  //     //hc->opRegs->usbCmd = (8 << CMD_ITC_SHIFT) | CMD_PSE | CMD_ASE | CMD_RS; WOR(usbCmdO, (8 << CMD_ITC_SHIFT) | CMD_PSE | CMD_ASE | CMD_RS); while (ROR(usbStsO)&STS_HCHALTED); // Configure all devices to be managed by the EHCI // ,   //hc->opRegs->configFlag = 1; WOR(configFlagO, 1);\ // Probe devices //   EhciProbe(hc); 

En realidad, el código para restablecer el puerto a su estado original:

  volatile u32 *reg = &hc->opRegs->ports[port]; //    ,  100 *reg|=(1<<12)|(1<<20); Wait(100); //  ,  50  EhciPortSet(reg, PORT_RESET | (1<<12) | (1<<20) | (1<<6)); Wait(50); EhciPortClr(reg, PORT_RESET); // Wait 100ms for port to enable (TODO - what is appropriate length of time?) //  100    ,   , //  100    uint status = 0; for (uint i = 0; i < 10; ++i) { // Delay Wait(10); // Get current status //    status = *reg; // Check if device is attached to port //      if (~status & PORT_CONNECTION) break; // Acknowledge change in status //    -    if (status & (PORT_ENABLE_CHANGE | PORT_CONNECTION_CHANGE)) { EhciPortClr(reg, PORT_ENABLE_CHANGE | PORT_CONNECTION_CHANGE); continue; } // Check if device is enabled //    ,    if (status & PORT_ENABLE) break; } return status; 

Solicitud de control al dispositivo:

 static void EhciDevControl(UsbDevice *dev, UsbTransfer *t) { EhciController *hc = (EhciController *)dev->hc; UsbDevReq *req = t->req; // Determine transfer properties //    uint speed = dev->speed; uint addr = dev->addr; uint maxSize = dev->maxPacketSize; uint type = req->type; uint len = req->len; // Create queue of transfer descriptors //   TDs EhciTD *td = EhciAllocTD(hc); if (!td) return; EhciTD *head = td; EhciTD *prev = 0; // Setup packet //   uint toggle = 0; uint packetType = USB_PACKET_SETUP; uint packetSize = sizeof(UsbDevReq); EhciInitTD(td, prev, toggle, packetType, packetSize, req); prev = td; // Data in/out packets packetType = type & RT_DEV_TO_HOST ? USB_PACKET_IN : USB_PACKET_OUT; u8 *it = (u8 *)t->data; u8 *end = it + len; //EhciPrintTD(td); while (it < end) { td = EhciAllocTD(hc); if (!td) return; toggle ^= 1; packetSize = end - it; if (packetSize > maxSize) packetSize = maxSize; EhciInitTD(td, prev, toggle, packetType, packetSize, it); it += packetSize; prev = td; } // Status packet //   td = EhciAllocTD(hc); if (!td) return; toggle = 1; packetType = type & RT_DEV_TO_HOST ? USB_PACKET_OUT : USB_PACKET_IN; EhciInitTD(td, prev, toggle, packetType, 0, 0); // Initialize queue head //   : EhciQH *qh = EhciAllocQH(hc); EhciInitQH(qh, t, head, dev->parent, false, speed, addr, 0, maxSize); // Wait until queue has been processed //       EhciInsertAsyncQH(hc->asyncQH, qh); EhciWaitForQH(hc, qh); } 

Código de procesamiento de cola:

  if (qh->token & TD_TOK_HALTED) { t->success = false; t->complete = true; } else if (qh->nextLink & PTR_TERMINATE) if (~qh->token & TD_TOK_ACTIVE) { if (qh->token & TD_TOK_DATABUFFER) kprintf(" Data Buffer Error\n"); if (qh->token & TD_TOK_BABBLE) kprintf(" Babble Detected\n"); if (qh->token & TD_TOK_XACT) kprintf(" Transaction Error\n"); if (qh->token & TD_TOK_MMF) kprintf(" Missed Micro-Frame\n"); t->success = true; t->complete = true; } if (t->complete) .... 

Y ahora la solicitud de punto final (solicitud masiva)

 static void EhciDevIntr(UsbDevice *dev, UsbTransfer *t) { EhciController *hc = (EhciController *)dev->hc; // Determine transfer properties //    uint speed = dev->speed; uint addr = dev->addr; uint maxSize = t->endp->desc->maxPacketSize; uint endp = t->endp->desc->addr & 0xf; EhciTD *td = EhciAllocTD(hc); if (!td) { t->success = false; t->complete = true; return; } EhciTD *head = td; EhciTD *prev = 0; // Data in/out packets uint toggle = t->endp->toggle; uint packetType = t->endp->desc->addr & 0x80 ? USB_PACKET_IN : USB_PACKET_OUT; uint packetSize = t->len; EhciInitTD(td, prev, toggle, packetType, packetSize, t->data); // Initialize queue head //    EhciQH *qh = EhciAllocQH(hc); EhciInitQH(qh, t, head, dev->parent, true, speed, addr, endp, maxSize); //printQh(qh); // Schedule queue //    EhciInsertPeriodicQH(hc->periodicQH, qh); } 

Creo que el tema es bastante interesante, en Internet en ruso casi no hay documentos, descripciones y artículos sobre este tema, y ​​si lo hay, es muy borroso. Si el tema de trabajar con el desarrollo de hardware y sistema operativo es interesante, entonces hay mucho que contar.

Muelles: especificación

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


All Articles