- PVSM.RU - https://www.pvsm.ru -
Рассказ об уровне взаимодействия с хост-контроллерами растянулся на две [1] статьи [2] и всё равно оставил за кадром некоторые детали — которые, как я надеюсь, заинтересованный читатель может восполнить непосредственно из исходников [3]. Уровень поддержки каналов куда проще и в основном занят тем, что преобразует вызовы API для вышележащих уровней в нужную последовательность действий, включая блокировки, с нужным хост-контроллером.
Функция USBOpenPipe из API, названная usb_open_pipe в коде pipe.inc [4], открывает новый канал по указанным характеристикам канала и «родительскому» каналу, где записаны характеристики устройства. Для этого она:
*hci_pipe+usb_pipe, описывающих канал и выравненных на контроллеро-специфичную границу, вызовом контроллеро-специфичной функции usb_hardware_func.AllocPipe;*hci_gtd+usb_gtd, описывающих пустой дескриптор передачи и выравненных на контроллеро-специфичную границу, вызовом контроллеро-специфичной функции usb_hardware_func.AllocTD;usb_hardware_func.InitPipe, охраняемую мьютексом, глобальным для контроллера;
Контроллеро-специфичная инициализация последним действием добавляет новый канал в соответствующий список. Для управляющих каналов, равно как и для каналов массивов данных, есть всего один список, а вот для каналов прерываний нужно ещё выбрать один из нескольких вариантов.

