Предлагаем погрузиться в мир 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.), то оптимальным способом будет использовать его. В противном случае от разработчика потребуется реализовать обработку прерываний с чтением регистров.


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

Комментарии (3)


  1. VBKesha
    12.01.2024 13:10

    Спасибо за статью!

    PS. Не я конечно, когда пошли имена файлов догадался о какой OS идёт речь, но всё же думаю стоило где нибудь в начале это упомянуть.


    1. a-n-d
      12.01.2024 13:10
      +2

      Так в самом первом же предложении, под картинкой. Да и в тегах.


      1. VBKesha
        12.01.2024 13:10
        +1

        Мдя отвык от русских названий, действительно всё есть. Тогда мои извинения.