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

в 10:30, , рубрики: kolibrios, usb, Блог компании KolibriOS Project Team, колибри, операционные системы, метки: , ,

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

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

Хост-контроллеры оповещают софт о происходящих событиях, генерируя прерывания. Прерывание может прийти и оторвать процессор от текущей задачи в любой момент времени; это накладывает жёсткие требования на обработчик прерывания. Обработчик прерывания не может захватывать никакие блокировки — ведь вполне возможно, что прерванный код как раз завладел блокировкой и уже не сможет её освободить. Единственным исключением является вариант спинлока, запрещающий прерывания на время блокировки, но из-за глобальности эффекта спинлок стоит применять пореже и для очень коротких участков кода. На однопроцессорных конфигурациях такой вариант вырождается в пару 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.

Поддержка USB в KolibriOS: что внутри? Часть 2: основы работы с хост контроллерами
Каналы сгруппированы в несколько списков. Внутри каждого списка есть три связи: физический адрес следующего канала для железа, виртуальные адреса следующего и предыдущего каналов для софта. Один список состоит из всех каналов для управляющих передач. Другой список состоит из всех каналов для передач массивов данных.
Списки каналов прерываний организованы в двоичное дерево так, как показано на рисунке, где кружки обозначают списки каналов прерываний, а стрелки — физические адреса следующих элементов. Хост-контроллер начинает каждую единицу времени (фрейм для 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).

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

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

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

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

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

Часть 1: общая схема
Часть 2: основы работы с хост-контроллерами

Автор: CleverMouse

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js