Usb endpoint что это
При подключении внешнего устройства к порту USB система может неожиданно выбросить ошибку «Недостаточно ресурсов USB-контроллера». Во многих случаях ее можно наблюдать при подключении к портам USB 3.0.
Интерфейсы
Оконечные точки USB завёрнуты в интерфейсы . USB интерфейсы обрабатывают только один тип логического USB соединения, такого как мышь, клавиатура или аудио поток. Некоторые USB устройства имеют несколько интерфейсов, таких как USB динамик, который может состоять из двух интерфейсов: USB клавиатура для кнопок и USB аудио поток. Поскольку USB интерфейс представляет собой основную функциональность, каждый драйвер USB управляет интерфейсом; так что в случае динамика, Linux необходимы два разных драйвера для одного аппаратного устройства.
USB интерфейсы могут иметь дополнительные параметры настройки, которые представляют собой разные наборы параметров интерфейса. Первоначальное состояние интерфейса находится в первой настройке под номером 0. Другие параметры могут быть использованы, чтобы управлять отдельными оконечными точками по-разному, например, резервировать разные размеры полосы пропускания USB устройства. Каждое устройство с изохронной оконечной точкой использует дополнительные параметры на том же самом интерфейсе.
USB интерфейсы описаны в ядре структурой struct usb_interface . Эта структура является тем, что ядро USB передаёт в USB драйверы и тем, за чьё управление затем отвечает драйвер USB. Важными полями в этой структуре являются:
struct usb_host_interface *altsetting
Массив интерфейсных структур, содержащий все дополнительные настройки, которые могут быть выбраны для этого интерфейса. Каждая структура struct usb_host_interface состоит из набора конфигураций оконечной точки, определённой структурой struct usb_host_endpoint , описанной выше. Заметим, что эти интерфейсные структуры не имеют какого-то определённого порядка.
Количество дополнительных параметров, на которые указывает указатель altsetting .
struct usb_host_interface *cur_altsetting
Указатель на массив altsetting , обозначающий активные в настоящее время настройки для этого интерфейса.
Если драйвер USB, связанный с этим интерфейсом, использует старший номер USB, эта переменная содержит младший номер, присвоенный интерфейсу USB ядром. Это справедливо только после успешного вызова usb_register_dev (описываемого далее в этой главе).
В структуре struct usb_interface есть и другие поля, но USB драйверам знания о них не требуются.
Дескриптор
Здесь мы видим сначала заголовок дескриптора с полной длиной и другими неинтересными параметрами. Потом идет описание единственного интерфейса запоминающего устройства, в котором важно правильно указать поля Class, Subclass и Protocol — именно они отвечают за правильную идентификацию устройства в системе. Также важное поле bNumEndpoints, которое показывает сколько конечных точек нашему интерфейсу принадлежит. В нашем случае их две: на чтение и на запись. И тут же идут их описания, в которых внимание нужно уделить номеру (в номере конечной точки типа IN также выставлен 7-й бит, что прописано в дескрипторе как OR с 0x80) и размеру. Организация конечных точек в STM32 позволяет один номер точки использовать как на передачу, так и на прием. Существует еще альтернативный режим, в котором направление у точки одно, а буфер «комплементарной» точки используется для двойной буферизации. По идее, это может повысить скорость, но мы так делать пока не будем и воспользуемся более простым способом — приемник и передатчик, 0x01 и 0x81. А вот поле частоты опроса роли не играет вообще: пока данные для передачи есть, хост будет нашу точку дергать так часто, как только сможет, а когда данные закончатся — оставит в покое.
Еще пару слов надо сказать про размер конечных точек. Согласно стандарту [1], он должен быть равен 8, 16, 32 или 64 байта. Правда, покупные флешки как-то умудряются использовать и 512-байтные… В любом случае, делать полноценную флешку на контроллере общего назначения не самая удачная идея, так что оставим 64 байта. Да и места под буферы у нас немного.
Приведенные в этом примере константы для Class / Subclass / Protocol не являются единственно возможными. Скажем, можно попытаться эмулировать флоппик (Subclass = 0x04 вместо нашего 0x06). И оно при подключении даже показывает красивую иконку дисекты. Правда, не в винде — очевидно, она использует какие-то специфичные запросы и не верит, что бывают флоппики, их не поддерживающие. Но до специфичных запросов мы еще доберемся. Еще, если поменять Protocol, можно воспользоваться для обмена не только Bulk-точками, но и Interrupt. Но опять же, Interrupt мы не проходили, да и реальные флешки таким тоже не пользуются.
Помните, у нас в DeviceDescriptor (который почти ни за что не отвечает, поэтому не меняется и поэтому же здесь не приведен) есть поле iSerialNumber? Так вот, на этом поле растут грабли! Стандарт предписывает последние 12 символов использовать для идентификации экземпляра устройства. Соответственно, «хвост» этой строки должен представлять собой последовательность шестнадцатеричных цифр ('0'-'9', 'A'-'F'), закодированных в 16-битной кодировке. Есть подозрение, что перед ними можно оставить осмысленный текст. А практика показала, что и количество «цифр» может быть меньше 12-и.
Скажем, в моем примере вся строка состоит из единственного символа u''1'' и, кажется, работает. Но вот подставлять туда не-шестнадцатеричные символы все же не стоит: некоторые версии Windows такого пугаются и не хотят с устройством работать.
Конфигурации
Сами USB интерфейсы завёрнуты в конфигурации . USB устройство может иметь множество конфигураций и может переключаться между ними с целью изменения состояния устройства. Например, некоторые устройства, которые позволяют загружать в них программное обеспечение, для решения этой задачи содержат несколько конфигураций. В один момент времени может быть разрешена только одна конфигурация. Linux не очень хорошо обрабатывает множественную конфигурацию USB устройства, но, к счастью, они встречаются редко.
Linux описывает USB конфигурации структурой struct usb_host_config и устройства USB в целом структурой struct usb_device . Драйверам устройств USB обычно даже не требуется читать или записывать какие-то значения в эти структуры, поэтому подробности их здесь не описываются. Любопытный читатель может найти их описания в файле include/linux/usb.h в дереве исходных текстов ядра.
Драйверу USB устройства обычно требуется преобразовать данные из заданной структуры struct usb_interface в структуру struct usb_device , которая необходима ядру USB для широкого круга функциональных вызовов. Для этого предоставляется функция interface_to_usbdev . Надеемся, что в будущем все USB вызовы, которым в настоящее время требуется struct usb_device будут преобразованы для приёма параметра struct usb_interface и не будут требовать от драйверов выполнения преобразования.
Так что подводя итоги, USB устройства являются довольно сложными и состоят из множества разных логических единиц. Отношения между этими частями можно описать просто следующим образом:
SETUP запросы
Несмотря на то, что обмен данными идет только через Bulk-точки, кое-какая информация передается и через ep0 по соответствующим запросам. Нам понадобится всего два таких запроса — USBCLASS_MSC_RESET и USBCLASS_MSC_GET_MAX_LUN, причем первый (ресет) мы пока проигнорируем. А вот второй стоит рассмотреть подробнее. Дело в том, что запоминающее устройство по логике авторов стандарта состоит из независимых логических блоков (адресуемых по logical unit number, LUN), с каждым из которых можно общаться независимо. Дальше мы увидим, что в протокол обмена всегда входит поле bLUN, именно за это отвечающее. Всего в одном устройстве их может быть до 15 штук. Правда, никто не запрещает сделать составное устройство, где по 15 «носителей» будет в каждом. В общем, важная это штука, обрабатываем обязательно. Тем более что в качестве ответа на этот запрос достаточно вернуть всего один байт с номером последнего unit'а. Важно! Не количество, а именно номер. То есть если устройство у нас всего одно с lun=0, то и вернуть надо 0, а не 1.
probe и disconnect в деталях
В структуре struct usb_driver structure , описанной в предыдущем разделе, драйвер указывает две функции, которые в соответствующее время вызывает ядро USB. Функция probe вызывается, когда установлено устройство, которым, как думает ядро USB, должен управлять этот драйвер; функция probe должна выполнять проверки информации, переданной ей об устройстве, и решать, действительно ли этот драйвер подходит для этого устройства. Функция disconnect вызывается, когда по каким-то причинам драйвер не должен больше управлять устройством и может делать очистку.
Оба функции обратного вызова probe и disconnect вызываются в контексте потока USB узла ядра, так что засыпать в них допускается. Тем не менее, рекомендуется, чтобы большая часть работы выполнялась, когда устройство открыто пользователем, если это возможно, чтобы сократить время зондирования USB к минимуму. Такое требование появляется потому, что USB ядро обрабатывает добавление и удаление устройств USB в одном потоке, так что любой медленный драйвер устройства может привести к замедлению обнаружения USB устройства и это станет заметно для пользователя.
В функции обратного вызова probe , USB драйвер должен проинициализировать любые локальные структуры, которые он может использовать для управления USB устройством. Следует также сохранить в локальные структуры любую необходимую информацию об устройстве, так как это обычно легче сделать в данное время. Например, USB драйверы обычно хотят обнаружить адрес оконечной точки и размеры буферов для данного устройства, так как они необходимы для общения с устройством. Вот пример некоторого кода, который определяет две оконечные точки ВХОДА и ВЫХОДА поточного типа и сохраняет некоторую информацию о них в локальной структуре устройства:
В прошлый раз мы познакомились с общими принципами организации USB и собрали простое устройство, иллюстрирующее работу конечной точки типа Control. Пришло время изучать следующий тип — Bulk. Конечные точки такого типа предназначены для обмена большими объемами информации, причем чувствительной к надежности, но не скорости обмена.
Классические примеры — запоминающие устройства и переходники вроде USB-COM. Но переходники требуют еще наличия конечной точки типа Interrupt, которую мы пока «не проходили», так что остановимся на эмуляции флешки. Точнее, двух флешек одновременно.
Рекомендую параллельно сравнивать написанное с исходным кодом.
Переключение некоторых устройств на USB 2.0
Если ошибка возникла при подключении к порту USB 3.0, попробуйте обойти ее путем переключения некоторых устройств на классический порт 2.0. Если пытаетесь подключить оборудование, которое использует много конечных точек (VR или 7.1 гарнитура), может возникнуть соблазн использовать концентратор USB 3.0, чтобы воспользоваться всеми преимуществами, поставляющими с новым протоком передачи данных.
Но концентраторы нужно использовать только в ограниченной степени, поскольку довольно быстро превысите лимит в 16 конечных точек, например, при подключении гарнитур VR + 7.1. Но можно легко обойти эту проблему, подключением одного из устройств к обычному порту USB 2.0.
Что такое конечная точка (endpoint) USB?
Это основная форма взаимодействия по USB. Конечная точка передает данные только в одном направлении (от компьютера к устройству или наоборот). Вот почему в интерфейсе используется две конечные точки — одна на прием (IN), вторая на передачу (OUT).
При подключении USB-устройства компьютер создает несколько конечных точек (каналов связи, идущих к оборудованию или от него). Обычно флеш-накопителям достаточно 3-4 конечных точек входа и выхода, тогда как гарнитуры и прочие устройства могут использовать до 10.
- Превышен лимит USB-контроллера. Если подключено много внешних устройств, то можно превысить доступное количество конечных точек, что приведет к указанной ошибке. Имейте в виду, что контроллеры USB 3.0 имеют ограничение в 96 точек на интерфейсе Intel XHCI, тогда как AM4 поддерживают до 254.
- Использование конечных точек порта превысило лимит. Большинство USB-контроллеров ограничены 16 входными и выходными конечными точками для каждого порта.
- Энергия, требуемая для USB-устройства, превысила максимальный предел. Если проблема возникла на ноутбуке, скорее всего, количество энергии, запрашиваемой внешним устройством, превышает допустимый предел. В этом случае проблему можно решить с помощью USB-концентратора с собственным источником питания.
Организация памяти и прочие извращения
Раз уж решили реализовать несколько LUN'ов, имеет смысл и внутреннюю организацию им сделать максимально различной (впрочем, максимальной она не получилась, чуть позже объясню почему). Причем желательно обойтись без возни с подключением к контроллеру периферии Допустим, LUN=0 будет отображением части флешки контроллера (а поскольку возиться с записью на нее данных опять же лень, сделаем ее read-only), а LUN=1 — оперативки.
Объем флешки у L151 целых 256 кБ, но ведь нам его еще программировать, а это долго.
Ограничимся объемом 100 кБ: на таком объеме уже можно создать файловую систему FAT и даже место для файлов останется. Оперативки у нас поменьше, всего 32 кБ, от которых мы откусим 29 кБ и заполним первый «сектор» копией из образа флешки. Пусть тоже будет считаться FAT'ом, хотя и корявым. Впрочем, если будете экспериментировать с моим кодом, рекомендую взять образ флешки поменьше, чтобы не ждать минуту пока оно прошьется.
Не уверен, что это нужно, но все же расскажу как можно подготовить этот образ. Первым делом создаем «болванку» dd if=/dev/zero of=fatexample.img bs=1k count=100
Дальнейшие действия, увы, придется производить от рута:
создаем на «болванке» файловую систему mkfs.vfat fatexample.img
куда-нибудь ее монтируем mount fatexample.img /mnt -o user,umask=0
флажки в конце нужны чтобы пользоваться этой файловой системой мог обычный пользователь.
Собственно, от его (своего) имени и кидаем туда файлы. Из хулиганских соображений я предпочел записать туда исходники прошивки. Только надо учитывать, что винда не поймет обычный конец строки '\n', ей надо '\r\n'. То есть открываем каждый скопированный файл и меняем ему формат конца строки. Возможно, это как-то делается и из консоли, но я не искал.
Наконец снова заходим в рута дабы отмонтировать образ umount /mnt
Сразу предупреждаю: я не знаю как подобное делается в Windows. Не исключено, что по-человечески оно там вообще не делается, придется искать и скачивать какие-то сторонние программы. Ну или пользоваться для тестов каким-то из моих образов.
Теперь, когда бинарный образ готов, его надо как-то слинковать с остальным проектом. Для этого преобразуем образ в обычный объектный файл: arm-none-eabi-ld -r -b binary -o fatexample.o fatexample.img
Проблема в том, что линкер попытается разместить его в оперативной памяти и закономерно потерпит неудачу. Поэтому преобразуем его еще раз: arm-none-eabi-objcopy --rename-section .data=.rodata,alloc,load,readonly,data,contents fatexample.o fatexample.o
Вот теперь можно линковать. Чтобы не вводить эти команды каждый раз вручную, я прописал их прямо в makefile.
Переустановка драйверов
Проблема может вызвана неисправным драйвером контроллера. Для переустановки выполните следующие шаги.
Откройте Диспетчер устройств командой devmgmt.msc , запущенной из окна Win + R.
Разверните вкладку контроллеры USB, щелкните правой кнопкой мыши на универсальном хост-контроллере и выберите «Удалить». Если их несколько, удалите все.
Перезагрузите компьютер. В ходе следующей загрузки Windows автоматически переустановит отсутствующие драйвера.
Заключение
Вот мы и познакомились в общих чертах с принципом передачи данных в запоминающих устройствах и даже заставили макетную плату прикидываться одновременно read-only флешкой, накоторую записаны ее же исходники, и энерго-зависимой флешкой, которая информацию не сохраняет при отключении. Заодно протестировали работу конечной точки типа Bulk и обнаружили что ее использование не особо отличается от других.
Хотелось бы, конечно, еще сэмулировать флоппик и оптический дисковод, но там используются какие-то свои, специфичные команды. В рамках данной статьи это было бы неуместно.
Если кто-то захочет повторить данную конструкцию, исходный код как обычно в репозитоии, туда же я добавил найденную литературу, поскольку это уже какая-то нездоровая традиция создавать на ровном месте проблемы со скачиванием официальной документации.
[1] Universal Serial Bus Mass Storage Class Bulk-Only Transport копия
[2] Universal Serial Bus Mass Storage Class Specification Overview копия
[3] SCSI Multimedia Commands – 2 (MMC-2) копия
[4] SCSI Primary Commands-3 (SPC-3) копия
[5] SCSI Commands Reference Manual (от Seagate) копия
Коротко о USB - откуда все начинается. D+ и D- это дифференциальная пара, данные передаются в противофазе с одной лишь целью уменьшить помехи. То есть линия передачи по сути одна ! Есть ведущее устройство (Хост) и ведомое (Device).
Ведущее и ведомое могут одновременно что-то посылать в канал. Поэтому протокол USB очень требовательно распределяет , что ведущий и когда посылает и что (и когда) ведомый должен ответить. Иначе никак нельзя.
Вот на картинке ниже все отчетливо видно (один пакет от ведомого):
Отключение опции XHCI в настройках BIOS
Если все еще сталкиваетесь с ошибкой «Недостаточно ресурсов USB-контроллера», попробуйте отключить Intel xHCI Mode в настройках BIOS. Это приведет к тому, что все USB-порты 3.0 будут понижены до уровня 2.0.
Перезагрузите компьютер и при отображении первого экрана несколько раз нажмите на клавишу для входа в BIOS, которая указана в строке «Press _ to enter Setup» .
На вкладке Дополнительно (Advanced) найдите опцию USB EHCI debug в разделе Device Options. Включение этой опции отключит контроллер xHCI. Сохраните изменения, и загрузите компьютер.
Рисунок 13-2. Обзор USB устройства
Как происходит дальше работа на примере обычной клавиатуры
Хост долбит периодически PID IN по адресу устройства плюс Endpoint устройства (у нас EndP 0x01), который отвечает за прием данных от клавиатуры (IN для хоста).
Если никакая клавиша не нажата ведомый обязан ответить и отвечает NAK. Такие пакеты хост передает примерно 1 раз в 10ms и устройство если не нажата клавиша передает NAK.
А вот когда на клавиатуре нажимается какая-нибудь клавиша, ведомый ответит сначала DATA0 пакетом и следом пакет ACK.
Количество передаваемых байт в DATA0 зависит от типа клавиатуры, то есть каждый решает сколько использовать байт для передачи скан кода нажатой клавиши. Клавиатура сообщает по стандартному протоколу через EP0 о своих настройках.
Тут есть нюанс , что хост всегда посылает запрос устройству на конкретный EP. Если запрос идет на EP для передачи данных (у нас EP1 ) это одно , если запрос идет на служебный EP0 - это хост хочет подключить , настроить устройство. То есть хост всегда определяет логику обмена , а девайс обязан подстраиваться под запрос.
Вообще кто есть хост? Это драйвер например клавиатура или сетевого адаптера и у каждого драйвера соответственно свой протокол , своя логика.
Таким образом если вы разрабатываете USB устройство и ПК шлет вам все пакеты на EP0 , а до других EP не доходит дело, то значит что-то еще не закончено с настройками устройства, что-то хосту не нравится.
Хост вообще говоря может ждать ответ одновременно от 2 и более конечных точек . Это абсолютно нормально. Выглядит это в логах анализатора LA1010 примерно так:
Видно как хост тупо чередует EP0 и EP2.
Сначала все просто:
Пакет всегда начинается с SYN (10000000).
Завершается пакет всегда EOP (End Of Packet ) . На картинке выше видна единственная ассиметрия в конце пакета, когда : 2 линии DP и DM различаются.
Примерная последовательность пакетов.
Инициализацию устройства пропускаем (запрос дескриптора, интерфейсов , конечных точек и т.д.), чтобы не терять времени (переходим к сути).
Периодические пакеты "НЕ СПАТЬ" SOF (Start Of Frame) - это примерно 1раз/1мс посылка от хоста ведомому ("не спи"). Их лучше сразу как-то отфильтровывать для понимания.
Далее остается три типа пакетов типа . Ниже их PID (Packet Identificator) , он же токен :
SETUP это служебные пакеты стандартного протокола настройки устройства
OUT это хост передает данные
IN это хост запрашивает данные от девайса
Эти пакеты вкладываются между SYNK и EOP .
Получается примерно такая структура [SYNC] [PID] [Address(7 бит)] [EndPoint] (4 бит) [EOP ]. На картинке выше видно как девайс отвечает NAK практически сразу и это нормально. Это означает , что девайсу надо подумать и сразу он не может ответить на команду.
PID это токен или (Program Identificator) SETUP, IN , OUT.
Address - это адрес нашего устройства на шине USB . Сначала он всегда 0 после подключения USB. Потом хост перенумеровывает все устройства на шине и присваивает каждому устройству уникальный адрес (размер всего 1 байт).
EndPoint - хост всегда общается не просто с устройством по адресу , а еще и с конкретной конечной точкой (end-point) устройства , которых может быть несколько. Как же хост узнает какие значения у конечных точек (EP) ? Правильно для этого зарезервировано значение 0 (конечная точка EP0), служебный end-point , через который хост получает первичную информацию от других конечных точках. Как всегда все просто.
Допустим наш хост уже получил всю информацию о конечных точках , интерфейсах, конфигурациях через EP0.
На начальном этапе обмена хосту надо выяснить информацию о подключаемом оборудовании через SETUP пакеты:
Примечание: на SETUP пакет устройство не должно отвечать пакетом NAK, пакет SETUP обязательный к рассмотрению.
Пакет STALL передается USB-устройством в случае серьезных ошибок, чтобы сообщить хосту о невозможности дальнейшей работы.
Использование USB-концентратора с собственным источником питанием
Если сталкиваетесь с проблемой на ноутбуке, скорее всего, она возникает из-за общего количества энергии, извлекаемой из USB-портов,
Если не можете ограничить количество внешних устройств, получающих питание от USB-портов, попробуйте устранить ошибку путем подключения USB-концентратора с собственным источником питания.
Если не возникает какого-то прерывания у девайса
То есть если на шине пакеты бегут, а прерывание необходимое не возникает. Например тупо не возникает прерывание IN bulk у RNDIS адаптера (DataIn у EP2). То есть на шине вижу , что девайс отсылает NAK на IN EP2, но самого прерывания в девайсе не возникает.
Тут надо в регистры лезть и отсрочки уже не будет. Какие мысли возникают в первую очередь. Прерывания маскируются вроде (надо проверить).
Так как у нас есть один рабочий проект но без FreeRTOS , то сначала тупо начинаем сверять регистры USB ( OTG_FS_GLOBAL и OTG_FS_DEVICE ): после инициализации , после открытия конечных точек, после приема нужного пакета и т.д. Эти регистры кстати удобно просматривать на закладке SFRS (в Atollic true Studio), тут видна их внутренняя структура. И еще с момента последней точки остановки подсвечиваются изменения.
В процессе сверки регистров мы находим отличия в OTG_FS_GLOBAL, исправляем, заодно изучаем назначение регистров и в какой-то момент даже ловим __HAL_PCD_IS_INVALID_INTERRUPT (на картинке выше видно). Ура хоть что-то.
На самом деле не знач - не ведая мы подошли к главному моменту. Мы наконец-то обратили внимание на USBD_LL_Init, а точнее на загадочные функции HAL_PCDEx_SetRxFiFo(..) и HAL_PCDEx_SetTxFiFo(..) .
Какие устройства поддерживает драйвер?
Структура struct usb_device_id содержит список различных типов USB устройств, которые поддерживает этот драйвер. Этот список используется ядром USB, чтобы решить, какой драйвер предоставить устройству, или скриптами горячего подключения, чтобы решить, какой драйвер автоматически загрузить, когда устройство подключается к системе.
Структура struct usb_device_id определена со следующими полями:
Определяет, какие из следующих полей в структуре устройства должны сопоставляться. Это битовое поле определяется разными значениями USB_DEVICE_ID_MATCH_* , указанными в файле include/linux/mod_devicetable.h . Это поле, как правило, никогда не устанавливается напрямую, а инициализируется с помощью макросов типа USB_DEVICE , описываемых ниже.
Идентификатор поставщика USB для устройства. Этот номер присваивается форумом USB для своих членов и не может быть присвоен кем-то еще.
Идентификатор продукта USB для устройства. Все поставщики, которые имеют выданный им идентификатор поставщика, могут управлять своими идентификаторами продукта, как они предпочитают.
Определяют нижнюю и верхнюю границу диапазона назначаемого поставщиком номера версии продукта. Значения bcdDevice_hi является включительным; его значение является значением наибольшего номера устройства. Обе эти величины представлены в двоично-десятичной (BCD) форме. Эти переменные в сочетании с idVendor и idProduct используются для определения данного варианта устройства.
Определяют класс, подкласс и протокол устройства, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB. Эти значения определяют поведение для всего устройства, в том числе все интерфейсы на этом устройстве.
Подобно зависимым от устройства вышеприведённым величинам, эти определяют класса, подкласс и протокол отдельного интерфейса, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB.
Это значение не используется для сравнения, но оно содержит информацию о том, что драйвер может использовать, чтобы отличать разные устройства друг от друга в функции обратного вызова probe драйвера USB.
Как и с PCI устройствами, существует ряд макросов, которые используются для инициализации этой структуры:
Создаёт struct usb_device_id , которая может быть использована только для соответствия указанными значениям идентификаторов поставщика и продукта. Она очень часто используется для устройств USB, которым необходим специальный драйвер.
USB_DEVICE_VER(vendor, product, lo, hi)
Создаёт struct usb_device_id , которая может быть использована только для соответствия указанным значениям идентификаторов поставщика и продукта внутри диапазона версий.
USB_DEVICE_INFO(class, subclass, protocol)
Создаёт struct usb_device_id , которая может быть использованы для соответствия определённому классу USB устройств.
USB_INTERFACE_INFO(class, subclass, protocol)
Создаёт struct usb_device_id , которая может быть использована для соответствия определённому классу USB интерфейсов.
Итак, для простого драйвера USB устройства, который управляет только одним USB устройством от одного поставщика, таблица struct usb_device_id будет определяться как:
/* таблица устройств, которые работают с этим драйвером */
static struct usb_device_id skel_table [ ] =
MODULE_DEVICE_TABLE (usb, skel_table);
Как и с драйвером PCI, необходим макрос MODULE_DEVICE_TABLE , чтобы разрешить инструментам пространства пользователя выяснить, какими устройствами может управлять этот драйвер. Но для USB драйверов первым значением в этом макросе должна быть строка usb .
Основной структурой, которую должны создать все USB драйверы, является struct usb_driver . Эта структура должна быть заполнена драйвером USB и состоит из ряда функций обратного вызова и переменных, описывающих USB драйвер для кода USB ядра:
struct module *owner
Указатель на модуль владельца этого драйвера. Ядро USB использует его для правильного подсчёта ссылок на этот драйвер USB, чтобы он не выгружался в несвоевременные моменты. Переменной должен быть присвоен макрос THIS_MODULE .
const char *name
Указатель на имя драйвера. Он должен быть уникальным среди всех USB драйверов в ядре и, как правило, установлен на такое же имя, что и имя модуля драйвера. Оно проявляется в sysfs в /sys/bus/usb/drivers/ , когда драйвер находится в ядре.
const struct usb_device_id *id_table
Указатель на таблицу struct usb_device_id , которая содержит список всех различных видов устройств USB, которые драйвер может распознать. Если эта переменная не установлена, функция обратного вызова probe в драйвере USB никогда не вызывается. Если вы хотите, чтобы ваш драйвер всегда вызывался для каждого USB устройства в системе, создайте запись, которая устанавливает только поле driver_info :
static struct usb_device_id usb_ids[ ] =
int (*probe) (struct usb_interface *intf, const struct usb_device_id *id)
Указатель на зондирующую функцию в USB драйвере. Эта функция (описанная в разделе "probe и disconnect в деталях") вызывается USB ядром, когда оно думает, что оно имеет структуру usb_interface , которую этот драйвер может обработать. Указатель на struct usb_device_id , который использовало USB ядро, чтобы принять это решение также передается в эту функцию. Если USB драйвер признаёт переданную ему структуру usb_interface , он должен правильно проинициализировать устройство и вернуть 0. Если драйвер не хочет признавать устройство или произошла ошибка, он должен вернуть отрицательное значение ошибки.
void (*disconnect) (struct usb_interface *intf)
Указатель на функцию отключения в USB драйвере. Эта функция (описанная в разделе "probe и disconnect в деталях") вызывается USB ядром, когда структура usb_interface была удалена из системы, или когда драйвер выгружается из ядра USB.
Таким образом, чтобы создать значимую структуру struct usb_driver , должны быть проинициализированы только пять полей:
static struct usb_driver skel_driver =
struct usb_driver содержит несколько больше обратных вызовов, которые, как правило, очень часто не используются, и не требуются для правильной работы USB драйвера:
int (*ioctl) (struct usb_interface *intf, unsigned int code, void *buf)
Указатель на функцию ioctl в USB драйвере. Если он присутствует, то вызывается, когда программа пользовательского пространства делает вызов ioctl для записи файловой системы устройств usbfs , связанной с устройством USB, относящемуся к этому USB драйверу. На практике только драйвер USB концентратора использует этот ioctl , так как любому другому USB драйверу нет иной реальной необходимости его использовать.
int (*suspend) (struct usb_interface *intf, u32 state)
Указатель на функцию приостановки в USB драйвере. Она вызывается, когда работа устройства должна быть приостановлена USB ядром.
int (*resume) (struct usb_interface *intf)
Указатель на функцию возобновления в USB драйвере. Она вызывается, когда работа устройства возобновляется USB ядром.
Чтобы зарегистрировать struct usb_driver в USB ядре, выполняется вызов usb_register_driver с указателем на struct usb_driver . Для USB драйвера это традиционно делается в коде инициализации модуле:
static int __init usb_skel_init(void)
/* регистрируем этот драйвер в подсистеме USB */
err("usb_register failed. Error number %d", result);
Когда драйвер USB будет выгружаться, необходимо разрегистрировать struct usb_driver в ядре. Это делается с помощью вызова usb_deregister . Когда происходит этот вызов, любые USB интерфейсы, которые в настоящее время связаны с этим драйвером, отключаются и для них вызывается функция disconnect .
static void __exit usb_skel_exit(void)
/* отменяем регистрацию этого драйвера в подсистеме USB */
READ / WRITE
Ну и наконец самое интересное — чтение и запись данных. На самом деле это не один запрос, а целое семейство, отличающихся размером. Скажем, бывают READ(6), READ(10), READ(12), даже READ(16), которые занимают в msc_cbw.CB соответственно 6, 10, 12 и 16 байтов. Мы будем пользоваться READ(10). И WRITE(10), естественно. Формат у них одинаковый, отличается только направление передачи: от хоста к устройству или от устройства к хосту. Структура запроса такова:
Из нее нас интересуют только поля block_address и length — номер первого блока, который предстоит прочитать и количество этих блоков. Напоминаю, что размер блока мы сообщили хосту раньше, по запросу SCSI_READ_CAPACITY и что составляет он у нас 512 байт.
Следовательно, адрес первого байта, с которого начнем чтение, равен block_address * 512, а их суммарное количество length * 512.
Из соображений простоты демонстрационного кода (ну и из лени, конечно) посекторной работы с внешней памятью вы здесь не увидите. В самом деле, что к оперативке, что к флешке контроллера доступ побайтный. Но вот при взаимодействии с более сложной периферией вроде SD-карточек уже придется лавировать между прерываниями USB и этой периферии. Возможно, кстати, работу с USB будет проще осуществлять опросом, чем прерываниями. По той же причине выбор нужного буфера на чтение или запись остался в обработчике IN и OUT, а не в scsi_command.
Принцип обмена bbb
BBB (bulk/bulk/bulk) или, что тоже самое, BOT (bulk only transport) — протокол обмена [1], при котором используется единственный тип конечной точки. Через нее передаются команды, через нее же передаются данные и через нее же успешность команд контролируется. Собственно всю логику обмена я сейчас и описал. Перейдем к подробностям:
Передача команд осуществляется всегда от хоста к устройству, то есть через конечную точку типа OUT, и представляет собой структуру следующего вида:
поле dSignature — волшебное чиселко, равное 0x43425355 (или же 4 символа ''USBC''), передающееся хостом для синхронизации. Благодаря им устройство могло более-менее достоверно отличить начало команды от простого потока данных. Дальше идет dTag — порядковый номер команды чтобы если хост не дождался завершения команды и послал другую, смог отличить ответы. Это число надо будет куда-нибудь сохранить, а потом хосту вернуть.
Следующее поле, dDataLength, ограничивает количество байтов ответа. То есть наша посылка не может быть больше, чем dDataLength байт. Поле bmFlags для нас бесполезно, оно по большей части состоит из устаревших настроек. А вот bLUN это то, о чем я говорил раньше — номер «носителя», с которым хочет пообщаться хост. Если носитель у вас единственный, оно всегда будет равно нулю. Но в данном примере мы сделаем их два, так что этот самый LUN придется активно читать. bCBLength — снова бесполезное поле, которое показывает размер дополнительных данных… как будто мы его и так не знаем. И наконец, CB[] — данные, специфичные для конкретного запроса. Их мы будем рассматривать только применительно собственно к запросам. Хотя нет, не совсем так. Поле CB[0] собственно за запрос отвечает, поэтому его мы будем читать и по нему же определять как на данный запрос реагировать.
А реакция может заключаться либо в чтении данных размером dDataLength от хоста, либо запись данных того же объема. Формат этих данных зависит от принятой команды, так что пока не будем углубляться.
И наконец идет подтверждение — специальная структура следующего вида:
Поле dSignature, как и в случае запроса, является магическим чиселком, но другим: 0x53425355 (оно же строка u''USBS''). А вот поле dTag должно в точности совпадать с полученным нами при начале обмена. Смысл следующего поля, dDataResidue, я не слишком понял. Вроде бы оно содержит количество данных, которое мы хотели передать хосту, но в dDataLength не влезло, но не похоже чтобы значение там на что-то влияло. Пожалуй, самое важное поле здесь — bStatus. Если что-то пошло не так, по нему хост может увидеть, что команда завершилась ошибкой и надо что-то делать.
В результате первая посылка всегда идет от хоста, вторая либо от хоста, либо от нас, и третья — всегда от нас.
Реализация этого алгоритма может показаться неочевидной, поэтому рассмотрю ее подробнее:
Как уже было сказано, обмен начинается с того, что в конечную точку OUT приходит посылка от хоста. Проблема в том, что размер посылки составляет 31 байт, а размер конечной точки может быть и 8 байт, так что стоит предусмотреть прием по частям. К счастью, пока мы не ответим на один запрос, другого нам слать не будут (если не вылетим по таймауту, конечно), поэтому для хранения запроса и ответа заведем глобальные переменные
И до тех пор пока количество принятых байтов не сравняется с размером запроса, читать будем именно туда. Если последний байт принят «в нашу смену», то не спешим выходить, а сразу запускаем обработчик команд scsi_command() и даже пересылку ответа (мы ведь помним, что для начала IN транзакции первый пакет надо передать вручную). Но вот запросы на чтение обрабатывать сразу не выйдет, ведь в буфере приема у нас «хвост» команды, а вовсе не данные.
Поэтому scsi_command() только выставляет количество данных (bytestoread). В частности, может выставить в 0 чтобы показать что чтение не нужно. Таким образом дальнейший прием будет повторяться пока количество реально принятых байтов bytescount не достигнет желаемого. После чего все равно произойдет ручной вызов обработчика конечной точки IN, которая пошлет если не данные, то хотя бы отчет об успешности.
Собственно, устройство точки IN не слишком отличается от OUT. Основная разница, что она сначала пытается передать bytestowrite байтов и только потом структуру msc_csw с использованием ее персонального счетчика msc_csw_count.
Оконечные точки
Самый основной формой USB взаимодействия является то, что называется endpoint (оконечная точка) . Оконечная точка USB может переносить данные только в одном направлении, либо со стороны хост-компьютера на устройство (называемая оконечной точкой OUT ) или от устройства на хост-компьютер (называемая оконечной точкой IN ). Оконечные точки можно рассматривать как однонаправленные трубы.
Оконечная точка USB может быть одной из четырёх типов, которые описывают, каким образом передаются данные:
Оконечные точки прерывания передают небольшие объёмы данных с фиксированной частотой каждый раз, когда USB хост запрашивает устройство для передачи данных. Эти оконечные точки являются основным транспортным методом для USB клавиатур и мышей. Они также часто используются для передачи данных на USB устройства для управления устройством, но обычно не используются для передачи больших объёмов данных. Эти передачи гарантируются протоколом USB, чтобы всегда иметь достаточную зарезервированную пропускную способность для их передачи.
Изохронные оконечные точки также передают большие объёмы данных, но этим данным не всегда гарантирована доставка. Эти оконечные точки используются в устройствах, которые могут обрабатывать потери данных, и больше полагаются на сохранение постоянного потока поступающих данных. При сборе данных в реальном времени, таком, как аудио- и видео-устройства, почти всегда используются такие оконечные точки.
Управляющие и поточные оконечные точки используются для асинхронной передачи данных, когда драйвер решает их использовать. Оконечные точки прерывания и изохронные точки являются периодическими. Это означает, что эти оконечные точки созданы для передачи данных непрерывно за фиксированное время, что приводит к тому, что их пропускная способность защищена ядром USB.
Оконечные точки USB описаны в ядро структурой struct usb_host_endpoint . Эта структура содержит реальную информацию оконечной точки в другой структуре, названной struct usb_endpoint_descriptor . Последняя структура содержит все специфичные USB данные точно в том формате, который указало само устройство. Полями этой структуры, с которыми имеют дело драйверы, являются:
Это тип оконечной точки. В этом значении должна присутствовать битовая маска USB_ENDPOINT_XFERTYPE_MASK , чтобы определить, является ли конечная точка типом USB_ENDPOINT_XFER_ISOC , USB_ENDPOINT_XFER_BULK или типом USB_ENDPOINT_XFER_INT . Эти макросы определяют изохронные, поточные и оконечные точки прерывания, соответственно.
Если эта оконечная точка имеет тип прерывания, это значение является установкой интервала для оконечной точки, то есть времени между запросами прерывания для оконечной точки. Это значение представляется в миллисекундах.
Поля этой структуры не имеют "традиционной" для ядра Linux схемы именования. Это происходит потому, что эти поля напрямую соотносятся с именами полей в спецификации USB. Программисты ядра USB посчитали, что было более важно использовать специфицированные имена, чтобы избежать путаницы при чтении спецификации, чем иметь имена переменных, которые выглядят знакомо для программистов Linux.
Причины ошибки
Как оказалось, ошибка редко связана с пропускной способностью или питанием устройства. Скорее всего, она возникает из-за ограничения конечной точки.
Обязательные команды
Все команды, с которыми мы будем иметь дело, относятся к семейству SCSI (Small Computer System Interface) — интерфейсу обмена данными с носителями информации и много чем еще. Причем не только по USB.
Обязательными для реализации являются следующие (достаточно подробно описаны тут):
Также еще несколько запросов, которые формально не обязательны, но лучше бы их реализовать
Первым делом хост запрашивает у носителей данных подробности их внутреннего устройства, посылая для этого запрос SCSI_INQUIRY. В ответ он ожидает очередную волшебную структуру, подробно рассматривать которую я не хочу. Для нашего случая достаточно скопировать готовую и немного поиграться с константами. Скажем, поменять строку вендора. Или, например, нулевой байт изменить с 0x00 на 0x05 чтобы данный LUN считался не просто носителем, а CD/DVD диском. Правда, одного этого недостаточно: необходимо дописать поддержку каких-то специфичных запросы. Поэтому уж настолько извращаться не будем… а жаль
Далее идет запрос емкости (SCSI_READ_CAPACITY), на который надо ответить двумя 32-битными числами (суммарно 8 байт, очевидно): 0-3 байты это номер последнего блока, а 4-7 это размер одного блока. Дело в том, что носители данных не обеспечивают доступ к отдельному байту — только к блоку размером обычно 512 байт. На такие же блоки устройство поделим и мы.
Обратите внимание, что передается не количество блоков, а, как и в случае LUN, номер последнего. То есть uint32_t last_lba = capacity / 512 — 1;
Внимание, грабли! Винда почему-то не полагается на запрос SCSI_READ_CAPACITY, а отправляет SCSI_MMC_READ_FORMAT_CAPACITY с немного другим форматом. И ее не волнует что этот запрос не обязателен для реализации, так что после получения ошибки «запрос не поддерживается» она еще долго отключает-подключает устройство в надежде что то образумится. Если не хотите полчаса ждать пока винда таки смирится что неподдерживаемый запрос не поддерживается, лучше этот запрос реализовать. Как несложно догадаться, линукс ведет себя адекватно и понимает с первого раза.
Еще один запрос, который имеет смысл обработать — SCSI_MODE_SENSE_6. Точнее, опять забить вместо ответа чей-то готовый кусок. 1-й байт в нем отвечает за размер посылки и будет равен 3, второй байт описывает тип носителя (что-то вендор-специфичное, оставим ноль).
Третий байт самый для нас интересный. Его 7-й бит означает защиту от записи, его мы и взведем для 0-го LUN чтобы хост даже не пытался писать в нашу флеш-память. Вот в LUN=1 (оперативка) — другое дело, там этот бит будет нулевым и пусть пишет на здоровье.
Потом хост проверяет готов ли вообще носитель с нужным номером к обмену (SCSI_TEST_UNIT_READY), причем пакет данных ему для ответа не нужен — достаточно кода ошибки: готов — не готов.
Далее по хронологии должен идти рассказ про чтение и запись, но их мы оставим на конец.
Хост может попытаться управлять подключением носителей данных при помощи команд SCSI_MMC_START_STOP_UNIT и SCSI_MMC_PREVENT_ALLOW_REMOVAL, причем если они не поддерживаются, ругается что не может отмонтировать устройство. Впрочем, заглушки, возвращающей «все хорошо» ему достаточно, так что глубже я и не копал. В реальном применении покопать все же придется, поскольку некоторой периферии нужно больше нуля секунд для корректного завершения работы. А пока она медленно отключается, надо попросить хоста чтобы подождал.
Если что-то пошло не так (на этапе подтверждения устройство вернуло ошибку), хост запрашивает по этой ошибке подробности (SCSI_REQUEST_SENSE) — массив из трех байтов, кратко поясняющих что же именно случилось. Чаще всего у нас будет случаться SBC_SENSE_KEY_ILLEGAL_REQUEST (неподдерживаемый запрос), но в принципе таким же способом сообщают о неготовности носителя данных и многом другом. Кстати, раз запрос подробностей ошибки — отдельный запрос, то и хранить результат предыдущего надо в отдельной переменной, msc_sense.
Опять момент истины
Дальше , если интересно немного о передаваемых скан кода клавиатуры . Проводная клавиатура Low Speed
Скан коды USB HID клавиатур это не ASCII коды и не не коды PS/2 клавы.
Момент истины
И выяснилось , что мы не понимаем и половины логики работы USB . Не зная регистры вообще нет возможности понять что делать. В данном случае HAL это вред.
Итак HAL_PCDEx_SetRxFiFo / HAL_PCDEx_SetTxFiFo создает таблицу во внутренней памяти контроллера USB. Да именно контроллера USB , а не контроллера STM32. Так как у STM32F имеется как-бы свой встроенный контроллер , отвечающий за USB. И у него есть своя память 512К, в которой надо создать таблицу с буферами приема / передачи для каждой конечной точки.
Где эта таблица, где ее адреса.
А вот сама структура USB_OTG_GlobalTypeDef .
HAL - кий код становится намного прозрачнее теперь.
Читайте также: