Не мышонок, не лягушка — а Kubuntu на Lenovo Tab M11

Lenovo Tab M11 на моём столе загружается с внутренней памяти в Kubuntu.
KWin работает через DRM/KMS, Mesa рисует через Panfrost, звук идёт через PipeWire, сетью занимается NetworkManager. Bluetooth виден как обычный hci0, сенсоры — как IIO, камеры — как V4L2. Стилус работает с давлением и наклоном.
Камеры тоже завезли: задняя умеет полноценно фотографировать не только в jpg, но и в RAW, и писать видео 1080p/30fps, а еще обе камеры при этом доступны обычным Linux‑приложениям как /dev/video-rear и /dev/video-front.
Android? Отправлен за 101-й километр.
Такого плана в начале у меня не было.
Не было у бабы хлопот, так купила порося
Изначально у меня было два планшета: TCL NXTPAPER 11 и Lenovo Tab M11. Никакого выбора платформы по datasheet, mainline status и доступности драйверов я не проводил. Первым под руку попался TCL.
Мысль была примерно такая: Android же всё равно работает на ядре Linux. Драйверы уже есть, железо работает. Если долго мучиться — что‑нибудь получится.
Что‑то получилось. Ключевое слово — что‑то.
На TCL довольно быстро выяснилось, что наличие ядра Linux ещё не означает наличия нормальных Linux‑интерфейсов. Особенно это касалось графики. Рисовать на дисплее мог только графический стек Android — HWC и SurfaceFlinger, а привычную цепочку DRM/KMS → Wayland композитор → Mesa просто‑напросто не завезли.
Пришлось обходить.
Появился собственный композитор, эволюционировавший в labwc с кастомным бэкендом, рядом жил bionic GPU демон, который общался с Android‑графикой, а между ними был собственный протокол.
Потом через него же пришлось тащить GLES. Потом пакетировать вызовы, потому что иначе накладные расходы начинали съедать всё живое. Потом разбираться с resize/scissoring, вспоминать что рисовать нужно давать больше чем одному приложению, и офигевать от объема работы, чтобы обеспечить совместимость.
Оно работало. Даже довольно неплохо.
Но в какой‑то момент стало очевидно, что вместо Linux‑планшета я пытаюсь натянуть сову на глобус, которой очень больно.
ls /dev/dri
Lenovo в эту историю попал почти случайно.
Я зачем‑то зашёл в adb shell и сделал:
$ ls /dev/dri
card0После TCL это выглядело почти подозрительно хорошо.
Сам по себе /dev/dri/card0, конечно, ничего не гарантирует. Там мог быть stub, DRM только для hwcomposer или что‑нибудь, от чего настоящий userspace развалился бы при первой попытке получить master.
Но нет.
За ним действительно оказался MediaTek DRM/KMS с DSI‑панелью, CRTC и нормальным режимом 1200×1920. Последовал короткий эксперимент под названием «а давайте поставим Debian в chroot, и научим Magisk не загружать андроид». Было весело, но недолго. Достаточно быстро был сделан vendor_boot.img, который умеет грузить систему с SD‑карты, чтобы можно было путем перепрошивки туда‑сюда менять андроид на линукс.
С GPU вышло похоже. MediaTek BSP остался отвечать за питание, частоты, power domains и прочую магию, а драйвером стал Panfrost.
Получилось примерно так:
MediaTek DRM/KMS → панель и scanout
Panfrost + Mesa → GPU
KWin/Wayland → десктопПосле этого продолжать работу с TCL уже не было никакого смысла. На Lenovo каждый кадр больше не проходил через мой протокол. Графический стек обслуживали обычные KWin и Mesa.
Где‑то в процессе я всё‑таки успел скрестить Lenovo с DRM‑драйвером Samsung для родственного MT6768. Назвал это «DRM по‑Габсбургски».
Мутант выводил изображение, потребовал отдельный quirk для поворота изображения на 180 градусов, пережил починку wakelock и в целом выглядел многообещающе.
А потом не пережил suspend, и улетел в топку. На этом генетические эксперименты закончились, а стоковый MediaTek DRM остался.
Где кончается Android
После графики стало понятно, что одного рецепта для всего планшета не будет.
Со звуком всё оказалось почти скучно: стоковые ASoC‑драйверы уже дают обычную ALSA‑карту MT6768/MT6358. Я написал UCM2 для динамиков, наушников и микрофона, сверху встал обычный PipeWire/WirePlumber — и всё.
С Wi‑Fi граница оказалась ниже. MediaTek‑модули, firmware, WMT/connsys и часть старой property‑driven логики нужны, чтобы железо вообще дошло до состояния, в котором появляется wlan0. После этого всё заканчивается: дальше обычный NetworkManager.
Отдельно стоит сказать пару слов про Binder, потому что дальше он будет вылезать регулярно. Это основной IPC‑механизм Android: ядро предоставляет Binder‑драйвер, а поверх него процессы вызывают методы друг у друга почти как обычный RPC. Вместе с вызовом можно передавать ссылки на другие Binder‑объекты и файловые дескрипторы. Грубо говоря, занимает в Android примерно то место, которое в обычном Linux userspace часто занимает D‑Bus.
Большие куски данных через сам Binder обычно не гоняют. Вместо этого передают дескриптор общей памяти или буфера — например gralloc/DMA‑BUF — и дальше обе стороны работают уже с ним. AIDL в этой схеме в основном описывает интерфейс и то, как его вызовы сериализуются поверх Binder.
У Bluetooth снизу остались MediaTek WMT и стоковый Bluetooth service. Наружу он даёт AIDL IBluetoothHci.
BlueZ на всё это, ессно, смотрит как баран на новые ворота. Ему нужен HCI controller.
Поэтому схема получилась такая:
MediaTek Bluetooth service
↓ AIDL
бридж
↓ Unix сокет
kernel HCI модуль
↓
hci0
↓
BlueZ / Bluedevil / PipeWire
С сенсорами примерно та же идея, только деталей меньше. Android sensor HAL отдаёт события через AIDL/FMQ, bridge передаёт их маленькому модулю ядра, а тот публикует четыре IIO‑устройства — акселерометр, гироскоп, освещенность и счетчик шагов.
Где стандартный Linux‑интерфейс присутствует, спасибо Lenovo, что все уже сделано до нас: так получилось со звуком и Wi‑Fi. Где железо наружу торчит только через Android HAL и Binder, пришлось делать прокси к тому интерфейсу, которого ждёт обычный Linux.

Android как цельная система при этом нигде не запускается. Это не Android с KDE сверху и не Linux в chroot. От Android остались отдельные бинарники, модули ядра и Binder‑инфраструктура, которые нужны для работы с железом.
К этому моменту появилась и картинка, довольно точно описывающая происходящее:

Идея была простой: Linux не победил Android, а женился на полезных остатках. Потом ChatGPT пририсовал Туксу сиськи. Файл стал называться tux-with-boobs.png.
Так и живём.
Загрузка без Android
Загрузка тоже не без приколов.
LK запускает стоковое GKI‑ядро Android. Дальше вместо Android init начинает работать статически собранный PID 1 из изменённого vendor_boot, задача которого проста: сделать execve() в /etc/stage1.sh.
Он монтирует ранние proc, sysfs и /dev, загружает стоковый набор модулей, ждёт появления накопителя и ищет rootfs.
Основной root — обычный ext4-раздел (бывший userdata) на внутренней памяти. Есть fallback на SD, что несколько раз спасало от особенно удачных экспериментов.
В двух словах:
LK — little kernel
↓
стоковое ядро + MediaTek BSP
↓
vendor_boot / BusyBox stage1
↓
Ubuntu ext4 root
↓
switch_root
↓
systemd
Обычного Ubuntu initramfs здесь нет и не предусмотрено архитектурой. Роль раннего железоспецифичного окружения уже выполняет vendor ramdisk.
После switch_root начинается почти нормальный Ubuntu. Plymouth стартует одним из ранних systemd‑сервисов, потом его прибивает SDDM и появляется Plasma.
Правда, ядро собрано без devtmpfs.
В обычной системе ядро само создаёт device nodes, а udev уже решает, что с ними делать. Здесь после перехода в Ubuntu пришлось отдельно написать tb330fu-devd: он проходит sysfs, слушает uevents и создаёт символьные и блочные устройства. После него приходит обычный systemd-udevd и занимается уже привычными udev rules, hwdb и свойствами устройств.
Stage1 до systemd создаёт только самый минимальный набор /dev, чтобы вообще дожить до switch_root. А еще если забыть про mediatek_wdt.ko, то можно долго удивляться, почему система живет 30 секунд:)
На login screen тоже остался один кусок Android, который выбрасывать было бы особенно глупо.
PIN‑greeter сделан как тема SDDM. Дальше PIN идёт в PAM‑модуль, через небольшой bionic bridge — в сохранённые Gatekeeper/Keymaster.
То есть интерфейс входа полностью линуксовый, а проверка PIN остаётся hardware‑backed.
Зарядка без systemd
Charger mode существует отдельно от обычной загрузки.
Stage1 читает MediaTek boot mode. Если планшет включился только потому, что воткнули зарядку, Ubuntu root монтируется read‑only, после чего вместо /sbin/init через switch_root запускается tb330fu-charger.
tb330fu-charger сам становится PID 1 и рисует зарядку через DRM/KMS.
В лоре он проходит под названием «свинья с зелёной помадой». Значок у батареи тоже зеленый. Долго объяснять.
Android init, но очень выборочно
С vendor daemon есть неприятная особенность: его редко можно просто запустить.
Ему нужен модуль, прошивка, какой‑нибудь Unix socket с нужными правами. Android property. Соседний Binder service. Иногда ещё несколько демонов, которые должны стартовать раньше.
А потом внезапно выясняется, что половина этого была описана в .rc Android init.
Вместо попытки воспроизвести Android init появились два корпуса — рабочий и неизменный. Из рабочего выкидывалось всё, что относилось к Android UI, framework, пакетам, recovery и вообще к вещам, которыми уже занимается Linux.
Оставшаяся часть компилируется в небольшой детерминированный граф. Executor знает подмножество Android init команд (спойлер: их достаточно) и не пытается косплеить полноценный init.
Отдельной проблемой оказались Android properties.
Для bionic‑программ это вовсе не файл с key=value: там своя shared memory, serial, futex, property_info, socket для записи и ещё немного исторических радостей. Пришлось написать нормальную реализацию под glibc/Linux.
Назвал её Rediska. Потому что Redis. Потому что key/value хранилка. Потому что редиска.
Камера
С камерой всё значительно веселее.
MediaTek ISP, Camera3 provider, allocator, mapper и остальные друзья остались из Android. Был опыт написания полноценного ISP в GPU‑шейдере, и реализации 3A на питоне. Нет, спасибо.
Camera daemon работает на bionic‑стороне, подключается к Camera3 интерфейсу через AIDL/Binder и получает буферы через gralloc, а дальше начинается Linux. Для обычных программ обе камеры публикуются как:
/dev/video-rear
/dev/video-frontВнутри сейчас используется чутка переписанный v4l2loopback, который научился принимать DMA‑BUF на вход (но отдавать их не умеет пока что).
Зато весь юзерспейс даже не догадывается, что где‑то под ним живут Camera3, Binder и MediaTek ISP.
Передняя камера на этом и заканчивается. На что‑то большее, чем показать лицо на звонке, она не годится.
Для задней камеры захотелось большего, поэтому появилась приложенька на Flutter, которая косплеит стандартное приложение камеры: live preview, JPEG, DNG, MP4 и позволяет поиграться с выдержкой/фокусом.
Не отказал себе в удовольствии костылестроения: приложенька общается с HTTP‑прокси на Python, который дальше говорит protobuf с bionic‑демоном камеры.

Остальное железо
USB оказался неожиданно приятным. Обычный systemd unit поднимает через configfs NCM gadget, цепляет его к musb-hdrc и получает usb0. Удобно, знаете ли, когда Wi‑Fi отваливается, а дебажить как‑то надо.
Suspend работает через обычный systemd/logind, но перед ним приходится немного попросить MediaTek не мешать. Пока CRTC активен, DRM держит disp_crtc0_wakelock, поэтому кастомный хук на suspend.target сначала просит KWin выключить output через DPMS и ждёт освобождения wakelock.
Есть и другие приколы от MediaTek — например, с подключённым USB‑кабелем система хочет зависнуть намертво, вместо того чтобы уйти в сон.
Штатный чехол‑книжка тоже потребовал маленького хелпера: датчик Холла выдаёт только события открытия/закрытия, которые переводятся в стандартный SW_LID. После этого PowerDevil уже сам решает, что дальше делать.
Стилус, наоборот, просто работает — маленький файлик hwdb и KDE счастлив. Иногда отсутствие геморроя — тоже хороший знак.
Пока так
В какой‑то момент я поймал себя на странном (но приятном, не скрою) ощущении: больше нечего особо «поднимать».
Графика работает как графика. PipeWire работает как PipeWire. NetworkManager видит wlan0. BlueZ видит hci0. Plasma поворачивает экран. Стилус рисует с давлением и наклоном. Обычная программа может открыть V4L2-камеру. Камера умеет снимать.
GNSS пока остался последней заметной дыркой. Vendor path уже понятен, но до нормального Linux‑интерфейса я его ещё не довёл.
Если не знать всю подноготную, планшет в целом ведёт себя просто как обычный Linux‑компьютер. Под капотом, конечно, всё несколько менее прилично. Там всё ещё есть Binder, Android properties, Camera3, AIDL, MediaTek демоны и стоковое ядро с довольно специфическими представлениями о жизни.
Проект также намеренно привязан к конкретному устройству. Внешние модули ядра должны совпадать со стоковым ядром по vermagic, symbol CRC и module BTF; попытка сделать вид, что последнее не важно, уже заканчивалась kernel panic. На гитхабе лежит то, что действительно можно и имеет смысл публиковать. Возможно, когда‑нибудь я сделаю образы, которые можно будет скачать и прошить.
Название nevedoma-zverushka сначала было просто hostname. Потом менять его стало поздно.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.