UHCI, или самый первый USB



    Доброго времени суток, дорогой читатель! Меня просили написать про UHCI — хорошо, пишу.

    Возможно, вам пригодиться эта статья, если, к примеру, вы не имеете достаточных навыков написания драйверов и чтение документации к хардвейру. Простой пример: хотите написать свою ОС для мини-ПК, дабы какая-нибудь винда или очередной дистрибутив линукса не загружали железо, и вы использовали всю его мощь исключительно в своих целях.

    Что такое UHCI?


    Думаю, чтобы еще раз не распыляться на тему что и зачем, просто оставлю ссылку на мою предыдущую статью про EHCI. Тык сюда
    UHCI — Universal Host Controller Interface, работает как PCI-устройство, но, в отличии от EHCI использует порты заместо MMIO(Memory-Mapped-IO).



    Термины, которые будут использованы далее


    • USB Driver (USBD) — сам USB драйвер
    • HC(Host Controller) — хост-контроллер, или же просто наш UHCI
    • Host Controller Driver (HCD) — драйвер, который связывает железо и USBD
    • USB Device — само USB-устройство

    Типы передачи данных


    Isochronous — изосинхронная передача, которая имеет заданную частоту передачи данных. Может быть использована, к примеру, для USB-микрофонов и т.п.

    Interrupt — Небольшие, спонтанные передачи данных с устройства. Тип передачи прерывания поддерживает устройства, которые требуют предсказуемого интервала обслуживания, но не обязательно обеспечивают предсказуемый поток данных. Обычно используются для таких устройств, как клавиатуры и указательные устройства, которые могут не выдавать данные в течение длительных периодов времени, но требуют быстрого ответа, когда у них есть данные для отправки.

    Control — Тип передачи информации о состоянии устройства, состоянии и конфигурации. Тип передачи Control используется для обеспечения канала управления с устройств Host to USB. Control-передачи всегда состоят из фазы настройки и нуля или более фаз данных, за которыми следует фаза состояния. Крайне важно, чтобы передача управления в заданную конечную точку обрабатывалась в режиме FIFO. Если управление передается на одну и ту же конечную точку, чередование может привести к непредсказуемому поведению.

    Bulk — тип передачи массивов данных. Используется, к примеру в MassStorage-устройствах.



    Вот так выглядит распределение 1мс времени — обработка одного фрейма.

    Распределение времени


    Контроллер хоста поддерживает доставку данных в реальном времени, генерируя пакет Start Of Frame (SOF) каждые 1 мс. SOF-пакет генерируется, когда истекает счетчик SOF в хост-контроллере (рис. 3). Контроллер хоста инициализирует счетчик SOF для времени кадра 1 мс. Могут быть внесены небольшие изменения в это значение (и, следовательно, период времени кадра) путем программирования регистра изменения SOF. Эта функция позволяет внести незначительные изменения в период времени кадра, если это необходимо, для поддержания синхронизации в реальном времени во всей системе USB.

    Контроллер хоста включает в себя номер кадра в каждом SOF-пакете. Этот номер кадра однозначно определяет период кадра в реальном времени. Условие окончания кадра (EOF) возникает в конце временного интервала 1 мс, когда хост-контроллер начинает следующее время кадра, генерируя еще один SOF-пакет с соответствующим номером кадра. В течение периода кадра данные передаются в виде пакетов информации. Период времени кадра строго соблюдается хост-контроллером, а пакеты данных в текущем кадре не могут выходить за пределы EOF (см. Главу 11 в спецификации USB). Контроллер хоста поддерживает синхронизацию передачи данных между кадрами в реальном времени, привязывая номер кадра к выполнению конкретной записи в списке кадров. Счетчик кадров хост-контроллера генерирует номер кадра (11-битное значение) и включает его в каждый пакет SOF. Счетчик программируется через регистры и увеличивается каждый период кадра. Контроллер хоста использует младшие 10 бит номера кадра в качестве индекса в списке кадров с 1024 фреймами, который хранится в системной памяти. Таким образом, поскольку счетчик кадров управляет выбором записи из списка кадров, хост-контроллер обрабатывает каждую запись в списке в заданный период кадра. Контроллер хоста увеличивается до следующей записи в списке кадров для каждого нового кадра. Это гарантирует, что изохронные передачи выполняются в определенном кадре.

    Рисунок 3:



    UHCI структуры


    Тут всё точно так же, как и с EHCI. Пример запросов к HC:



    Настройка и доступ к UHCI


    И так, как я уже и сказал ранее, UHCI работает через порты, значит от PCI нам надо узнать базу регистров UHCI.



    По смещению 0x20 лежит 4 байта — IO Base. Относительно IO Base мы можем воспользоваться следующими регистрами:



    Регистры UHCI


    • USBCMD — регистр для управления HC'ом. Биты:
      • Бит 6 — флаг того, что устройство сконфигурировано и инициализировано успешно.
      • Бит 1 — HC Reset. Устанавливается для сброса HC'а.
      • Бит 0 — Run/Stop. Отображает состояние HC. 1 — работает, 0 — нет.
    • USBSTS — Регистр статуса. Биты:
      • Бит 5 — HC Halted. Произошла ошибка, либо же контроллер успешно выполнил HC Reset.
      • Бит 4 — Host Controller Process Error. Бит устанавливается в 1 когда произошла критическая ошибка и HC не может продолжить выполнение очередей и TD.
      • Бит 3 — Host System Error. Ошибка PCI.
      • Бит 1 — Error Interrupt. Показывает то, что произошла ошибка и HC сгенерировал прерывание.
      • Бит 0 — Interrupt. Показывает, что HC сгенерировал прерывание.
    • USBINTR — Регистр настройки прерываний. Биты:
      • Бит 2 — IOC — Interrupt on complete — генерирует прерывание при завершении транзакции.
    • FRNUM — Номер текущего фрейма(Брать его & 0x3FF для правильного значения).
    • FLBASEADD — Frame List Base Address — адрес списка фреймов.
    • PORTSC — Port status and control — регистр статуса и управления портом. Биты:
      • Бит 9 — Port Reset — 1- порт ресетиться.
      • Бит 8 — показывает, что к порту подключено Low-speed устройство
      • Бит 3 — показывает, что состояние включенности порта изменено
      • Бит 2 — показывает, включен ли порт
      • Бит 1 — показывает, что изменено состояние подключенности устройства к порту
      • Бит 0 — показывает, что устройство подключено к порту.

    Структуры


    Frame List Pointer




    Transfer Descrptor




    TD CONTROL AND STATUS
    . Биты:
    • Биты 28-27 — счетчик ошибок, аналогично EHCI.
      • Бит 26 — 1=Low-speed устройство, 0=Full-speed устройство.
      • Бит 25 — 1=изосинхроный TD
      • Бит 24 — IOC
      • Биты 23-16 — статус:
      • Бит 23 — Показывает то, что это активный TD
      • Бит 22 — Stalled
      • Бит 21 — Data Buffer Error
      • Бит 20 — Babble Detected
      • Бит 19 — NAK
    • Биты 10-0: количество байт, переданных хост-контроллером.

    TD Token

    • Биты 31:21 — Max Packet Len, аналогично EHCI
    • Бит 19 — Data Toggle, аналогично EHCI
    • Биты 18:15 — Номер конечной точки
    • Биты 18:14 — адрес устройства
    • Биты 7:0 — PID. In=0x69, Out = 0xE1, Setup=0x2D

    Queue Head




    Код


    Инициализация и настройка HC:

    	PciBar bar;
    	PciGetBar(&bar, id, 4);
    	if (~bar.flags & PCI_BAR_IO)
    	{
    		// Only Port I/O supported
    		return;
    	}
    	unsigned int ioAddr = bar.u.port;
    	UhciController *hc = VMAlloc(sizeof(UhciController));
    	hc->ioAddr = ioAddr;
    	hc->frameList = VMAlloc(1024 * sizeof(u32) + 8292);
    	hc->frameList = ((int)hc->frameList / 4096) * 4096 + 4096;
    	hc->qhPool = (UhciQH *)VMAlloc(sizeof(UhciQH) * MAX_QH + 8292);
    	hc->qhPool = ((int)hc->qhPool / 4096) * 4096 + 4096;
    	hc->tdPool = (UhciTD *)VMAlloc(sizeof(UhciTD) * MAX_TD + 8292);
    	hc->tdPool = ((int)hc->tdPool / 4096) * 4096 + 4096;
    
    	memset(hc->qhPool, 0, sizeof(UhciQH) * MAX_QH);
    	memset(hc->tdPool, 0, sizeof(UhciTD) * MAX_TD);
    	memset(hc->frameList, 0, 4 * 1024);
    	// Frame list setup
    	UhciQH *qh = UhciAllocQH(hc);
    	qh->head = TD_PTR_TERMINATE;
    	qh->element = TD_PTR_TERMINATE;
    	qh->transfer = 0;
    	qh->qhLink.prev = &qh->qhLink;
    	qh->qhLink.next = &qh->qhLink;
    	hc->asyncQH = qh;
    	for (uint i = 0; i < 1024; ++i)
    		hc->frameList[i] = 2 | (u32)(uintptr_t)qh;
    	IoWrite16(hc->ioAddr + REG_INTR, 0);
    	IoWrite16(hc->ioAddr + REG_CMD, IoRead16(hc->ioAddr + REG_CMD)&(~1));
    	unsigned short cfg = PciRead16(id, 4);
    	PciWrite16(id, 4, cfg & (~1));
    	PciWrite16(id, 0x20, (short)-1);
    	unsigned short size = ~(PciRead16(id, 0x20)&(~3)) + 1;
    	PciWrite16(id, 0x20, hc->ioAddr);
    	PciWrite16(id, 4, cfg | 5);
    
    	// Disable Legacy Support
    	IoWrite16(hc->ioAddr + REG_LEGSUP, 0x8f00);
    
    	// Disable interrupts
    	IoWrite16(hc->ioAddr + REG_INTR, 0);
    
    	// Assign frame list
    	IoWrite16(hc->ioAddr + REG_FRNUM, 0);
    	IoWrite32(hc->ioAddr + REG_FRBASEADD, (int)hc->frameList);
    	IoWrite16(hc->ioAddr + REG_SOFMOD, 0x40);
    
    	// Clear status
    	IoWrite16(hc->ioAddr + REG_STS, 0xffff);
    
    	// Enable controller
    	IoWrite16(hc->ioAddr + REG_CMD, 0x1);
    
    	// Probe devices
    	UhciProbe(hc, size);
    

    Запросы к конечным точкам и управляющие запросы:

    // ------------------------------------------------------------------------------------------------
    static void UhciDevControl(UsbDevice *dev, UsbTransfer *t)
    {
    	UhciController *hc = (UhciController *)dev->hc;
    	UsbDevReq *req = t->req;
    
    	// Determine transfer properties
    	uint speed = dev->speed;
    	uint addr = dev->addr;
    	uint endp = 0;
    	uint maxSize = dev->maxPacketSize;
    	uint type = req->type;
    	uint len = req->len;
    
    	// Create queue of transfer descriptors
    	UhciTD *td = UhciAllocTD(hc);
    	if (!td)
    	{
    		return;
    	}
    
    	UhciTD *head = td;
    	UhciTD *prev = 0;
    
    	// Setup packet
    	uint toggle = 0;
    	uint packetType = TD_PACKET_SETUP;
    	uint packetSize = sizeof(UsbDevReq);
    	UhciInitTD(td, prev, speed, addr, endp, toggle, packetType, packetSize, req);
    	prev = td;
    
    	// Data in/out packets
    	packetType = type & RT_DEV_TO_HOST ? TD_PACKET_IN : TD_PACKET_OUT;
    
    	u8 *it = (u8 *)t->data;
    	u8 *end = it + len;
    	while (it < end)
    	{
    		td = UhciAllocTD(hc);
    		if (!td)
    		{
    			return;
    		}
    
    		toggle ^= 1;
    		packetSize = end - it;
    		if (packetSize > maxSize)
    		{
    			packetSize = maxSize;
    		}
    
    		UhciInitTD(td, prev, speed, addr, endp, toggle, packetType, packetSize, it);
    
    		it += packetSize;
    		prev = td;
    	}
    
    	// Status packet
    	td = UhciAllocTD(hc);
    	if (!td)
    	{
    		return;
    	}
    
    	toggle = 1;
    	packetType = type & RT_DEV_TO_HOST ? TD_PACKET_OUT : TD_PACKET_IN;
    	UhciInitTD(td, prev, speed, addr, endp, toggle, packetType, 0, 0);
    
    	// Initialize queue head
    	UhciQH *qh = UhciAllocQH(hc);
    	UhciInitQH(qh, t, head);
    
    	// Wait until queue has been processed
    	UhciInsertQH(hc, qh);
    	UhciWaitForQH(hc, qh);
    }
    
    // ------------------------------------------------------------------------------------------------
    static void UhciDevIntr(UsbDevice *dev, UsbTransfer *t)
    {
    	UhciController *hc = (UhciController *)dev->hc;
    
    	// Determine transfer properties
    	uint speed = dev->speed;
    	uint addr = dev->addr;
    	uint endp = t->endp->desc->addr & 0xf;
    
    	// Create queue of transfer descriptors
    	UhciTD *td = UhciAllocTD(hc);
    	if (!td)
    	{
    		t->success = false;
    		t->complete = true;
    		return;
    	}
    
    	UhciTD *head = td;
    	UhciTD *prev = 0;
    
    	// Data in/out packets
    	uint toggle = t->endp->toggle;
    	uint packetType = TD_PACKET_IN;
    	//Here for compiler, on some last expression hadn't worked
    	if (t->endp->desc->addr & 0x80)
    		packetType = TD_PACKET_IN;
    	else
    		packetType = TD_PACKET_OUT;
    	uint packetSize = t->len;
    
    	UhciInitTD(td, prev, speed, addr, endp, toggle, packetType, packetSize, t->data);
    
    	// Initialize queue head
    	UhciQH *qh = UhciAllocQH(hc);
    	UhciInitQH(qh, t, head);
    
    	// Schedule queue
    	UhciInsertQH(hc, qh);
    	if(t->w)
    		UhciWaitForQH(hc, qh);
    }
    
    • +15
    • 8,1k
    • 5
    Поделиться публикацией

    Комментарии 5

      +4
      hc->frameList = VMAlloc(1024 * sizeof(u32) + 8292);
      hc->frameList = ((int)hc->frameList / 4096) * 4096 + 4096;
      hc->qhPool = (UhciQH *)VMAlloc(sizeof(UhciQH) * MAX_QH + 8292);
      hc->qhPool = ((int)hc->qhPool / 4096) * 4096 + 4096;
      hc->tdPool = (UhciTD *)VMAlloc(sizeof(UhciTD) * MAX_TD + 8292);
      hc->tdPool = ((int)hc->tdPool / 4096) * 4096 + 4096;

      memset(hc->qhPool, 0, sizeof(UhciQH) * MAX_QH);
      memset(hc->tdPool, 0, sizeof(UhciTD) * MAX_TD);
      memset(hc->frameList, 0, 4 * 1024);

      Не делайте так никогда, пожалуйста.

      Если вам нужно выделить памяти с определенным выравниванием, сделайте функцию для этого, какую-нибудь VMAllocAligned(), принимающую выравнивание в качестве параметра.
      Сейчас у вас в коде куча магических констант (8292 выглядит как опечатка в 8192, например), памяти выделено больше чем нужно (и потому guard page при выходе за границы может не сработать), вызов VMFree на любой из выделенных подобным образом указателей освободит что-то непонятное в большинстве случаев, а memset зануляет не весь выделенный буфер.
        +1
        Соглашусь. Кроме того, код в статье, как бы это сказать, не вполне «оригинальный». Оригинал здесь: github.com/pdoane/osdev/blob/master/usb/uhci.c
        Понятно, что «мы все учились понемногу...», но источники ведь тоже нужно не забывать указывать.
          0
          Эти драйвера мы писали с другом 6 лет назад, так что код вполне оригинальный.
          +2
          hc->qhPool = ((int)hc->qhPool / 4096) * 4096 + 4096;

          Где-то заплакал один uintptr_t

          Впрочем в оригинале никаких выравниваний нет, т.к. VMAlloc возвращает адрес уже выравненный на страницу.
          –1
          Спасибо, интересная статья.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое