Поддержка USB в KolibriOS: что внутри? Часть 4: уровень поддержки каналов

  • Tutorial
Рассказ об уровне взаимодействия с хост-контроллерами растянулся на две статьи и всё равно оставил за кадром некоторые детали — которые, как я надеюсь, заинтересованный читатель может восполнить непосредственно из исходников. Уровень поддержки каналов куда проще и в основном занят тем, что преобразует вызовы API для вышележащих уровней в нужную последовательность действий, включая блокировки, с нужным хост-контроллером.

Открытие канала


Функция USBOpenPipe из API, названная usb_open_pipe в коде pipe.inc, открывает новый канал по указанным характеристикам канала и «родительскому» каналу, где записаны характеристики устройства. Для этого она:
  • выделяет пару структур *hci_pipe+usb_pipe, описывающих канал и выравненных на контроллеро-специфичную границу, вызовом контроллеро-специфичной функции usb_hardware_func.AllocPipe;
  • выделяет пару структур *hci_gtd+usb_gtd, описывающих пустой дескриптор передачи и выравненных на контроллеро-специфичную границу, вызовом контроллеро-специфичной функции usb_hardware_func.AllocTD;
  • заполняет указатели: в структуре канала копирует указатель на структуру контроллера и указатель на данные устройства, общие для всех каналов, из «родительского» канала; между структурой канала и структурой пустого дескриптора заполняет указатели туда-обратно; структуру пустого дескриптора делает единственным элементом двусвязного списка каналов;
  • инициализирует мьютекс, который будет охранять все операции с этим каналом. Хотя вся обработка событий от USB-контроллеров происходит в потоке USB, про обращения к API нельзя сказать того же: чтение приложением файла с USB-флешки инициирует постановку передачи — и даже не одной — в очередь в контексте потока приложения. Чтобы новая передача не мешала USB-потоку обрабатывать завершение старой передачи, и нужен этот мьютекс;
  • захватывает мьютекс набора каналов устройства и убеждается, что устройство ещё не отключено;
  • вызывает контроллеро-специфичную инициализацию usb_hardware_func.InitPipe, охраняемую мьютексом, глобальным для контроллера;
  • добавляет новый канал в набор каналов устройства и отпускает мьютекс набора каналов;
  • при ошибке на одном из этапов откатывает все предыдущие этапы. Поскольку откатить контроллеро-специфичную инициализацию сложнее всего, она сделана на последнем этапе, после которого ошибок быть не может.

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

Здесь в игру вступает планировщик scheduler.inc. Он как раз и выбирает один из списков каналов прерываний, а также убеждается, что для нового канала «достаточно места». Я напомню, что в каждом фрейме FullSpeed-шины под периодические передачи нельзя использовать более 90% времени, а в каждом микрофрейме HighSpeed-шины — более 80% времени.

Здесь я должна отметить, что если вы зачем-то пишете реализацию USB, которая должна работать в ваших условиях, на планировщике можно серьёзно сэкономить. Вам придётся в том или ином виде реализовать всё остальное, что описано в этой серии статей, но при отсутствии большой нагрузки можно вместо полного дерева обойтись всего одним списком каналов прерываний, обрабатываемым каждый фрейм/микрофрейм. Чуть более экономная схема, не слишком усложняющая реализацию, — один список каналов для каждого интервала обработки 1, 2, 4, 8, 16, 32 фреймов. Пока не нужно одновременно обрабатывать более одного устройства с большим трафиком на один хост-контроллер, такой подход ничем не уступает полноценному планировщику. Простая схема «сломается» в некоторых специфичных конфигурациях с двумя или более изохронными каналами FullSpeed-устройств или тремя или более изохронными каналами HighSpeed-устройств, но, быть может, никто и не будет запускать вашу реализацию в столь специфичных условиях?

Если же вы пишете реализацию USB, которая должна работать везде и всегда, планировщик вам тоже придётся написать.


Оценка времени транзакции


