EHCI人为俄语

图片

引言


我欢迎大家 今天,我想分享我的经验,但我认为,乍一看,我仍然清楚地解释了USB 2.0主机控制器的简单标准。

最初,您可以想象一个USB 2.0端口只有4个引脚,其中两个仅用于传输数据(例如,一个COM端口),但实际上并非如此,甚至完全相反。 原则上,USB控制器不允许我们像通过常规COM端口那样传输数据。 EHCI是一个相当复杂的标准,它允许以相反的方向从软件到设备本身进行可靠,快速的数据传输。

例如,如果您没有足够的驱动程序编写技能和硬件的阅读文档,您可能会发现本文很有用。 一个简单的例子:您要为小型PC编写OS,以使某些Windows或其他Linux发行版本不下载硬件,并且将其所有功能专用于您自己的目的。

什么是EHCI?


好吧,让我们开始吧。 EHCI-增强型主机控制器接口,旨在将数据和控制请求传输到USB设备,并且在另一个方向上进行传输,在99%的情况下,它是任何软件和物理设备之间的链接。 EHCI用作PCI设备,因此使用MMIO(内存映射的IO)来控制控制器(是的,我知道有些PCI设备使用端口,但是这里我概括了所有内容)。 英特尔的文档仅描述了操作原理,而对于至少以伪代码编写的所有算法都没有任何提示。 EHCI有两种类型的MMIO寄存器:功能和操作。 前者用来获得控制器的特性,而后者用来控制它。 实际上,我将附上软件与EHCI控制器之间连接的本质:

图片

每个EHCI控制器都有几个端口,每个端口都可以连接到任何USB设备。 另外,请注意,EHCI是UHCI的改进版本,它也是英特尔几年前开发的。 为了向后兼容,任何版本低于EHCI的UHCI / OHCI控制器都将作为EHCI的伴侣。 例如,您有一个可以在USB 1.1上使用的USB键盘(到目前为止,到目前为止,大多数键盘都是这样的)(请注意,USB 1.1的最大速度为每秒12兆位,而FullSpeed USB 2.0具有带宽最高可达480 Mbps),并且您有一台带有USB 2.0端口的计算机,将键盘连接到计算机时,EHCI主机控制器可以与USB 1.1一起使用。 下图显示了该模型:

图片

另外,将来,我想立即警告您,由于这种荒唐的情况,您的驱动程序可能无法正常工作:您先初始化UHCI,然后初始化EHCI,同时添加两个相同的设备,将端口所有者控制位设置为端口寄存器,然后由于EHCI自动将端口拖到自身上,并且UHCI上的端口停止响应,因此UHCI停止工作,需要对此情况进行监视。

另外,让我们看一下显示EHCI体系结构本身的图:

图片

右边写的是关于队列的信息,稍后再写。

EHCI控制器寄存器


首先,我想再次澄清一下,通过这些寄存器,您将控制您的设备,因此它们非常重要-没有它们,EHCI编程是不可能的。

首先,您需要获取提供给该控制器的MMIO地址,偏移量+ 0x10将是我们等待已久的寄存器的地址。 有一件事情:首先,能力寄存器进入,并且只有在它们之后-操作,因此,偏移量为0(相对于EHCI MMIO的起始地址在偏移量0x10处接收到的先前地址),只有一个字节—能力寄存器的长度。

能力寄存器


HCIVERSION寄存器位于偏移量2 -该HC的修订版号,占2个字节,并包含修订版的BCD版本(可以在Wikipedia上找到什么BCD)。
HCSPARAMS寄存器位于偏移量+4 ,其大小为2个字,它包含器件的结构参数,其位如下所示:

  • 位16-端口指示器-已连接的USB设备的可用LED。
  • 位15:12-分配给该控制器的配套控制器的编号
  • 位11:8-随播控制器上的端口数
  • 位7-端口路由规则-显示这些端口如何映射到配套端口
  • 位4-端口电源控制-指示是否需要打开每个端口的电源,0-自动提供电源
  • 位3:0-此控制器的端口数。
  • HCCPARAMS寄存器位于偏移量+8处-它显示兼容性参数,其位表示以下内容:
  • 位2-异步队列可用性,
  • 位1-定期(顺序)队列可用性
  • 位0-64位兼容性

操作寄存器


在偏移0处, USBCMD寄存器是控制器的命令寄存器,其位表示以下含义:

  • 位23:16-中断阈值控制-显示一个常规帧将使用多少个微帧。 越大,速度越快,但如果大于8,则微帧将以与8相同的速度处理。
  • 位6-异步队列中的每个事务之后中断,
  • 位5-是使用的异步队列
  • 第4位-顺序队列的使用,
  • 位3:2-FrameList'a的大小(稍后会详细介绍)。 0表示1024个元素,1-512,2-256,3-保留
  • 位1 —设置为重置主机控制器。
  • 位0-运行/停止

接下来,在偏移量+4处,有一个USBSTS寄存器-主机控制器的状态,

  • 位15指示是否正在使用异步队列。
  • 位14指示是否正在使用顺序队列,
  • 位13-指示已检测到空异步队列,
  • 位12设置为1,如果在处理事务时发生错误,则主机控制器将停止所有队列。
  • 位4设置为1,如果发生严重错误,主机控制器将停止所有队列。
  • Bit 3 FrameList(寄存器)翻转-当主机控制器处理整个frameList时,设置为1。
  • 位1-USB错误中断-是否生成错误中断?
  • 位0-USB中断-如果在TD中安装了IOC,则在成功进行事务处理后置位

不累吗 您可以给自己倒一个坚固的海鸥并带上肝脏,我们才刚刚起步!

在偏移+8处,有一个USBINTR寄存器-中断使能寄存器
为了长时间不写入,甚至更长时间不写入,可以在规范中找到该寄存器的位值,其链接将留在下面。 在这里我只写0,因为 我绝对不希望编写处理程序,映射中断等,因此我认为这几乎是毫无意义的。

在偏移量+12(0x0C)处,位于FRINDEX寄存器中,当前帧号仅位于其中,我要注意的是,最后4位显示微帧号,在高28位中显示帧号(该值不一定小于frameList的大小)但是,如果您需要索引,最好带0x3FF(或0x1FF等)的掩码。

CTRLDSSEGMENT寄存器的偏移量为+ 0x10;它向主机控制器显示了帧表地址的最高32位。

PERIODICLISTBASE寄存器的偏移量为+ 0x14,可以将帧表的低32位放入其中,请注意,该地址应与内存页的大小(4096)对齐。

ASYNCLISTADDR寄存器的偏移量为+ 0x18,您可以将异步队列的地址放入其中,请注意,它必须在32字节的边界处对齐,而它必须在物理内存的前4 GB中。

CONFIGFLAG寄存器指示是否配置了器件。 完成设备设置后,必须将位0设置为0,偏移量为+ 0x40。

让我们继续进行端口寄存器。 每个端口都有自己的命令状态寄存器,每个端口寄存器的偏移量为+ 0x44 +(PortNumber-1)* 4 ,其位表示以下含义:

  • 位12-端口电源,1-提供电源,0-否。
  • 位8-端口静止-设置为复位设备。
  • 位3-端口启用/禁用更改-更改端口的“包含”状态时设置。
  • 位2-端口打开/关闭。
  • 位1-更改连接状态,例如,如果您连接或断开了USB设备,则设置为1。
  • 位0-连接状态,1-已连接,0-否。

现在让我们继续汁液本身。

数据传输和查询结构


组织用于处理请求的结构包括队列和传输描述符(TD)。

目前,我们仅考虑3种结构。

顺序表


顺序(周期性,周期性)列表的组织方式如下:

图片

