Поддержка USB в KolibriOS: что внутри? Часть 2: основы работы с хост-контроллерами

  • Tutorial

Прежде, чем объяснять код поддержки хост-контроллеров, необходимо рассказать о некоторых принципах работы железа, а также об используемых структурах данных. Как я выяснила при написании текста, одна статья обо всём уровне поддержки хост-контроллеров получилась бы слишком большой, поэтому вторая часть цикла — которую вы сейчас читаете — рассказывает о том, что необходимо знать для понимания кода, а описание действий, происходящие в коде, я отложу до следующей части.

Прерывания и потоки


Хост-контроллеры оповещают софт о происходящих событиях, генерируя прерывания. Прерывание может прийти и оторвать процессор от текущей задачи в любой момент времени; это накладывает жёсткие требования на обработчик прерывания. Обработчик прерывания не может захватывать никакие блокировки — ведь вполне возможно, что прерванный код как раз завладел блокировкой и уже не сможет её освободить. Единственным исключением является вариант спинлока, запрещающий прерывания на время блокировки, но из-за глобальности эффекта спинлок стоит применять пореже и для очень коротких участков кода. На однопроцессорных конфигурациях такой вариант вырождается в пару cli/sti без собственно спинлока, на многопроцессорных внутри cli/sti остаётся обычный спинлок. Кроме того, контроллер прерываний во время обработки одного прерывания блокирует остальные с тем же или более низким приоритетом.

По этим двум причинам в KolibriOS обработчики прерываний от хост-контроллеров USB передают основную часть работы в выделенный под USB поток ядра, а сами ограничиваются сообщением хост-контроллеру «спасибо, сигнал принят». Сам USB-поток имеет наивысший приоритет, чтобы задумавшиеся пользовательские приложения не мешали обработке. Все функции вышележащих уровней, которые вызываются из уровня поддержки хост-контроллера, работают в контексте потока USB и, как следствие, вполне могут использовать примитивы синхронизации. Приятным побочным эффектом является автоматическая сериализация вызовов: ни обработчик завершения второй передачи из очереди канала, ни функция DeviceDisconnected не будут вызваны, пока не закончит работу обработчик завершения первой передачи из очереди канала, что есть логичное требование к API.

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


Структуры данных


Зависящие и не зависящие от контроллера компоненты структур
Для уровня поддержки хост-контроллеров важны следующие структуры: структура данных контроллера *_controller, структура данных канала *_pipe, структура данных неизохронной передачи *_gtd. Каждая из них состоит из двух частей: специфичной для хост-контроллера *hci_* и общей для всех контроллеров usb_*. Хост-контроллер требует выравнивания своих структур. Данные контроллера используют выравнивание на границу страницы, то есть 1000h байт. Выравнивание прочих данных разное для разных контроллеров.

В KolibriOS обе части каждой структуры располагаются в памяти последовательно. Память под обе структуры выделяется одним приёмом с учётом требуемого выравнивания. Первой в памяти идёт часть, отвечающая за общение с хост-контроллером, чтобы обеспечить выравнивание. Для адресации обеих частей используется один указатель, указывающий на границу между частями; по отрицательным смещениям находятся данные *hci_*, по неотрицательным — данные usb_*. Указатель на usb_controller постоянно размещается в регистре esi. Хэндл канала представляет собой указатель на usb_pipe; одним из полей usb_pipe является указатель на соответствующий usb_controller.

Код, выделяющий память под структуры, должен знать размеры обеих структур и требуемое выравнивание. Для *_controller используется постраничный аллокатор, автоматически гарантирующий выравнивание на границу страницы. Аллокатор вызывается кодом, ответственным за usb_controller, размер структуры *hci_controller берётся из usb_hardware_func.DataSize; как я упоминала в общем обзоре, usb_hardware_func описывает вещи, специфичные для хост-контроллера, остальному коду.
Для *_pipe и *_gtd выделять по странице на каждый экземпляр было бы крайне расточительно, а использовать общую кучу ядра для малых блоков — неудобно из-за требований выравнивания. Поэтому для них код использует аллокатор блоков фиксированного размера, который, выделив страницу, нарезает её на блоки заданного размера и отдаёт их один за другим. Если выделяемый размер кратен, например, 16 байтам, то все выделяемые блоки будут иметь адрес, кратный 16. Здесь аллокатору для каждого размера нужны отдельные данные; чтобы не включать их все в структуру usb_hardware_func, последняя содержит функции выделения/освобождения AllocPipe/FreePipe для пары структур *_pipe и AllocTD/FreeTD для пары структур *_gtd.

Хост-контроллер должен знать физические адреса всех структур, чтобы работать с ними. Адрес структуры *hci_controller заносится в ходе инициализации контроллера. Адреса структур данных неизохронных передач собраны в односвязный список с физическим адресом первого элемента внутри *hci_pipe и физическим адресом каждого следующего элемента внутри *hci_gtd.



Каналы сгруппированы в несколько списков. Внутри каждого списка есть три связи: физический адрес следующего канала для железа, виртуальные адреса следующего и предыдущего каналов для софта. Один список состоит из всех каналов для управляющих передач. Другой список состоит из всех каналов для передач массивов данных.
Списки каналов прерываний организованы в двоичное дерево так, как показано на рисунке, где кружки обозначают списки каналов прерываний, а стрелки — физические адреса следующих элементов. Хост-контроллер начинает каждую единицу времени (фрейм для UHCI и OHCI, микрофрейм для EHCI) с того, что берёт младшие n бит номера фрейма (именно фрейма, даже если это EHCI), берёт соответствующий элемент таблицы адресов, являющейся частью *hci_controller, и начинает идти по ссылкам на следующий элемент. Первый список, таким образом, будет обрабатываться один раз каждые 2n миллисекунд. Дальше пары ссылок «склеиваются»: на следующий список ведёт две ссылки так, чтобы следующий список получал внимание контроллера дважды за полный цикл по таблице адресов, один раз каждые 2n-1 миллисекунд. В конце располагается список, элементы которого обрабатываются каждую миллисекунду. Такая организация каналов прерываний позволяет реализовать каналы с интервалом обработки, выражающимся в миллисекундах степенью двойки. Спецификация USB разрешает делать реальный интервал опроса меньше запрошенного.

В EHCI единица планирования — микрофрейм, который в 8 раз меньше фрейма. Тем не менее, прогулки по спискам каналов по-прежнему руководствуются номером фрейма. Поэтому в каждом канале прерываний есть битовая маска на 8 бит, в которой каждый бит соответствует одному микрофрейму внутри фрейма, нулевое значение бита приводит к немедленному продолжению прогулки по ссылкам. В некоторых каналах таких масок даже две, не пересекающихся по единичным битам, но об этом позже.

Поддержка изохронных передач находится на стадии разработки, поэтому пока я скажу только несколько слов про аппаратную часть. В OHCI изохронные передачи адресуются аналогично остальным: в ohci_pipe есть бит, отвечающий за формат структур данных передачи, изохронные и остальные используют разный формат. В UHCI и EHCI структуры данных для изохронных каналов как таковой нет, а структуры изохронных передач вставляются в таблицу адресов наравне со структурами каналов прерываний. Чтобы контроллер мог понять, указывает ли адрес на канал или на изохронную передачу (которых на самом деле есть два разных типа), два бита адреса отводятся под тип структуры, которая по этому адресу находится. Как следствие, число n для UHCI и EHCI равно 10, но не для поддержки интервалов опроса в секунду с лишним, а для того, чтобы после обработки фрагмента изохронной передачи у софта была секунда на запрос следующего фрагмента. В OHCI n=5.

Передачи и транзакции


