EHCI humanamente em russo

imagem

1. Introdução


Congratulo-me com todos. Hoje, quero compartilhar minha experiência e ainda, na minha opinião, explicar claramente sobre tal, à primeira vista, um padrão simples para o controlador host USB 2.0.

Inicialmente, você pode imaginar que uma porta USB 2.0 tem apenas 4 pinos, dois dos quais simplesmente transmitem dados (como, por exemplo, uma porta COM), mas, na verdade, nem tudo é assim, e até o contrário. O controlador USB, em princípio, não nos permite transferir dados como através de uma porta COM comum. O EHCI é um padrão bastante complexo que permite a transferência rápida e confiável de dados do software para o próprio dispositivo e na direção oposta.

Você pode achar este artigo útil se, por exemplo, você não tiver habilidades suficientes de escrita para drivers e leitura de documentação para um hardware. Um exemplo simples: você deseja gravar seu sistema operacional para um mini PC, para que algumas distribuições Windows ou Linux não façam o download de hardware e você use toda a sua energia exclusivamente para seus próprios fins.

O que é EHCI?


Bem, vamos começar. O EHCI - Enhanced Host Controller Interface, foi projetado para transferir dados e controlar solicitações para dispositivos USB, e em outra direção, e em 99% dos casos, é um link entre qualquer software e um dispositivo físico. O EHCI funciona como um dispositivo PCI e, portanto, usa o MMIO (IO mapeado na memória) para controlar o controlador (sim, eu sei que alguns dispositivos PCI usam portas, mas aqui generalizei tudo). A documentação da Intel descreve apenas o princípio de operação e não há dicas para todos os algoritmos escritos pelo menos em pseudo-código. O EHCI possui 2 tipos de registradores MMIO: Capacidade e Operacional. Os primeiros servem para obter as características do controlador, enquanto os últimos servem para controlá-lo. Na verdade, anexarei a essência da conexão entre o software e o controlador EHCI:

imagem

Cada controlador EHCI possui várias portas, cada uma das quais pode ser conectada a qualquer dispositivo USB. Além disso, observe que o EHCI é uma versão aprimorada do UHCI, que também foi desenvolvida pela Intel alguns anos antes. Para compatibilidade com versões anteriores, qualquer controlador UHCI / OHCI que tenha uma versão menor que o EHCI será um companheiro para o EHCI. Por exemplo, você tem um teclado USB (e a maioria dos teclados do ano até agora tem sido assim) que funciona no USB 1.1 (observe que a velocidade máxima do USB 1.1 é de 12 megabits por segundo, e o FullSpeed ​​USB 2.0 tem largura de banda 480 Mbps) e você possui um computador com uma porta USB 2.0. Ao conectar o teclado ao computador, o controlador host EHCI funcionará de alguma forma com o USB 1.1. Este modelo é mostrado no diagrama a seguir:

imagem

Além disso, no futuro, quero avisar imediatamente que seu driver pode não funcionar corretamente devido a uma situação tão absurda: você inicializou o UHCI e o EHCI, ao adicionar dois dispositivos idênticos, defina o bit Port Owner Control como o registro da porta e, em seguida, O UHCI parou de funcionar, devido ao fato de o EHCI arrastar automaticamente a porta para si mesma e a porta no UHCI parar de responder, essa situação precisa ser monitorada.

Além disso, vejamos um diagrama mostrando a arquitetura EHCI:

imagem

À direita está escrito sobre a fila - sobre eles um pouco mais tarde.

Registradores do controlador EHCI


Para começar, quero esclarecer mais uma vez que através desses registros você controlará seu dispositivo, portanto eles são muito importantes - e sem eles a programação EHCI é impossível.

Primeiro, você precisa obter o endereço MMIO que é dado a este controlador, no deslocamento + 0x10 será o endereço dos nossos tão aguardados registradores. Há uma coisa: primeiro, os registros do Capability vão, e somente depois deles - Operacional, portanto, no deslocamento 0 (do endereço anterior, que recebemos no deslocamento 0x10 em relação ao início do MMIO do nosso EHCI), há um byte - o comprimento dos registros do Capability.

Registros de capacidade


No deslocamento 2, o registro HCIVERSION está localizado - o número de revisão deste HC, que ocupa 2 bytes e contém a versão BCD da revisão (o que o BCD pode ser encontrado na Wikipedia).
No deslocamento +4, o registro HCSPARAMS está localizado , seu tamanho é 2 palavras, contém os parâmetros estruturais do dispositivo e seus bits mostram o seguinte:

  • Bit 16 - Indicadores de porta - LEDs disponíveis para dispositivos USB conectados.
  • Bits 15:12 - o número do controlador associado que está atribuído a este controlador
  • Bits 11: 8 - o número de portas no controlador complementar
  • Bit 7 - Port Routing Rules - mostra como essas portas são mapeadas para portas complementares
  • Bit 4 - Port Power Control - indica se é necessário ligar a energia de cada porta, 0 - a energia é fornecida automaticamente
  • Bits 3: 0 - o número de portas para este controlador.
  • No deslocamento +8 está o registro HCCPARAMS - mostra parâmetros de compatibilidade, seus bits significam o seguinte:
  • Bit 2 - disponibilidade de fila assíncrona,
  • Bit 1 - disponibilidade da fila periódica (sequencial)
  • Compatibilidade com bits 0 - 64 bits

