Статья посвящена основам создания приложений в среде windows, взаимодействующих с usb устройствами. Кратко рассматривается архитектура usb шины, программная модель и отрывки кода реализации драйвера usb устройства, доступ к драйверу из приложения.
Краткий обзор шины
usb - universal serial bus - универсальная последовательная шина. Поскольку шина последовательная, то в каждый момент времени передается только один поток данных. Это следует держать в голове, поскольку когда мы будем рассматривать программную модель, может возникнуть ощущение, что данные передаются параллельно. Шина usb чем-то похожа на ethernet - данные передаются пакетами. Каждый пакет имеет заголовок отвечающий за транспортировку и маршрутизацию данных. Но есть и существенно отличие: в usb полоса (bandwidth) (этот термин попросту говоря можно понимать как "общая пропускная способность") шины делится между устройствами не по принципу "кто первый занял" (random access). В usb ресурсы распределяются централизовано - концентратором (hub) шины. У шины может быть только один корневой концентратор (root hub), управляющий работой всей шины (он то как раз и распределяет ресурсы шины между устройствами). Поэтому нельзя соединить два компьютер напрямую проводом (наподобие нуль-модема) через usb - получится конфигурация с двумя корневыми концентраторами. К корневому концентратору подключаются устройства - всего до 127. Устройство может быть в свою очередь концентратором, контролирующим нижележащий сегмент шины. Таким образом, usb шина может выглядеть как многоранговая звезда (дерево).
Спецификация usb 2.0 предусматривает три возможные скорости передачи:
high speed - 480Мб/c
full speed - 12Мб/c
low speed - 1.5Мб/c
Следует помнить, что максимальную скорость должен поддерживать корневой концентратор. К 12Мб/c концентратору можно подключить низкоскоростное устройство и нельзя - высокоскоростное (т.е. можно, но оно будет работать на скорости 12Мб/с, а не 480). Следует заметить, что современные чипсеты персональных компьютеров зачастую обеспечиваю скорость только 12Мб/c (они хотя и позиционируются как usb 2.0 совместимые, но по сути являются usb1.1), поэтому не стоит полагать, что usb обеспечит во всех случаях жизни высокоскоростной обмен.
Различают 4 различных типа передачи по usb:
Управляющий - используется для конфигурации устройства на шине, также может использоваться для специфичных для устройства целей;
Пакетный - передача больших пакетов информации, допускающая большие задержки в доставке данных;
"По прерыванию" - надежная, кратковременная передача (например, кода клавиши от клавиатуры);
Изохронный - для передачи данных в режиме реального времени ( с минимальной задержкой).
Каждое устройство предоставляет одну (или несколько) т.н. функцию. Некоторые устройства могут содержать также один (или несколько) концентратор, к которому подключаются подчиненные устройства.
Каждая функция может иметь несколько точек подключения (endpoint). Каждая точка отвечает за определенный тип передачи данных. Каждое устройство имеет управляющую точку подключения (control endpoints).
2. Программная модель
Рассматривая программную модель usb для windows прежде всего стоит отметить следующие особенности:
1)Каждое usb устройство должно обслуживаться собственным драйвером. В отличие от, скажем, устройств, подключаемых к lpt, для которых наличие собственного драйвера в общем необязательно. На шину usb нельзя просто передать сырой поток данных.
2)Все usb контроллеры соответствуют спецификации acpi, т.е поддерживают функции pnp и управления питанием. Поэтому работать с устройством можно только если оно подключено к шине (как это ни удивительно j ).
3)Драйвера в windows образуют т.н стек, по которому и передаются данные вверх и вниз, не следует пытаться работать напрямую с портами контроллера.
Держа в голове эти три пункта (чего не нужно делать j ), посмотрим как же работать с этим хозяйством.
Для начала рассмотрим аппаратную реализацию на примере популярного (в недавнем прошлом) чипсета i815. Обслуживанием всех устройств ввода/вывода в этом чипсете занимается специализированный контроллер - ich (i/o controller hub) - 82801ba. Чтобы перечислить все его функции не хватит листа. Нас будет интересовать тот факт, что в состав этой микросхемы входит в том числе два независимых usb контроллера, каждый из которых имеет по два порта. Контроллеры поддерживают скорость передачи 12Мб/c (т.е usb 1.1). Каждый контроллер имеет в диапазоне ввода/вывода набор портов, через которые ими можно управлять.
Тут вспоминаем п.3 и отказываемся от мысли управлять usb контроллерами напрямую. Производитель контроллера постарался и написал для нас драйвер. Те, кто имел опыт взаимодействия с драйверами из режима приложения, возможно, уже сокрушается: "Что толку от этого драйвера! Нужно знать еще его интерфейс!". Абсолютно верно, но дело в том, что нам его интерфейс не понадобиться. В операционной системе есть уже универсальный драйвер для корневых концентраторов. Это соответствует концепции модели драйверов: "Логическое устройство - Физическое устройство". В данном случае драйвер корневого концентратора (usbhub.sys) создает логическое устройство позволяющие для вышележащих драйверов абстрагироваться от подробностей работы драйвера usb контроллера. Нетерпеливый читатель опять воскликнет: "И что толку!". Для организации интерфейса с клиентом (драйвером) существует третий драйвер - драйвер шины (usbd.sys). Его интерфейс документирован в ddk. Это набор управляющих кодов для диспетчера (просто процедура, обрабатывающая запросы) запросов типа irp_mj_internal_device_control. С помощью этих запросов можно получить разнообразную информацию о состоянии шины, количестве портов, их статусе и пр. Все эти коды имеют символические имена вида: ioctl_internal_usb_ХХХХ. Особое внимание следует обратить на запрос ioctl_internal_usb_submit_urb. Управление устройством, запись и чтение данных осуществляется именно через этот запрос. Из этого следует, чтобы передать данные на шину, нельзя вызвать writefile для драйвера шины или корневого концентратора - он может вообще не иметь диспетчера для irp_mj_write (. С другой стороны обратиться к диспетчеру irp_mj_internal_device_control из пользовательского приложения нельзя. Посему вспоминаем п.1 - каждое устройство обязано иметь свой драйвер, который разрешает эту дилемму - он имеет диспетчер для irp_mj_write (который вызывается диспетчером ввода-вывода когда приложение вызывает writefile). Все тоже самое относится к чтению. Для некоторых классов устройств существую стандартные драйвера: для hid устройств (устройства интерфейса - мыши, клавиатуры и.т.п.), usb audio - устройства воспроизведения звука (ЦАП располагается прямо в локальной аудиоустановке, а по шине передается цифровой поток). Для устройств, принадлежащих к этим классам, не нужно драйверов.
Поговорим немного о модели ввода/вывода. Для разработчиков пользовательских программ это, возможно, абсолютно незнакомая тема. С аппаратной точки зрения вывод/вывод бывает программный - использование инструкций in, out, mov а также (наиболее интересный случай!) использование строковых операций с префиксом повтора (rep movsb) или с использованием контроллера прямого доступа к памяти (dma). Однако, в данном случае речь идет о другом. Аппаратным вводом/выводом занимается как раз драйвер usb контроллера. А нас интересует, куда попадают принятые данные (и куда помещаются данные, предназначенные для передачи)? Существует два подхода: "буферизованный" и "прямой" ввод/вывод.
При буферизованном выводе происходит следующее:
1)Пользовательское приложение вызывает функцию writefile, передав указатель на буфер содержащий данные;
2)ОС вызывает диспетчер irp_mj_write драйвера и передает туда (через структуру irp) указатель на буфер данных
3)Драйвер копирует данные в свой внутренний буфер. После этого, возможно, сообщает, что данные переданы или откладывает это сообщение до момента актуальной передачи.
4)Актуальная передача осуществляется когда-то позже.
При буферизованном вводе все аналогично. Главное достоинство этого метода - принятые данные не могут "пропасть" (если только внутренний буфер не переполниться). Буферизованный ввод/вывод осуществляет, например, драйвер последовательного порта. Для медленных устройств ввода/вывода это рекомендованный способ.
Для быстрых устройств, особенно передающих данные большими пакетами, использование программных буферов имеет два основных недостатка - большие накладные расходы на копирование данных в(из) промежуточный буфер, нерационально используется системная память. Поэтому в данном случае используется прямой ввод/вывод - данные принимаются непосредственно в буфер (или передаются из этого буфера), зарезервированный пользовательской программой. Производительность такого метода заметно выше. Однако возникает некоторые сложности с приемом данных - у драйвера всегда должен быть "под рукой" буфер. И это должен обеспечить клиент (пользовательская программа или вышележащий драйвер).
3. Основы программирования драйвера usb устройства
Коль мы установили, что любое usb устройство (кроме hid и usb audio) должно иметь собственный драйвер, то рассмотрим далее как устроен такой драйвер. Любой драйвер, удовлетворяющий модели драйверов nt или wdm, начинается стандартной точки входа - driverentry. В этой процедуре должны быть заданы диспетчеры - процедуры, по средством которых драйвер взаимодействует с системой. Рассмотрим следующий листинг. Здесь и далее опущен вывод отладочной информации, проверка возвращаемых значений на ошибки, обработка исключений, короче все то, что делает код надежным, но ухудшает его читабельность, в коде реального драйвера, естественно, следует это все предусмотреть. Более того, опущены некоторые детали, которые не являются специфичными для usb драйверов. Тем не менее без них драйвер просто не будут работать. Разработчикам драйверов советую обратиться к примерам из ddk или driverstudio. Приведенный же код рассчитан в первую очередь на разработчиков приложений - чтоб знали что по чем!!! j
ntstatus driverentry(in pdriver_object driverobject, in punicode_string registrypath )
{
driverobject->majorfunction[irp_mj_create] = usbdrv_create;
driverobject->majorfunction[irp_mj_close] = usbdrv_close;
driverobject->majorfunction[irp_mj_device_control] = usbdrv _processioctl;
driverobject->majorfunction[irp_mj_write] = usbdrv_write;
driverobject->majorfunction[irp_mj_read] = usbdrv_read;
driverobject->majorfunction[irp_mj_system_control] = usbdrv _processsyscontrolirp;
driverobject->majorfunction[irp_mj_pnp] = usbdrv_processpnpirp;
driverobject->majorfunction[irp_mj_power] = usbdrv_processpowerirp;
driverobject->driverextension->adddevice = usbdrv_adddevice;
driverobject->driverunload = usbdrv_unload;
return status_success; }
usbdrv_create - извещает драйвер, что на нем был открыт файл, т.е была вызвана функция ntcreatefile (в пользовательском режиме приложения обычно вызывают эту функцию, экспортируемую ntdll.dll через вызов createfile из kernel32.dll; дайвера режима ядра вызывают zwcreatefile, экспортируемую ядром - ntoskrn.exe). Отметим следующее:
1. с помощью этого диспетчера можно и отказать в открытии файла - достаточно вернуть что-то отличное от status_success;
2. в диспетчер через имя файла может быть передана дополнительная текстовая информация - она просто "прицепляется" к имени сзади.
usbdrv_close - извещает драйвер о закрытии файла. Вызывается в ответ на вызов closehandle из пользовательского приложения.
usbdrv_processioctl - передает драйверу управляющий код. Пользовательское приложение для передачи такого кода использует deviceiocontrol
usbdrv_write - передает драйверу буфер для записи в устройство. Вызывается менеджером ввода/вывода в ответ на вызов writefile(ex).
usbdrv_read - передает драйверу буфер для чтения.
Вышеперечисленные диспетчеры предназначены для общения драйвера со своим клиентом. Следующие три диспетчера используются системой:
usbdrv _processsyscontrolirp - обработчик системных запросов. Как правило, драйвер передает их просто нижележащему драйверу.
usbdrv_processpnpirp - обработчик pnp запросов. usb устройство удовлетворяет спецификации acpi и его драйвер должен иметь этот диспетчер.
usbdrv_processpowerirp - обработчик запросов системы управления питанием.
usbdrv_adddevice - драйвер любого pnp устройства обязан иметь эту функцию. Она вызывается при обнаружении нового устройства, обслуживаемого драйвером.
usbdrv_unload - вызывается при выгрузке драйвера из памяти
Далее мы не будем рассматривать полностью код, рассмотрим только важные моменты, которые должны быть интересны также и прикладным программистам. Будем рассматривать код в "событийном" порядке.
После того, как система обнаруживает устройство, обслуживаемое нашим драйвером, она вызывает функцию adddevice. Для сопоставления драйвера обнаруженному устройству используется реестр. Те, кто интересуется подробностями этого процесса, может заглянуть в реестр hklmsystemcurrentcontrolsetusb. По pid (product id) и vid (vendor id) устанавливается имя сервиса. Если сервис еще не загружен - он загружается в память и вызывается driverentry. Затем вызывается та самая функция adddevice. Если драйвер не загружен в память он для начала туда загружается. Если же не найденa соответствующая запись в реестре - появляется знакомая надпись об обнаружении нового устройства.
Рассмотрим, что делает функция adddevice:
ntstatus usbdrv_pnpadddevice(in pdriver_object driverobject, in pdevice_object physicaldeviceobject )
{
ntstatus ntstatus = status_success;
pdevice_object deviceobject = null;
pdevice_extension deviceextension;
unicode_string devicelink;
ulong i;
ntstatus = ioregisterdeviceinterface(physicaldeviceobject, (lpguid)&guid_class_usb_drv, null, &devicelink);
ntstatus = iosetdeviceinterfacestate(&devicelink, true);
ntstatus = iocreatedevice (driverobject,
sizeof (device_extension),
null,
file_device_unknown,
file_autogenerated_device_name, false,
&deviceobject);
deviceextension = (deviceobject)->deviceextension;
rtlcopymemory(deviceextension ->devicelinknamebuffer, devicelink.buffer, devicelink.length);
deviceextension ->pdo = physicaldeviceobject;
deviceextension ->topstackdevice =
ioattachdevicetodevicestack(deviceobject, physicaldeviceobject);
deviceobject ->flags /= do_direct_io;
deviceobject ->flags &= ~do_device_initializing;
return status_success;
}
Первым делом, нужно зарегистрировать интерфейс устройства с помощью ioregisterdeviceinterface. Если устройство поддерживает несколько интерфейсов (например, контроллер может обслуживать мышь и клавиатуру) - это функция может вызываться несколько раз. Далее создается объект - устройство device_object. Если у вас установлен softice, можете попробовать набрать в режиме отладки device и найти свое устройство. После того, как интерфейс будет разрешен ( iosetdeviceinterfacestate ), автоматически будет создана соответствующая символическая ссылка в разделе dosdevices, с помощью которой пользовательские приложения смогут получить доступ к устройству. Созданный объект устройство следует подключить (ioattachdevicetodevicestack) к стеку устройств. После этого наш драйвер в контексте созданного устройства будет обрабатывать запросы pnp и системы управления питанием. Кроме того, наш драйвер сможет отправлять irp запросы вниз по стеку, к которому он приаттачился. Одним из первых будет получен pnp запрос irp_mn_start_device, с помощью которого драйвер получит всю необходимую информацию об устройстве. Приведем код соответствующего обработчика:
ntstatus usbdrv_onstartdevice(in pdevice_object deviceobject )
{
ntstatus ntstatus = status_success;
pdevice_extension pdevext = null;
purb purb = null;
pusb_device_descriptor pdevdesc = null;
pusb_configuration_descriptor pconfigdesc = null;
Первым делом, получим всякую полезную информацию об устройстве, которое мы собираемся обслуживать. Сразу заметим, что всю работу за нас будут делать нижележащие драйвера (usbd.sys). Нам остается только грамотно формировать запросы urb - usb request block. Для этого:
Для начала получаем т.н deviceextension - область памяти, выделенной при вызове iocreatedevice, которую мы можем использовать по собственному усмотрению - т.н контекст устройства:
pdevext = deviceobject->deviceextension;
Формируем urb запрос:
purb = exallocatepool(nonpagedpool, sizeof(struct _urb_control_descriptor_request) ) ;
pdevdesc = exallocatepool(nonpagedpool, sizeof(usb_device_descriptor) );
usbbuildgetdescriptorrequest(purb,(ushort)sizeof(struct _urb_control_descriptor_request),
usb_device_descriptor_type, 0, 0, pdevdesc, null, sizeof(usb_device_descriptor), null);
Любое общение между драйверами происходит с помощью irp. В нашем случае мы через irp передаем наш urb запрос:
pirp = iobuilddeviceiocontrolrequest(ioctl_internal_usb_submit_urb,
pdevext ->topstackdevice, null, 0, null, 0, true, &event, &iostatus);
nextstack = iogetnextirpstacklocation(pirp);
nextstack->parameters.others.argument1 = urb;
И вот кульминация - передаем запрос нижележащему драйверу:
iocalldriver(pdevext ->topstackdevice, pirp);
При нормальном стечении обстоятельсив мы получим следующую информацию об устройстве:
kdprint(("usblink device descriptor:n"));
kdprint(("-------------------------n"));
kdprint(("blength %dn", pdevdesc->blength));
kdprint(("bdescriptortype 0x%xn", pdevdesc->bdescriptortype));
kdprint(("bcdusb 0x%xn", pdevdesc->bcdusb));
kdprint(("bdeviceclass 0x%xn", pdevdesc->bdeviceclass));
kdprint(("bdevicesubclass 0x%xn", pdevdesc->bdevicesubclass));
kdprint(("bdeviceprotocol 0x%xn", pdevdesc->bdeviceprotocol));
kdprint(("bmaxpacketsize0 0x%xn", pdevdesc->bmaxpacketsize0));
kdprint(("idvendor 0x%xn", pdevdesc->idvendor));
kdprint(("idproduct 0x%xn", pdevdesc->idproduct));
kdprint(("bcddevice 0x%xn", pdevdesc->bcddevice));
kdprint(("imanufacturer 0x%xn", pdevdesc->imanufacturer));
kdprint(("iproduct 0x%xn", pdevdesc->iproduct));
kdprint(("iserialnumber 0x%xn", pdevdesc->iserialnumber));
kdprint(("bnumconfigurations 0x%xn", pdevdesc->bnumconfigurations));
Запомним эту информацию в контексте устройства
pdevext ->usbdevicedescriptor = pdevdesc;
Теперь используем полученную информацию для инициализации устройства:
Построим еще один urb запрос. В данном случае мы хотим получить т.н. конфигурационный дескриптор. Но дело в том, что мы не знаем его длину и не можем выделить гарантированно достаточный буфер памяти. Поэтому мы для начала формируем запрос с недостаточным размером буфера - нам в ответ вернут необходимую длину буфера:
pconfigdesc = exallocatepool(nonpagedpool, sizeof(usb_configuration_descriptor) );
usbbuildgetdescriptorrequest(purb,
(ushort) sizeof (struct _urb_control_descriptor_request), usb_configuration_descriptor_type, 0, 0, pconfigdesc, null, sizeof(usb_configuration_descriptor), null);
pirp = iobuilddeviceiocontrolrequest(ioctl_internal_usb_submit_urb,
pdevext ->topstackdevice, null, 0, null, 0, true, &event, &iostatus);
nextstack = iogetnextirpstacklocation(pirp);
nextstack->parameters.others.argument1 = urb;
iocalldriver(pdevext ->topstackdevice, pirp);
А вот теперь попробуем получить актуальную конфигурационную информацию:
pdevext ->usbconfigdescriptor = exallocatepool(nonpagedpool, pconfigdesc ->wtotallength);
usbbuildgetdescriptorrequest(purb, (ushort) sizeof (struct _urb_control_descriptor_request),
usb_configuration_descriptor_type, 0, 0, pdevext ->usbconfigdescriptor, null,
pconfigdesc ->wtotallength, null);
nextstack = iogetnextirpstacklocation(pirp);
nextstack->parameters.others.argument1 = urb;
iocalldriver(pdevext ->topstackdevice, pirp);
kdprint(("usblink configuration descriptor:n"));
kdprint(("-------------------------n"));
kdprint(("bdescriptortype 0x%xn", pdevext ->usbconfigdescriptor->bdescriptortype));
kdprint(("wtotallength 0x%xn", pdevext ->usbconfigdescriptor-> wtotallength));
kdprint(("bnuminterfaces 0x%xn", pdevext ->usbconfigdescriptor->bnuminterfaces));
kdprint(("iconfiguration 0x%xn", pdevext ->usbconfigdescriptor->iconfiguration));
kdprint(("bmattributes 0x%xn", pdevext ->usbconfigdescriptor->bmattributes));
kdprint(("maxpower 0x%xn", pdevext ->usbconfigdescriptor->maxpower));
exfreepool(pconfigdesc);
exfreepool(purb);
Если читатель утомился рассмотрением кода - могу только посочувствовать - далее начинается самое главное.
Получив конфигурационный описатель (дескриптор) получим список интерфейсов, предоставляемых устройством. Этим занимается usbd.sys. А мы будем использовать функции, экспортируемые этим драйвером (драйвера, как любые pe файлы могут экспортировать функции, чего тут удивительного? j ).
pusbd_interface_list_entry pinterfaceslist;
pusb_interface_descriptor pcurrentdescriptor;
ulong i;
pinterfaceslist = exallocatepool(nonpagedpool, sizeof(usbd_interface_list_entry)*
(pdevext ->usbconfigdescriptor -> bnuminterfaces + 1) );
for (i = 0; i < pdevext ->usbconfigdescriptor ->bnuminterfaces; i++ )
{
pcurrentdescriptor = usbd_parseconfigurationdescriptorex(pdevext->usbconfigdescriptor,
pdevext->usbconfigdescriptor, i, 0, -1, -1, -1 );
pinterfaceslist[i].interfacedescriptor = pcurrentdescriptor;
pinterfaceslist[i].interface = null;
}
pinterfaceslist[i].interfacedescriptor = null;
pinterfaceslist[i].interface = null;
purb = usbd_createconfigurationrequestex(pdevext ->usbconfigdescriptor, pinterfaceslist);
for (i = 0; i < pinterfaceslist[0].interface->numberofpipes; ++i)
pinterfaceslist[0].interface -> pipes[i].maximumtransfersize = max_transfer_size;
usbbuildselectconfigurationrequest(purb, sizeof(struct _urb_select_configuration),
pdevext->usbconfigdescriptor);
pirp = iobuilddeviceiocontrolrequest(ioctl_internal_usb_submit_urb,
pdevext ->topstackdevice, null, 0, null, 0, true, &event, &iostatus);
nextstack = iogetnextirpstacklocation(pirp);
nextstack->parameters.others.argument1 = urb;
iocalldriver(pdevext ->topstackdevice, pirp);
Я честно говоря утомился комментировать каждую строчку кода, так что займитесь этим сами j. Вместо этого я лучше обращу ваше внимание на следующий факт: получив конфигурационный дескриптор мы знали число интерфейсов, предоставленных нам устройством (bnuminterfaces). А выделили памяти на один элемент списка больше. Зачем? Дело в том, что в функции нет параметра задающего длину списка usbd_createconfigurationrequestex. Вместо этого длина списка определяется на манер работы с нуль-терминированными строками - последним стоит элемент, у которого pinterfaceslist[i].interfacedescriptor = null;
После обработки запроса, сформированного с помощью usbd_createconfigurationrequestex для каждого элемента списка интерфейсов мы получим кое-что интересное: pinterfaceslist[num].interface ->pipes - список пайпов данного интерфейса (массив структур usbd_pipe_information). Для нас в этой структуре самым важным будет поле usbd_pipe_handle pipehandle - когда мы захотим что-то заслать в пайпу, нужно будет создать соответствующий urb запрос urb_bulk_or_interrupt_transfer и указать этот самый дескриптор пайпа. Как раз и рассмотрим, как что-либо записать в пайп. Запись производится либо по инициативе драйвера, либо по инициативе клиента драйвера - при обработке запроса irp_mj_write.
ntstatus ntstatus = status_success;
pdevice_extension pdevext;
pio_stack_location pirpstack, pnextstack;
ulong length = 0;
purb purb = null, pprevurb = null, pfirsturb = null;
pmdl pmdl;
pdevext = deviceobject -> deviceextension;
pirpstack = iogetcurrentirpstacklocation (irp);
if (irp->mdladdress) length = mmgetmdlbytecount(irp->mdladdress);
purb = exallocatepool(nonpagedpool,
sizeof(struct _urb_bulk_or_interrupt_transfer) );
rtlzeromemory(purb, sizeof(struct _urb_bulk_or_interrupt_transfer));
purb ->urbbulkorinterrupttransfer.hdr.length = (ushort) sizeof(struct _urb_bulk_or_interrupt_transfer);
purb ->urbbulkorinterrupttransfer.hdr.function = urb_function_bulk_or_interrupt_transfer;
purb ->urbbulkorinterrupttransfer.pipehandle = pdevext ->writepipehandle;
purb ->urbbulkorinterrupttransfer.transferflags = usbd_transfer_direction_in ;
purb ->urbbulkorinterrupttransfer.transferflags /= usbd_short_transfer_ok;
purb ->urbbulkorinterrupttransfer.urblink = null;
purb ->urbbulkorinterrupttransfer.transferbuffermdl = irp->mdladdress;
purb->urbbulkorinterrupttransfer.transferbufferlength = length;
pnextstack = iogetnextirpstacklocation(irp);
pnextstack->parameters.others.argument1 = purb;
pnextstack->majorfunction = irp_mj_internal_device_control;
pnextstack->parameters.deviceiocontrol.iocontrolcode = ioctl_internal_usb_submit_urb;
iocalldriver(pdevext ->topstackdevice, irp );
Если вы ознакомились внимательно с кодом, приведенным в этом разделе, то листинг функции, производящей запись в устройство не должен быть совсем уж непонятным. Обратим внимание на детали:
mdladdress - это указатель на структуру mdl (memory description list), описывающую буфер, переданный через irp c кодом irp_mj_write. Как уже упоминалось выше, такое поведение характерно для драйверов с прямой моделью ввода-вывода. Повторюсь, но тем не менее замечу: при чтении данных это приводит к необходимости упреждающего вызова readfile - в противном случае, данные не будут буферизированы и просто будут утеряны.
На этом мы закончим с рассмотрением внутренностей usb драйвера. В следующем разделе рассмотрим взаимодействие пользовательского приложения с драйвером.
4. Взаимодействие пользовательского приложения с драйвером usb устройства
Пользовательское приложение взаимодействует с устройством опосредованно - через драйвер этого устройства. Драйвер, как мы помним из предыдущего раздела, регистрирует и разрешает интерфейс, после чего система сама создает соответствующее символическое имя, через которое можно обратиться к устройству, как к файлу. Перво-наперво, это символическое имя надо выяснить. Для этого нам понадобиться библиотека, доступная для приложений - setupapi.dll. Описания функций, экспортируемых этой библиотекой, есть в ddk (но нет в sdk!). Рассмотрим код, позволяющий получить доступ к драйверу устройства как к файлу. Сначала нужно получить описатель класса устройства:
hdevinfo hdevinfo = setupdigetclassdevs ( (guid*)& guid_class_usb_drv, null, null, digcf_present / digcf_interfacedevice );
Мы попросили вернуть нам описатель для устройств, предоставляющих интерфейс с guid = guid_class_usb_drv и присутствующих в данный момент в системе.
Далее получаем краткую информацию для интерфейсов (в данном случае, для первого интерфейса в списке с подходящим guid):
psp_device_interface_data devinfodata =
(psp_device_interface_data)malloc(sizeof(sp_device_interface_data));
devinfodata ->cbsize = sizeof(sp_device_interface_data);
setupdienumdeviceinterfaces(hdevinfo, null, (guid*)&guid_class_usb_link, 0,
devinfodata);
После того, можно попытаться узнать символическое имя для устройства с заданным интерфейсом. Но для этого сначала определим необходимую для хранения этого имени длину буфера:
ulong requiredlength;
setupdigetinterfacedevicedetail (hdevinfo, devinfodata, null, 0, &requiredlength, null);
А теперь можно наконец узнать имя, присвоенное устройству:
psp_device_interface_detail_data devinfodetail =
(psp_device_interface_detail_data)malloc(requiredlength);
devinfodetail ->cbsize = sizeof(sp_device_interface_detail_data);
setupdigetinterfacedevicedetail (hdevinfo, devinfodata, devinfodetail,
requiredlength, &requiredlength, null);
После этого, открываем устройство как файл:
husbdevice = createfile ( devinfodetail->devicepath, generic_read / generic_write, file_share_read / file_share_write, null, open_existing, 0, null);
После этого, с устройством можно работать как с обычным файлом: используя функции readfile(ex), writefile(ex), deviceiocontrol.
Обратите внимание! Библиотека setupapi.dll требует, чтобы структуры, передаваемые в нее, имели однобайтовое вырвнивание. Проверьте опции компилятора (по умолчанию, скорее всего, выравнивание будет другим).
Ссылки по теме