Данные для расчётов и внутреннее устройство транзакций
Один бит на скорости FullSpeed номинально передаётся за 1/12000 часть фрейма, что даёт скорость 12 мегабит/с. Иными словами, «размер» одного фрейма, измеряемый хост-контроллером, равен 12000 бит. И на хосте, и на устройстве тикает таймер, отсчитывающий 1/12000 миллисекунды, по отсчётам таймера хост или устройство начинают пересылать очередной бит. Требования к точности таймера хоста достаточно жёсткие, и при расчётах можно считать таймер хоста точным. Для внешних FullSpeed-устройств спецификация допускает погрешность таймера ±0.25%, что означает, что время приёма 400 бит от устройства может соответствовать времени от 399 до 401 «номинальных FS-бит». Один бит на скорости LowSpeed номинально передаётся в 8 раз дольше, чем на скорости FullSpeed, что даёт скорость 1.5 мегабит/с. LowSpeed задумывался как режим с ослабленными требованиями для простых устройств типа мыши/клавиатуры, и погрешность таймера LowSpeed-устройства должна быть в пределах ±1.5%: время приёма 50 бит от устройства может соответствовать времени от 394 до 406 «номинальных FS-бит».

Один бит на скорости 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 бит.

Хост-контроллер скрывает в себе реализацию всех этих деталей транзакции, для планирования достаточно лишь знать, сколько времени транзакция займёт. Время зависит от типа транзакции, направления транзакции и скорости устройства.
  • Транзакция чтения по прерыванию состоит из пакета с токеном к устройству, ожидания ответа от устройства, пакета с данными от устройства, паузы после принятого пакета, пакета с обратной связью к устройству, паузы после отправленного пакета.
    • HighSpeed-шина: 68 бит в первом пакете, 736 бит ожидания, 40+(7/6)*8*(размер данных+3) бит во втором пакете, 8 бит паузы, 49 бит в последнем пакете, ещё 88 бит паузы — итого 989 + 8*(7/6)*(размер данных+3) бит максимум.
    • FullSpeed-шина: 39 бит в первом пакете, 18 бит ожидания, (401/400)*(11+(7/6)*8*(размер данных+3)) бит во втором пакете, 2 бита паузы, 20 бит в последнем пакете, ещё 2 бита паузы — итого 93 + 2807/300*(размер данных+3) бит максимум.
    • LowSpeed-шина: 16 FS-бит преамбулы к первому пакету и 4 FS-бита на реакцию хабов, 8*39 FS-бит в первом пакете, 8*18 FS-бит ожидания, (406/50)*(11+(7/6)*8*(размер данных+3)) FS-бит во втором пакете, 8*2 FS-бита паузы, 16 FS-бит преамбулы к последнему пакету и 4 FS-бита на реакцию хабов, 8*20 FS-бит в последнем пакете, ещё 8*2 бита паузы — итого 778 + 5684/75*(размер данных+3) FS-бит максимум.
  • Транзакция записи по прерыванию состоит из пакета с токеном к устройству, паузы после отправленного пакета, пакета с данными к устройству, ожидания ответа от устройства, пакета с обратной связью от устройства, паузы после принятого пакета. Отличие от чтения, не считая порядка слагаемых, только в направлении передачи.
    • HighSpeed-шина: те же 989 + 8*(7/6)*(размер данных+3) бит максимум.
    • FullSpeed-шина: множитель 401/400 «переезжает» от пакета данных к пакету обратной связи, итого 93 + 28/3*(размер данных+3) бит максимум.
    • LowSpeed-шина: множители 406/50 и 8 в двух пакетах данных и обратной связи меняются местами, итого 778 + 224/3*(размер данных+3) FS-бит максимум.
  • Изохронная транзакция чтения состоит из пакета с токеном к устройству, ожидания ответа от устройства, пакета с данными от устройства, паузы после принятого пакета
    • HighSpeed-шина: 68 бит в первом пакете, 736 бит ожидания, 40+(7/6)*8*(размер данных+3) бит во втором пакете, 8 бит паузы — итого 852 + 8*(7/6)*(размер данных+3) бит максимум.
    • FullSpeed-шина: 39 бит в первом пакете, 18 бит ожидания, (401/400)*(11+(7/6)*8*(размер данных+3)) бит во втором пакете, 2 бита паузы — итого 71 + 2807/300*(размер данных+3) бит максимум.
    • На LowSpeed-шине изохронные транзакции запрещены спецификацией.
  • Изохронная транзакция записи состоит из пакета с токеном к устройству, паузы после отправленного пакета, пакета с данными к устройству, ещё одной паузы после отправленного пакета.
    • HighSpeed-шина: 68 бит в первом пакете, 88 бит паузы, 40+(7/6)*8*(размер данных+3) бит во втором пакете, ещё 88 бит паузы — итого 284 + 8*(7/6)*(размер данных+3) бит максимум.
    • FullSpeed-шина: 39 бит в первом пакете, 2 бита паузы, 11+(7/6)*8*(размер данных+3) бит во втором пакете, ещё 2 бита паузы — итого 54 + 8*(7/6)*(размер данных+3) бит максимум.
    • На LowSpeed-шине изохронные транзакции запрещены спецификацией.
