Статья
Версия для печати
Обсудить на форуме
Опыт применения Human Interface Driver (HID) при разработке USB устройства на базе PIC16С745/765



Статья содержит материал, связанный с реализацией системы передачи данных между устройством, подключенным к USB шине компьютера и приложением. Статья не содержит материал о системной организации шины USB и архитектуре HID. Эта информация может быть получена из других источников.

1.   Постановка задачи

Постановка задачи, не учитывая требования к функциональности устройства, необычайно кратка: требуется разработать USB устройство для оцифровки сигнала, незначительной предобработки и ввода данных в компьютер. Частота передачи данных в компьютер не менее 300 раз в секунду. Одно значение данных представляет собой два байта.

2.   Выбор модели микроконтроллера и способа передачи данных

Для создания системы был выбран микроконтроллер PIC16C745, т.к. он обладает интерфейсом USB, имеет встроенный 8-ми битный АЦП, программное обеспечение для работы с шиной USB и бесплатную среду разработки. Выбор способа передачи данных после выбора микроконтроллера был очевиден. Компания Microchip предоставляет пример прошивки микроконтроллера, в котором эмулируется USB мышь с HID интерфейсом. Задача разработки системы передачи и приема данных в микроконтроллере сводится к правильной конфигурации дескриптора HID устройства и использовании процедур записи и чтения предоставленных в ПО для работы с USB. Задача приложения - проводить поиск устройства на USB шине, получать к нему доступ, работать с ним и потом правильно закрывать.

3.   ПО микроконтроллера

ПО микроконтроллера рассматривается с целью приведения примера рабочего дескриптора, использующегося в разработанной системе, и улучшения понимания структуры системы в целом. По желаниям читателей, эта часть статьи может быть расширена.

3.1.   Структура

ПО микроконтроллера можно разделить на 2 части: основная программа, отвечающая за функциональность прибора, и модули для взаимодействия с шиной USB. Поскольку в данной статье нет интереса описывать само устройство и принцип его функционирования, представим, что основная программа по таймеру вызывает процедуру передачи 8-ми байтного пакета данных в компьютер. Также в ней реализован механизм приема управляющих пакетов. Обработка и пересылка информации начинается после получения по USB пакета с кодом начала работы. После того, как получит пакет с кодом конца работы, она перестает производить оцифровку и передавать данные. Таким образом, основная программа для нас абстрактна. ПО взаимодействия с USB производит непосредственный прием и передачу данных через USB, обработку стандартных запросов шины USB и запросов специфичных для HID устройства.

3.2.    Взаимодействие с USB

При подключении устройства к шине USB происходит процесс инициализации устройства, т.е. его сброса и получения о нем информации. Все эти функции реализованы в ПО для взаимодействия c USB. Для того, чтобы устройство правильно определялось операционной системой, нужно правильно сконфигурировать дескрипторы самого устройства, его интерфейсов и HID дескрипторы. Рассмотрим этот вопрос поподробнее.

3.3.   Дескрипторы

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

3.3.1.   Device Descriptor

Device Descriptor описывает устройство в целом, в нем указывается версия протокола USB, ID производителя устройства и самого устройства, максимальный размер пакета, который может отправить или принять устройство.
Код:
StartDevDescr
retlw 0x12 ; bLength длина дескриптора
retlw 0x01 ; bDescType это DEVICE дескриптор
retlw 0x10 ; bcdUSBUSB версия USB 1.10 (low byte)
retlw 0x01 ; high byte
retlw 0x00 ; bDeviceClass ноль означает что каждый
      ;    интерфйс работает независимо
retlw 0x00 ; bDeviceSubClass
retlw 0x00 ; bDeviceProtocol
retlw 0x08 ; bMaxPacketSize0 - inited in UsbInit()
retlw 0x08 ; idVendor Low byte
retlw 0x32 ; high order byte
retlw 0x40 ; idProduct
retlw 0x48
retlw 0x41 ; bcdDevice
retlw 0x04
retlw 0x01 ; iManufacturer
retlw 0x02 ; iProduct
retlw 0x00 ; iSerialNumber - 0
retlw NUM_CONFIGURATIONS ; bNumConfigurations

3.3.2.   Config Descriptor

