Разработка HID-драйвера: шаг за шагом

от автора

Предлагаем погрузиться в мир Human Interface Device (HID) в контексте операционной системы реального времени «Нейтрино». В статье мы расскажем про архитектуру HID и коснемся практических аспектов создания драйверов для устройств ввода.

Кроме того, затронем вопросы системной разработки и изучения драйверного API для встраиваемых систем реального времени. Расскажем, почему создание драйверов для взаимодействия с HID-устройствами является достаточно важным, но, при этом, достаточно простым процессом.


Общее представление архитектуры HID драйверов

Для того чтобы полностью раскрыть потенциал HID-устройств, требуется изучить их природу и механизмы взаимодействия.

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

Чтобы можно было классифицировать HID-устройство из обилия оборудования, способного работать в соответствии с определенными правилами, была разработана спецификация с учетом следующих принципов:

  • Минимизация объема программного кода, необходимого для функционирования устройства.
  • Возможность программного обеспечения верифицировать информацию, поступающую от устройства.
  • Проектирование протокола с возможностью расширения и стабильной работой. Сам протокол состоит из дескриптора репорта и отчетов.

Ниже перечислены основные характеристики и требования к функционированию HID-устройства:

  1. Скорость передачи данных: в контексте USB существуют полноскоростное и низкоскоростное HID-устройства. Первые способны передавать до 60 000 байт в секунду, что соответствует 64 байтам в каждом кадре длительностью 1 мс, а вторые имеют скорость передачи 800 байт в секунду, что соответствует 8 байтам каждые 10 мс.
  2. HID-устройство может устанавливать частоту своего опроса для проверки наличия новых данных для передачи;
  3. Обмен данными с использованием протокола репортов: вся связь с HID-устройством осуществляется через структуру, называемую «дескриптор репорта» (report descriptor). Дескриптор репорта может содержать до 65 535 байт данных и имеет гибкую структуру для его модифицирования.
  4. HID-устройство должно содержать дескриптор устройства и один или более дескрипторов репорта;
  5. Поддержка управляющих запросов.

Для разработчика HID-драйвера важно реализовать поддержку структур, описывающих HID-интерфейс, и обеспечить обмен данными.

В нашей ОС общая структура взаимодействия устройств ввода с системой выглядит так:

Общая структура подсистемы ввода на примере клиента в виде оконного окружения Photon (legacy)

HID-драйвер состоит из дескриптора модуля драйвера и интерфейса организации драйвера. Их взаимное расположение в архитектуре имеет вид:

Компоненты HID-драйвера

HID-драйвер представляет собой программный модуль, который обеспечивает взаимодействие между HID-менеджером и HID-устройствами.

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

Как уже было сказано выше, HID-драйвер напрямую «общается» с устройством, что позволяет быстро реагировать на контакт пользователя с устройством ввода.


HID-драйвер изнутри

Для детального понимания из чего же состоит HID-драйвер разберем его составляющие:

  1. io_hid_dll_entry_t – структура, определяющая модуль драйвера:

      #include <sys/io-hid.h>    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 (функции инициализации и деинициализации).

  2. io_hid_registrant_funcs_t – структура, определяющая интерфейс драйвера:

      #include <sys/io-hid.h>    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 мыши.


Имплементация HID-Драйвера

Заполняем структуру io_hid_dll_entry_t

#include <sys/io-hid.h>  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 с devh-sample.so

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?

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_tsample_funcs является ею.

Сначала реализуем управляющие функции. Если функции не будут выполнять никаких действий, то необходима stub реализация с кодом возврата EOK. Например:

int client_attach( int reg_hdl, void *user ) {     return (EOK); }

В нашем примере все функции реализованы заглушками.

Заполняем io_hid_registrant_funcs_t макросом _IO_HID_REG_NFUNCS и функциями:

#include <sys/io-hid.h>  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-драйвера

С примерами реально работающих драйверов вы можете ознакомиться по ссылке. Там представлены HID-драйвера для USB-тачскринов Egalax и GPIO-клавиатур.


Вместо заключения

В этой статье мы постарались кратко и доступно описать процесс реализации HID-драйверов для операционной системы реального времени Нейтрино. Подробную информацию про драйверное API вы можете найти по ссылке.

Стоит добавить, что в данном примере мы разобрали частный случай взаимодействия с устройством. Если присутствует готовый инструмент (менеджер ресурсов) для управления ресурсами интерфейса (I2C, SPI, etc.), то оптимальным способом будет использовать его. В противном случае от разработчика потребуется реализовать обработку прерываний с чтением регистров.

Подписывайтесь на наш канал, чтобы быть в курсе свежих новостей


ссылка на оригинал статьи https://habr.com/ru/articles/783582/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *