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

mc3d2dfobdlwboi6iwg--bnzc_o.jpeg

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

Кроме того, затронем вопросы системной разработки и изучения драйверного API для встраиваемых систем реального времени. Расскажем, почему создание драйверов для взаимодействия с 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-драйвер разберем его составляющие:


  1. 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 (функции инициализации и деинициализации).


  2. 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 с 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_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-драйвера

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


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

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


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

f2fa7a9678d8b92a5ca57a59aeaea0fe.png

© Habrahabr.ru