Недавно мне довелось написать драйверы для специализированного
USB-сканера. Этот сканер работает в режиме непрерывной развертки с
частотой 10 кадров в секунду и обеспечивает скорость потока на чтение
3,2 МБайт/сек.
Драйверы сначала были разработаны под ядро Linux версии 2.6.15, а
затем, по требованию заказчика, адаптированы под ядро версии 2.4.26. К
моему удивлению, в обоих упомянутых версиях ядра написать драйвер не
составило особого труда. Для этого лишь необходимо четко представлять,
как работает USB и какими функциями обеспечивается взаимодействие
драйвера с USB-устройством. Все остальное - уже давно реализовано в
ядре linux.
При написании этой статьи я вижу перед собой следующие задачи:
1. рассказать об основных свойствах обмена устройств по USB
2. описать программный интерфейс взаимодействия драйвера с
USB-устройством
3. привести примеры использования функций ядра
Я надеюсь, что данная информация будет интересна программистам
системного уровня.
Введение в USB
Наверное, нет смысла говорить о таких очевидных вещах, как широкое
распространение USB, высокая скорость обмена по USB, возможность
горячего подключения устройств. Сейчас уже каждый пользователь ПК так
или иначе оценил преимущества и плюсы USB.
Поэтому сразу перейдем к менее очевидным вещам - к тому, как USB
устроен внутри.
Кто главный?
Одна из главных концепций USB заключается в том, что в USB-системе
может быть только один мастер. Им является host-компьютер. USB -
устройства всегда отвечают на запросы host-компьютера - они никогда не
могу посылать информацию самостоятельно.
Есть только одно исключение: после того, как хост перевел устройство в
suspend-режим, устройство может посылать запрос remote wakeup. Во всех
остальных случаях хост формирует запросы, а устройства отвечают на
них.
Направление
Хост всегда является мастером, а обмен данными должен осуществляться в
обоих направлениях:
* OUT - отсылая пакет с флагом OUT, хост отсылает данные устройству
* IN - отсылая пакет с флагом IN, хост отправляет запрос на прием
данных из устройства.
К этому нужно просто привыкнуть. Чтобы принять данные из устройства,
хост отсылат пакет с флагом IN. smile
Классификация пакетов в USB
По USB может передаваться несколько типов пакетов:
1. Token - запрос, содержит управляющую информацию: направление
операции (IN, OUT), номер endpoint
2. Data - пакет данных
3. Handshake - служебные пакеты, могут содержать подтверждение (ACK),
сообщение об ошибке, отказ (NACK)
4. Special - служебные пакеты, такие как PING
Более подробную информацию о пакетах, предусмотренных в спецификации
USB, можно прочитать в [2,3]. Рассмотрим несколько примеров
использования этих пакетов.
Пример: отсылка данных устройству
Чтобы отослать данные устройству, хост посылает пакет Token "OUT",
затем пакет Data. Если устройство готово обработать принятые данные,
оно отсылает пакет Handshake "ACK", подтверждающий транзакцию. Если
оно занято, оно отсылает отказ - Handshake "NACK". Если произошла
какая-то ошибка, то устройство может не отсылать Handshake.
Пример: отсылка данных хосту
Как уже говорилось, устройство самостоятельно никогда не отсылает
данные. Только по запросу. Чтобы принять данные, хост посылает пакет
Handshake "IN". Устройство по запросу может отослать пакет Data, а
затем Handshake "ACK". Либо может отослать Handshake "NACK", не
посылая Data.
Типы передачи данных
Спецификация USB определяет 4 типа потоков данных:
1. bulk transfer - предназначен для пакетной передачи данных с
размером пакетов 8, 16, 32, 64 для USB 1.1 и 512 для USB 2.0.
Используется алгоритм перепосылки (в случае возникновения ошибок),
а управление потоком осуществляется с использованием handshake
пакетов, поэтому данный тип является достоверным. Поддерживаются
оба направления - IN и OUT.
2. control transfer - предназначен для конфигурирования и управления
устройством. Также, как и в bulk, используются алгоритмы
подтверждения и перепосылки, поэтому этот тип обеспечивает
гарантированный обмен данными. Направления - IN (status) и
OUT(setup, control).
3. interrupt transfer - похож на bulk. Размер пакета - от 1 до 64
байт для USB 1.1 и до 1024 байт для USB 2.0. Этот тип гарантирует,
что устройство будет опрашиваться (то есть хост будет отсылать ему
token "IN") хостом с заданным интервалом. Направление - IN.
4. isochronous transfer - предназначен для передачи данных без
управления потоком (без подтверждений). Область применения -
аудио-потоки, видео-потоки. Размер пакета - до 1023 байт для USB
1.1 и до 1024 байт для USB 2.0. Предусмотрен контроль ошибок (на
приемной стороне) по CRC16. Направления - IN и OUT.
Endpoint - источник/приемник данных
Спецификация USB определеят endpoint (EP), как источник или приемник
данных. Устройство может иметь до 32 EP: 16 на прием и 16 на передачу.
Обращение к тому или иному endpoint'у происходит по его адресу.
Например, допустим, что хост хочет прочитать пакет данных из EP4 (4го
endpoint'a) устройства, пользуясь типом "bulk transfer". Он отсылает
пакет token "in", в котором указывает адрес endpoint'a.
Соответствующий источник в устройстве отсылает пакет данных хосту.
Аналогично происходит передача пакета данных.
Endpoint No.0
EP0 имеет особое значение для USB. Это Control EP. Он должен быть в
каждом USB-устройстве. Этот EP использует token "setup", чтобы
сигнализировать, что данные, отправляемые после него, предназначены
для управления устройством.
Используя этот EP0, хост может передавать setup-пакет длиной 8 байт и
данные, которые следуют за этим пакетом. Во многих случаях может
хватать передачи только setup-пакета. Однако устройство может
использовать и передачу данных по EP0, например для смены прошивок
компонентов устройства, или получения расширенной информации об
устройстве.
Рассмотрим немного подробнее setup-пакет.
EP0: setup-пакет
Содержимое setup-пакета представлено в таблице
Байт (No.) Имя Назначение
0 bmRequestType Поле для указания типа запроса, направления,
получателя
1 bRequest идентификатор запроса
2 wValueL 16-битное значение wValue, зависит от запроса.
3 wValueH
4 wIndexL 16-битное значение wIndex, зависит от запроса.
5 wIndexH
6 wLengthL количество байт, отсылаемых после setup-пакета.
7 wLengthH
Как видно из таблицы, setup-пакет содержит 5 полей. bmRequestType и
bRequest определяют запрос, а wValue, wIndex и wLength - его свойства.
Спецификация USB резервирует диапазон значений bRequest под
стандартные запросы. Каждое устройство обязано отвечать на все
стандартные запросы. В следующей таблице приведены только несколько
стандартных запросов, с которыми мы будем сталкиваться дальше.
bRequest Имя Описание
0x05 Set Address установка уникального адреса устройства в системе
0x06 Get Descriptor получение информации об устройстве. Тип информации
зависит от поля wValue.
Остальной диапазон устройство может использовать по своему усмотрению.
Более расширенную информацию можно получить в [2,3].
Распознавание устройства
Как происходит распознавание устройства, только что подключившегося к
системе? Уже упоминалось, что каждое устройство обязано обеспечить
доступ к EP0. Но кроме этого, оно еще должно отвечать на запросы,
указанные в спецификации USB для EP0. Пользуясь этими запросами и
происходит распознавание устройства в системе.
Алгоритм детектирования нового устройства следующий:
1. хост отсылает setup-пакет "Get Descriptor" (wValue = "device").
2. хост получает идентифицирующую информацию об устройстве
3. хост отсылает setup-пакет "Set address", после чего устройство
получает уникальный адрес в системе
4. хост отсылает остальные setup-пакеты "Get Descriptor" и получает
дополнительную информацию об устройстве: количество EP, требования
к питанию, и т.п.
Поддержка USB в ядре Linux
Программный интерфейс для взаимодействия с USB устройствами в ядре
Linux очень прост. За простым интерфейсом скрываются все алгоритмы
отсылки запросов, отслеживания подтверждений, контроля ошибок и т.п.
Все тонкости, описанные в предыдущей главе, уже реализованы в ядре
Linux.
В ядре файлы программ располагаются в drivers/usb/, а заголовочные
файлы - в include/linux/. Информации, представленной в этих
директориях, достаточно, чтобы самостоятельно написать драйвер для
любого USB-устройства.
Драйвер, взаимодействующий с USB-устройством(-ами), как правило,
выполняет следующие действия:
1. регистрация/выгрузка драйвера
2. регистрация/удаление устройства
3. обмен данными: управляющий и информационный.
Рассмотрим их по порядку более подробно, применительно к реализации
под ядро 2.6.15. Для большей ясности необходимо понимать основные
принципы UDM [4, 5], появившиеся в ядре 2.6.
Регистрация/выгрузка драйвера.
Регистрация USB-драйвера подразумевает:
1. заполнение структуры usb_driver
2. регистрацию структуры в системе
Структура usb_driver описана в include/linux/usb.h Рассмотрим наиболее
важные поля этой структуры.
Очевидно, что name - это имя драйвера. id_table - это массив структур
usb_device_id. Этот список предназначен для определения cоответствия
подключаемого устройства определенным параметрам. Только те
устройства, которые соответствуют перечисленным параметрам, могут быть
подключены к драйверу. Если массив пуст, система будет пытаться
подключить каждое устройство к драйверу.
Поле driver говорит о том, что usb_driver унаследован от device_driver.
В самом простом случае каждый элемент id_table[i] содержит пару
идентификаторов:
* идентификатор производителя (Vendor ID)
* идентификатор устройства (Device ID).
Определение структуры usb_device_id можно видеть в
include/linux/mod_devicetable.h
probe и disconnect - это callback-функции, вызываемые системой при
подключении и отключении USB-устройства. probe будет вызыван для
каждого устройства, если список id_table пуст, или только для тех
устройств, которые соответствуют параметрам, перечисленным в списке.
В этом примере регистрируется драйвер USB-устройства, которое имеет
значения полей PRODUCT_ID = 0x1, VENDOR_ID = 0x1234. Только для
устройства с такими параметрами будет вызвана функция my_probe.
Вызов my_probe фактически означает регистрацию устройства в драйвере
my_driver, а вызов my_disconnect - удаление устройства. Поэтому
перейдем к следующему этапу - регистрация/удаление устройства.
Регистрация устройства
Один зарегистрированный драйвер может "подключать" несколько
устройств. Для подключения устройства к драйверу система вызывает
функцию драйвера probe, которой передает 2 параметра:
interface - это интерфейс USB-устройства. Обычно USB-драйвер
взаимодействует не с устройством напрямую, а с его интерфейсом. id -
содержит информацию об устройстве. Если функция возвращает 0, то
устройство успешно зарегистрировано, иначе - система попытается
"привязать" устройство к какому-нибудь другому драйверу.
Для отключения устройства от драйвера система вызывает функцию
disconnect, которой передается один параметр - интерфейс:
В общем случае, в функции probe для каждого подключаемого устройства
выделяется структура в памяти, заполняется, затем регистрируется,
например, символьное устройство, и проводится регистрация устройства в
sysfs. Также в функции probe может выполняться проверка на наличие у
устройства необходимых EP.
В качестве примера регистрации/удаления устройства лучше обратиться к
примеру из ядра linux, который находится в drivers/usb/usb-skeleton.c.
Использование USB Major
Остановимся более подробно на регистрации символьного устройства. Как
известно, для регистрации символьного устройства необходимо получить
число major - либо статически (обратиться к maintainer'у ядра и
занести его в include/linux/major.h), либо динамически (вызвать
register_chrdev с параметром major = 0). Когда символьное устройство
зарегистрировано, необходимо создать файл в директории /dev. Для этого
можно воспользоваться либо командой mknod (из user-space) или
функциями devfs, если данное ядро поддерживает devfs.
В ядре версии 2.6 вышеописанная процедура сильно упрощена благодаря
появлению udev и sysfs. В системе работает программа-daemon udevd,
которая отслеживает появление файлов в sysfs (/sys/class/). На
основании информации, читаемой из этих файлов, она автоматически,
пользуясь правилами udev для данного устройства, создает необходимые
файлы в dev.
В программном интерфейсе USB для этих целей есть функция
usb_register_dev, которая выполняет все необходимое, чтобы udev
выполнил вышеописанные процедуры:
name - это имя устройства. Директория с этим именем появится в sysfs.
fops - файловые операции символьного устройства. minor_base - базовый
minor номер.
Функция usb_register_dev выполняет следующие действия:
* регистрирует символьное устройство с major номером 180 (см
include/linux/major.h) и резервирует диапазон из 16 minor номеров.
Поэтому minor_base должен иметь младший полубайт = 0.
* в зарезервированном диапазоне minor номеров выделяет один номер
для данного устройства. Этот номер записывает в interface->minor.
* создает все необходимые файлы в sysfs: после этого udev создает
файлы в /dev/
Вызов usb_deregister_dev выполняет обратные процедуры, поэтому должен
вызываться в функции disconnect.
Обмен данными с устройством
Рассмотрим обмен данными для наиболее часто используемых типов:
control и bulk. Соответствующие функции определены в
drivers/usb/core/message.c.
control transfer
Для отсылки/приема данных в 0й EP используется функция
usb_control_msg:
extern int usb_control_msg(struct usb_device *dev, unsigned int pipe,
__u8 request, __u8 requesttype, __u16 value, __u16 index,
void *data, __u16 size, int timeout);
dev - указатель на usb_device. Этот указатель может быть получен с
помощью вызова функции interface_to_usbdev(interface). pipe - EP pipe.
Этот параметр хранит в себе: тип передачи данных (bulk, control, ...),
направление, номер EP. Для задания pipe в include/linux/usb.h
определены макросы. Приведем только некоторые:
Параметры request, request_type, value, index - это поля setup-пакета.
Их содержимое зависит от приложения. data, size - это массив для
отсылки/приема. См выше и [2,3].
timeout - параметр, задающий в tick'ах, сколько времени дается на
отсылку/прием.
Необходимо помнить, что функция usb_control_msg вернет значение только
после того, как setup-пакет и данные будут доставлены, или, если
произойдет ошибка или тайм-аут. Эта функция использует вызов
interruptible_sleep_on, поэтому эту функцию нельзя вызывать в
контексте прерывания.
Пример. Отсылка setup-пакета с заданными bRequest и wValue может
выглядеть следующим образом:
// ...
int ret;
struct usb_device* udev = interface_to_usbdev(interface);
В этом примере отсылается только setup-пакет. Поэтому data = udev,
size = 0.
bulk transfer
Для использования bulk transfer используется функция usb_bulk_msg:
extern int usb_bulk_msg(struct usb_device *dev, unsigned int pipe,
void *data, int size, int *actual_size,
int timeout);
Параметры dev, pipe, data, size, timeout имеют тот же смысл, что и в
usb_control_msg. actual_size - количество переданных байт в
действительности. actual_size <= size.
usb_bulk_msg имеет то же свойство, что и usb_control_msg - ее нельзя
вызывать в контексте прерываний (см выше).
Пример. Прием 100 байт из 5го EP может выглядеть следующим образом:
Спецификация USB определяет большое количество типов передачи данных,
пакетов, алгоритмов. Все алгоритмы USB реализованы в ядре linux, а
программисту драйверов предоставляется удобный и простой интерфейс в
виде набора функций, макросов, структур. В ядре linux 2.6 подсистема
USB вписывается в модель UDM [3,4], что делает ее очень гибкой для
взаимодействия с udev, sysfs.
Павел Курочкин, НТЦ Метротек
Литература
1. http://www.kernel.org
2. EZ-USB FX2 Technical Reference Manual (ver 2.2), Cypress
Semiconductor, 2003
3. http://www.usb.org
4. Использование и расширение Linux Unified Device Model (UDM),
Курочкин Павел, Metrotek, 2006
5. Linux 2.6 Kernel: первые шаги к sysfs, Курочкин Павел,
Metrotek, 2006
1651 Прочтений • [Разработка драйверов для USB-устройств под Linux (gcc driver linux usb)] [08.05.2012] [Комментариев: 0]