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

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

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

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

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

«Печально» — подумал я, но рисовать в винде что-то не хотелось, я уже привык к линуксам — «а может самому написать? Там вроде ничего сложного… 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 failedn");             return rc;         }                  rc = hid_hw_start(hdev, HID_CONNECT_HIDRAW);         if (rc) {             hid_err(hdev, "hw start failedn");             return rc;         } 

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

        rc = hid_hw_open(hdev);         if (rc) {             hid_err(hdev, "cannot open hidrawn");             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+ сделана так:

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

Читайте также