如您在图中所看到的,处理开始于从工作表框架中获取所需的框架,其每个元素占用4个字节,并具有以下结构:

图片

如图所示,队列地址/描述符传输在32个字节的边界对齐,位0表示主机控制器将不处理此元素,位3:1表示主机控制器将处理的类型:0-等时TD (iTD),第1转,第2和第3条,我将不予考虑。

异步队列


仅当顺序帧为空或主机控制器已处理整个序列表时,主机控制器才处理此队列。

异步队列是指向包含其他需要处理的队列的队列的指针。 方案:

图片

qTD(队列元素传输描述符)


该TD具有以下结构:

图片

下一个qTD指针 -指向要继续处理的队列的指针(用于水平执行),位0下一个qTD指针指示没有其他队列。
qTD令牌 -TD令牌,显示数据传输参数:

  • 第31位-数据切换(稍后会详细介绍)
  • 位30:16-要传输的数据量,在事务完成之后,其值将减少传输的数据量。
  • 位15-IOC-中断完成-在描述符处理完成后引起中断。
  • 位14:12显示了当前缓冲区的交换编号,从该缓冲区交换数据,稍后将对此进行更多说明。
  • 位11:10-允许的错误数。 下表显示错误计数减少的时间:

    图片

    脚注1-检测到Babble或Stall会自动停止队列开头的执行。 脚注3-主机存在数据缓冲区错误。 他们不考虑设备重试。
  • 9:8-PID代码-令牌类型:0-输入令牌(从主机到设备),1-输出令牌(从设备到主机),2-“ SETUP”令牌
  • 位7:0指示TD状态:
    位7表示TD处于活动状态(即,主机控制器处理该TD)
    位6-暂停-表示已发生错误,并且TD执行已停止。
    位4-已检测到Babble-我们发送到设备的数据量(或每转一圈)少于我们传输的数据量,例如,该设备向我们发送了100字节的数据,而我们仅读取了50个字节,然后又读取了50个字节如果该位设置为1,还将设置Halted位。
    位3-交易错误-交易期间发生错误。

qTD缓冲区页面指针列表 -5个缓冲区中的任何一个。 它包含一个链接,指向要在内存中进行交易的位置(将数据发送到设备/从设备接收数据),缓冲区中除第一个缓冲区外的所有地址都应与页面的大小(4096字节)对齐。

线头


队列头具有以下结构:

图片

队列头水平链接指针 -指向下一个队列的指针,位2:1根据队列类型具有以下值:

图片

端点功能/特征 -队列特征:

  • 位26:16包含用于传输的最大数据包大小
  • 位14:数据切换控制-显示主机控制器应在何处获取初始数据切换值,0-忽略qTD中的DT位,将DT位保存为队列头。
  • 位13:12-传输速率特征: 图片
  • 位11:8-向其发出请求的端点的编号
  • 位6:0-设备地址

端点功能:队列头DWord 2-上一个双字的延续:

  • 位29:23-集线器编号
  • 位22:16-集线器地址

当前qTD链接指针 -指向当前qTD的指针

我们传递给最有趣的。

EHCI驱动程序


让我们从EHCI可以满足的查询开始。 有两种类型的请求:控制-一个la命令,以及批量-到端点,用于数据交换,例如,绝大多数USB闪存驱动器(USB MassStorage)使用数据传输类型Bulk / Bulk / Bulk。 鼠标和键盘还使用批量请求进行数据传输。

初始化EHCI并配置异步和顺序队列:

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

实际上,用于将端口重置为其原始状态的代码:

  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; 

对设备的控制请求:

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

队列处理代码:

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

现在是端点请求(批量请求)

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

我认为该主题非常有趣,俄语版本的互联网上几乎没有有关该主题的文档,描述和文章,如果有的话,则非常模糊。 如果涉及硬件和OS开发的主题很有趣,那么有很多事情要说。

底座: 规格

Source: https://habr.com/ru/post/zh-CN426421/


All Articles