Registros de operação


No deslocamento 0, o registro USBCMD é o registro de comando do controlador, seus bits significam o seguinte:

  • Bits 23:16 - Controle de limite de interrupção - mostra quantos micro-quadros serão usados ​​para um quadro regular. Quanto maior, mais rápido, mas se for maior que 8, os micro-frames serão processados ​​na mesma velocidade que para 8.
  • Bit 6 - interrompa após cada transação na fila assíncrona,
  • Bit 5 - é a fila assíncrona usada
  • Bit 4 - uso de fila seqüencial,
  • Bits 3: 2 - o tamanho de FrameList'a (mais sobre isso mais tarde). 0 significa 1024 elementos, 1 - 512, 2 - 256, 3 - reservados
  • Bit 1 - Configure para redefinir o controlador host.
  • Bit 0 - Executar / Parar
.
Em seguida, no deslocamento +4, há o registro USBSTS - o status do controlador host,

  • O bit 15 indica se uma fila assíncrona está sendo usada.
  • O bit 14 indica se uma fila seqüencial está sendo usada,
  • Bit 13 - indica que uma fila assíncrona vazia foi detectada,
  • O bit 12 é definido como 1, se ocorrer um erro ao processar a transação, o controlador host interromperá todas as filas.
  • O bit 4 é definido como 1; se ocorrer um erro grave, o controlador host interromperá todas as filas.
  • Bit 3 Rollover FrameList (Register) - definido como 1 quando o controlador host processou o frameList inteiro.
  • Bit 1 - USB Error Interrupt - Eu gero uma interrupção de erro?
  • Bit 0 - USB Interrupt - definido após o processamento bem-sucedido da transação, se o IOC foi instalado no TD

Não está cansado? Você pode derramar uma gaivota forte e trazer o fígado, estamos no começo!

No deslocamento +8, há um registro USBINTR - o registro de ativação de interrupção
Para não escrever por muito tempo, e mais ainda, para você não ler por muito tempo, os valores dos bits desse registro podem ser encontrados na especificação, um link para ele será deixado abaixo. Aqui eu apenas escrevo 0, porque Não tenho absolutamente nenhum desejo de escrever manipuladores, interrupções de mapas etc., então acho que isso é quase completamente inútil.

No deslocamento +12 (0x0C), o registro FRINDEX está localizado , no qual o número do quadro atual simplesmente se encontra, e quero observar que os últimos 4 bits mostram o número do micro-quadro, nos 28 bits superiores o número do quadro (o valor também não é necessariamente menor que o tamanho do quadro Mas se você precisar de um índice, é melhor usá-lo com uma máscara de 0x3FF (ou 0x1FF, etc.).

O registro CTRLDSSEGMENT está no deslocamento + 0x10; mostra ao controlador host os 32 bits mais significativos do endereço da folha de quadros.

O registro PERIODICLISTBASE possui um deslocamento de + 0x14; você pode inserir os 32 bits inferiores da folha de quadros, observe que o endereço deve estar alinhado ao tamanho da página de memória (4096).

O registro ASYNCLISTADDR tem um deslocamento de + 0x18, você pode colocar o endereço da fila assíncrona, observe que ele deve estar alinhado no limite de 32 bytes, enquanto deve estar nos primeiros quatro gigabytes de memória física.

O registro CONFIGFLAG indica se o dispositivo está configurado. Você deve definir o bit 0 após concluir a configuração do dispositivo, pois possui um deslocamento de + 0x40.

Vamos para os registros de portas. Cada porta tem seu próprio registro de status de comando, cada registro de porta é compensado + 0x44 + (PortNumber - 1) * 4 , seus bits significam o seguinte:

  • Bit 12 - energia da porta, 1 - energia é fornecida, 0 - não.
  • O Bit 8 - Port Rest - está definido para redefinir o dispositivo.
  • Bit 3 - Port Enable / Disable Change - defina ao alterar o status da "inclusão" da porta.
  • Bit 2 - porta ligada / desligada.
  • Bit 1 - Altere o status da conexão, é definido como 1, por exemplo, se você conectou ou desconectou um dispositivo USB.
  • Bit 0 - status da conexão, 1 - conectado, 0 - não.

Agora vamos ao suco em si.

Transferência de dados e estruturas de consulta


A organização de uma estrutura para processar solicitações inclui filas e descritores de transferência (TDs).

No momento, consideraremos apenas três estruturas.

Lista sequencial


A lista sequencial (Periódica, Pereodic) está organizada da seguinte maneira:

imagem

Como você pode ver no diagrama, o processamento começa com a obtenção do quadro desejado a partir do quadro da folha, cada um de seus elementos ocupa 4 bytes e possui a seguinte estrutura:

imagem

Como você pode ver na figura, a transferência do endereço da fila / descritor está alinhada no limite de 32 bytes; o bit 0 significa que o controlador host não processará esse elemento; os bits 3: 1 indicam o tipo de processamento do controlador host: 0 - TD isossíncrono (iTD), 1 turno, 2 e 3 neste artigo, não considerarei.

Fila assíncrona


O controlador host processa essa fila apenas quando o quadro seqüencial está vazio ou o controlador host processou a lista serial inteira.

Uma fila assíncrona é um ponteiro para uma fila que contém outras filas que precisam de processamento. Esquema:

imagem

qTD (Descritor de transferência de elementos da fila)


Este TD tem a seguinte estrutura:

imagem

Próximo ponteiro qTD - um ponteiro para a continuação da fila para processamento (para execução horizontal), bit 0 O próximo ponteiro qTD indica que não há mais fila.
qTD Token - token TD, mostra os parâmetros de transferência de dados:

  • Bit 31 - Alternância de dados (mais sobre isso mais tarde)
  • Bits 30:16 - a quantidade de dados a serem transferidos, após a conclusão da transação, seu valor diminui pela quantidade de dados transferidos.
  • Bit 15 - IOC - Interrupt On Complete - causa interrupção após a conclusão do processamento do descritor.
  • Os bits 14:12 mostram o número do buffer atual para o qual os dados são trocados, mais sobre isso posteriormente.
  • Bits 11:10 - o número de erros permitido. Esta tabela mostra quando a contagem de erros diminui:

    imagem

    Nota de rodapé 1 - a detecção de Babble ou Stall interrompe automaticamente a execução do cabeçalho da fila. Nota de rodapé 3 - Erros no buffer de dados são problemas com o host. Eles não consideram novas tentativas do dispositivo.
  • 9: 8 - Código PID - tipo de token: 0 - token para a entrada (do host para o dispositivo), 1 - token para a saída (do dispositivo para o host), 2 - token “SETUP”
  • Os bits 7: 0 indicam o status do TD:
    O bit 7 indica que o TD está em um estado ativo (ou seja, o controlador host processa esse TD)
    Bit 6 - Halted - indica que ocorreu um erro e a execução do TD foi interrompida.
    Bit 4 - Babble Detected - a quantidade de dados que enviamos ao dispositivo, ou por rotação, é menor do que a que transmitimos, ou seja, por exemplo, o dispositivo nos enviou 100 bytes de dados, e lemos apenas 50 bytes e depois outros 50 O bit interrompido também será definido se esse bit estiver definido como 1.
    Bit 3 - Erro de transação - Ocorreu um erro durante a transação.

qTD Buffer Page Pointer List - qualquer um dos 5 buffers. Ele contém um link para o local em que a transação deve ser realizada na memória (enviar dados ao dispositivo / receber dados do dispositivo); todos os endereços nos buffers, exceto o primeiro, devem estar alinhados ao tamanho da página (4096 bytes).

Chefe de linha


O cabeçalho da fila tem a seguinte estrutura:

imagem

Ponteiro de link horizontal da cabeça da fila - ponteiro para a próxima fila, os bits 2: 1 têm os seguintes valores, dependendo do tipo de fila:

imagem

Recursos / características do terminal - características da fila:

  • Os bits 26:16 contêm o tamanho máximo de pacote para transmissão
  • Bit 14: Controle de alternância de dados - mostra onde o controlador host deve receber o valor inicial de alternância de dados, 0 - ignora o bit DT em qTD, salva o bit DT para o cabeçalho da fila.
  • Bit 13:12 - características da taxa de transmissão: imagem
  • Bits 11: 8 - o número do terminal no qual a solicitação é feita
  • Bits 6: 0 - endereço do dispositivo

Recursos do terminal: Cabeçalho da fila DWord 2 - continuação da palavra dupla anterior:

  • Bits 29:23 - Número do cubo
  • Bits 22:16 - Endereço do hub

Ponteiro de Link qTD atual - ponteiro para o qTD atual.

Passamos para o mais interessante.

Driver EHCI


Vamos começar com as consultas que o EHCI pode atender. Existem 2 tipos de solicitações: Controle - a la command e Bulk - para terminais, para troca de dados, por exemplo, a grande maioria das unidades flash USB (USB MassStorage) usa o tipo de transferência de dados Bulk / Bulk / Bulk. O mouse e o teclado também usam solicitações em massa para transferência de dados.

Inicialize o EHCI e configure filas assíncronas e seqüenciais:

// 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); 

Na verdade, o código para redefinir a porta para seu 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; 

Solicitação de controle para o 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 processamento da fila:

  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) .... 

E agora a solicitação de terminal (solicitação em massa)

 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); } 

Eu acho que o tópico é bastante interessante, na Internet em russo quase não há documentação, descrições e artigos sobre esse tópico, e se houver, é muito embaçado. Se o tópico de trabalhar com o desenvolvimento de hardware e sistema operacional for interessante, há muito a dizer.

Docas: Especificação

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


All Articles