Хотя протоколы архитектуры USB ниже передач почти неинтересны, но есть некоторые вещи, которые о них знать всё же необходимо при реализации уровней ниже уровня драйверов.
Размер передачи по шине USB практически неограничен; чтобы одно устройство не занимало шину слишком надолго, передачи разбиваются на транзакции. За одну транзакцию передаётся очередной фрагмент данных ограниченной длины. Максимальная длина транзакции — одна из характеристик канала. Для одного этапа передачи (я напомню, что управляющие передачи состоят из двух или трёх этапов, а остальные — из одного этапа) все транзакции, кроме последней, имеют максимальный размер; последняя транзакция передаёт оставшиеся данные и может быть короче остальных.

Размер данных, которые может описать одна пара структур *_gtd, также ограничен. Если все данные не умещаются в одну *_gtd, передачу нужно разбивать на несколько частей. Места разбиения нужно выбирать так, чтобы с точки зрения устройства происходящее оставалось одной передачей, то есть размер всех частей, кроме последней, должен делиться на максимальный размер транзакции.

UHCI — хронологически первый интерфейс, созданный Intel; в UHCI упор делается на простоту аппаратной реализации. Как следствие, UHCI-контроллер ничего не знает про передачи, а одна структура uhci_gtd описывает одну транзакцию. Для больших передач это приводит к большим накладным расходам на отдельную память для всех транзакций.
В OHCI и EHCI контроллер уже умеет самостоятельно разбивать длинные передачи на транзакции, здесь ограничения слабее. В ohci_gtd есть два поля для двух страниц данных, в лучшем случае получается 2000h байт, в худшем (если данные начинаются с адреса xxxxxFFFh) — 1001h байт = 4 килобайта + 1 байт. В ehci_gtd помещаются уже пять страниц, что в худшем случае даёт ограничение 4001h байт. Если данных больше, то передачу по-прежнему нужно разбивать на несколько фрагментов.

В USB2 появились расщеплённые транзакции (split transactions). Спецификация USB2 добавила новую скорость передачи данных 480 мегабит/с (high-speed, HS), но по-прежнему поддерживает две скорости USB1, 12 мегабит/с (full-speed, FS) и 1.5 мегабит/с (low-speed, LS). На одной шине USB в каждый момент времени можно общаться только с одним устройством. В USB1 шина, управляемая одним хост-контроллером, была единой, и во время транзакции к LS-устройству она (способная на 12 мегабит/с) работала со скоростью 1.5 мегабит/с. В USB2 аналогичным образом замедлять HS-шину было бы непрактично, поэтому выделяется одна общая шина, которая всегда работает на high-speed, и несколько FS/LS-шин, к которым подключаются FS/LS-устройства. За связь между шинами отвечает хаб, к которому подключено низкоскоростное устройство; спецификация называет соответствующую часть хаба Transaction Translator (TT).

Пока хаб медленно общается с низкоскоростным устройством по низкоскоростной шине, высокоскоростная шина оказывается свободной, причём довольно надолго. Чтобы полученное время можно было использовать с толком, транзакция по HS-шине расщепляется на две: начальную (start-split transaction) и конечную (complete-split transaction).



Детали расщепления несколько различаются для периодических транзакций (передач по прерыванию и изохронных передач) и непериодических (управляющих передач и передач массивов данных). На рисунке выше показана схема происходящего внутри хаба для периодических расщеплённых транзакций. Хорошая новость: для непериодических транзакций дополнительные действия по поддержке минимальны — нужно правильно инициализировать структуру канала и при ошибке HS-шины очищать буфер хаба с данными, за остальным будет следить сам контроллер. Для периодических транзакций всё сложнее. Именно отсюда возникает вторая битовая маска в структуре канала прерываний, которую я ранее упоминала, — для каналов прерываний FS/LS-устройств первая битовая маска отвечает за микрофреймы, в которые нужно инициировать начальную расщеплённую транзакцию, вторая — за микрофреймы, в которые нужно инициировать конечную расщеплённую транзакцию. Отсюда же появляется второй тип изохронных транзакций в EHCI — структуры обычной и расщеплённой изохронных транзакций различаются.

EHCI и компаньоны




При проектировании хост-контроллера для USB2 Intel решила по возможности задействовать уже существующую базу в виде железа UHCI/OHCI и программной поддержки. В корневом хабе EHCI отсутствует Transaction Translator; вместо него каждый порт может быть подключён к контроллеру-компаньону, им может быть UHCI или OHCI. Компаньонов может быть несколько. Пока EHCI-контроллер не инициализирован, все порты подключены к компаньонам; код, умеющий программировать UHCI и OHCI, сможет работать со всеми устройствами и в такой конфигурации, естественно, на скорости USB1. После инициализации EHCI-контроллера каждому порту можно назначить владельца независимо от других. Контроллер, не являющийся владельцем, воспринимает порт в состоянии «нет устройства». Порты, на которых действительно нет устройства, а также порты с HS-устройствами назначаются контроллеру EHCI; порты с низкоскоростными устройствами назначаются контроллеру-компаньону.



Позднее Intel решила, что больше не хочет ставить UHCI рядом с EHCI. Чтобы не переделывать спецификацию и не заставлять всех переписывать драйверы, Intel не стала менять контроллер, но на пути от «настоящих» портов до контроллера поставила «виртуальный» хаб с официальным названием Rate Matching Hub (RMH), а контроллеру оставила только два порта, к одному из которых всегда подключён хаб. Назначение второго порта, к сожалению, мне выяснить не удалось. С программной точки зрения «виртуальный» хаб ничем не отличается от обычного, просто при написании своей реализации следует иметь в виду, что для доступа к устройствам на некоторых конфигурациях придётся реализовать не только поддержку EHCI, но и поддержку хабов.

Все статьи серии


