Уровень поддержки хост-контроллеров, как я писала в общем обзоре, должен вызывать вышележащие уровни при наступлении некоторых событий и предоставлять функции, необходимые вышележащим уровням для работы.
Для удобства восприятия я буду рассказывать о различных элементах кода поддержки в том порядке, в котором они получают управление.
Запуск подсистемы USB
Подготовка: USB-контроллеры в списке PCI-устройств
Подсистема USB запускается вызовом usb_init
из init.inc в ходе загрузки системы.
К моменту запуска USB уже подготовлен список найденных PCI-устройств pcidev_list
. USB-контроллеры опознаются среди всех PCI-устройств по коду класса, подкласса и интерфейса:
Тип | Класс | Подкласс | Интерфейс |
---|---|---|---|
UHCI | 0Ch | 03h | 00h |
OHCI | 0Ch | 03h | 10h |
EHCI | 0Ch | 03h | 20h |
XHCI | 0Ch | 03h | 30h |
usb_init
проходит по списку PCI-устройств несколько раз, каждый раз выделяя USB-контроллеры.
Отключение контроля BIOS
Некоторые BIOS умеют обрабатывать USB-мыши, USB-клавиатуры и USB-флешки, предоставляя данные для операционных систем, не знающих про USB. Данные от мышей и клавиатур преобразуются в формат PS/2 и тем или иным способом доводятся до операционной системы так же, как если бы в системе существовала настоящая PS/2-мышь и/или клавиатура. USB-флешка представляется жёстким диском с точки зрения int 13h
— такая поддержка встречается куда чаще поддержки мышей, ибо необходима для загрузки с флешек.
Операционная система может использовать любой режим процессора и самостоятельно обрабатывать любые прерывания. Чтобы BIOS в таких условиях всё же могла получать управление с предсказуемым окружением, ещё в районе 486-х (начиная со специальной версии i386SL, если точно) Intel придумала специальный режим процессора System Management Mode (SMM), в котором и работает BIOS, прерывая операционную систему. В SMM невозможно попасть средствами самого процессора; процессор попадает в этот режим, когда железо материнской платы подаёт специальный сигнал System Management Interrupt (SMI). USB-контроллеры, встроенные в чипсет, как правило, могут генерировать SMI вместо прерывания в зависимости от настроек.
Первый проход по списку pcidev_list
служит для того, чтобы сообщить BIOS, что USB-контроллеры переходят под управление операционной системы, поэтому BIOS больше не должна с ними ничего делать.
Важно, чтобы на этом проходе все компаньоны обрабатывались до парного EHCI-контроллера. Например, обход в порядке возрастания PCI-координат это гарантирует. В первых версиях кода этот проход не был отдельно выделен, и сначала обрабатывались все контроллеры EHCI; это работало на многих конфигурациях, но в ходе тестов выяснилось, что не все BIOSы корректно написаны и что неправильный порядок может привести к зависанию системы внутри SMM. Более конкретно, возможен следующий сценарий:
- BIOS обнаруживает запрос на прекращение работы с EHCI;
- BIOS деконфигурирует контроллер и забывает про него;
- владение портами переходит к компаньону;
- флешка, подсоединённая к порту, также переходит к компаньону;
- компаньон сигнализирует о новом устройстве;
- поскольку компаньон всё ещё находится под контролем BIOS, та считает устройство своим, выясняет, что это за устройство; видит старую флешку;
- и тут выясняется, что про флешку BIOS помнит то, что она размещалась на контроллере, про который BIOS успешно забыла. Попытка использовать данные забытого контроллера быстро приводит к печальному концу.
Обработка компаньонов перед EHCI исключает возможность этого сценария.
Метод сообщения BIOS о том, что контроллер пора отдать, зависит от контроллера. Соответствующие функции называются {u,o,e}hci_kickoff_bios
. UHCI, будучи хронологически первым интерфейсом, не предоставляет специального сообщения, поэтому uhci_kickoff_bios
просто выключает генерацию SMI и останавливает контроллер. В OHCI и EHCI процедура общения с BIOS теоретически описана в спецификации, но практически… есть некоторые детали. В обоих случаях процедура выглядит следующим образом: установить некоторый бит в некотором регистре и подождать нужного состояния другого бита; если BIOS работает с контроллером, то контроллер сгенерирует SMI, процессор перейдёт в SMM и вызовет BIOS, BIOS отвяжется от контроллера, переведёт другой бит в нужное состояние и выйдет из SMM. Деталь процедуры для OHCI состоит в том, что, как выяснилось в ходе тестов, BIOS вполне может оставить контроллер со включёнными источниками прерываний; если какое-либо событие сгенерирует прерывание и прерывание окажется размаскированным в контроллере прерываний до того, как драйвер OHCI установит свой обработчик, система опять же зависнет — OHCI будет снова и снова генерировать прерывание, которое некому обработать. Поэтому ohci_kickoff_bios
запрещает прерывания на время разговора с BIOS и выключает все источники прерываний OHCI в конце разговора. В EHCI на самом деле нельзя сразу устанавливать бит «операционная система запросила владение контроллером», сначала необходимо проверить, что BIOS владеет контроллером; некоторые BIOS «отпускают» контроллер и забывают про него ещё перед загрузкой системы, не снимают бит генерации SMI и не смогут обработать запрос — система снова повиснет из-за SMI, генерируемого снова и снова.
Инициализация контроллеров
Если на первом проходе usb_init
не обнаружила USB-контроллеров, она прекращает свою работу. Если USB-контроллеры есть, то usb_init
создаёт поток, выделенный под обработку USB, и делает ещё два прохода по списку PCI-устройств, вызывая usb_init_controller
для каждого USB-контроллера, передавая в edi
указатель на соответствующую структуру usb_hardware_func
. Второй проход обрабатывает EHCI, третий — UHCI и OHCI. Здесь EHCI обрабатывается перед компаньонами, чтобы HS-устройства сразу находились под управлением EHCI вместо того, чтобы просигнализировать о себе компаньону и тут же быть отобранным у него.
Функция usb_init_controller
находится в hccommon.inc. Она выделяет память под пару структур *hci_controller
/usb_controller
, инициализирует их обеих нулями, инициализирует некоторые поля usb_controller
, среди которых следует отдельно отметить HardwareFunc
, указывающее на usb_hardware_func
. Далее она же вызывает контроллеро-специфичную инициализацию usb_hardware_func.Init
. Если не произошло ошибки, то последним действием usb_init_controller
регистрирует контроллер в общем списке usb_controllers_list
и пробуждает USB-поток, чтобы тот мог обновить информацию о времени следующего пробуждения.
Специфичная инициализация usb_hardware_func.Init
делает следующее:
- настраивает группы каналов и взаимосвязи между ними, в том числе двоичное дерево каналов прерываний;
- устанавливает обработчик прерывания от контроллера
*hci_irq
; - пишет правильные значения в регистры хост-контроллера, в том числе записывает физический адрес структуры
*hci_controller
; - вычисляет количество портов корневого хаба
usb_controller.NumPorts
и подаёт питание на все порты.
Двоичное дерево каналов прерываний имеет одинаковую структуру на всех контроллерах, я описывала её в предыдущей статье. Связи между каналами остальных типов зависят от контроллера.
Связи между каналами: UHCI
UHCI — хронологически первый контроллер с самым простым железом. У него есть только один регистр-указатель UhciBaseAddressReg
, указывающий на таблицу, входящую в uhci_controller
. Каждый фрейм железо загружает соответствующий элемент таблицы и начинает идти по ссылкам. Последним в цепочке периодических передач находится список каналов прерываний, обрабатываемых каждый фрейм. Из последнего канала списка ссылка ведёт на первый канал в списке управляющих передач. Все каналы управляющих передач и передач массивов данных связаны в кольцо. Когда контроллер заканчивает обработку всех непериодических каналов, он возвращается к началу списка непериодических каналов; при такой схеме новая непериодическая передача начинает обрабатываться немедленно, не дожидаясь следующего фрейма.
Очевидный плюс такой схемы — простота железа при полном соблюдении требований спецификации USB на приоритеты обработки. Минусы: при большой нагрузке, когда контроллер не успевает обработать все передачи до конца фрейма, каналы одного типа неравноправны — те, кому посчастливилось оказаться в начале списка, получают существенное преимущество, — а при малой нагрузке контроллер постоянно занят чтением из памяти только для того, чтобы убедиться, что новой работы нет. Разработчики следующих контроллеров учли минусы UHCI.
Связи между каналами: OHCI
В OHCI регистров-указателей целых семь, шесть из которых связаны с каналами. Один из регистров OhciHCCAReg
указывает на таблицу периодических каналов, аналогичную остальным контроллерам (с поправками на изохронные передачи), второй OhciPeriodCurrentEDReg
указывает на текущий периодический канал и неинтересен настолько, что даже не показан на схеме.
Управляющие каналы и каналы массивов данных собраны в независимые списки. В каждом списке есть два указателя: указатель на начало списка Ohci{Control,Bulk}HeadEDReg
и указатель на текущий канал Ohci{Control,Bulk}CurrentEDReg
. Контроллер может продвигаться по трём спискам независимо друг от друга; приоритеты списков находятся в соответствии с требованиями USB. Каждый фрейм контроллер пробегает по списку периодических каналов, но, в отличие от UHCI, при этом не теряет позиции в других списках, поэтому в пределах одного списка все каналы равноправны; это закрывает первый минус UHCI. В регистрах OHCI также есть два бита «в списке есть новая передача», один для списка управляющих каналов, второй для списка каналов массивов данных, софт должен устанавливать соответствующий бит при добавлении новой передачи. Когда контроллер начинает идти по списку, он сбрасывает бит. Когда контроллер доходит до конца списка, он смотрит на бит; если бит установлен, то контроллер переходит в начало списка, иначе останавливает обработку списка. Это закрывает второй минус UHCI.
Связи между каналами: EHCI
EHCI ограничивается двумя регистрами-указателями. Один из них, EhciPeriodicListReg
, указывает на таблицу периодических каналов, аналогичную остальным контроллерам. Другой, EhciAsyncListReg
, указывает на текущий непериодический канал. Все непериодические каналы замкнуты в кольцо, теперь это — обязательное условие, в отличие от UHCI, где это одна из возможных реализаций. Прогулки по периодическим и непериодическим каналам не зависят друг от друга, что делает все непериодические каналы равноправными. Требованием привилегированности управляющих каналов перед каналами массивов данных Intel решила здесь пренебречь. Чтобы контроллер при отсутствии работы в непериодических каналах не слишком увлекался бесконечными чтениями из памяти, один из каналов в кольце помечается как «начало» кольца; когда контроллер второй раз встречает «начало» кольца, не обнаружив активных передач, он на некоторое время останавливает обработку кольца.
Работа с USB-устройством
Картинка из спецификации USB, описывающая различные состояния устройства и переходы между ними:
Подключение устройства
В ходе инициализации контроллеры OHCI и EHCI настраиваются так, чтобы генерировать прерывание при подключении и отключении устройства. В UHCI такой возможности, увы, нет, поэтому при наличии контроллера UHCI поток USB периодически просыпается сам по себе и опрашивает порты контроллера, проверяя изменения статуса подключённости. Интервал опроса составляет UHCI_POLL_INTERVAL
тиков таймера, что при текущем значении составляет 1 секунду.
Подключённое USB-устройство начинает взаимодействие с хост-контроллером в состоянии Powered.
Инициализация нового USB-устройства начинается с… паузы в 100 миллисекунд. Поскольку USB-устройства подключаются динамически, при подключении возможен дребезг контактов и несколько подряд идущих событий подключения/отключения. При каждом новом событии на одном и том же порту отсчёт времени перезапускается. Когда состояние Powered длится 100 миллисекунд подряд, связь считается стабильной и код переходит к следующему этапу инициализации *hci_new_port
.
По причине, о которой я расскажу чуть позже, следующие несколько этапов обработки подключения нельзя проводить для нескольких устройств параллельно. Поэтому перед следующим этапом новому USB-устройству, возможно, придётся подождать, пока текущее устройство не пройдёт все эти этапы. Конец ожидания отмечен меткой *hci_new_port.reset
, которая есть часть внутренней функции *hci_new_port
на случай, если ожидание оказалось ненужным, и одновременно вынесена в интерфейс для вышележащих уровней usb_hardware_func.InitiateReset
.
Далее код *hci_new_port.reset
подаёт контроллеру команду на включение сигнала сброса для порта нового устройства. По спецификации сброс должен длиться не менее 10 миллисекунд. OHCI отсчитывает время самостоятельно, по окончании интервала отключает сигнал сброса и генерирует прерывание, обработчик которого сигнализирует потоку USB о переходе к следующему этапу. В UHCI и EHCI считать время нужно программно. Чтобы не загружать процессор холостым циклом ожидания, поток USB после включения сигнала сброса планирует пробуждение по системному таймеру и засыпает. Таймер в KolibriOS тикает с частотой 100 Гц, один раз в 10 миллисекунд. Чтобы гарантированно получить минимум 10 миллисекунд сброса, код дожидается двух отсчётов таймера. После второго тика таймера код выключает сигнал сброса для порта нового устройства, *hci_port_reset_done
, и переходит к следующему этапу.
В USB2 в процессе сброса выясняется скорость устройства. Я напомню из предыдущей части, что EHCI умеет работать только с HighSpeed-устройствами, а все устройства, работающие на одной из скоростей USB1, должны быть направлены к USB1-компаньону или хабу. С программной точки зрения после окончания сброса EHCI либо разрешает передачу данных к порту, либо нет. Если после сброса порт остаётся запрещённым, значит, подключённое устройство не работает на скорости USB2. В таком случае остаётся только сообщить логике выбора владельца о перенаправлении порта компаньону; после этого компаньон увидит обычное событие подключения и будет его обрабатывать.
После сброса многие устройства уже готовы к настройке на вышележащих уровнях. Но не все. Спецификация требует паузы в минимум 10 миллисекунд после сброса, и, как показывают тесты, некоторым устройствам эта пауза действительно нужна. Аналогично предыдущему этапу, USB-поток засыпает на два отсчёта таймера, на сей раз для всех контроллеров, включая OHCI.
После сброса и последующей паузы устройство переходит из состояния Powered в состояние Default — одно из двух «полурабочих» состояний, когда устройство уже готово получать и отсылать данные для нулевой конечной точки, но ещё не полностью инициализировано. В состоянии Default устройство отзывается на нулевой адрес на шине USB. Это и есть причина, по которой нельзя проводить сброс для двух устройств параллельно: иначе получились бы два устройства, каждое из которых думало бы, что последующая настройка относится именно к нему.
Нулевая конечная точка устройства готова к работе, пора открывать канал. Структура канала содержит характеристики самого канала — например, его тип — и характеристики устройства: скорость устройства, адрес устройства на шине, указатель на usb_controller
. Код *hci_port_init
, вызываемый на этом этапе, делегирует работу функции *hci_new_device
, вынесенной в API для хабов usb_hardware_func.NewDevice
. Последняя подготавливает структуру псевдо-канала, в которой характеристики устройства корректно заполнены, а значения характеристик канала не имеют значения. На этом код поддержки хост-контроллеров заканчивает самостоятельные действия и передаёт управление функции usb_new_device
уровня логического устройства; дальше вплоть до отключения устройства код поддержки хост-контроллеров лишь выполняет запросы от вышележащих уровней.
Открытие канала и передачи по каналу
С точки зрения контроллера очередь дескрипторов передач (один дескриптор может описывать целую передачу, а может какую-то часть, в зависимости от контроллера) организована как односвязный список, физический адрес первого дескриптора находится в структуре канала, физический адрес следующего дескриптора находится в предыдущем дескрипторе. Когда контроллер заканчивает обрабатывать один дескриптор, он обновляет структуру канала, записывая туда адрес следующего дескриптора. Отсюда следует, что при работающей очереди передач код поддержки хост-контроллера не может сам обновлять адрес следующего дескриптора во избежание состояния гонки с железом. Чтобы организовать работу, нужен дополнительный пустой дескриптор в конце очереди. Пустой дескриптор помечен для контроллера как неактивный. Когда код хочет добавить дескриптор, он заполняет текущий пустой дескриптор, выделяет следующий пустой дескриптор, проставляет в старом дескрипторе ссылку на новый, последним действием активирует старый дескриптор — и всё это без пересечений по записи с контроллером. Когда контроллер доходит до неактивного дескриптора, он останавливает обработку очереди, оставляя в структуре канала ссылку на неактивный дескриптор.
После осознания необходимости пустого дескриптора открытие канала и постановка передач в очередь достаточно прямолинейны. При открытии канала usb_hardware_func.InitPipe = *hci_init_pipe
нужно:
- скопировать характеристики устройства из существующего канала или псевдо-канала,
- заполнить характеристики канала на основании переданных данных,
- прописать в структуре канала физический адрес пустого дескриптора,
- инициализировать пустой дескриптор как неактивный,
- последним действием вставить канал в соответствующий список.
Для периодических каналов нужно ещё выбрать нужный список; этим занимается планировщик, о котором я расскажу в следующей статье. Добавление передачи в очередь разбито на заполнение дескрипторов для одного этапа передачи usb_hardware_func.AllocTransfer = *hci_alloc_transfer
, которое для управляющих передач вызывается дважды или трижды, и активация передачи usb_hardware_func.InsertTransfer = *hci_insert_transfer
. Функция AllocTransfer
разбивает этап передачи на отдельные дескрипторы и заполняет их, начиная с пустого дескриптора и выделяя дополнительные дескрипторы при необходимости. Функция InsertTransfer
активирует бывший пустой дескриптор.
В каждом дескрипторе есть бит IOC, Interrupt on completion. Когда контроллер завершает обработку дескриптора с установленным битом IOC, он генерирует прерывание. Код поддержки хост-контроллеров устанавливает этот бит в последнем дескрипторе каждой передачи.
В OHCI седьмой регистр-указатель используется для того, чтобы связать все обработанные дескрипторы (в том числе со сброшенным битом IOC) в список: при окончании обработки дескриптора в дескриптор записывается значение регистра, в регистр записывается физический адрес дескриптора. При генерации прерывания значение регистра перемещается в одну из переменных ohci_controller
, а сам регистр обнуляется, начиная заново список дескрипторов. Обработчик прерывания OHCI просто «раскручивает» этот список в обратном порядке, преобразуя физические адреса в линейные.
В UHCI и EHCI такого механизма нет, поэтому обработчику прерывания UHCI и EHCI приходится просматривать все каналы и проверять, какие дескрипторы уже обработаны, то есть неактивны, но не совпадают с пустым дескриптором этого канала.
В любом случае окончательная обработка дескриптора происходит в потоке USB, функция *hci_process_finalized_td
. Если дескриптор — не последний в передаче, то обработчик прибавляет длину данных к длине данных следующего дескриптора; таким образом накапливается суммарная длина обработанных данных. Если дескриптор — последний в передаче, то с ним должна быть ассоциирована callback-функция обработки, которую код и вызывает.
Изменение характеристик канала
Иногда необходимо поменять характеристики канала. Есть три таких случая, два из них возникают в процессе настройки уровнем логического устройства, третий — при закрытии канала (в этом случае меняется поле «следующий канал» у предыдущего канала). Первый, обрабатываемый usb_hardware_func.SetDeviceAddress
: после назначения адреса устройству нужно обновить адрес устройства в структуре канала. Второй, обрабатываемый usb_hardware_func.SetEndpointPacketSize
: уровень логического устройства может поменять максимальный размер транзакции в ходе инициализации. В UHCI оба этих поля в структуре канала — исключительно программные, контроллер на них даже не смотрит, поэтому с обновлением проблем нет. Первая версия кода просто обновляла параметры в памяти для всех контроллеров, и это даже работало на некоторых конфигурациях. Но в ходе тестов выяснилось, что не на всех. В EHCI контроллер имеет право кэшировать структуру канала, и некоторые контроллеры этим правом активно пользуются, в результате чего могут увидеть обновление в дескрипторе передачи, но проигнорировать обновление в структуре канала. Строго говоря, расхождение возможно даже без кэша: если контроллер только что прочёл структуру канала, потом надолго задумался, в ходе чего код успел обновить структуру канала и поставить в очередь следующую передачу, после чего контроллер оставил размышления и прочитал дескриптор передачи, получится такая же ситуация.
Для корректного обновления в OHCI и EHCI нужно, чтобы контроллер гарантированно не приступал к обработке дескриптора передачи, пока не прочтёт обновлённые данные структуры канала. Для периодического канала достаточно подождать, пока не сменится номер фрейма. Для обновления непериодического канала в OHCI приходится временно останавливать обработку соответствующего списка каналов и сбрасывать указатель на текущий канал в списке. В EHCI предусмотрен специальный механизм Interrupt on Async Advance Doorbell: код устанавливает в одном из регистров бит «продвинься в кольце непериодических каналов и сообщи, когда сделаешь» и ждёт сигнала. Как правило, прерывание приходит. Иногда не приходит, поэтому не помешает подстраховка в виде таймаута.
Отключение устройства
Здесь всё просто: событие приходит аналогично событию подключения устройства — прерыванием на OHCI и EHCI, периодическим опросом на UHCI — и код вызывает функцию usb_device_disconnected
уровня работы с каналами, чтобы тот корректно закрыл все каналы и освободил все ресурсы. Кроме того, если устройство на этом порту было подключено менее 100 миллисекунд назад и ждало следующего этапа обработки, код вычёркивает устройство из списка ожидающих.
Все статьи серии
Часть 1: общая схема
Часть 2: основы работы с хост-контроллерами
Часть 3: код поддержки хост-контроллеров
Автор: CleverMouse