Описывает конфигурацию устройства, количество конечных точек и их назначение.
Код:
Config1
retlw 0x09 ; bLength длина дескриптора
retlw 0x02 ; bDescType2 = CONFIGURATION
retlw EndConfig1 - Config1
retlw 0x00
retlw 0x01 ; bNumInterfaces количество интерфейсов
retlw 0x01 ; bConfigValue
retlw 0x04 ; iConfigString
retlw 0xA0 ; bmAttributesattributes - потребление тока от USB
retlw 0x32 ; MaxPowerself- максимальное потребление тока от шины
;может быть 64 mA .
Interface1 ;Дестриптор интерфейса устройства
retlw 0x09 ; длина дескриптора
retlw INTERFACE ;тип - INTERFACE
retlw 0x00 ; номер интерфейса (начинается с нуля)
retlw 0x00 ; alternate setting
retlw 0x02 ; количество конечных точек, используемых интерфейсом
retlw 0x03 ; класс интерфейса - 3 для HID
retlw 0x00 ; подкласс интерфейса (для загрузочных устройств =1)
retlw 0x00 ; протокол интерфейса  (0-нет, 1-мышь, 2-клавиатура)
retlw 0x05 ;индекс строкового дескриптора, описывающего этот
;интерфейс
HID_Descriptor  ;дескриптор HID
retlw 0x09 ; длина дескриптора  (9 bytes)
retlw 0x21 ; тип дескриптора (HID)
retlw 0x00 ; HID class release number (1.00) low byte
retlw 0x01 ;high byte
retlw 0x00 ; код страны для которой предназначено устройство
retlw 0x01 ; количество следующих дескрипторов HID (1)
retlw 0x22 ; тип дескриптора - Report descriptor (HID)
retlw (end_ReportDescriptor - ReportDescriptor) ; длина дескриптора low byte
retlw 0x00 ;high byte
Endpoint1 ; дескриптор первой конечной точки
retlw 0x07 ; длина дескриптора
retlw ENDPOINT ;тип дескриптора - ENDPOINT
retlw 0x01 ; адрес конечной точки:
;Bit 0..3 The endpoint number;
;Bit 4..6 Reserved, reset to zero
;Bit 7 Direction, ignored for
;Control endpoints:
;0 - OUT endpoint
;1 - IN endpoint
; т.е. здесь EP1, Out
retlw 0x03 ; тип передач - Interrupt
retlw 0x08 ;максимальный размер пакета (8 bytes) low order byte
retlw 0x00 ; high order byte
retlw 0x01 ; интервал времени (в мс) через который система будет
;опрашивать конечную точку на наличие пакета для ;передачи (1ms)
Endpoint2
retlw 0x07 ; длина дескриптора
retlw ENDPOINT ;тип дескриптора - ENDPOINT
retlw 0x81 ; EP1, In
retlw 0x03 ; Interrupt
retlw 0x08 ; max packet size (8 bytes) low order byte
retlw 0x00 ; max packet size (8 bytes) high order byte
retlw 0x01 ; polling interval (1ms)

EndConfig1

3.3.3.   Report Descriptor

Этот дескриптор описывает формат сообщения HID устройства и то, как устройство будет представлено в панели диспетчера устройств Windows. При данной конфигурации устройство может принимать и посылать пакеты длиной 8 байт и представляется как устройство ручного ввода ReportDescriptor
Код:
retlw 0x06
retlw 0x00 ; usage page (vendor defined)
retlw 0xFF
retlw 0x09
retlw 0x01 ; usage (vendor defined 1)

retlw 0xA1
retlw 0x01 ; collection (application)

retlw 0x19
retlw 0x01 ;usage (vendor usage )
retlw 0x29
retlw 0x08 ;usage (vendor usage )
retlw 0x15
retlw 0x00
retlw 0x26
retlw 0xFF
retlw 0x00

retlw 0x75
retlw 0x08 ; report size (8)
retlw 0x95
retlw 0x08 ; report count (2)

retlw 0x81
retlw 0x02 ; input (2 position bytes X & Y)

retlw 0x19
retlw 0x01 ;usage (vendor usage )
retlw 0x29
retlw 0x08 ;usage (vendor usage )
retlw 0x91
retlw 0x02 ; input (2 position bytes X & Y)
retlw 0xC0 ; end collection
end_ReportDescriptor

3.4.    Общий порядок работы программы

При включении начинает выполняться основная программа микроконтроллера. Она вызывает функцию инициализации USB и ждет, пока пройдет инициализация. Вся работа с USB происходит по прерываниям. Прерывание от шины USB обрабатывается, и затем производится какое-либо действие (выдача дескриптора, просто прием или передача пакета и др.). При инициализации устройства оно передает дескрипторы компьютеру и, если все удачно (если они построены правильно), то устройство появляется в списке устройств Windows. С этого момента приложение пользователя может начать с ним работать.

4.   Приложение
4.1.   Структура

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

4.2.   Инициализация

Определения переменных выглядят следующим образом:
Код:
HANDLE DeviceHandle;
GUID HidGuid;
ULONG Length;
HANDLE hDevInfo;
ULONG Required;
HIDP_CAPS Capabilities;
HANDLE ReadHandle;
HANDLE hEventObject;
OVERLAPPED HIDOverlapped;
BYTE InputReport[21];
BYTE OutputReport[21];
DWORD NumberOfBytesRead;
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData;
bool DeviceDetected;
CHANNELS Channels;
DWORD WINAPI ReadThread();

Для определения присутствия USB устройства на шине вызывается функция FindTheHID(). Думаю, русские комментарии здесь излишни.
Код:
bool CDevice::FindTheHID()
{
//Use a series of API calls to find a HID with a matching Vendor and Product ID.
HIDD_ATTRIBUTES Attributes;
SP_DEVICE_INTERFACE_DATA devInfoData;
bool LastDevice = FALSE;
int MemberIndex = 0;
bool MyDeviceDetected = FALSE;
LONG Result;

//These are the vendor and product IDs to look for.

const unsigned int VendorID = 0x3208;
const unsigned int ProductID = 0x4840;

Length = 0;
detailData = NULL;
DeviceHandle=NULL;

/*
API function: HidD_GetHidGuid
Get the GUID for all system HIDs.
Returns: the GUID in HidGuid.
*/

HidD_GetHidGuid(&HidGuid);

/*
API function: SetupDiGetClassDevs
Returns: a handle to a device information set for all installed devices.
Requires: the GUID returned by GetHidGuid.
*/

hDevInfo=SetupDiGetClassDevs
(&HidGuid, NULL, NULL, DIGCF_PRESENT|DIGCF_INTERFACEDEVICE);

devInfoData.cbSize = sizeof(devInfoData);

//Step through the available devices looking for the one we want.
//Quit on detecting the desired device or checking all available devices without success.
MemberIndex = 0;
LastDevice = FALSE;

do
{
MyDeviceDetected=FALSE;

/*
API function: SetupDiEnumDeviceInterfaces
On return, MyDeviceInterfaceData contains the handle to a
SP_DEVICE_INTERFACE_DATA structure for a detected device.
Requires:
The DeviceInfoSet returned in SetupDiGetClassDevs.
The HidGuid returned in GetHidGuid.
An index to specify a device.
*/


Result=SetupDiEnumDeviceInterfaces (hDevInfo,
 0, &HidGuid, MemberIndex, &devInfoData);

if (Result != 0)
{
//A device has been detected, so get more information about it.

/*
API function: SetupDiGetDeviceInterfaceDetail
Returns: an SP_DEVICE_INTERFACE_DETAIL_DATA structure
containing information about a device.
To retrieve the information, call this function twice.
The first time returns the size of the structure in Length.
The second time returns a pointer to the data in
DeviceInfoSet.
Requires:
A DeviceInfoSet returned by SetupDiGetClassDevs
The SP_DEVICE_INTERFACE_DATA structure returned by
SetupDiEnumDeviceInterfaces.

The final parameter is an optional pointer to an
SP_DEV_INFO_DATA structure.
This application doesn't retrieve or use the structure.
If retrieving the structure, set
MyDeviceInfoData.cbSize = length of MyDeviceInfoData.
and pass the structure's address.
*/

//Get the Length value.
//The call will return with a "buffer too small" error which
can be ignored.
Result = SetupDiGetDeviceInterfaceDetail
(hDevInfo, &devInfoData, NULL, 0, &Length, NULL);

//Allocate memory for the hDevInfo structure, using the
returned Length.
detailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(Length);

//Set cbSize in the detailData structure.
detailData -> cbSize =
sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);

//Call the function again, this time passing it the returned
 buffer size.
Result = SetupDiGetDeviceInterfaceDetail
(hDevInfo, &devInfoData, detailData, Length, &Required, NULL);

//Open a handle to the device.

/*
API function: CreateFile
Returns: a handle that enables reading and writing to the
device.
Requires:
The DevicePath in the detailData structure
returned by SetupDiGetDeviceInterfaceDetail.
*/

DeviceHandle=CreateFile
(detailData->DevicePath,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);

// DisplayLastError("CreateFile: ");

/*
API function: HidD_GetAttributes
Requests information from the device.
Requires: the handle returned by CreateFile.
Returns: a HIDD_ATTRIBUTES structure containing
the Vendor ID, Product ID, and Product Version Number.
Use this information to decide if the detected device is
the one we're looking for.
*/

//Set the Size to the number of bytes in the structure.
Attributes.Size = sizeof(Attributes);

Result = HidD_GetAttributes
(DeviceHandle,
&Attributes);

// DisplayLastError("HidD_GetAttributes: ");

//Is it the desired device?
MyDeviceDetected = FALSE;


if (Attributes.VendorID == VendorID)
{
if (Attributes.ProductID == ProductID)
{
//Both the Product and Vendor IDs match.
GetDeviceCapabilities();
PrepareForOverlappedTransfer();
MyDeviceDetected = TRUE;
} //if (Attributes.ProductID == ProductID)
else
//The Product ID doesn't match.
CloseHandle(DeviceHandle);
} //if (Attributes.VendorID == VendorID)
else
//The Vendor ID doesn't match.
CloseHandle(DeviceHandle);

//Free the memory used by the detailData structure (no longer
needed).
free(detailData);
}  //if (Result != 0)

else
//SetupDiEnumDeviceInterfaces returned 0, so there are no
more devices to check.
LastDevice=TRUE;

//If we haven't found the device yet, and haven't tried every available device,
//try the next one.
MemberIndex = MemberIndex + 1;

} //do
while ((LastDevice == FALSE) && (MyDeviceDetected == FALSE));
SetupDiDestroyDeviceInfoList(hDevInfo);
return MyDeviceDetected;
}

Если функция возвращает TRUE, то начинаем работать с устройством. Если нет, то оно не подключено. Для работы используются операции чтения и записи информации в устройство. Они производятся в отдельном потоке, но для того, чтобы он был управляемым, целесообразно сделать операции чтения и записи перекрытыми (overlapped). Это позволяет, в случае, если данные с прибора не идут, не застревать на операции чтения.
С этой целью выполняются следующие действия:
Код:
void CDevice::PrepareForOverlappedTransfer()
{
//Get another handle to the device for the overlapped ReadFiles.
ReadHandle=CreateFile
(detailData->DevicePath,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);

//Get an event object for the overlapped structure.

/*API function: CreateEvent
Requires:
  Security attributes or Null
  Manual reset (true). Use ResetEvent to set the event object's state to non-signaled.
  Initial state (true = signaled)
  Event object name (optional)
Returns: a handle to the event object
*/

if (hEventObject == 0)
{
hEventObject = CreateEvent (NULL, TRUE, TRUE, "");

//Set the members of the overlapped structure.
HIDOverlapped.hEvent = hEventObject;
HIDOverlapped.Offset = 0;
HIDOverlapped.OffsetHigh = 0;
}
}

Теперь в нашем распоряжении есть HANDLE ReadHandle, позволяющий производить overlapped чтение и запись данных в устройство. Сам же процесс выглядит примерно следующим образом:
Код:
DWORD WINAPI ReadThread()
{
DWORD Result;
DWORD BytesWritten = 0;
/* Здесь инициализация прибора и занесение информации в OutputReport */
/* Кстати, значения Capabilities.OutputReportByteLength и Capabilities.InputReportByteLength задаются в HID дескрипторе устройства */
Result=WriteFile (ReadHandle, OutputReport,
Capabilities.OutputReportByteLength,
&NumberOfBytesRead, (LPOVERLAPPED) &HIDOverlapped);
Result = WaitForSingleObject(hEventObject, 1000);
if (Result!=0)
{
Result=GetLastError();
}

while (Пока кипит работа)
{

Result = ReadFile (ReadHandle, InputReport,
Capabilities.InputReportByteLength,
&NumberOfBytesRead, (LPOVERLAPPED) &HIDOverlapped); 
Result = WaitForSingleObject(hEventObject, 1000);
switch (Result)
{

case WAIT_OBJECT_0:
case 1:
{

DataReaded=TRUE;
/* тут нужно обрабатывать то, что пришло в инпутрепорте*/

break;
}

case WAIT_TIMEOUT:
{
break;
}

default:
{
break;
}

}
ResetEvent(hEventObject);

}
/* Здесь код завершения работы*/
WriteFile (ReadHandle, OutputReport, Capabilities.OutputReportByteLength,
&NumberOfBytesRead, (LPOVERLAPPED) &HIDOverlapped);
Result = WaitForSingleObject(hEventObject, 1000);
if (Result!=0)
{
Result=GetLastError();
}
return 0;
}

5.   Заключение

В статье был описан один из способов передачи данных от устройства USB в приложение пользователя. Помимо достоинств, одним из которых, и самым важным, является отсутствие необходимости писать драйвер USB, у этого способа есть недостатки. Одним из недостатков, следующих из основного достоинства, является то, что в списке устройств Windows разработанное устройство будет выглядеть, как Устройство ручного ввода или что-нибудь еще из стандартных названий HID устройств, а не как Мое Супер устройство. Список достоинств и недостатков будет пополнен после обсуждения того, что уже написано на форуме.

Первичный код работы с USB был взят из примера usbhidioc by Jan Axelson (jan@lvr.com). Лицензионных ограничений там, вроде, не было.

(версия документа - 0.01)
Александр Баранов
axel@sstu.ru
Версия для печати
Обсудить на форуме