Часть 1: общая схема
Часть 2: основы работы с хост-контроллерами
Часть 3: код поддержки хост-контроллеров
Часть 4: уровень поддержки каналов
Часть 5: уровень логического устройства
Часть 6: драйвер хабов
KolibriOS Project Team
74,86
Быстрая операционная система для бизнеса и хобби
Поделиться публикацией

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

    +1
    хм… я вот в прнципе понимаю, можно запрограммировать хост контроллер и выполнить какую-то операцию, например прочитать DeviceDescriptor. А вот дальше что?
    Нужны ведь драйвера устройств? Ну хотя бы некоторых классов устройств.
    Даже простой HID класс не так просто сделать — так как нужно парсить ReportDescriptor и он может быть довольно сложным — некоторые мыши посылают репорт 4 байта, некоторые 5 или 6. У клавиатур так же заморочки — переменное число светодиодов LED или вот разный набор multimedia кнопок. А еще джойстики, трекболы, тачпады, тачскрины — и все это HID. Неужели вы все это реализовали?
    А другие классы? Тот же Storage?

    И вот еще неприятная часть в USB стеке — scheduler транзакций. Когда много устройств подключены через хаб все ходят доступа к шине и как их запросы уместить во фреймы и микрофреймы наиболее оптимальным образом?

    В общем поражает объем работы, который нужно выполнить, чтобы система как-то зажила с этим…
      +3
      Spoiler alert!
      KolibriOS WebSVN: USB HID, USB Storage
      p.s. Мультимедиа-кнопки — это, как правило, «отдельное устройство» внутри «обычной» клавиатуры.
        0
        В принципе, в анонсе заявлены были:
        Клавиатуры (USB keyboard)
        Мышки (USB mouse)
        Флешки (USB flash disk / USB thumb-drive)
        Хабы (USB hub)

        В приведенном выше коде тоже кроме этого ничего нет.
          +4
          я не спорю, что работа проделана большая и героическая.
          Но вот взять например HID:

          mov edx, [esp+12]
          push ebx; save used register to be stdcall
          mov cx, word [edx+interface_descr.bInterfaceSubClass]
          ; 1b. For boot protocol, subclass must be 1 and protocol must be either 1 for
          ; a keyboard or 2 for a mouse. Check.
          cmp cx, 0x0101
          jz .keyboard
          cmp cx, 0x0201
          jz .mouse
          ; 1c. If the device is neither a keyboard nor a mouse, print a message and
          ; go to 6c.
          DEBUGF 1,'K: unknown HID device\n'
          jmp .nothing


          То есть джойстики, тачскрины и прочее HID пока не работают.

          Вот еще фрагмент:

          ; Parse the packet, comparing with the previous packet.
          ; For boot protocol, USB keyboard packet consists of the first byte
          ; with status keys that are currently pressed. The second byte should
          ; be ignored, and other 5 bytes denote keys that are currently pressed.
          push esi ebx; save used registers to be stdcall
          ; 2. Process control keys.
          ; 2a. Initialize before loop for control keys. edx = mask for control bits
          ; that were changed.
          mov ebx, [esp+20+8]
          movzx edx, byte [ebx+device_data.packet]; get state of control keys
          xor dl, byte [ebx+keyboard_data.prevpacket]; compare with previous state


          У меня есть клавиатура у которой пакет выглядит не так. Формат другой. То есть она работать не будет.
          Вся подлость USB вообще — чтобы убедиться в работоспособности системы нужно брать 50 разных моделей и тестировать.
          За это я ее (USB) и не люблю. Вроде и стандарты есть, но многие их по своему интерпретируют и получаются странные вещи.

          Поэтому я и говорю, что сделать USB — это нереально сложная задача. Героическая.
          А проекту, конечно, пожелаю удачи.
            +1
            Спасибо за пожелания. Автор USB-подсистемы проводила тестирование в течении очень длительного времени, последние полгода было открытое тестирование. Если ваша клавиатура не работает — я думаю, стоит помочь разработке хотя бы баг-репортами.
              +3
              Будет. При инициализации текущий драйвер выставляет Set_Protocol(Boot Protocol). Структура данных в Boot Protocol и Report Protocol может отличаться; если вы смотрели на пакеты, читая передачи по шине USB под управлением Windows или Linux, то вы видели структуру Report Protocol.
                0
                возможно вы правы, а я нет.
                Действительно я снифил пакеты устройств подключенных только к виндовс и линукс.
                Тогда за свои слова о неработающей клавиатуре извиняюсь.
            +5
            Вы совершенно правы, но несколько опережаете события. К сожалению, уместить всё в одну статью не представляется возможным из-за размера, поэтому о всех этих вещах я буду рассказывать в следующих частях.

            Про планировщик транзакций будет раньше остального — это уровень поддержки каналов, так что будет либо в следующей статье вместе с кодом поддержки хост-контроллеров, либо через одну. Если вкратце — непериодические транзакции планирует сам контроллер в round-robin стиле и как-либо проконтролировать это невозможно. Для периодических транзакций планировщик действительно есть.

            Из классов, не считая хабов, сейчас поддерживаются основные варианты HID и Storage. Про HID будет отдельная статья, если вкратце:
            * для базовой функциональности мышек и клавиатур есть Boot Protocol, при использовании которого парсить Report Descriptor не нужно; прямо сейчас на svn лежит именно этот вариант, поддержку Report Protocol я написала наполовину и никуда ещё не коммитила, следите за обновлениями;
            * HID класс неоднороден, поддержка мышек не зависит от поддержки джойстиков. В Report Descriptor каждое поле данных передачи снабжено комментарием Usage, описывающем назначение поля. Каждый подкласс устройств интерпретирует только «свои» поля, для поддержки мыши не нужно ничего знать о джойстике.
            +2
            Да, это поражает!
            А я-то думал, что USB достаточно высокоуровневый протокол и железками обеспечивается. А, оказывается…
            Спасибо за статьи!

            P.S. Неужели за это время не могли ничего более щадящего с точки зрения программирования придумать?
              +7
              Знаете, в университете я учился на инженера электроники, и неоднократно видел такую ситуацию:
              -Из-за такого схемотехнического решения будет трудно написать управляющую программу.
              -Так это же проблемы программистов.
              -А, ну да, ладно.

              Не могу сказать, что такое везде, конечно.
                +1
                USB — как раз такой случай.
                Очень сложная управляющая программа (драйвер хост контроллера, драйвер хаба, драйвера устройств) при относительно простом железе.

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

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