Расщеплённые транзакции содержат три типа «элементарных» транзакций на двух шинах: транзакция Start-Split на HighSpeed-шине между хостом и TT, обычная транзакция на FullSpeed/LowSpeed-шине между TT и устройством, транзакция Complete-Split на HighSpeed-шине между хостом и TT. Время транзакции в середине отличается от времени такой же транзакции без TT только дополнительной паузой, вносимой TT и описанной в дескрипторе хаба с TT. Транзакции Start-Split и Complete-Split начинаются со специального пакета SPLIT размером 4 байта, не считая SYNC+EOP.
  • Расщеплённая транзакция чтения по прерыванию и изохронная транзакция чтения. Транзакция Start-Split состоит из пакета SPLIT, паузы после отправленного пакета, пакета с токеном, паузы после отправленного пакета — итого 321 бит. Транзакция Complete-Split состоит из пакета SPLIT, паузы после отправленного пакета, пакета с токеном, ожидания ответа от устройства, пакета с данными, паузы после принятого пакета — итого 1017 + 8*(7/6)*(размер данных+3) бит.
  • Расщеплённая транзакция записи по прерыванию. Транзакция Start-Split состоит из пакета SPLIT, паузы после отправленного пакета, пакета с токеном, паузы после отправленного пакета, пакета с данными, паузы после отправленного пакета — итого 449 + 8*(7/6)*(размер данных+3) бит. Транзакция Complete-Split состоит из пакета SPLIT, паузы после отправленного пакета, пакета с токеном, ожидания ответа устройства, пакета с обратной связью, паузы после принятого пакета — итого 1026 бит.
  • Расщеплённая изохронная транзакция записи. Транзакция Start-Split имеет такую же структуру, как в случае транзакции записи по прерыванию, 449 + 8*(7/6)*(размер данных+3) бит. Транзакция Complete-Split отсутствует.


Планировщик


На 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, которая:
  • проходит по очереди оставшихся передач, вызывает callback-функции с ошибкой «канал закрылся» и освобождает все дескрипторы передач;
  • если это не последний канал устройства и бит «драйвер отказался от устройства» установлен, то просто освобождает память, выделенную под структуры канала;
  • если это не последний канал устройства и бит «драйвер отказался от устройства» сброшен, то добавляет канал в список каналов устройства, подлежащих освобождению;
  • если это последний канал устройства, то вызывает функции 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: общая схема
Часть 2: основы работы с хост-контроллерами
Часть 3: код поддержки хост-контроллеров
Часть 4: уровень поддержки каналов
Часть 5: уровень логического устройства
Часть 6: драйвер хабов
KolibriOS Project Team 69,56
Быстрая операционная система для бизнеса и хобби
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 0

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

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