Пишем драйвер для графического планшета

    Немного занимаюсь рисованием, и вот купил себе Huion Q11K — качество на уровне такого же Интоуса Про, но ценник ниже чуть ли не в 3 раза. Подключил, порисовал даже, на Windows 10 всё работает. Перезагрузился в линукс, и началось…

    image

    Заранее извиняюсь, если где-то есть ошибки — пишите в комментарии, что не так. Я не программист под ядро и это мой первый опыт в написании драйверов с нуля.

    В сусе по-умолчанию есть драйвер uclogic, он при подключении загружается, говорит, что vid\pid знает, но девайс не поддерживается, и всё. Погуглил. Глухо везде — планшет новый, и упомниание линуксового драйвера для этого планшета есть только в одном проекте на гитхабе, жалоба в разделе багов в духе «когда будет драйвер». Больше ни одного тематического упоминания.

    «Печально» — подумал я, но рисовать в винде что-то не хотелось, я уже привык к линуксам — «а может самому написать? Там вроде ничего сложного… SO и гугл всё знают же!»
    Ох, как я ошибался…

    Открыл исходники ядра /drivers/hid/, начал смотреть, как сделан скелет. По образу и подобию набросал свой Makefile, накидал собираемый скелет модуля. Состоит он из шапки инклудов, пустых объявлений нужных функций да из нижней части с описателями функционала. Сделал make, пустой модуль собрался, уже хорошо. Нижняя часть изначально получилась такой:

    static const struct hid_device_id q11k_device[] = {
        { HID_USB_DEVICE(USB_VENDOR_ID_HUION, USB_DEVICE_ID_HUION_TABLET)
        {}
    };
    
    static struct hid_driver q11k_driver = {
    	.name                  = MODULENAME,
    	.id_table              = q11k_device,
    	.probe                 = q11k_probe,
        .remove                = q11k_remove, 
    	.report_fixup          = q11k_report_fixup,
    	.raw_event             = q11k_raw_event,
    #ifdef CONFIG_PM
    	.resume	               = uclogic_resume,
    	.reset_resume          = uclogic_resume,
    #endif
    };
    module_hid_driver(q11k_driver);
    
    MODULE_AUTHOR("Konata Izumi <konachan.700@localhost>");
    MODULE_DESCRIPTION("Huion Q11K device driver");
    MODULE_LICENSE("GPL");
    MODULE_VERSION("1.0.0");
    
    MODULE_DEVICE_TABLE(hid, q11k_device);
    

    Теперь разберем поближе, что тут к чему.

    Структура hid_device_id, передаваемая в макрос MODULE_DEVICE_TABLE, описывает некую информацию относительно ID устройств поддерживаемых драйвером. Туда же кладется дополнительная информация, но о ней ниже.

    Структура hid_driver описывает скелет драйвера, его основной функционал.

    • .name — отображаемое имя драйвера.
    • .id_table — сюда кладем hid_device_id.
    • .probe — старт драйвера, но он не простой. Он вызывается несколько раз, для каждого логического устройства (интерфейса), в данном случае их два — кнопки и сам планшет.
    • .remove — остановка драйвера, если донгл или кабель планшета вытащен. Тоже вызывается несколько раз.
    • .report_fixup — вот тут гуру, подскажите, что это. Я верно понял, что это позволяет менять HID-репорт?
    • .raw_event — вызывается, когда от устройства прилетает репорт
    • .resume и .reset_resume — насколько я понял, это восстановление работы драйвера после возвращения компьютера из спячки

    Ну ладно, скелет накидал, искать информацию уже запарился. Реально, ядро линукса это не РНР или жава, когда вбиваешь в гугл «java reflection get default constructor» — два миллиона ссылок, и десяток сразу с решением проблемы. Вбиваешь «ByteArrayOutputBuffer» — и сразу жавадок, сразу тысячи примеров применения… Я наивный, полез искать структуры ядра по тому же принципу…

    А там всё оказалось сурово: три страницы на китайском, куча мусора из багтрекеров, древнючие списки рассылки. Местами какой-то странный сайт, похожий на дорвей, где заиндексированы и перелинкованы все исходники. И 2 страницы гугла. Где статьи, где SO? А нет их.

    Ну ладно, у нас вроде как есть исходники, будем смотреть там. Для начала надо выцепить hid report-ы от железки, желательно в равках, не разобранные, чтобы анализировать было удобнее. Еще раз порылся по /drivers/hid/, нарыл там процесс регистрации hid-устройства. Выглядит так:

            int rc;
            rc = hid_parse(hdev);
            if (rc) {
                hid_err(hdev, "parse failed\n");
                return rc;
            }
            
            rc = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
            if (rc) {
                hid_err(hdev, "hw start failed\n");
                return rc;
            }
    

    Казалось бы, ничего сложного… Но снова подводный камень — репорты в нужную функцию не падают. Полез ковырять исходники дальше на предмет «почему». Оказалось, что для приёма репортов нужно еще и открыть устройство, чего в некоторых драйверах не делают по разным причинам.

            rc = hid_hw_open(hdev);
            if (rc) {
                hid_err(hdev, "cannot open hidraw\n");
                return rc;
            }
    

    Ура, репорты посыпались! Но вот незадача — как из ядра вывести hex-дампы-то? Побайтово некошерно. Опять начал искать, в этот раз решение нашлось в гугле на второй странице:

    printk("q11k_raw_event - %*phC", size, data);
    

    Выводит hex-дамп, обрезает массив до первых 64 байт — то что надо. Посыпались равки, не буду тащить их сюда, дабы не мусорить. Внезапно открыл для себя dmesg -wH, очень удобно оказалось… Анализ занял несколько минут, ибо равки были все по 8 байт, и структура была примитивная: первый байт постоянный, второй битовая маска действия, дальше или фиксированный репорт для кнопок, или по два байта Х, Y и нажатие. Распарсил, получил нужные значения, вывел отладочную строку уже с координатами — ура, первая часть сделана. Еще чуть поковырял на тему закрытия ресурсов, ибо гадить в ядре опасно. Понял, что надо сделать так:

    void q11k_remove(struct hid_device *dev) {
        hid_hw_close(dev);
        hid_hw_stop(dev);
    }
    

    Теперь надо как-то послать эти данные системе. Полез снова в исходники ядра, потому что этого гугл не знает, предлагая только методичку в двух словах из документации и всякую фигню. Через час где-то накопал, что надо выделить память под input device, потом заполнить какую-то структуру, потом зарегистрировать input device, и только уже потом можно с этим устройством ввода работать.

    Набросал по быстрому код, где было одно устройство, объединяющее кнопки и планшет — не, ну зачем много-то их разводить? Это было ошибкой… В консоли и планшет и кнопки отлично видны, нужный /dev/input/eventX данные выплёвывает, но вот X-сервер этого гибрида не жрет, плюясь вот так:

    лог убрал под спойлер
    [ 7477.255] (II) config/udev: Adding input device Huion Q11K Tablet (/dev/input/event19)
    [ 7477.255] (**) Huion Q11K Tablet: Applying InputClass «evdev keyboard catchall»
    [ 7477.255] (**) Huion Q11K Tablet: Applying InputClass «evdev tablet catchall»
    [ 7477.255] (**) Huion Q11K Tablet: Applying InputClass «evdev keyboard catchall»
    [ 7477.255] (**) Huion Q11K Tablet: Applying InputClass «evdev tablet catchall»
    [ 7477.255] (**) Huion Q11K Tablet: Applying InputClass «evdev tablet catchall»
    [ 7477.255] (**) Huion Q11K Tablet: Applying InputClass «libinput keyboard catchall»
    [ 7477.255] (**) Huion Q11K Tablet: Applying InputClass «libinput tablet catchall»
    [ 7477.255] (II) Using input driver 'libinput' for 'Huion Q11K Tablet'
    [ 7477.255] (**) Huion Q11K Tablet: always reports core events
    [ 7477.255] (**) Option «Device» "/dev/input/event19"
    [ 7477.255] (**) Option "_source" «server/udev»
    [ 7477.256] (II) event19 — (II) Huion Q11K Tablet: (II) is tagged by udev as: Keyboard Tablet
    [ 7477.256] (EE) event19 — (EE) Huion Q11K Tablet: (EE) libinput bug: device does not meet tablet criteria. Ignoring this device.
    [ 7477.256] (II) event19 — (II) Huion Q11K Tablet: (II) device is a tablet
    [ 7477.324] (II) event19 — failed to create input device '/dev/input/event19'.
    [ 7477.324] (EE) libinput: Huion Q11K Tablet: Failed to create a device for /dev/input/event19
    [ 7477.324] (EE) PreInit returned 2 for «Huion Q11K Tablet»
    [ 7477.324] (II) UnloadModule: «libinput»

    libinput bug: device does not meet tablet criteria. Ignoring this device.
    И что это означает? Ну хорошо, хотя бы есть эта строка с названием библиотеки. Скачиваю исходник libinput, grep-ом ищу в ней строку, нахожу процедуру, проверяющую корректность получаемых настроек. Тут я застрял еще на час, ибо непонятно, что именно не понравилось libinput. Данные, необходимые для инициализации, вроде все передаю. Гугл не находит вообще ничего путного, мы забрались слишком глубоко.

    Ладно, думаю, зайду с другой стороны. Прикинусь Вакомом, и попробую скормить данные другому драйверу xorg. Начал ковырять исходник вакомовкого драйвера… Во-первых, он универсальный. Во-вторых, он объемный. Я застрял тут, плюнул и пошел спать — с утра, думаю, разберусь.

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

    Сама длинная структура
    struct wacom_features {
    	const char *name;
    	int x_max;
    	int y_max;
    	int pressure_max;
    	int distance_max;
    	int type;
    	int x_resolution;
    	int y_resolution;
    	int numbered_buttons;
    	int offset_left;
    	int offset_right;
    	int offset_top;
    	int offset_bottom;
    	int device_type;
    	int x_phy;
    	int y_phy;
    	unsigned unit;
    	int unitExpo;
    	int x_fuzz;
    	int y_fuzz;
    	int pressure_fuzz;
    	int distance_fuzz;
    	int tilt_fuzz;
    	unsigned quirks;
    	unsigned touch_max;
    	int oVid;
    	int oPid;
    	int pktlen;
    	bool check_for_hid_type;
    	int hid_type;
    };
    


    И ее заполнение, не полностью:

    static const struct wacom_features wacom_features =
    	{ "Wacom Penpartner", 32640, 32640, 8192, 0, 4, 40, 40 };
    

    Сразу скажу, что вот эти данные в идеале надо получать от планшета, но это надо выцеплять из на windows через wireshark, чего очень не хотелось. Потому данные были проставлены эмпирически, по ряду проведенных опытов.

    Дальше поменял VID на вакомовский:

    idev->id.vendor = 0x56a;

    После заполненную структуру надо передать в структуру hid_device_id отдельным полем:

    static const struct hid_device_id q11k_device[] = {
        { HID_USB_DEVICE(USB_VENDOR_ID_HUION, USB_DEVICE_ID_HUION_TABLET), .driver_data = (kernel_ulong_t)&wacom_features },
        {}
    };
    

    Еще час на всевозможные опыты, и, ура — планшет ожил!

    Данные о позиции и давлении передаем через вот такой нехитрый код:

            if (data[1] == 0xc0) {
                input_report_key(idev, BTN_TOOL_PEN, 0);
                input_report_abs(idev, ABS_PRESSURE, 0);
            } else {
                input_report_key(idev, BTN_TOOL_PEN, 1);
                input_report_abs(idev, ABS_PRESSURE, pressure);
            }
            
            input_report_abs(idev, ABS_X, x_pos);
            input_report_abs(idev, ABS_Y, y_pos);
            
            input_sync(idev);
    

    Однако кнопки работать и не думали, несмотря на то, что код работал исправно и в консоли события были видны. Снова начал думать, что делать… И так, и так настройки менял — ничего не выходило. Плюнул и создал новый input device, отдельный для кнопок. Там тоже оказались подводные камни — чтобы этот input device снова нам не выводил ту самую ошибку в xorg, и был клавиатурой, а не планшетом, надо убрать ссылки на родительское hid-устройство из структуры инициализации:

                idev_keyboard = input_allocate_device();
                if (idev_keyboard == NULL) {
                    hid_err(hdev, "failed to allocate input device [kb]\n");
                    return -ENOMEM;
                }
                
                idev_keyboard->name = "Huion Q11K Keyboard";
                idev_keyboard->id.bustype = BUS_USB;
                idev_keyboard->id.vendor  = 0x04b4;
                idev_keyboard->id.version = 0;
                idev_keyboard->keycode = def_keymap;
                idev_keyboard->keycodemax  = Q11K_KEYMAP_SIZE;
                idev_keyboard->keycodesize = sizeof(def_keymap[0]);
                
                set_bit(EV_REP, idev_keyboard->evbit);
                set_bit(EV_KEY, idev_keyboard->evbit);
                
                input_set_capability(idev_keyboard, EV_MSC, MSC_SCAN);
                
                for (i=0; i<Q11K_KEYMAP_SIZE; i++) {
                    input_set_capability(idev_keyboard, EV_KEY, def_keymap[i]);
                }
                
                rc = input_register_device(idev_keyboard);
                if (rc) {
                    hid_err(hdev, "error registering the input device [kb]\n");
                    input_free_device(idev_keyboard);
                    return rc;
                }
    

    и добавить список используемых клавиш:

    #define Q11K_KEYMAP_SIZE 11
    static unsigned short def_keymap[Q11K_KEYMAP_SIZE] = {
        KEY_0, KEY_1, KEY_2, KEY_3,  
        KEY_4, KEY_5, KEY_6, KEY_7,  
        KEY_8, KEY_9, KEY_RIGHTCTRL
    };
    

    Вот теперь кнопки заработали как надо! Отправка сочетания CTRL+<number> сделана так:

    static void __q11k_rkey_press(unsigned short key, int b_key_raw, int s) {
        input_report_key(idev_keyboard, KEY_RIGHTCTRL, s);
        input_sync(idev_keyboard);
        input_report_key(idev_keyboard, key, s);
        input_sync(idev_keyboard);
    }
    
    static void q11k_rkey_press(unsigned short key, int b_key_raw) {
        __q11k_rkey_press(key, b_key_raw, 1);
        __q11k_rkey_press(key, b_key_raw, 0);
    }
    

    Вот теперь планшет полноценно заработал.

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

    Код полностью выложен на гитхабе: github.com/konachan700/Q11K_Driver

    P.S. Очень удивило, что в гугле крайне мало информации о кодинге под ядро. Почему так? Столько кода написано, миллионы человекочасов — и ни у кого не возникло желание хоть что-то описать или задокументировать?
    Поделиться публикацией
    Похожие публикации
    Никаких подозрительных скриптов, только релевантные баннеры. Не релевантные? Пиши на: adv@tmtm.ru с темой «Полундра»

    Зачем оно вам?
    Реклама
    Комментарии 28
    • +1
      Зашлите в апстрим, пожалуйста. Это делают через LKML или (лучше) через мейнтейнера соответствующей подсистемы. Я не уверен, но, видимо, HID CORE, www.kernel.org/doc/linux/MAINTAINERS
      • +1
        мейнтейнер подсистемы выясняется с помощью

        scripts/get_maintainer.pl
      • 0
        gtmail.com в коде как почтовый адрес специально оставлен или опечатка?
        а то редиректит на не очень хорошие ресурсы и хром ругается
        • +1
          Извиняюсь, опечатка. Поправлю чуть позже.
        • +3
          крайне мало информации о кодинге под ядро

          Да вообще них*я нет. Удалось что-нибудь более менее перевариваемое найти? Поделитесь ссылками, пожалуйста.
          • 0

            Сам я разработчиком под ядро не являюсь, поэтому оценивать не берусь, но натыкался на такую книгу (переводы и т.д. / читать здесь). Она не то, чтобы прицельно про разработку драйверов — насколько я понимаю, человек изучал кодинг под ядро, читал исходники и документацию и попутно писал. То есть это не истина в последней инстанции, но может пригодиться — в том числе для развлечения почитать — там много всего описано. :)

            • 0
              Не удалось. Мне ковырять чужой код не впервой, я до того так же ковырял очень слабо документированную libopencm3, даже баг в криптогафии там нашел… Потому разобрался только по исходникам и обрывкам инфы в сети.
              Замечено, что проекты на С почти сплошняком плохо документированны.
              • 0
                При работе с ядром есть папка doc там в принципе как отправная точка много чего есть. А читать код ядра удо, нее через lxr( тотже lxr.free-electrons.com например)
            • +6
              Вот они — героические люди, которые делают какую-то магию, чтобы разные непонятные железяки начинали работать. Спасибо) Присоединюсь к amarao — надо в апстрим отправить.
              • +1
                Мне кажется что в таком виде, с хаками вроде вакомовского VID в idev->id.vendor, в апстрим его никто не примет.
                • 0
                  Да, не возьмут. Во-первых, уже есть родственный проект для других планшетов Huion, и надо было бы по-хорошему писать туда. Во-вторых, этот планшет очень схож по hid-репорту с Wacom Bamboo одной из серий, и надо бы не только модуль ядра писать, но и патч для Xf86-input-wacom — но там вообще темный лес, с какой стороны подходить даже непонятно, и еще более непонятно, как отлаживать без перезапуска Х… В-третьих, очень много хаков, от чего не все программы видят его — на гитхабе уже отписали, что в крите работает (я в крите и рисую, проверял только там), в блендере тоже, а вот в какой-то платной софтине для моделирования — нет, работает как мышка.
                  • 0
                    А если в готовом и уже рабочем коде драйвера убрать VID вакомовский и прописать, скажем от какого-нибудь другого планшета или вообще левый VID, которого нет в системе, то работать перестанет? А вообще, круто, да.
                    • 0
                      Да, будет ошибка libinput, что данный планшет чему-то там не соответствует внутри либы. А вот чему — я не понял, там, в libinput, проверка на наличие передаваемых от драйвера размеров и еще кое-чего. Все это передавал, но нет, не взлетало.
              • +4
                Очень удивило, что в гугле крайне мало информации о кодинге под ядро.

                • Linux Device Drivers, Greg Kroah-Hartman
                • Linux Kernel Development, Robert Love
                • Linux System Programming, Robert Love


                Для поиска референсов и кто кого инициализирует нужно использовать LXR — например elixir.free-electrons.com/linux/latest/source
                вот например твой hid_device и кто его использует elixir.free-electrons.com/linux/latest/ident/hid_device
              • 0
                Извиняюсь, нет времени подробно вчитываться в статью. Можно коротко — где взяли информацию о самом устройстве, какой у него рабочий интерфейс?
                • +1
                  Там же hid, что очень упрощает дело. xxd /dev/hidrawX и смотреть, что прилетает. Можно еще wireshark использовать, он умеет usb перехватывать и парсить.
                  • 0
                    То есть все устанавливали экспериментальным путем? Никакой официальной документации нет?
                    • +1
                      Нет конечно, какая документация… У производителя есть драйвер для Win10 бинарником и больше ничего. Hid не так сложно реверсить в общем случае.
                • 0
                  Huion будет работать!
                  • +3
                    Вы отлично разобрались с тем как работают драйверы ввода, и заставили планшет работать, поздравляю :)!

                    Как я уже вам писал, .report_fixup вызывается для того чтобы драйвер мог подправить, или заменить HID report descriptor — структуру полученную с утройства, и, в теории, описывающую структуру посылаемых им reports. Функция несколько неудачно названа и скорее должна называться ".report_descriptor_fixup".

                    Большая часть документации находится в Documentation, затем в заголовках, а потом уже нужно читать сам код. В том числе смотрите Documentation/hid, include/linux/hid.h, drivers/hid/hid-core.c, drivers/hid/hid-input.c, и drivers/hid/usbhid.

                    Однако, было бы хорошо, если бы вы смогли помочь проекту DIGImend вместо того чтобы разрабатывать еще один драйвер. Таким образом вы могли бы получить лучше поддержку в userspace, скорее поддержку в upstream, и научились бы еще много чему. В частности нужно подтвердить способ инициализации вашего планшета, чтобы он работал с полным разрешением, и потестировать драйвер.
                    • +1
                      Да, я поддерживаю объединение драйверов, поскольку мое решение больше костыль. Не думаю, что будет быстро, но тем не менее, постараюсь помочь.
                      Инициализации у этого планшета вроде как нет, у меня есть полный дамп трафика между планшетом и драйвером windows — хост ничего не посылает устройству. Может быть, я плохо смотрел, не знаю, но вроде бы пакетов нет.
                      • +1
                        Почитайте наше обсуждение на GitHub. По-умолчанию планшет шлёт отчеты с report ID 0x0a и обрезанным разрешением, под Windows он шлёт отчеты с report ID 0x08 и полным разрешением. Предположительно инициализация выполняется запросом определённого string descriptor.
                        • +1
                          Да, запросами нужных string descriptors действительно получил другой репорт, 12 байт, и большее разрешение. Переписал драйвер, в вики добавил, что нужно сделать для инициализации.
                          • +1
                            Ага, хорошо. Значит работает. Теперь я смогу это в digimend-kernel-drivers добавить, а потом в ядро. Но всё это когда время найдется.
                            • +1
                              Да, не могли бы вы сузить набор запрашиваемых дескрипторов до минимально необходимого? Предположительно это должен быть 0xc8.

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