Немного занимаюсь рисованием, и вот купил себе 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. Очень удивило, что в гугле крайне мало информации о кодинге под ядро. Почему так? Столько кода написано, миллионы человекочасов — и ни у кого не возникло желание хоть что-то описать или задокументировать?

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


  1. amarao
    13.10.2017 12:23

    Зашлите в апстрим, пожалуйста. Это делают через LKML или (лучше) через мейнтейнера соответствующей подсистемы. Я не уверен, но, видимо, HID CORE, www.kernel.org/doc/linux/MAINTAINERS


    1. RZK333
      13.10.2017 18:33
      +1

      мейнтейнер подсистемы выясняется с помощью

      scripts/get_maintainer.pl


  1. xhumanoid
    13.10.2017 13:36

    gtmail.com в коде как почтовый адрес специально оставлен или опечатка?
    а то редиректит на не очень хорошие ресурсы и хром ругается


    1. Konachan700 Автор
      13.10.2017 13:39

      Извиняюсь, опечатка. Поправлю чуть позже.


  1. TuMoXEP
    13.10.2017 14:14
    +1

    крайне мало информации о кодинге под ядро

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


    1. atrosinenko
      13.10.2017 14:48

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


    1. Konachan700 Автор
      13.10.2017 16:51

      Не удалось. Мне ковырять чужой код не впервой, я до того так же ковырял очень слабо документированную libopencm3, даже баг в криптогафии там нашел… Потому разобрался только по исходникам и обрывкам инфы в сети.
      Замечено, что проекты на С почти сплошняком плохо документированны.


      1. a1ien_n3t
        13.10.2017 18:39

        При работе с ядром есть папка doc там в принципе как отправная точка много чего есть. А читать код ядра удо, нее через lxr( тотже lxr.free-electrons.com например)


  1. Meklon
    13.10.2017 14:51

    Вот они — героические люди, которые делают какую-то магию, чтобы разные непонятные железяки начинали работать. Спасибо) Присоединюсь к amarao — надо в апстрим отправить.


    1. opxocc
      13.10.2017 15:40

      Мне кажется что в таком виде, с хаками вроде вакомовского VID в idev->id.vendor, в апстрим его никто не примет.


      1. Konachan700 Автор
        13.10.2017 17:03

        Да, не возьмут. Во-первых, уже есть родственный проект для других планшетов Huion, и надо было бы по-хорошему писать туда. Во-вторых, этот планшет очень схож по hid-репорту с Wacom Bamboo одной из серий, и надо бы не только модуль ядра писать, но и патч для Xf86-input-wacom — но там вообще темный лес, с какой стороны подходить даже непонятно, и еще более непонятно, как отлаживать без перезапуска Х… В-третьих, очень много хаков, от чего не все программы видят его — на гитхабе уже отписали, что в крите работает (я в крите и рисую, проверял только там), в блендере тоже, а вот в какой-то платной софтине для моделирования — нет, работает как мышка.


        1. ARD8S
          13.10.2017 19:53

          А если в готовом и уже рабочем коде драйвера убрать VID вакомовский и прописать, скажем от какого-нибудь другого планшета или вообще левый VID, которого нет в системе, то работать перестанет? А вообще, круто, да.


          1. Konachan700 Автор
            13.10.2017 22:59

            Да, будет ошибка libinput, что данный планшет чему-то там не соответствует внутри либы. А вот чему — я не понял, там, в libinput, проверка на наличие передаваемых от драйвера размеров и еще кое-чего. Все это передавал, но нет, не взлетало.


  1. RZK333
    13.10.2017 18:41

    Очень удивило, что в гугле крайне мало информации о кодинге под ядро.

    • 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


    1. RZK333
      13.10.2017 18:49

      чуть менее популярные вещи:

      • Advanced Linux Programming, Mark Mitchell
      • Essential Linux Device Drivers, Sreekrishnan Venkateswaran
      • Understanding the Linux Kernel, Daniel P. Bovet


    1. myxo
      14.10.2017 11:19

      также есть курс на степике stepik.org/course/2051. Сам его не слушал пока, но проходил другие курсы этого преподавателя, он классный.

      ps. Ещё интересно почему этот пост на гиктаймсе, а не на хабре.


  1. robo2k
    14.10.2017 12:07

    Извиняюсь, нет времени подробно вчитываться в статью. Можно коротко — где взяли информацию о самом устройстве, какой у него рабочий интерфейс?


    1. Konachan700 Автор
      14.10.2017 12:21

      Там же hid, что очень упрощает дело. xxd /dev/hidrawX и смотреть, что прилетает. Можно еще wireshark использовать, он умеет usb перехватывать и парсить.


      1. robo2k
        14.10.2017 12:28

        То есть все устанавливали экспериментальным путем? Никакой официальной документации нет?


        1. Konachan700 Автор
          14.10.2017 12:46

          Нет конечно, какая документация… У производителя есть драйвер для Win10 бинарником и больше ничего. Hid не так сложно реверсить в общем случае.