Разработка HID-драйвера: шаг за шагом
Предлагаем погрузиться в мир Human Interface Device (HID) в контексте операционной системы реального времени «Нейтрино». В статье мы расскажем про архитектуру HID и коснемся практических аспектов создания драйверов для устройств ввода.
Кроме того, затронем вопросы системной разработки и изучения драйверного API для встраиваемых систем реального времени. Расскажем, почему создание драйверов для взаимодействия с HID-устройствами является достаточно важным, но, при этом, достаточно простым процессом.
Для того чтобы полностью раскрыть потенциал HID-устройств, требуется изучить их природу и механизмы взаимодействия.
HID подразумевает стандарт, который определяет протокол обмена данными между устройствами ввода. Он сыграл ключевую роль в вычислительных системах. Помог обеспечить универсальное и простое взаимодействие между пользователем, различным оборудованием и компьютером: от клавиатур до геймпадов, от мышей до сенсорных экранов. Все это создает интерфейс между человеком и машиной.
Чтобы можно было классифицировать HID-устройство из обилия оборудования, способного работать в соответствии с определенными правилами, была разработана спецификация с учетом следующих принципов:
- Минимизация объема программного кода, необходимого для функционирования устройства.
- Возможность программного обеспечения верифицировать информацию, поступающую от устройства.
- Проектирование протокола с возможностью расширения и стабильной работой. Сам протокол состоит из дескриптора репорта и отчетов.
Ниже перечислены основные характеристики и требования к функционированию HID-устройства:
- Скорость передачи данных: в контексте USB существуют полноскоростное и низкоскоростное HID-устройства. Первые способны передавать до 60 000 байт в секунду, что соответствует 64 байтам в каждом кадре длительностью 1 мс, а вторые имеют скорость передачи 800 байт в секунду, что соответствует 8 байтам каждые 10 мс.
- HID-устройство может устанавливать частоту своего опроса для проверки наличия новых данных для передачи;
- Обмен данными с использованием протокола репортов: вся связь с HID-устройством осуществляется через структуру, называемую «дескриптор репорта» (report descriptor). Дескриптор репорта может содержать до 65 535 байт данных и имеет гибкую структуру для его модифицирования.
- HID-устройство должно содержать дескриптор устройства и один или более дескрипторов репорта;
- Поддержка управляющих запросов.
Для разработчика HID-драйвера важно реализовать поддержку структур, описывающих HID-интерфейс, и обеспечить обмен данными.
В нашей ОС общая структура взаимодействия устройств ввода с системой выглядит так:
HID-драйвер состоит из дескриптора модуля драйвера и интерфейса организации драйвера. Их взаимное расположение в архитектуре имеет вид:
HID-драйвер представляет собой программный модуль, который обеспечивает взаимодействие между HID-менеджером и HID-устройствами.
Драйвер принимает на себя задачу интерпретации данных, поступающих от HID-устройства, для их правильного восприятия HID-менеджером. Это подразумевает не только определение стандартных действий пользователя (клик мыши, нажатие клавиши на клавиатуре), но и предоставление удобного интерфейса для управления HID-устройством.
Как уже было сказано выше, HID-драйвер напрямую «общается» с устройством, что позволяет быстро реагировать на контакт пользователя с устройством ввода.
Для детального понимания из чего же состоит HID-драйвер разберем его составляющие:
io_hid_dll_entry_t — структура, определяющая модуль драйвера:
#include
typedef struct _io_hid_dll_entry { char *name; int nfuncs; int (*init)( void *dll_hdl, dispatch_t *dpp, io_hid_self_t *ioh, char *args ); int (*shutdown)( void *dll_hdl ); } io_hid_dll_entry_t; Она включает поля name (имя драйвера), nfuncs (количество функций модуля драйвера), init и shutdown (функции инициализации и деинициализации).
io_hid_registrant_funcs_t — структура, определяющая интерфейс драйвера:
#include
typedef struct _io_hid_registrant_funcs { _Uint32t nfuncs; int (*client_attach)( int reg_hdl, void *user ); int (*client_detach)( int reg_hdl, void *user ); /* … */ } io_hid_registrant_funcs_t; io_hid_registrant_funcs_t содержит набор функций для взаимодействия HID-драйвера с HID-устройством, иначе — управляющие запросы. Каждая функция вызывается по определенном запросу от HID-менеджера. Например, client_attach/client_detach вызываются в том случае, когда клиент подключается/отключается к/от io-hid. Более подробно изучить назначение каждого управляющего запроса можно тут.
Теперь мы имеем представление о том, что из чего состоит HID-драйвер. Остается узнать, как он должен функционировать, и написать код, к примеру, для подключаемой по RS-232 мыши.
Заполняем структуру io_hid_dll_entry_t
#include
io_hid_dll_entry_t io_hid_dll_entry = {
"devh-sample",
_IO_HID_DLL_NFUNCS,
init,
stop
};
name будет использоваться для определения библиотеки при монтировании в io-hid. Префикс «devh-» является обязательным и обусловлен процессом парсинга HID-менеджером имен драйверов. _IO_HID_DLL_NENTRY — макрос для определения количества функций в структуре io_hid_dll_entry_t.
io-hid -d sample &
или
io-hid &
mount -T io-hid devh-sample.so
Заведем глобальную структуру для хранения выделяемых/предоставляемых ресурсов и структуру для регистрации управляющего дескриптора:
typedef struct _ctrl
{
void *dll_hdl;
io_hid_self_t *ioh;
char *path;
pthread_t tid;
int fd;
} ctrl_t;
typedef struct _sample
{
int hid_hdl;
} sample_t;
ctrl_t Ctrl;
Описанная структура управляющего дескриптора регистрируется в HID-менеджере и в дальнейшем передается в управляющие вызовы.
Управляющая структура, созданная разработчиком, может содержать различные данные в зависимости от конкретных потребностей устройства. Одним из примеров может быть использование дескриптора для передачи специфической информации о состоянии устройства, калибровочных данных, настроек и т. д. Управляющие дескрипторы могут также использоваться для управления устройством и настройки его параметров.
В отличие от стандартных репортов, предусмотренных HID-протоколом, которые используются для передачи ванильных данных устройств ввода (таких как нажатия клавиш, перемещения мыши и т. д.), управляющий дескриптор предоставляет разработчикам большую свободу в определении структуры данных, которые они хотят передавать через HID-интерфейс.
Управляющий дескриптор будет передаваться в управляющие вызовы, где будем заполнять необходимые поля или использовать их, но в первую очередь она необходима для хранения зарегистрированного дескриптора модуля драйвера (hid_hdl).
В этих структурах можно объявлять н-ое количество полей. В нашем примере содержатся базово необходимые поля для реализации драйвера.
io_hid_dll_entry_t: init
Выше упомянуто, что это инициализирующая функция. Она вызывается при монтировании библиотеки в io-hid.
В ней необходимо выполнить следующие шаги:
Обработать опции (в нашем случае реализуем одну опцию — путь к менеджеру ресурсов serial-порта):
char *opts[] = { "path", NULL }; int start_driver( char *options ) { char *value; Ctrl.path = "/dev/ser1"; while ( options && *options != '\0' ) { switch ( getsubopt( &options, opts, &value ) ) { case 0: if ( value != NULL ) Ctrl.path = value; break; default: break; } } return (EOK); }
Заполнить hidd_device_ident_t и io_hid_registrant_t, а также зарегистрировать дескриптор отчета:
static unsigned char rdesc[] = { /* ... */ }; int get_report_descriptor ( sample_t *sample ) { hidd_device_ident_t device_ident; io_hid_registrant_t dev; int sample_hdl; int result = EOK; device_ident.vendor_id = 0; device_ident.product_id = 0; device_ident.version = 0x100; dev.flags = 0; dev.device_ident = &device_ident; dev.desc = rdesc; dev.dlen = sizeof (rdesc); dev.user_hdl = sample; dev.funcs = &sample_funcs; result = (*Ctrl.ioh->reg)( Ctrl.dll_hdl, &dev, &sample_hdl ); if ( result == EOK ) sample->hid_hdl = sample_hdl; return (result); }
В hidd_device_ident_t объявляется информация об устройстве ввода, которая потом передается io_hid_registrant_t — регистрирующую дескриптор устройства.
Создать поток для получения данных:
void *event_hdl_polling( void *data ) { sample_t *sample; uint8_t buffer[256]; int count, i; uint32_t packet_len; sample = (sample_t *)data; memset( buffer, 0, sizeof( uint8_t ) * 256 ); for ( ;; ) { memset( buffer, 0, sizeof( uint8_t ) * 256 ); count = read( Ctrl.fd, buffer, sizeof( uint8_t ) * 256 ); if ( count > 0 ) { /* * Здесь необходима реализация обработки принятых * данных в соответствии с протоколом устройства, * заложенным производителем */ /* ... */ /* Когда обработали данные отправляем их в io-hid */ (*Ctrl.ioh->send_report)( sample->hid_hdl, (uint8_t *)buffer, (uint32_t)packet_len ); } } return NULL; }
Тогда функция инициализации будет иметь следующий вид:
int init( void *dll_hdl, dispatch_t *dpp, io_hid_self_t *ioh, char *args )
{
pthread_attr_t attr;
sample_t sample;
int result = EOK;
Ctrl.dll_hdl = dll_hdl;
Ctrl.ioh = ioh;
ThreadCtl( _NTO_TCTL_IO, 0 );
memset( &Ctrl, 0, sizeof( Ctrl ) );
start_driver( args );
if ( (Ctrl.fd = open( Ctrl.path, O_RDONLY )) == -1 )
return (EXIT_FAILURE);
get_report_descriptor( &sample );
pthread_attr_init( &attr );
result = pthread_create( &Ctrl.tid, &attr,
(void *)event_hdl_polling, &sample );
if ( result != EOK )
return (EXIT_FAILURE);
return (EOK);
}
rdesc — дескриптор репортов (Report Descriptors). Используется для описания формата данных, которые HID-устройство отправляет или принимает в рамках своей функциональности. Cодержит информацию о том, какие элементы данных присутствуют в передаваемых или принимаемых отчетах, и как они организованы.
static unsigned char rdesc[] = {
0x05, 0x01, // Usage Page(Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xa1, 0x01, // Collection(Application)
0x09, 0x01, // Usage(Pointer)
0xa1, 0x00, // Collection(Physical)
0x05, 0x09, // Usage Page(Button)
0x19, 0x01, // Usage Min(1)
0x29, 0x03, // Usage Max(3)
0x15, 0x00, // Logical Min(0)
0x25, 0x01, // Logical Max(1)
0x95, 0x03, // Report Count(3)
0x75, 0x01, // Report Size(1)
0x81, 0x02, // Input(Data,Variable,Absolute)
0x95, 0x01, // Report Count(1)
0x75, 0x05, // Report Size(5)
0x81, 0x03, // Input(Cnst,Variable,Absolute)
0x05, 0x01, // Usage Page(Generic Desktop)
0x09, 0x30, // Usage(X)
0x09, 0x31, // Usage(Y)
0x15, 0x81, // Logical Min(-127)
0x25, 0x7F, // Logical Max(127)
0x75, 0x08, // Report Size(8)
0x95, 0x02, // Report Count(2)
0x81, 0x06, // Input(Data,Variable,Rel)
0xc0, // End Collection
0xc0 // End Collection
};
io_hid_dll_entry_t: stop
При демонтировании библиотеки необходимо освободить все выделенные в init ресурсы. В нашем случае следует завершить поток и закрыть дескриптор менеджера ресурсов.
int stop( void *dll_hdl )
{
pthread_cancel( Ctrl.tid );
pthread_join( Ctrl.tid, NULL );
close( Ctrl.fd );
return (EOK);
}
Заполняем структуру io_hid_registrant_funcs_t
Внимательный читатель, возможно, заметил, что при заполнении io_hid_registrant_t мы не рассказали про sample_funcs. Выше упоминалось про структуру io_hid_registrant_funcs_t — sample_funcs является ею.
Сначала реализуем управляющие функции. Если функции не будут выполнять никаких действий, то необходима stub реализация с кодом возврата EOK. Например:
int client_attach( int reg_hdl, void *user )
{
return (EOK);
}
В нашем примере все функции реализованы заглушками.
Заполняем io_hid_registrant_funcs_t макросом _IO_HID_REG_NFUNCS и функциями:
#include
static io_hid_registrant_funcs_t sample_funcs = {
_IO_HID_REG_NFUNCS,
client_attach,
client_detach,
rbuffer_alloc,
rbuffer_free,
report_read,
report_write,
get_idle,
set_idle,
get_protocol,
set_protocol,
get_string,
get_indexed_string,
reset,
};
В итоге получаем шаблон драйвера для мыши RS-232, нуждающегося лишь в реализации обработки входных данных.
Схема работы типичного HID-драйвера будет выглядеть так:
С примерами реально работающих драйверов вы можете ознакомиться по ссылке. Там представлены HID-драйвера для USB-тачскринов Egalax и GPIO-клавиатур.
В этой статье мы постарались кратко и доступно описать процесс реализации HID-драйверов для операционной системы реального времени Нейтрино. Подробную информацию про драйверное API вы можете найти по ссылке.
Стоит добавить, что в данном примере мы разобрали частный случай взаимодействия с устройством. Если присутствует готовый инструмент (менеджер ресурсов) для управления ресурсами интерфейса (I2C, SPI, etc.), то оптимальным способом будет использовать его. В противном случае от разработчика потребуется реализовать обработку прерываний с чтением регистров.
Подписывайтесь на наш канал, чтобы быть в курсе свежих новостей