Здесь в игру вступает планировщик scheduler.inc [5]. Он как раз и выбирает один из списков каналов прерываний, а также убеждается, что для нового канала «достаточно места». Я напомню, что в каждом фрейме FullSpeed-шины под периодические передачи нельзя использовать более 90% времени, а в каждом микрофрейме HighSpeed-шины — более 80% времени.
Здесь я должна отметить, что если вы зачем-то пишете реализацию USB, которая должна работать в ваших условиях, на планировщике можно серьёзно сэкономить. Вам придётся в том или ином виде реализовать всё остальное, что описано в этой серии статей, но при отсутствии большой нагрузки можно вместо полного дерева обойтись всего одним списком каналов прерываний, обрабатываемым каждый фрейм/микрофрейм. Чуть более экономная схема, не слишком усложняющая реализацию, — один список каналов для каждого интервала обработки 1, 2, 4, 8, 16, 32 фреймов. Пока не нужно одновременно обрабатывать более одного устройства с большим трафиком на один хост-контроллер, такой подход ничем не уступает полноценному планировщику. Простая схема «сломается» в некоторых специфичных конфигурациях с двумя или более изохронными каналами FullSpeed-устройств или тремя или более изохронными каналами HighSpeed-устройств, но, быть может, никто и не будет запускать вашу реализацию в столь специфичных условиях?
Если же вы пишете реализацию USB, которая должна работать везде и всегда, планировщик вам тоже придётся написать.
Один бит на скорости HighSpeed номинально передаётся за 1/60000 часть микрофрейма, что даёт скорость 480 мегабит/с. Требования к точности таймера HighSpeed-устройств повышены до ±0.05%, так что при планировании транзакций возникающей ошибкой из-за расхождения таймеров можно пренебречь.
Транзакции имеют свою внутреннюю структуру. Оставим пока в стороне расщеплённые транзакции. Нормальные транзакции состоят из нескольких пакетов, следующих по шине USB строго последовательно, не перемежаясь с другими пакетами: пакета с токеном (Token), опционального пакета с данными (Data), опционального пакета обратной связи (Handshake). Перед пакетами, направленными от хоста к LowSpeed-устройству, следует отдельный специальный пакет PRE. Пакет PRE и пауза после него минимум в 4 «номинальных FS-бита» нужны для того, чтобы хабы на шине успели разблокировать порты, к которым подключены LowSpeed-устройства. Обычный FullSpeed-трафик не передаётся на такие порты.
Каждый пакет начинается с синхропосылки SYNC размером 8 бит = 1 байт на Low/Full-Speed и 32 бит = 4 байта на HighSpeed. Каждый пакет, кроме специального пакета PRE, заканчивается последовательностью EOP (end of packet) размером 3 бита на Low/Full-Speed и 8 бит на HighSpeed.
Токен определяет действие, направление передачи, адрес и конечную точку устройства, принимающую/передающую данные. В нормальных транзакциях возможны три токена: IN, OUT и SETUP для приёма данных, отправки данных и первого этапа управляющей передачи соответственно. Пакет с токеном занимает 3 байта, не считая SYNC+EOP: 8 бит для типа пакета, 7 бит адреса устройства, 4 бита конечной точки и 5 бит CRC, подтверждающей отсутствие ошибок при передаче адреса устройства и конечной точки.
Пакет с данными содержит собственно данные, отправленные или принятые от устройства, а также 3 дополнительных байта, не считая SYNC+EOP: 8 бит для типа пакета и 16 бит CRC данных.
Пакет обратной связи состоит из одного байта, не считая SYNC+EOP: 8 бит для типа пакета. Пакет посылается в направлении, обратном предыдущему пакету. Пакет ACK означает, что все данные успешно приняты. В IN-транзакциях пакет NAK посылается устройством вместо пакета с данными и означает, что пока данных нет. Например, так будет отвечать мышь, пока контроллер регулярно её опрашивает, но состояние с момента последнего опроса не изменилось. В OUT-транзакциях пакет NAK посылается устройством после пакета с данными и означает, что пока устройство занято внутренними делами, так что хост должен повторить попытку позднее. NAK — не ошибка. Для сигнализации ошибки устройство может вообще ничего не ответить, ответить некорректным пакетом или ответить пакетом STALL. В первых двух случаях контроллер посчитает это ошибкой где-то на шине и будет повторять попытку до трёх раз, после чего сдастся и сообщит об ошибке. В последнем случае контроллер сообщит об ошибке сразу же.
В изохронных транзакциях пакет обратной связи отсутствует. В транзакциях по прерыванию пакет обратной связи обязателен.
В данных, передаваемых по USB, после каждых шести единичных бит вставляется нулевой бит. Единичные биты кодируются так, что состояние шины при единичном бите не меняется, для выделения отдельных бит используется таймер. Нулевой бит вставляется для того, чтобы допускаемое расхождение в таймерах не создавало проблем. Поэтому в худшем случае время доставки пакета нужно умножить на 7/6. Множитель не относится к SYNC и EOP: они кодируются специальным образом, порождающим гарантированные изменения состояния шины.
Если хост-контроллер отсылает два пакета подряд, достаточно лишь небольшой паузы (inter-packet delay), соответствующей передаче 2 бит в случае FullSpeed и LowSpeed и 88 бит в случае HighSpeed. Если контроллер принял пакет и должен послать ответный пакет, пауза снижается до 8 бит в случае HighSpeed и тех же 2 бит в случае FullSpeed и LowSpeed. Если же хост-контроллер послал пакет и ждёт ответного пакета, то нужно учесть задержку на прохождение пакета до устройства и ответного пакета от устройства (turn-around time). Для FullSpeed и LowSpeed шины спецификация определяет максимальную задержку, включая прохождение сигнала в обе стороны и реакцию устройства, как время передачи 18 бит с соответствующей скоростью. Для HighSpeed максимальная задержка эквивалентна времени передачи 736 бит.
Хост-контроллер скрывает в себе реализацию всех этих деталей транзакции, для планирования достаточно лишь знать, сколько времени транзакция займёт. Время зависит от типа транзакции, направления транзакции и скорости устройства.
Расщеплённые транзакции содержат три типа «элементарных» транзакций на двух шинах: транзакция Start-Split на HighSpeed-шине между хостом и TT, обычная транзакция на FullSpeed/LowSpeed-шине между TT и устройством, транзакция Complete-Split на HighSpeed-шине между хостом и TT. Время транзакции в середине отличается от времени такой же транзакции без TT только дополнительной паузой, вносимой TT и описанной в дескрипторе хаба с TT. Транзакции Start-Split и Complete-Split начинаются со специального пакета SPLIT размером 4 байта, не считая SYNC+EOP.
На FullSpeed/LowSpeed-шине за один фрейм может быть не более одной транзакции по одному каналу, передачи из более чем одной транзакции разбиваются на несколько фреймов. На HighSpeed-шине максимальное количество транзакций за микрофрейм может доходить до 3 и является одной из характеристик канала наряду с максимальным размером транзакции.
При открытии канала планировщик должен зарезервировать за каналом часть от 90% фрейма / 80% микрофрейма, исходя из наихудшего случая использования канала — предполагая максимально возможное число максимально длительных транзакций. Длительность транзакций описана в предыдущем разделе. Если зарезервировать часть канала не получается из-за того, что всё уже занято другими каналами, планировщик должен вернуть ошибку. Драйвер, обнаружив ошибку, может, например, попытаться договориться с устройством о снижении трафика за счёт чего-нибудь или сообщить пользователю (посредством управляющей программы), что в таких условиях работать невозможно.
Расщеплённые транзакции добавляют сложностей. Во-первых, резервировать и вести учёт нужно на двух шинах. Во-вторых, на FullSpeed/LowSpeed шине появляются микрофреймы: если транзакция Start-Split от хоста к TT приходит в микрофрейме N, то TT сможет начать эту транзакцию только в микрофрейме N+1, а вернуть результаты в транзакции Complete-Split — не раньше микрофрейма N+2. В-третьих, хотя максимум в 90% фрейма на все периодические транзакции в худшем случае остаётся, планирование на FS/LS-шине должно исходить из оптимистичной оценки без множителя 7/6 из-за вставки битов, спецификация USB2 в лице раздела 11.18 «Periodic Split Transaction Pipelining and Buffer Management» называет такую оценку «best-case budget» — это уменьшает шансы на то, что FS/LS-шина будет простаивать из-за того, что одна периодическая транзакция завершилась раньше рассчитанного, следующая периодическая транзакция сможет начаться не раньше следующего микрофрейма, потому что для неё ещё не пришли данные Start-Split транзакции, а для очередной непериодической транзакции в остатке текущего микрофрейма не хватает времени. Наконец, хост не знает, когда точно завершится транзакция, а буферы TT для хранения результатов не резиновые, так что транзакцию Complete-Split нужно планировать несколько раз — по одному в каждом микрофрейме, следующем после микрофрейма, в котором транзакция может завершиться. Конкретные требования озвучены в том же разделе 11.18: в составе транзакции по прерываниям, по бюджету начинающимся в микрофрейме N, должны быть запланированы одна транзакция Start-Split в микрофрейме N-1 и три транзакции Complete-Split в микрофреймах N+1,N+2,N+3. Изохронные транзакции чтения отличаются только тем, что могут занимать несколько микрофреймов N,...,L в бюджете, из-за чего транзакции Complete-Split нужно планировать в микрофреймах от N+1 до L+3 включительно. В изохронных транзакциях записи транзакций Complete-Split не предусмотрено, зато может быть несколько транзакций Start-Split: в одном микрофрейме на FS-шине умещается менее 188 байт, и если данных больше, то они будут разбиты на несколько транзакций Start-Split с ограничением 188 байт в одной транзакции.
Планировщик KolibriOS пытается достичь как можно более равномерного распределения зарезервированных частей в различных фреймах/микрофреймах, стремясь к тому, чтобы во фразе «в каждом фрейме/микрофрейме есть X или более свободного времени» число X было бы как можно больше. Если позднее появится канал, требующий внимания каждый фрейм/микрофрейм, большое значение X необходимо для успешного резервирования. Если не появится — время пригодится для непериодических передач.
Сначала планировщик выбирает реальный интервал, с которым хост-контроллер будет опрашивать канал. Это лёгкая часть задачи: выбрать из чисел 1, 2, 4, 8, 16, 32 максимальное, не большее заданного. Дальше нужно выбирать из всех списков с уже выбранным интервалом. Я возьму для примера USB1 и интервал в 8 фреймов. Тогда есть 8 вариантов: список каналов, обрабатываемый во фреймах вида 8k+0, ..., список каналов, обрабатываемый во фреймах вида 8k+7. Планирование повторяется каждые 32 фрейма; вариант 8k+0 в одном диапазоне планирования содержит 4 фрейма 0,8,16,24, в каждом из которых могут быть уже запланированы разные наборы каналов. Чтобы вычислить общее время, зарезервированное для периодических транзакций во фрейме, например, 24, нужно просуммировать данные по спискам уже открытых каналов, соответствующим 32k+24, 16k+8, 8k+0, 4k+0, 2k+0, каждому фрейму. Вычислив такое время для каждого из фреймов 0,8,16,24, планировщик берёт максимум и связывает его с вариантом 8k+0. Аналогично вычисляется максимальное время, зарезервированное под уже существующие каналы, для остальных вариантов 8k+1, ..., 8k+7. Лучший из вариантов — тот, у которого максимальное зарезервированное время меньше остальных, а свободное, соответственно, больше остальных. Выбрав лучший вариант, планировщик проверяет, что резервирование времени для нового канала не переполнит 90% фрейма, и назначает выбранный вариант новому каналу.
Планирование HighSpeed-транзакций в USB2 аналогично USB1 с двумя отличиями: вариантов в 8 раз больше за счёт микрофреймов; если есть несколько вариантов с одинаковым временем, то планировщик выбирает тот, который ближе к концу фрейма, чтобы минимизировать конфликты с расщеплёнными транзакциями.
Планирование расщеплённых транзакций в USB2 следует тем же принципам, но детали ещё сложнее. Варианты, где на хотя бы одной из шин невозможно зарезервировать достаточное время, отбрасываются ещё до сравнения. В качестве основного критерия качества варианта используется бюджет на FS-шине как более ограниченной. При равном бюджете планировщик выбирает вариант, ближайший к началу фрейма. Если и тогда остаются варианты, последним критерием выступает время на HS-шине, максимальное из всех Start-Split и Complete-Split транзакций.
Функция USBClosePipe из API, названная usb_close_pipe в коде pipe.inc, закрывает указанный канал. Для этого она захватывает мьютекс набора каналов устройства, устанавливает бит «владелец отказался от канала», вызывает общую функцию закрытия канала usb_close_pipe_nolock, отпускает мьютекс набора каналов устройства и будит USB-поток, чтобы тот обработал изменение в списках каналов.
При отключении устройства контроллеро-специфичный код или драйвер хабов вызывает функцию usb_device_disconnected, которая захватывает мьютекс набора каналов устройства, проходит по всем каналам, для каждого вызывает общую функцию usb_close_pipe_nolock, предварительно запомнив, какой канал идёт следующим, в конце отпускает мьютекс набора каналов устройства. Будить USB-поток нет необходимости, поскольку он и так не спит, будучи занят выполнением этого кода.
Общая функция usb_close_pipe_nolock:
usb_hardware_func.UnlinkPipe, охраняемую глобальным для контроллера мьютексом, которая удалит канал из соответствующего списка каналов и сообщит планировщику scheduler.inc об удалении канала.
Удалённый канал какое-то время ещё может обрабатываться контроллером, поэтому окончательное удаление происходит немного позже, когда контроллеро-специфичный код вызовет функцию usb_pipe_closed, которая:
DeviceDisconnected всех драйверов устройства, помечает адрес устройства на шине, если таковой был назначен, как свободный, освобождает память, выделенную под структуры канала, а также проходит по списку каналов устройства, подлежащих освобождению, и освобождает память, выделенную под них.
Такие передачи состоят из одного этапа, так что функция USBNormalTransferAsync из API, названная usb_normal_transfer_async в коде pipe.inc, проста: захватывает мьютекс канала, проверяет, что канал ещё не закрыт, создаёт передачу вызовом контроллеро-специфичной функции usb_hardware_func.AllocTransfer, указывая использовать направление, сохранённое в структуре канала при открытии, активирует передачу вызовом usb_hardware_func.InsertTransfer и освобождает мьютекс канала.
Управляющие передачи состоят из двух или трёх этапов с данными, передаваемыми в различных направлениях, поэтому функция USBControlTransferAsync из API, названная usb_control_async в коде pipe.inc, несколько сложнее USBNormalTransferAsync.

Здесь контроллеро-специфичная функция usb_hardware_func.AllocTransfer вызывается дважды или трижды, по числу этапов. Направление передачи задаётся явно. Кроме того, явно задаётся ещё одна характеристика передачи — бит Toggle. Неизохронные транзакции в USB «красятся в два чередующихся цвета» для отслеживания ситуации, когда какой-нибудь пакет не дошёл до адресата. Для каналов прерываний и каналов массивов данных чередование сквозное по всем передачам: если передача закончилась на транзакции с Toggle=0, следующая передача начнётся с Toggle=1 и наоборот. В управляющих передачах бит Toggle сбрасывается с каждым новым этапом, как показано на рисунке.
Часть 1: общая схема [6]
Часть 2: основы работы с хост-контроллерами [1]
Часть 3: код поддержки хост-контроллеров [2]
Часть 4: уровень поддержки каналов
Автор: CleverMouse
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/usb/39004
Ссылки в тексте:
[1] две: http://habrahabr.ru/company/kolibrios/blog/183184/
[2] статьи: http://habrahabr.ru/company/kolibrios/blog/183284/
[3] исходников: http://websvn.kolibrios.org/listing.php?repname=Kolibri+OS&path=%2Fkernel%2Ftrunk%2Fbus%2Fusb%2F&#a1eebe490efc09ee240646740fdcf3bf1
[4] pipe.inc: http://websvn.kolibrios.org/filedetails.php?repname=Kolibri+OS&path=%2Fkernel%2Ftrunk%2Fbus%2Fusb%2Fpipe.inc
[5] scheduler.inc: http://websvn.kolibrios.org/filedetails.php?repname=Kolibri+OS&path=%2Fkernel%2Ftrunk%2Fbus%2Fusb%2Fscheduler.inc
[6] Часть 1: общая схема: http://habrahabr.ru/company/kolibrios/blog/181586/
[7] Источник: http://habrahabr.ru/post/186276/
Нажмите здесь для печати.