Исходники закрыты, но мы не сдадимся: Пишем полностью нативное GUI-приложение под No-Name смартфон без Android
Для многих разработчиков приложений далеко не секрет, что экосистема Android не предполагает написание полностью нативных приложений: в этой платформе очень многое завязано на Java и без ART можно запустить только простые службы без какого-либо интерфейса. Однако, есть один способ писать практически под «голый» Linux, не перекомпилируя ядро и при этом пользоваться самыми интересными фишками устройства без оверхеда в виде тяжелого Android: ускорение 3D-графики (OpenGLES), микшер звука, ввод с различных устройств, OTG, Wi-Fi и если очень постараться — даже 3G. Это открывает множество разных интересных применений старым устройствам: «железо» смартфонов зачастую гораздо мощнее современных недорогих одноплатников. Сегодня я покажу вам, как написать и запустить программу, которая полностью написанное на C без Android, на No-Name Android-смартфоне практически без модификаций. Интересно? Жду вас в статье!
❯ Что нам нужно знать?
Даже относительно старые устройства флагманского сегмента обладают весьма неплохими характеристиками. Зачастую они гораздо мощнее современных дешевых одноплатников и могут выполнять самые разные задачи: эмуляция консолей, работа в качестве плееров, да даже просто сделать настольные часики самому было бы здорово. Но есть одно но — это Android. Платформа от Google может тормозить даже на достаточно мощном железе, что резко ограничивает потенциально возможные применения подобных гаджетов. Да и многие программисты не особо хотят заморачиваться и учить API Android для реализации каких-то своих проектов.
Но конечно же, есть один способ писать нативные программы, при этом используя все ресурсы смартфона/планшета. Для этого нужно понимание, как работает процесс загрузки на многих Android-гаджетах:
Первичный загрузчик (BootROM) инициализирует какую-то часть периферии и загружает вторичный загрузчик (U-boot/LK).
Вторичный загрузчик, используя определенные аргументы (например зажата ли какая-то кнопка) выбирает, с какого раздела грузить ядро системы.
После загрузки ядра Linux и подключения ramdisk начинается выполнение процессов системы.
Как раз в третьем пункте и лежит ключ к способу, который будем использовать мы. Дело в том, что в смартфоне обычно есть несколько boot-разделов и у каждого свой образ ядра Linux со своим ramdisk. Первый из них — это знакомый моддерамboot.img, который отвечает за загрузку системы и инициализирует железо/монтирует разделы/подготавливает окружение к работе (.rc файлы) и запускает главный процесс Android —zygote. При этом используется собственная реализация init от Android.
Второй, не менее знакомый многим раздел —recovery, отвечает за так называемый режим восстановления, в котором мы можем сбросить данные до заводских настроек/очистить кэши или прошить кастомную прошивку. Вероятно, многие из вас замечали, насколько быстро ваш девайс загружает этот режим, гораздо быстрее, чем загрузка обычного Android. И именно в его реализацию нам нужнозаглянуть(я намеренно выбрал бранч версии 2.3 — т.е Gingerbread для простоты):
А recovery оказывается самой обычной нативной программой, написанной на C со своим небольшим фреймворком для работы с графикой и вводом. В процессе загрузки режима recovery, скрипт запускает одноименную программу в /sbin/, благодаря которому мы видим простую и понятную менюшку. Так почему бы не использовать этот раздел в своих целях и не написать какую-нибудь нативную программу самому?
Как я уже говорил выше, в этом режиме доступны многие аппаратные возможности вашего смартфона, за исключением модема. Используя полученную информацию, предлагаю написать наше небольшое приложение под Android-смартфон без Android сами!
❯ Подготавливаем окружение
В первую очередь, хотелось бы отметить, что программы под «голый» смартфон можно писать не только на C/C++. Нам доступен как минимум FPC, который довольно давно умеет компилировать голые бинарники под Android. Кроме того, мы можем портировать маленькие embedded-версии интерпретаторов таких языков, как lua, micropython и duktape (JS).
Однако в случае нативных программ, есть два важных правила, которые необходимо понимать. Во-первых, в Android используется собственную реализацию стандартной библиотеки libc — bionic, в то время как на десктопных дистрибутивах используется glibc. Между собой они не совместимы — именно поэтому вы не можете просто взять и запустить консольную программу для Raspberry Pi, например.
А второе правило заключается в том, что начиная с версии 4.1, Androidтребует, чтобы все нативные программы были скомпилированы в режиме -fPIE — т. е. выходной код должен не зависеть от адреса загрузки программы в виртуальную память. Для этого достаточно добавить ключ -fPIE, однако учтите, что если вы разрабатываете программу под Android 4.0 и ниже, то fPIE наоборот необходимо убрать — старые версии Androidнеподдерживают такой способ генерации кода и будут вылетать с Segmentation fault.
Для разработки нам понадобится ndk — там есть все необходимые заголовочники и компиляторы для нашей работы. Я используюndk r9c, поскольку в свежих версиях Google регулярно может что-то сломать.
ndk-build, к сожалению, здесь работать не будет, поэтому Makefile придется написать самому. Я составил полностью рабочий Makefile, который без проблем скомпилирует валидную программу, вам остаётся лишь поменять NDK_DIR.
NDK_DIR = D:/android-ndk-r11c/
TOOLCHAIN_DIR = $(NDK_DIR)toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64/bin/
GCC = $(TOOLCHAIN_DIR)arm-linux-androideabi-g++
PLAT_DIR = $(NDK_DIR)platforms/android-17/arch-arm/usr/
LINK_LIBS = -l:libEGL.so -l:libGLESv1_CM.so
OUTPUT_NAME = cmdprog
build:
$(GCC) -I $(PLAT_DIR)include/ -L $(PLAT_DIR)lib/ -fPIE -Wl,-dynamic-linker=/sbin/linker $(LINK_LIBS) -static -o $(OUTPUT_NAME) main.cpp micro2d.cpp
После этого пишем простенькую программу, которая должна вывести «Test» и компилируем её.
❯ Деплоим на устройство
Несмотря на то, что грузиться мы будем в режим recovery, нам всё равно будет доступен adb, через который мы сможем запускать и отлаживать нашу программу. Это очень удобно, однако по умолчанию adb включен только в TWRP, который нужно сначала найти или портировать под ваш девайс (на большинство старых брендовых устройств порты есть, на нонейм придется портировать самому — гайды есть в интернете). Под ваше устройство есть TWRP? Отлично, распаковываете recovery.img с помощью так называемой «кухни» (MTKImgTools как вариант):
Открываете init.recovery.service.rc и убираете оттуда запуск одноименной службы (можно просто оставить файл пустым).
Запаковываем образ обратно тем же MTKImgTools и прошиваем флэшером для вашего устройства — в моём случае, это SP Flash Tool (MediaTek):
Заходим в режим рекавери и видим зависшую заставку устройства и звук подключения устройства к ПК. Если у вас установлены драйвера, то вы сможете без проблем зайти в adb shell и попасть в терминал для управления устройством. Теперь можно закинуть программу — прямо в корень рамдиска (записывается программа в ОЗУ, но при переполнении, телефон уйдет в ребут — осторожнее с этим). Пишем:
adb push cmdprog /: adb shell chmod 777 cmdprog ./cmdprog
И видим результат. Наша программа запускается и работает!
Это просто отлично. Однако я ведь обещал вам, что мы напишем программу, которая сможет выводить графику и обрабатывать ввод, предлагаю перейти к практической реализации!
❯ Выводим графику
Для вывода графики без оконных систем, мы будем использовать API фреймбуфера Linux, которое позволяет нам получить прямой доступ к массиву пикселей на экране. Однако учтите, что этот способ полностью программный и может оказаться тормозным для вашего приложения: скорость работы прямо-пропорциональна разрешению дисплея вашего устройства. Чем выше разрешение, тем ниже филлрейт. В моём случае, матрица была с разрешением 960x540, 32млн цветов, IPS — очень недурно, согласны?
Фреймбуфер Linux может работать с самыми разными форматами пикселя, имейте это ввиду. На некоторых устройствах может быть 16-битный формат (262 тысячи цветов, RGB565), на моём же оказался 32х-битный с выравниванием по строкам (имейте это также ввиду). 32х битный формат. Работать с ним легко: открываем устройство /dev/graphics/fb0, получаем параметры (разрешение, формат пикселя), делаем mmap для отображения буфера с пикселями на экране в память нашего процесса и выделяем второй буфер для двойной буферизации дабы избежать неприятных мерцаний.
void m2dAllocFrameBuffer()
{
fbDev = open(PRIMARY_FB, O_RDWR);
fb_var_screeninfo vInfo; fb_fix_screeninfo fInfo;
ioctl(fbDev, FBIOGET_VSCREENINFO, &vInfo);
ioctl(fbDev, FBIOGET_FSCREENINFO, &fInfo); fbDesc.width = vInfo.xres;
fbDesc.height = vInfo.yres;
fbDesc.pixels = (unsigned char*)mmap(0, fInfo.smem_len, PROT_WRITE, MAP_SHARED, fbDev, 0); f
bDesc.length = fInfo.smem_len; fbDesc.lineLength = fInfo.line_length;
backBuffer = (unsigned char*)malloc(fInfo.smem_len); memset(backBuffer, 128, fInfo.smem_len);
printf("Framebuffer is %s %ix%ix%i\n", (char*)&fInfo.id, fbDesc.width, fbDesc.height, vInfo.bits_per_pixel, fInfo.type);
}
Если не сделать предыдущий шаг и запускать нашу программу параллельно с recovery, то они обе будут пытаться друг друга «перекрыть» — эдакий race condition:
После этого пишем простенькие функции для блиттинга картинок (в том числе с альфа-блендингом). В инлайнах и критичных к скорости функциям лучше не делать условия на проверку границ нашего буфера — лучше «отрезать» ненужное еще на этапе просчета ширины/высоты:
__inline void pixelAt(int x, int y, byte r, byte g, byte b, float alpha)
{
if(x < 0 || y < 0 || x >= fbDesc.width || y >= fbDesc.height) return;
unsigned char* absPtr = &backBuffer[(y * fbDesc.lineLength) + (x * 4)];
if(alpha >= 0.99f)
{
absPtr[0] = b;
absPtr[1] = g;
absPtr[2] = r;
}
else {
absPtr[0] = (byte)(b * alpha + absPtr[0] * (1.0f - alpha));
absPtr[1] = (byte)(g * alpha + absPtr[1] * (1.0f - alpha));
absPtr[2] = (byte)(r * alpha + absPtr[2] * (1.0f - alpha));
} absPtr[3] = 255; }
for(int i = 0; i < image->height; i++)
{
for(int j = 0; j < image->width; j++)
{
byte* ptr = &image->pixels[((image->height - i) * image->width + j) * 3]; pixelAt(x + j, y + i, ptr[0], ptr[1], ptr[2], alpha);
}
}
И загрузчик TGA:
CImage* m2dLoadImage(char* fileName) {
FILE* f = fopen(fileName, "r");
printf("m2dLoadImage: Loading %s\n", fileName);
if(!f)
{
printf("m2dLoadImage: Failed to load %s\n", fileName);
return 0;
}
CTgaHeader hdr;
fread(&hdr, sizeof(hdr), 1, f);
if(hdr.paletteType)
{
printf("m2dLoadImage: Palette images are unsupported\n");
return 0;
}
if(hdr.bpp != 24) {
printf("m2dLoadImage: Unsupported BPP\n");
return 0;
}
byte* buf = (byte*)malloc(hdr.width * hdr.height * (hdr.bpp / 8));
assert(buf);
fread(buf, hdr.width * hdr.height * (hdr.bpp / 8), 1, f);
fclose(f);
CImage* ret = (CImage*)malloc(sizeof(CImage));
ret->width = hdr.width;
ret->height = hdr.height;
ret->pixels = buf;
printf("m2dLoadImage: Loaded %s %ix%i\n", fileName, ret->width, ret->height);
return ret;
}
И попробуем вывести картинку:
m2dInit();
test = m2dLoadImage("test.tga");
test2 = m2dLoadImage("habr.tga");
while(1)
{
m2dClear();
m2dDrawImage(test, 0, 0, 1.0f);
m2dDrawImage(test2, tsX - (test2->width / 2), tsY - (test2->height / 2), 0.5f);
m2dFlush();
}
Не забываем про порядок пикселей в TGA (BGR, вместо RGB), меняем канали b и r местами в pixelAt и наслаждаемся картинкой на большом и классном IPS-дисплее:
Производительность отрисовки не очень высокая, однако если оптимизировать код (копировать непрозрачные картинки сразу сканлайнами и убрать проверки в инлайнах), то будет немного шустрее. Google для подобных целей сделали собственный простенький софтрендер —libpixelflinger.
Есть вариант для быстрой и динамичной графики: использовать GLES, который без проблем доступен и из recovery. Однако, насколько мне известно (в исходники драйверов посмотреть не могу), указать фреймбуфер в качестве окна не получится, поэтому в качестве Surface для рендертаргета у нас будет служить Pixmap (так называемый off-screen rendering), которому нужно задать правильный формат пикселя (см. документацию EGL). Рисуем туда картинку с аппаратным ускорением и затем просто копируем в фреймбуфер с помощью memcpy.
❯ Обработка нажатий
Однако, ни о каких GUI-программах не идёт речь, если мы не умеет обрабатывать нажатия на экране с полноценным мультитачем! Благо, даже механизм обработки событий в Linux очень простой и приятный: мы точно также открываем устройство и просто читаем из него события в фиксированную структуру. Эта черта мне очень нравится в архитектуре Linux!
Каждое устройство, которое может передавать данные о нажатиях, находится в папке /dev/input/ и имеет имя вида event. Как узнать нужный нам event? Нам нужен mtk-tpd — реализация драйвера тачскрина от MediaTek (у вашего чипсета может быть по своему), для этого загружаемся в Android и пишем getevent. Он покажет доступные в системе устройства ввода — в моём случае, это event2:
Из event можно читать как в блокирующем, так и не в блокирующем режиме, нам нужен второй. Более того, в них можно инжектить события, что я показывал в статье про создание своей консоли из планшета с нерабочим тачскрином:
// Open input device evDev = open(INPUT_EVENT_TPD, O_RDWR | O_NONBLOCK);
После этого, читаем события с помощью read и обрабатываем их. На устройствах с резистивным тачскрином, передается просто ABS_POSITION_X, на устройствах с поддержкой нескольких касаний — используетсяпротокол MT. Когда пользователь нажал на экран, посылается нажатие BTN_TOUCH с значением 1, а когда отпускает — соответственно BTN_TOUCH с значением 0. Разные драйверы тачскрина используют разные координатные системы (насколько я понял), в случае MediaTek — это абсолютные координаты на дисплее (вплоть до ширины и высоты). На данный момент, я реализовал поддержку только одного касания, но при желании можно добавить трекинг нескольких нажатий:
void m2dUpdateInput()
{
input_event ev;
int ret = 0;
while((ret = read(evDev, &ev, sizeof(input_event)) != -1))
{
if(ev.code == ABS_MT_POSITION_X) tsState.x = ev.value;
if(ev.code == ABS_MT_POSITION_Y) tsState.y = ev.value;
if(ev.code == BTN_TOUCH) tsState.isPressed = ev.value == 1;
}
tsState.cb(tsState.isPressed, tsState.x, tsState.y); }
Теперь мы можем «возить» логотип Хабра по всему экрану:
void onTouchUpdate(bool isTouching, int x, int y) {
if(isTouching)
{
tsX = x;
tsY = y;
}
}
int main(int argc, char** argv) {
printf("Test\n");
m2dInit();
test = m2dLoadImage("test.tga");
test2 = m2dLoadImage("habr.tga");
printf("Volume: %i %i\n", vol, muteState);
m2dAttachTouchCallback(&onTouchUpdate);
while(1) {
m2dUpdateInput();
m2dClear();
m2dDrawImage(test, 0, 0, 1.0f);
m2dDrawImage(test2, tsX - (test2->width / 2), tsY - (test2->height / 2), 0.5f);
m2dFlush();
}
return 0;
}
В целом, это уже можно назвать минимально-необходимым минимумом для взаимодействия с устройством и использованию всех его возможностей на максимум без Android. Более того, такой метод заработает почти на любом устройстве, в том числе и китайских NoName, где ни о каких исходниках ядра и речи нет. Теперь вы можете попытаться использовать ваше старое Android-устройство для чего-нибудь полезного без необходимости изучать API Android.
❯ Звук, модем и другие возможности
Для звука нам придётся использовать ALSA — поскольку эта подсистема звука сейчас используется в большинстве устройств на Linux. Судя по всему, тут есть режим эмуляции старого и удобного OSS, поскольку устройства /dev/snd/dsp присутствует. Однако, вывод в него какого либо PCM-потока не даёт ничего, поэтому нам пригодится ALSA-lib.
Другой вопрос касается модема и сети. И если Wi-Fi ещё можно поднять (wpa_supplicant можно взять из раздела /system/), то с модемом будут проблемы — нет единого протокола по общению с ним и кое-где, чтобы его заставить работать, нужно будет немного попотеть. Не стесняйтесь изучать исходники ядра (MediaTek охотно делится реализацией вообще всего — там и RIL, и драйвер общения с модемом) и смотреть интересующие вас фишки!
❯ Заключение
Как мы с вами видим, у старых девайсов все еще есть перспективы стать полезными в какой-либо сфере даже без Android на борту. На тех устройствах, где нет порта Ubuntu или обычного десктопного Linux, всё равно сохраняется возможность писать нативные программы и попытаться приносить пользу.
Не стесняйтесь лезть и изучать вендорские исходники — это даёт понимание, как работают устройства изнутри. Собственно, благодаря такому ежедневному копанию исходников системы и появилась данная статья! :)
Сам себе игровая консоль: превращаем планшет с нерабочим тачскрином в игровой девайс из 8 кнопок и микроконтроллера
К сожалению, в наше время многие старые, но весьма неплохие по характеристикам гаджеты отправляются напрямую в помойку, и их владельцы не подозревают, что им можно найти применение. Сервер, мультимедийная-станция, да даже просто как TV-приставка — люди в упор не замечают сфер, где старенький планшет мог бы быть полезен. Но как быть, если посвящаешь жизнь портативным гаджетам, кодингу и копанию в железе? Правильно: сделать довольно мощную игровую консоль из старого планшета самому! Сегодня вам расскажу, как я сделал свою портативную приставку из планшета с нерабочим тачскрином, Raspberry Pi Pico и 8 кнопок! За рабочим результатом прячется несколько дней работы: поиск UART на плате, разработка контроллера геймпада на базе RPi Pico, написание приложения-сервиса, которое слушает события и отправляет их в подсистему ввода Linux в обход Android. Интересно? Тогда жду вас под катом!
❯ Мотивация
Прошло уже практически 10 лет с того момента, как у меня появилась моя первая портативная консоль. Несмотря на то, что я был заядлым ПК-игроком, я уже успел посмотреть на PS3 и PSP, но денег на их покупку у меня особо не было, да и к тому времени уже был в наличии Android-планшет. Но к моему 13-летию в 2014 году, когда я ходил и выбирал себе будущий девайс на день рождения, отец и мама решили подарить мне мою первую портативную консоль. Изначально, я уговаривал её купить мне целых два девайса, но бюджет был ограничен 4.000 рублей, а я хотел взять смартфон Fly IQ239 и консоль JXD S601 одновременно:
Однако, увидев здоровую 7-дюймовую консоль в магазине TREC (думаю, жители южной части РФ помнят такой), мама уговорила меня взять именно её, мотивируя это «ну и чего ты будешь тыкаться в этот мелкий экран? Возьми большую». После покупки гаджета, я был доволен: играл какие-то игрушки с ретро-платформ, устанавливал игры на Android, сидел в ВК через Kate Mobile. Что еще нужно было школяру? Однако, планшет прожил у меня недолго: с очередного лага я психанул и ударил по нему кулачком, унеся на тот свет и дисплей и тачскрин. Так консолька и пролежала в подвале около 8 лет. Впрочем, мне продолжали импонировать подобные устройства и в прошлом году я купил и написал про несколько подобных девайсов.
Несколько месяцев назад, мой читатель Кирилл Севостьянов с Хабра прислал мне HTC HD2 в качестве донора и планшет Prestigio PMP7170B3G, который был рабочим, но… у него отказал тачскрин. Я всё думал, чего бы с ним сделать и решил реализовать игровую консольку своими руками из подручных средств. Идея крутилась в голове довольно давно, но реализовал я её только сейчас.
❯ Что нам нужно сделать?
Итак, что должно быть у портативной консоли? Чипсет, дисплей, звук, ОС — это всё нам уже предоставляет планшет. Нам остаётся лишь сделать свой геймпад. Давайте подумаем, что нам будет нужно для того, чтобы его сделать и передавать от него события на планшет:
Контроллер для геймпада: тут нам подойдет практически любой микроконтроллер, который работает от 3.3в. Выбор большой: Arduino Pro Mini 3.3v, ESP32, RPi Pico. Я остановился на последнем: недавно я взял себе две штучки «пощупать» их — и они мне очень понравились!
Физический интерфейс: с планшетом нужно как-то общаться. У нас есть три варианта: USB (не факт, что поддержка преобразователей включена в ядре), UART и SPI/I2C на пятачках тачскрина (потребуют написания драйвера т. к. в android-устройствах нет прямого доступа к SPI/I2C из userland'а). Я остановился на UART: его легко найти на большинстве китайских планшетов, а если не получилось — то на помощь может прийти схема платы.
Программная реализация: как это будет работать? Я решил реализовать геймпад в виде сервиса на Android, который слушает состояния кнопок с UART и «инжектит» события напрямую в драйвер ввода. Таким образом, поддержка нашего геймпада появляется даже в самой системе — можно управлять менюшкой или приложениями как с клавиатуры!
С планом определились, пора начать с программной части: сначала нам обязательно понадобится ROOT-доступ. Его получение на разных девайсах отличается — на prestigio уже был порт CWM и я просто поставил SuperSU. Без ROOT доступа мы не сможем использовать UART!
Теперь нам нужно найти пятачки UART на плате. Разведен он не везде, но в случае устройств на MediaTek — почти всегда, ещё и пятачки подписаны. На моём планшете он нашёлся сразу: был между двух металлических экранов и соответствовал 4-ому каналу UART. Получить к нему доступ можно в /dev/ttyMT3. Я использую ESP32 в качестве UART преобразователя: подпаиваемся к RX/TX, запускаем putty и заходим в adb shell. Определяем бодрейт (скорость) нашего UART порта — на MediaTek он обычно равен 921600, на других чипсетах — 115200. Пытаемся что-то вывести и хоба — мы уже можем «поболтать» с планшетом!
❯ Приложение-сервис
Итак, у нас уже есть доступ к UART и мы можем общаться с планшетом из внешнего мира. Но получить события с кнопок пол дела, нужно их ещё и послать в систему. Для этого есть целых три способа:
InputManager.injectInputEvent — именно этим методом пользуется команда input, которую вы можете использовать через adb. Но увы, он работает только при наличие разрешения INJECT_EVENTS, который доступен только системным приложениям — находятся они в /system/app и подписаны тем же сертификатом, что и остальная прошивка.
Модуль uinput дает возможность создать виртуальное устройство ввода и посылать события из userland'а — т. е. из прикладного приложения. У моего планшета было устройство /dev/uinput, но lsmod показывал, что сам модуль не загружен. Так что отметаем — он есть не везде.
Прямой инжект событий в character устройство — весьма грязный хак, который позволяет инжектить события, не притворяясь системным приложением, но имеет некоторые ограничения. Именно его я и выбрал и о ограничениях ниже.
Сначала нам нужно узнать, какие кнопки поддерживают загруженные устройства ввода в системе. Для этого используем команду getevent -li. Там есть разные устройства ввода, в том числе и тачскрин (если вам нужно симулировать нажатия на экран), мне же подошёл драйвер физических кнопок mtk-kpd. Он занимается обработкой кнопок громкости, включения и т. п. Тут важно обратить внимание на то, что если попытаться послать кнопку, которое устройство не реализует (например пробел), то ничего не произойдет:
Инжект событий я писал на C, т. к. это требовало прямой записи input_event, а в Java прокинул его через Jni. Концепция простая: открываем устройство /dev/input/event2 и посылаем в него события ввода и синхронизации (это обязательно!), которые затем Android читает и обрабатывает:
#include <linux/uinput.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <android/log.h>
#include <jni.h>
int uinput;
extern "C" JNIEXPORT void JNICALL Java_com_monobogdan_inputservicebridge_InputNative_init(JNIEnv *env, jclass clazz) {
uinput = open("/dev/input/event2", O_WRONLY);
__android_log_print(ANDROID_LOG_DEBUG , "Test", uinput >= 0 ? "Open event OK" : "Failed to open event"); }
void emit(int fd, int type, int code, int val) {
struct input_event ie; ie.type = type;
ie.code = code; ie.value = val;
ie.time.tv_sec = 0;
ie.time.tv_usec = 0;
write(fd, &ie, sizeof(ie)); }
extern "C" JNIEXPORT void JNICALL Java_com_monobogdan_inputservicebridge_InputNative_sendKeyEvent(JNIEnv *env, jclass clazz, jint key_code, jboolean pressed) {
__android_log_print(ANDROID_LOG_DEBUG , "Test", "Send");
emit(uinput, EV_KEY, key_code, (bool)pressed ? 1 : 0);
emit(uinput, EV_SYN, SYN_REPORT, 0);
}
Основной обработкой занимается сервис, который я реализовал в отдельном потоке: он слушает события с UART и посылает соответствующие изменения состояния через sendKeyEvent. На вход приходят простые сообщения вида:
U L где U/D — нажато, не нажато, а L — однобайтовый идентификатор кнопки. В случае L — это влево, R — вправо и т. п. Вся доступная раскладка хранится в словаре. Причём само чтение из UART реализовано костылем с чтением «чужого» stdout, т. к. android-приложения не умеют сами по себе работать с root правами. В теории, это могло дать неприятный оверхед, но на практике никакого серьезного инпут лага это не создает. Не забываем сделать устройство event записываемым — ставим ему права 777:
package com.monobogdan.inputservicebridge;
public class InputListener extends Service {
private static final int tty = 3;
private InputManager iManager;
private Map<Character, Integer> keyMap;
private Method injectMethod;
private Process runAsRoot(String cmd)
{
try {
return Runtime.getRuntime().exec(new String[] { "su", "-c", cmd });
}
catch (IOException e)
{
e.printStackTrace();
return null;
}
}
public void onCreate() {
super.onCreate();
// According to linux key map (input-event-codes.h)
keyMap = new HashMap<>();
keyMap.put('U', 103);
keyMap.put('D', 108);
keyMap.put('L', 105);
keyMap.put('R', 106);
keyMap.put('E', 115);
keyMap.put('B', 158);
keyMap.put('A', 232);
keyMap.put('C', 212);
InputNative.init();
try {
runAsRoot("chmod 777 /dev/input/event2").waitFor();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Executors.newSingleThreadExecutor().execute(new Runnable() {
public void run() {
Process proc = runAsRoot("cat /dev/ttyMT" + tty);
BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
while(true)
{
try {
String line = reader.readLine();
if(line != null && line.length() > 0) {
Log.i("Hi", "run: " + line);
boolean pressing = line.charAt(0) == 'D';
int keyCode = keyMap.get(line.charAt(2));
Log.i("TAG", "run: " + keyCode);
InputNative.sendKeyEvent(keyCode, pressing);
}
}
catch(IOException e)
{
e.printStackTrace();
}
/*try {
Thread.sleep(1000 / 30);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}
});
}
public IBinder onBind(Intent intent) {
return null;
}
}
Таким образом, если мы отправляем с ПК «D L» — система считает, что мы зажали стрелку влево, а U L — считает что мы отпустили. Но если mtk-kpd поддерживает стрелки и еще некоторые действия без каких либо проблем, то enter в список обрабатываемых кнопок не входит: придется мудрить! И тут нам приходит на помощь механизм трансляции кодов кнопок в действия: они хранятся в специальных файлах .kl в /system/usr/keylayout/. Я назначил DPAD_CENTER на… кнопку регулировки громкости звука! Ну, а почему бы и нет. :) Таким образом можно переназначить уже имеющиеся кнопки громкости на, например, start/select.
❯ Геймпад
После того, как сервис был готов и отлажен, нужно было реализовать хардварную часть проекта — сам геймпад. В качестве контроллера я, как уже говорил, выбрал Raspberry Pi Pico на базе МК RP2040 — бодреньком контроллере с двумя ARM Cortex-M0 ядрами. Стоит копейки, а в отличии от ESP'шек, его SDK не такое перегруженное и выглядит более приближенным к bare-metal.
На данный момент, я решил развести все кнопки на бредборде — макетной плате без пайки, т. к. макеток для пайки у меня под рукой не было. Сделал примитивный геймпад:
Развел на соответствующие GPIO:
И написал примитивную прошивку, которая отслеживает состояние кнопок. В прошивке точно так же есть словарь, задающий ассоциацию между физическими пинами и «виртуальными» кнопками. При нажатии или отжатии кнопки, программа изменяет стейт и отсылает новое состояние планшету.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pico/stdlib.h"
#include "pico/time.h"
#include "hardware/uart.h"
struct keyMap
{
int gpio;
char key;
bool pressed;
int lastTick;
};
keyMap keys[] = {
{
15,
'L',
false,
0
},
{
14,
'U',
false,
0
},
{
13,
'D',
false,
0
},
{
12,
'R',
false,
0
},
{
11,
'E',
false,
0
},
{
10,
'B',
false,
0
},
{
20,
'A',
false,
0
},
{
21,
'C',
false,
0
}
};
#define KEY_NUM 8
int main() {
stdio_init_all();
uart_init(uart0, 921600);
gpio_set_function(PICO_DEFAULT_UART_TX_PIN, GPIO_FUNC_UART);
gpio_set_function(PICO_DEFAULT_UART_RX_PIN, GPIO_FUNC_UART);
sleep_ms(1000); // Allow serial monitor to settle
for(int i = 0; i < KEY_NUM; i++)
{
gpio_init(keys[i].gpio);
gpio_set_dir(keys[i].gpio, false);
gpio_pull_up(keys[i].gpio);
}
while(true)
{
int now = time_us_32();
for(int i = 0; i < KEY_NUM; i++)
{
char buf[5];
buf[1] = ' ';
buf[3] = '\n';
buf[4] = 0;
if(!gpio_get(keys[i].gpio) && !keys[i].pressed && now - keys[i].lastTick > 15500)
{
buf[0] = 'D';
buf[2] = keys[i].key;
puts(buf);
keys[i].lastTick = now;
keys[i].pressed = true;
continue;
}
if(gpio_get(keys[i].gpio) && keys[i].pressed && now - keys[i].lastTick > 15500)
{
buf[0] = 'U';
buf[2] = keys[i].key;
puts(buf);
keys[i].pressed = false;
keys[i].lastTick = now;
}
}
}
}
Собираем всё вместе и тестируем. Хоба, всё работает, мы можем перемещаться по менюшке используя наш геймпад!
А почему бы не попробовать поиграть в какую-нибудь игру? Ну мы же консоль вроде делаем: берём эмулятор NES, биндим кнопки в настройках и наслаждаемся игрой в Марио!
❯ Заключение
Реализация этого проекта заняла у меня не так уж и много времени: всего около 3-х дней работы по вечерам. Вероятно кто-то спросит: «а чего ты просто Bluetooth геймпад не купил?». Так это не прикольно ведь. Гораздо приятнее играть в девайс, к которому ты приложил руку сам. Более того, не у всех старых планшетов есть BT. Обошёлся на данной стадии проект недорого: планшет мне подарили бесплатно (точно также у вас дома может лежать подобный), RPi Pico — 350 рублей, кнопки по 10 рублей/штучка.
В целом, я сам по себе обожаю копаться в различных железках и их софтварной части (вспомнить хотя-бы статью про перекомпиляциюu-boot из вендорских исходников для нонейм консоли), а созидать что-то свое вообще вызывает какие-то нереальные всплески эндорфина — оно и понятно! :)
Однако несмотря на то, что мы уже имеем рабочий «прототип», проект далёк от завершения: я намерен довести его до конца и окончательно перевоплотить старый планшет в автономную игровую консоль (и рассказать об этом во второй части статьи). Для этого мне понадобится распечатать корпус и кнопки на 3D-принтере. К сожалению, у меня в городе ни у кого особо нет 3D-принтеров, поэтому начну копить на Ender 3, а от вас, читателей, с удовольствием почитаю мнение в комментариях и советы касательно выбора принтера!
Статья подготовлена при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud, чтобы не пропускать еженедельные статьи про моддинг различных гаджетов!
Проверьте, как работает ваш мозг
Если сможете обыграть компьютер в древней индийской игре — то нормально!
В ядре Linux найдена забытая заплата, влияющая на производительность CPU AMD
В ядро Linux 6.0, релиз которого ожидается в следующий понедельник, принято изменение, решающее проблемы с производительностью систем на процессорах AMD Zen. Источником падения производительности оказался код, добавленный 20 лет назад для обхода аппаратной проблемы в некоторых чипсетах. Аппаратная проблема давно устранена и не проявляется в актуальных чипсетах, но старый обход проблемы был забыт и стал источником снижения производительности на системах на базе современных CPU AMD. Новые системы с CPU Intel старый обходной манёвр не затрагивает, так как доступ к ACPI в них осуществляется при помощи отдельного драйвера intel_idle, а не общего драйвера processor_idle.
Обходной манёвр был добавлен в ядро в марте 2002 года для блокирования проявления ошибки в чипсетах, связанной с отсутствием должной установки состояние простоя (idle) из-за задержки обработки сигнала STPCLK#. Для обхода проблемы в реализации ACPI добавлялась дополнительная инструкция WAIT, замедляющая процессор чтобы чипсет успевал перейти в состояние простоя. При проведении профилирования с использованием инструкций IBS (Instruction-Based Sampling) на процессорах AMD Zen3 было выявлено, что процессор проводит значительное время, выполняя заглушку, которая приводит к неверной трактовке состояния нагрузки на процессор и выставлению более глубоких режимов сна (C-State) обработчиком cpuidle.
Подобное поведение отражается в снижении производительности при нагрузках, в которых часто чередуются состояния простоя (idle) и активности (busy). Например, при использовании патча, отключающего обходной манёвр, средние показатели теста tbench увеличиваются с 32191 MB/s до 33805 MB/s.
О документации
Разработчики: Документация должна быть понятной, по делу и в официальном стиле
Так же разработчики:
Официальная документация по ядру linux.
P.S. перевод:
Внимание
Наименование «tasklet» вводит в заблуждение: оно не имеет ничего общего с «tasks», скорее всего оно связано с плохой водкой, которую в то время выпил Алексей Кузнецов(один из мэйнтейнеров ядра linux).
Какое ядро Linux лучше использовать для игр
Миры Linux обширны и иногда требуется приложить немало усилий для его изучения, но время идёт и платформа становится всё популярней среди обычных пользователей, которые не хотят тратить время на изучение скучных мануалов и проведения ненужных тестов. В этом материале, мы протестируем основные кастомные ядра Linux и сведём их в битве за звание лучшего игрового фундамента нашей системы.
Введение
Существует большое количество пользовательских ядер, которые мало чем отличаются от стандартного, но даже небольшое изменение переменных и использование альтернативных модулей, позволяет добиться внушительных результатов.
В основном ядра делятся на:
Binary Based — обычное ядро, которое вы стягиваете с репозитория вашего Linux-дистрибутива, может обновляться автоматически и не требует сборки из исходного кода, так как заранее собрано для множество машин.
Типичные представители таких ядер: Linux, Linux-zen.
Source Based — кастомное ядро, что собирается из исходников под вашу систему с заданными флагами. Не может быть обновлено без пересборки. Максимальная производительность.
Типичные представители таких ядер: Linux-lqx, Linux-xanmod, Linux-rt.
Тестирование производилось в:
Counter-Strike: Global Offensive
Все тесты прогонялись по три раза во избежание погрешности.
Симуляция обыденной работы
Geekbench 5
Standard/zen/lqx/rt
Из теста симуляции ежедневных задач отчётливо видно, что Linux-zen не зря считается лучшим ядром для повседневной работы. Все остальные бинарные ядра показали одинаковые результаты.
Работа с OpenGL
Superposition Benchmark
Standard/zen/lqx/rt
На удивление, несмотря на упор теста в гпу при средних настройках, мы опять наблюдаем преимущество Zen ядра. Можно отметить, что из-за особенностей теста в обсчёте физики, Linux-rt набирает дополнительные балы, так как особенность RT ядра это выполнение сверхточного обсчёта без задержек.
Goverlay — счётчик, что призван показать чистую выжимку производительности из железа с шагом в 5 секунд.
Отчётливо видно, что наибольший результат за игровым ядром LQX, ибо он обеспечивает максимальную производительность. Следом идёт Zen, что обеспечивает плавность без проседаний. Все остальные ядра показали плачевный результат.
Counter-Strike: Global Offensive
Тестирование на минимальных настройках графики и без упора в гпу, отчётливо показывает преимущество LQX и Zen ядра.
Выводы
Несмотря на заявление отечественных «Гуру» Linux, замена ядра может существенно повысить комфорт работы на Linux, а также получить до 10-20% чистой производительности из воздуха.
Если вам понравился материал, то пожалуйста — посетите блог автора ТЫК.
Видеоверсия
Ядру Linux исполнилось 30 лет
25 августа 1991 года после пяти месяцев разработки 21-летний студент Линус Торвальдс объявил в телеконференции comp.os.minix о создании рабочего прототипа новой операционной системы Linux, для которой было отмечено завершение портирования bash 1.08 и gcc 1.40. Первый публичный выпуск ядра Linux был представлен 17 сентября. Ядро 0.0.1 имело размер 62 Кб в сжатом виде и содержало около 10 тысяч строк исходного кода. Современное ядро Linux насчитывает более 28 млн строк кода. По данным исследования, проведённого в 2010 году по заказу Евросоюза, приблизительная стоимость разработки с нуля проекта, аналогичного современному ядру Linux, составила бы более миллиарда долларов США (расчёт производился, когда в ядре было 13 млн строк кода), по другим оценкам - более 3 миллиардов.
Ядро Linux было создано под впечатлением от операционной системы MINIX, которая не устраивала Линуса своей ограниченной лицензией. Впоследствии, когда Linux стал известным проектом, недоброжелатели пытались обвинить Линуса в прямом копировании кода некоторых подсистем MINIX. Нападение отразил Эндрю Таненбаум, автор MINIX, который поручил одному из студентов провести детальное сравнение кода Minix и первых публичных версий Linux. Результаты исследования показали наличие только четырёх несущественных совпадений блоков кода, обусловленных требованиями POSIX и ANSI C.
Первоначально Линус задумал назвать ядро Freax, от слов «free», «freak» и X (Unix). Но имя «Linux» ядро получило с лёгкой руки Ари Лемке (Ari Lemmke), который по просьбе Линуса разместил ядро на FTP-сервере университета, назвав директорию с архивом не «freax», как просил Торвальдс, а «linux». Примечательно, что предприимчивый делец Вильям Делло Крок (William Della Croce) сумел зарегистрировать торговую марку Linux и хотел со временем собирать отчисления, но позднее передумал и передал все права на торговую марку Линусу. Официальный талисман Linux-ядра, пингвин Tux, был выбран в результате соревнования, состоявшегося в 1996 году. Имя Tux расшифровывается как Torvalds UniX.
Динамика роста кодовой базы (количество строк исходного кода) ядра:
0.0.1 - сентябрь 1991, 10 тыс. строк кода;
1.0.0 - март 1994, 176 тыс. строк кода;
1.2.0 - март 1995, 311 тыс. строк кода;
2.0.0 - июнь 1996, 778 тыс. строк кода;
2.2.0 - январь 1999, 1.8 млн. строк кода;
2.4.0 - январь 2001, 3.4 млн. строк кода;
2.6.0 - декабрь 2003, 5.9 млн. строк кода;
2.6.28 - декабрь 2008, 10.2 млн. строк кода;
2.6.35 - август 2010, 13.4 млн. строк кода;
3.0 - август 2011, 14.6 млн. строк кода.
3.5 - июль 2012, 15.5 млн. строк кода.
3.10 - июль 2013, 15.8 млн. строк кода;
3.16 - август 2014, 17.5 млн. строк кода;
4.1 - июнь 2015, 19.5 млн. строк кода;
4.7 - июль 2016, 21.7 млн. строк кода;
4.12 - июль 2017, 24.1 млн. строк кода;
4.18 - август 2018, 25.3 млн. строк кода.
5.2 - июль 2019, 26.55 млн. строк кода.
5.8 - август 2020, 28.4 млн. строк кода.
5.13 - июнь 2021, 29.2 млн. строк кода.
Прогресс развития ядра:
Linux 0.0.1 - сентябрь 1991, первый публичный выпуск, поддерживающий только CPU i386 и загружающийся с дискеты;
Linux 0.12 - январь 1992, код начал распространяться под лицензией GPLv2;
Linux 0.95 - март 1992, обеспечена возможность запуска X Window System, реализована поддержка виртуальной памяти и раздела подкачки.
Linux 0.96-0.99 - 1992-1993, началась работа над сетевым стеком. Представлена файловая система Ext2, добавлена поддержка формата файлов ELF, представлены драйверы для звуковых карт и контроллеров SCSI, реализована загрузка модулей ядра и файловой системы /proc.
В 1992 году появились первые дистрибутивы SLS и Yggdrasil. Летом 1993 года были основаны проекты Slackware и Debian.
Linux 1.0 - март 1994, первый официально стабильный релиз;
Linux 1.2 - март 1995, существенное увеличение числа драйверов, поддержка платформ Alpha, MIPS и SPARC, расширение возможностей сетевого стека, появление пакетного фильтра, поддержка NFS;
Linux 2.0 - июнь 1996 года, поддержка многопроцессорных систем;
Март 1997: основан LKML, список рассылки разработчиков ядра Linux;
1998 год: запущен первый попавший в список Top500 кластер на базе Linux, состоящий из 68 узлов с CPU Alpha;
Linux 2.2 - январь 1999, увеличена эффективность системы управления памятью, добавлена поддержка IPv6, реализован новый межсетевой экран, представлена новая звуковая подсистема;
Linux 2.4 - февраль 2001, обеспечена поддержка 8-процессорных систем и 64 Гб ОЗУ, файловая система Ext3, поддержка USB, ACPI;
Linux 2.6 - декабрь 2003, поддержка SELinux, средства автоматического тюнинга параметров ядра, sysfs, переработанная система управления памятью;
В 2005 году представлен гипервизор Xen, который открыл эру виртуализации;
В сентябре 2008 года сформирован первый релиз платформы Android, основанной на ядре Linux;
В июле 2011 года после 10 лет развития ветки 2.6.x осуществлён переход к нумерации 3.x. Число объектов в Git-репозитории достигло 2 млн;
В 2015 году состоялся выпуск ядра Linux 4.0. Число git-объектов в репозитории достигло 4 млн;
В апреле 2018 года преодолён рубеж в 6 млн git-объектов в репозитории ядра.
В январе 2019 года сформирована ветка ядра Linux 5.0. Репозиторий достиг уровня 6.5 млн git-объектов.
Опубликованное в августе 2020 года ядро 5.8 стало самым крупным по числу изменений из всех ядер за всё время существования проекта.
В ядре 5.13 был поставлен рекорд по числу разработчиков (2150), изменения от которых вошли в состав ядра.
В 2021 году в ветку ядра Linux-next добавлен код для разработки драйверов на языке Rust. Ведётся работа по включению компонентов для поддержки Rust в основной состав ядра.
68% всех изменений в ядро внесены 20 наиболее активными компаниями. Например, при разработке ядра 5.13 10% всех изменений подготовлено компанией Intel, 6.5% - Huawei, 5.9% - Red Hat, 5.7% - Linaro, 4.9% - Google, 4.8% - AMD, 3.1% - NVIDIA, 2.8% - Facebook, 2.3% - SUSE, 2.1% - IBM, 1.9% - Oracle, 1.5% - ARM, 1.4% - Canonical. 13.2% изменений подготовлены независимым участниками или разработчиками, явно не заявившим о своей работе на определённые компании. 1.3% изменений подготовлены студентами, аспирантами и представителями учебных заведений. По числу добавленных в ядро 5.13 строк кода лидирует компания AMD, доля которой составила 20.2% (драйвер amdgpu насчитывает около 3 млн строк кода, что примерно 10% от общего размера ядра - 2.4 млн строк приходится на сгенерированные автоматически заголовочные файлы с данными для регистров GPU).
Релиз ядра Linux 5.5
После двух месяцев разработки Линус Торвальдс представил релиз ядра Linux 5.5. Среди наиболее заметных изменений: возможность назначения сетевым интерфейсам альтернативных имён, интеграция криптографических функций из библиотеки Zinc, возможность зеркалирования на более чем 2 диска в Btrfs RAID1, механизм отслеживания состояния Live-патчей, фреймворк unit-тестирования kunit, повышение производительности беспроводного стека mac80211, возможность доступа к корневому разделу через протокол SMB, верификация типов в BPF.
В новую версию принято 15505 исправлений от 1982 разработчиков, размер патча - 44 Мб (изменения затронули 11781 файлов, добавлено 609208 строк кода, удалено 292520 строк). Около 44% всех представленных в 5.5 изменений связаны с драйверами устройств, примерно 18% изменений имеют отношение к обновлению кода специфичного для аппаратных архитектур, 12% связано с сетевым стеком, 4% - файловыми системами и 3% c внутренними подсистемами ядра.
Подробнее в Яндекс.Дзен