Вышел новый видос. В нём я рассказал о том, как реализовал клиенты ВК и YouTube на смартфонах Lumia с Windows Phone на борту!
А новая статья выйдет завтра :)
А новая статья выйдет завтра :)
Пожалуй, немалая часть моих читателей так или иначе интересуется DIY-тематикой. И в различных самодельных девайсах порой есть необходимость вывести какую-либо информацию на дисплей, будь это текст, графики или даже какая-то анимация! Для разных задач существуют самые разные дисплеи и в сегодняшнем материале я хотел бы систематизировать и собрать подробнейший гайд об использовании дисплеев с нерабочих мобильных телефонов: какие бывают протоколы и шины данных, как читать схемы устройств и определять контроллеры дисплеев, какие дисплеи стандартизированы, а какие придётся реверсить самому и как быть с подсветкой. В практической части статьи мы подключим дисплей используя протокол MIPI DBI к RP2040 с использованием DMA. Интересно? Тогда добро пожаловать в статью!
Пожалуй, ЖК-дисплеи с самого момента их появления стали основным инструментом для вывода информации и взаимодействия с пользователями. Первые ЖК-панели были монохромными и требовали отдельный драйвер, который занимался выводом изображения на экран и формированием необходимых для его работы напряжений.
Сейчас же всё гораздо проще и каждый любитель DIY-электроники может и сам подключить дисплейчик к своему проекту и использовать в необходимых ему целях. Ведь не зря написаны десятки библиотек по типу AdaFruit LCD, которые упрощают задачу программисту и дают ему возможность оперировать готовыми и простыми операциями по типу «вывести линию» или «отрисовать изображение». Однако, готовые библиотеки — это, конечно, здорово, но они не всегда дают понимание о том, как работают такие дисплеи на программном и аппаратном уровне. И первая часть статьи как раз и будет посвящена этому.
Всего в мире дисплейных матриц существует несколько общепринятых аппаратных протоколов. Некоторые из них можно легко использовать в собственных проектов с микроконтроллерами, с другими придется повозиться:
Параллельная шина 8080 — одна из самых простых и понятных шин данных, как в теории, так и на практике. Суть её очень простая: на каждый бит отводится по одной сигнальной линии, плюс две дополнительные линии для сообщения статуса передачи: RD означает запрос чтения, а WR — запрос на запись. Большинство дисплеев использует девятый, неявный бит D/C, который сообщает контроллеру, задаём ли мы номер команды, или уже пишем аргументы для этой команды. Что самое приятное — шина по сути стандартизирована и во многих дисплеях команды на старт записи в видеопамять, а также получение ID-контроллера идентичны. Шина бывает 8-битной и 16-битной (её состояние задаётся битами IM0..IM2 и используется не только для подключения дисплеев, но и микросхем параллельной флэш-памяти, ОЗУ и т. д. Такие шины используются в дисплеях с разрешением до 480x320.
SPI — шина, которая наверняка знакома большинству моих читателей. Достаточно простая — у нас есть две сигнальные линии с входным (MISO) и выходным (MOSI) битом, плюс сигнал тактирования, который согласовывает передачу данных. Таким образом, шина получается полнодуплексной. Фактически, каждый байт передаётся по одному биту через одну сигнальную линию, что, по сравнению с 8080, заставляет повышать тактовую частоту контроллера SPI, но при этом занимает гораздо меньше пинов самого МК или процессора. В программном плане, большинство дисплеев представленных в различных интернет-магазинах полностью совместимы с дисплеями 8080, ведь SPI — просто один из режимов работы. Единственный нюанс — из SPI дисплея не всегда можно вычитать ID-контроллера и вообще что-либо читать из регистров дисплея.
I2C — относительно редко используемая шина для дисплеев из-за её невысокой производительности, однако, тем не менее, очень подходящая для МК (благодаря использованию только двух сигнальных линий — SDA для данных и SCL для тактирования. Даже чипселект здесь программный благодаря тому, что каждое устройство имеет собственный адрес!), однако её можно найти в дисплеях некоторых телефонов из самого начала 2000-х годов.
TTL/параллельный RGB — тут, в общем-то, меня упрекали пару раз из-за того, что я продолжаю называть её TTL, но так сложилось исторически — даже в даташитах эту шину называют именно так. С логической точки зрения она очень простая: у нас есть 16/24 сигнальные линии, где 5 (или 8) бит используются для красного и синего канала и 6 (или опять же 8) бит используются для зеленого цвета (т. е. в 16-битном цвете у нас RGB565, а в 24-битном — RGB888). К ним идут сигналы HSYNC для горизонтальной синхронизации и VSYNC для вертикальной. Вообще, необязательно использовать все сигнальные линии предоставляемые дисплеем — можно использовать, например, RGB332 и использовать всего 8 сигнальных линий. Однако для отображения картинки, необходимо строго соблюдать тайминги синхронизации, иначе дисплей будет просто показывать белый цвет. Помимо цифрового варианта, бывает также аналоговый, очень похожий на телевизионный RGB или VGA. Такие дисплеи обычно используются для матриц до 1024x768 включительно.
MIPI DSI — протокол, используемый для дисплеев высокого разрешения — от 480x800 и выше, его можно встретить в большинстве современных смартфонов и планшетов. Кроме того, такие дисплеи используют относительно мало пинов — по два на каждый канал LVDS (обычно в смартфоне около двух-четырех каналов) + две сигнальные линии на тактирование. Звучит всё хорошо? Как-бы не так: протокол дифференциальный и на каждый канал (т. е. логический бит) приходится по две сигнальные линии — одна с положительная, а вторая отрицательная. Затем одна вычитается из другой и получается окончательный сигнал, а сделано это для уменьшения помех от передачи данных по нескольким линиям с очень высокой тактовой частотой без увеличения битности шины.
LVDS/eDP — Протоколы, используемые в матрицах ноутбуков, телевизоров и иногда планшетов. На физическом уровне близки к DSI, на программном — если честно, не знаю, но наслышан о некой стандартизации и высоком уровне совместимости. Даже «неродные» ноутбучные матрицы вполне «заводятся», максимум после перепрошивки родной EEPROM, даже если дисплей другого разрешения!
В списке выше, мы рассмотрели несколько популярных аппаратных шин для дисплеев. В данной статье, мы разберемся в программных особенностях таких дисплеев и узнаем, где взять по дисплею одного из следующих типов: SPI, I2C, а также 8080.
Пожалуй, писать статью, где были бы только готовые примеры без объяснения принципов работы «под капотом» было бы плохим тоном. Поэтому предлагаю немного разобраться в системе команд для самых распространенных контроллеров дисплеев в наше время.
У рассматриваемых нами дисплеев есть собственная видеопамять, благодаря чему нет необходимости соблюдать тайминги, а также общий набор команд (или аппаратных регистров), которые мы можем записывать и тем самым менять поведение дисплея. Если мы просто подадим питание на дисплей и попытаемся что-то вывести — у нас ничего не выйдет, поскольку при каждом аппаратном RESET'е, состояние большинства регистров, кроме SleepOn и PowerOn не определено и может содержать в себе любой «мусор». Для корректной работы дисплея, нам необходимо послать определенный набор команд, называемый инициализацией, который установит настройки драйвера дисплея, такие как контраст, параметры цветности, направление развертки изображения из VRAM и т. д. Пожалуй, стоит сразу отметить, что некоторые люди называют регистры дисплея командами — это означает одно и тоже!
Пример инициализации. На самом деле, не все люди делают такую простыню из вывозов функций чтения/записи регистров дисплея, поскольку это кушает драгоценный ROM. На AVR, например, команды инициализации можно хранить в ROM и читать из PROGMEM.
Если дисплей инициализирован неправильно, то мы можем наблюдать некорректную развертку, артефакты на дисплее и полосы: если вы когда-нибудь прошивали смартфоны прошивками других ревизий, то могли замечать подобный эффект сами.
Набор команд для контроллеров дисплеев частично стандартизирован спецификацией MIPI DBI, которая описывает и закрепляет некоторые конкретные адреса регистров, общие для всех контроллеров дисплея. К ним относится, например, установка «окна» для записи (0x2B и 0x2A), sleepout (0x11) и некоторые другие. Проприетарными командами остаются настройки питания, развертки, контраста и самого драйвера дисплея. Ну и всяческие LUT, а также палитровые режимы (если они есть) тоже проприетарные.
Пример одной из таких стандартизированных команд:
Почти во всех дисплеях есть разделение отправляемых байтов на команду (или выборка номера регистра для чтения/записи) и на данные. Как обработать текущий байт определяет отдельный пин (или бит, в зависимости от конфигурации дисплея), называемый D/C (Data/Command), иногда также можно встретить названиеRS. Обычно, при записи команды, D/C должен быть на низком уровне, при записи данных, соответственно, на высоком. Суть простая: записываем номер команды (или регистра) при низком D/C, а затем дописываем необходимые аргументы (или конфигурацию регистра) при высоком уровне D/C.
Примерно так:
Касательно сброса, то в дисплеях обычно существуют два вида этого процесса: аппаратный сброс через соответствующий пин и программный с помощью специальной команды. Пин RESET никогда нельзя оставлять в «воздухе» (т. е. не подключенным) в надежде что «да состояние пинов МК после ресета известно, мусора на шине явно не будет». Мусора может и не будет, а вот дисплей упадет в вечный ресет, поскольку ожидает перехода сигнала RESET в высокий уровень. Тоже самое касается и пина CS, отвечающий за выбор устройства на шине. Если вам не нужен CS и у вас висит только одно устройство на шине — просто притяните его к массе. Некоторые контроллеры (например, ILI9325) адекватно реагируют на CS «в воздухе», некоторые — нет. Только после того, как RESET оказался на высоком уровне, дисплей начнёт принимать команды:
Переходим конкретно в выводу данных. Для начала вывода изображения на дисплей, нам необходимо выполнить команду 0x2C, которая переведет контроллер дисплея в режим записи данных в видеопамять. После этого, нам остаётся лишь установить высокий уровень на пине D/C и просто слать непрерывный поток пикселей. Контроллер дисплея сам инкрементирует координаты на дисплее и после того, как координаты выйдут за границы нужной области, дисплей сам их переведет в изначальные. Таким образом, достаточно лишь один раз проинициализировать дисплей и просто гонять в него данные, например, с помощью DMA.
Всё просто и понятно :)
Пожалуй, подобные дисплеи найти проще всего, поскольку они использовались в большинстве кнопочных телефонов из нулевых. Такие экранчики можно встретить во многих моделях Nokia, Samsung, LG, Fly, Sony Ericsson и большинстве китайских телефонов. С поиском распиновки и разводкой таких дисплеев всё относительно просто и одновременно сложно: на некоторые модели телефонов (например, почти на все Nokia) можно свободно найти схему в гугле и узнать распиновку коннектора дисплея… однако этот коннектор сначала надо сдуть и развести на breakout-плате, или под микроскопом вывести перемычки. В некоторых случаях (например, Siemens S-серии), дисплей просто прижимался к контактам на плате, а сами контакты имели более чем паябельный шаг.
Из схемы на Nokia N70. Этот дисплей применялся во многих Symbian-смартфонах Nokia тех лет: N-Gage/N-Gage QD, N70, N72, 6600 и некоторых других.
Но особо удобными можно считать дисплеи с паябельными шлейфами с большим шагом пинов — такие можно встретить в некоторых телефонах Samsung и большинстве китайских телефонов. Пытливый читатель спросит «так это ж китаец, где ты на него схему будешь искать?». И вот тут, китайские производители нас приятно порадуют, поскольку за редким исключением, такие дисплеи имеют стандартизированную распиновку: лично мне известны матрицы 37 Pin, 39 Pin и 44 Pin. Как найти для них распиновку? Пишем на «алике» или «таобао» 37 pin lcd tft и смотрим: в описании продавец частенько прилагает распиновку (правда учтите, что 37 pin не имеет пинов IM для настройки ширины шины, а 16-битный интерфейс может быть слишком прожорилвый по числу пинов):
В случае с китайцами, иногда можно найти и схему (нажимайте на зеленую стрелку) на устройство: например, почти на все модели Fly схемы лежат в свободном доступе, где почти всегда можно найти распиновку дисплея. Иногда производитель даже выводит тестпоинты на все сигнальные линии и дисплей с тачскрином можно использовать, не выпаивая его с платы!
Распиновка на Fly IQ239. На нижней части изображения, вы можете увидеть, что такие, безусловно, здоровенные дисплеи можно купить за копейки и сейчас :)
Но задумывались ли вы когда-нибудь, откуда на тачскринах в дисплеях с «али» взялись кнопки «домой», «сообщения», «телефон»? Это ведь те самые дисплеи, которые использовались в «ноклах», просто припаянные к удобной плате! :) Кроме того, на китайские дисплеи без проблем можно найти даташит: обычно они используют контроллеры от ST или ILI, в зависимости от разрешения дисплея.
Кстати, про девайс на фото выше есть отдельная статья, где я рассказываю что такие девайсы необязательно дербанить но запчасти, ведь под них можно писать полноценные нативные программы!
Концептуально, аппаратная реализация протокола одновременно простая и понятна любому: программа устанавливает состояние каждого бита передаваемого байта на сигнальных линиях D0..D7 (либо D00..D15, если шина у нас 16-битная), а затем просто «дёргает» линию RD (Read или чтение), либо WR (Write или запись) по переходу из низкого уровня в высокий, благодаря чему контроллер дисплея понимает, что байт (или слово в случае 16-битного интерфейса) можно «забирать» с шины. По переходу из высокого уровня в низкий, контроллер снова переходит в режим ожидания следующего байта с шины.
Где взять такие дисплейчики? Да почти везде! Но лучше всего брать дисплеи с китайчиков, которые можно развести на вот таких breakout-платах, которые можно заказать на алике за пару сотен рублей.
Обратите внимание на то, как по свински припаивают подсветку на некоторых дисплеях. И это завод! Лучше сразу прозвоните прежде чем подавать питание. Я, вот, забыл, понадеялся на производителя и по итогу сжёг подсветку :(
Другой вопрос, где искать на них информацию? Помимо схем, можно просто поискать на алике «37 pin lcd tft», «39 pin tft lcd», «24 pin tft lcd» и т. п. Обычно продавцы сами выкладывают распиновку и даже прикладывают ID контроллера дисплея. Поскольку иногда различия в распиновках всё же попадаются, обращайте внимание на то, куда у вас идут дорожки от подсветки и от резистивного тачскрина (если есть), а также вызванивайте все пины с массой — это поможет подобрать правильную распиновку без логического анализатора. Вот, например, дисплейчик из китайской нерабочей реплики Nokia 130 с здоровым 2.4" дисплеем… казалось бы, вообще не понятно что за дисплей, однако воспользовавшись смекалкой, мы находим его распиновку!
SPI-дисплеи в телефонах встречались относительно редко. В основном, подобные дисплейчики можно было найти в моделях начала 2000х годов: сименсах, моторолах, ранних сонериках T-серии и Nokia на S40. Иногда SPI-дисплеи можно встретить в современных кнопочных телефонах — обычно они имеют шлейф с менее чем 15 пинами, как некоторые модели Fly. Обычно контроллер дисплея поддерживал сразу несколько аппаратных шин, а производитель телефона ещё на этапе установки шлейфа к контроллеру дисплея замыкал необходимые IM-пины выбирая необходимую шину, поэтому программный протокол фактически идентичен дисплеям с шиной 8080.
Несомненным плюсом SPI-дисплеев можно назвать малое число пинов для работы с матрицей: достаточно всего два (плюс сигнал D/C, если дисплей не 9-битный), если повесить RESET на VIO, либо три (четыре), если хотите управлять аппаратным RESET вручную. Но есть и, в некоторой степени, минусы: например, не все микроконтроллеры умеют работать в 9-битном режиме и возможно последний бит придётся досылать «ногодрыгом» (что ломает любую возможность реализации DMA).
Многие дисплеи с этим интерфейсом задокументированы ещё в начале 2000х годов на известных форумах и сайтах, таких как VRTP, Радиокот и easyelectronics, поэтому проблем с их подключением не возникнет даже у новичка. Даже такой крутой и уважаемый дядька, как @DIHALT, когда-то писал полезный материал об использовании FSMC в STM32.
Достать их новыми можно и сейчас: различные магазины запчастей для телефонов бывают продают их по 20-30-40 рублей… Я недавно себе целую коробочку накупил, в том числе и просто для ремонта смартфонов для будущих статей :)
Дисплеи с такой шиной — настоящая редкость и обычно попадались в телефонах самого начала нулевых годов с низким разрешением дисплея. Из известных мне — Ericsson'ы и ранние Sony Ericsson T-серии, ODM Motorola (головастики например) и… пожалуй всё.
Казалось бы, разве I2C может быть полезен для работы с дисплеями, где требуется активный вывод графики? Ведь он совсем медленный! Однако, даже он может пригодится для некоторых проектов, а в большинстве МК частенько попадается аппаратный TWI.
Кроме того, I2C дисплейчики удобно отлаживать: благодаря тому, что периферийное устройство должно отрапортовать ACK (состояние успешности получения байта) мастер-устройству, можно сразу определить обрыв линий до дисплея. Но какой-то конкретной информации по ним я не смогу написать — они все совсем разные :( Правда, полезным линком поделюсь, ребята с форума VRTP собрали хорошую таблицу с различными контроллерами дисплеев, где бывают и i2c!
Отдельного радела стоит тема подсветки дисплеев. По первой может показаться, что тут всё просто: современным дисплеями достаточно 5В, а на старых можно замерить напряжение бустера на живом девайсе и смастерить свой DC-DC повышающий преобразователь, или взять, например, уже готовый драйвер, как известный в определенных кругах LTYN. На самом деле и тут есть свои нюансы.
Итак, каким образом реализована подсветка в том или ином устройстве? Обычно её реализация заключается в последовательном соединении двух и более светодиодов, которые формируют небольшую ленту под рассеивающей плёнкой. На современных китайских дисплейчиках, для работы в полную яркость достаточно всего лишь 5В источника питания + токоограничивающего резистора. Но что самое приятное, подсветка в таких дисплеях способна работать и при 3.3В, пусть менее ярко, но всё равно вполне читабельно.
Если вы делаете портативное маломощное устройство, работающее от одного Li-Ion аккумулятора, то достаточно лишь пустить 3.3В с линейного стабилизатора, который формирует напряжение VSYS для микроконтроллера. Таким образом, у вас будет стабильная подсветка среднего уровня яркости. В качестве альтернативного «бомж» варианта, когда нет возможности собрать нормальный драйвер подсветки, можно попробовать подключить светодиоды напрямую к АКБ, но при разряде дисплей будет потихоньку «тухнуть». Ещё один «бомж» вариант — разобрать дисплейный модуль, порезать дорожки на ленте и соединить пару светодиодов параллельно, выведя их через отверстие, откуда выходит шлейф дисплея, однако в таком случае, потребление подсветки заметно увеличится.
Правильным выходом будет взять с того-же телефона бустер подсветки с индуктивностью и иной необходимой обвязкой, и собрать бустер самому. Особой популярностью когда-то пользовались вышеупомянутые LTYN из телефонов Samsung (это маркировка известного драйвера LT1937). Уровнем подсветки на подобных бустерах телефоны управляют с помощью встроенного ШИМ-контроллера, чем можете воспользоваться и вы :)
В первой части статьи, я постарался ввести вас в курс дела и кратко рассказать о том, как работают такие дисплейчики «под капотом». Как видите — с теоретической точки зрения, ничего сложного нет: пересылаем данные на дисплей, да вовремя дёргаем пин D/C. Но какого же это на практике?
К сожалению, у меня на руках не нашлось подходящего дисплейчика от мобильного телефона (я ведь брал новые по уценке, не все заработали нормально), поэтому в качестве примера работы мы возьмём фактически такой же «китайский» дисплей с алика. Но будьте уверены — с большинством дисплеев, принцип работы будет идентичен (если мы говорим о дисплеях 2005г.в и моложе).
В качестве МК, мы возьмём мой любимый RP2040, который, по моему мнению, незаслуженно обделен вниманием. Время от времени я делаю всякие прикольные девайсы на базе этого МК, поэтому крайне рекомендую его всем моим читателям :)
Давайте же перейдем к практической части статьи!
Обычно при создании проекта, я просто клонирую с гита RPi сэмплы с уже готовыми файлами CMake, беру hello world, конфигурирую CMakeLists.txt и пишу свою программу. На малинке пока что нет такого удобного способа создания проекта, как idf.py create-project :)
Само собой, для удобства отладки я всегда включаю встроенную в чипсет эмуляцию UART через USB.
if (TARGET tinyusb_device)
add_executable(hello_usb
main.cpp
)
# pull in common dependencies
target_link_libraries(hello_usb pico_stdlib hardware_spi)
# enable usb output, disable uart output
pico_enable_stdio_usb(hello_usb 1)
pico_enable_stdio_uart(hello_usb 0)
# create map/bin/hex/uf2 file etc.
pico_add_extra_outputs(hello_usb)
# add url via pico_set_program_url
example_auto_set_url(hello_usb)
elseif(PICO_ON_DEVICE)
message(WARNING "not building hello_usb because TinyUSB submodule is not initialized in the SDK")
endif()
И инициализирую USB-стек и биндинги stdout к нему:
stdio_init_all();
sleep_ms(1000);
Задержка здесь важна, иначе девайс отказывается определятся в системе. Переходим, собственно, к разводке дисплея. Для работы нам достаточно лишь питания, подсветки, общей массы и четырёх сигнальных линий: MOSI, CLK, DC, RESET. На CS я обычно ставлю перемычку с массой, т. к обычно не вешаю что-то ещё на одну шину с дисплеем.
Переходим к инициализации дисплея. Наш экранчик работает на базе контроллера ST7735R и имеет разрешение 128x160. Сначала, назначаем функции для пинов и дёргаем RESET:
gpio_set_function(LCM_SPI_CLK, GPIO_FUNC_SPI);
gpio_set_function(LCM_SPI_MOSI, GPIO_FUNC_SPI);
// HW reset
gpio_init(LCM_RESET);
gpio_set_dir(LCM_RESET, true);
gpio_put(LCM_RESET, false);
sleep_ms(400);
gpio_put(LCM_RESET, true);
gpio_init(LCM_DC);
gpio_set_dir(LCM_DC, true);
spi_init(spi0, 105535000);
Весьма негусто скажете вы? Ну, с минорными изменениями, здесь заработает дисплейчик любого разрешения, даже 480x320! Переходим к фактической инициализации:
void lcmCommand(unsigned char byte)
{
gpio_put(LCM_DC, 0);
spi_write_blocking(spi0, &byte, sizeof(byte));
}
void lcmData(unsigned char byte)
{
gpio_put(LCM_DC, 1);
spi_write_blocking(spi0, &byte, sizeof(byte));
}
...
lcmCommand(0x11);
sleep_ms(120);
lcmCommand(0xB1);
lcmData(0x01);
lcmData(0x2C);
lcmData(0x2D);
lcmCommand(0xB2);
lcmData(0x01);
lcmData(0x2C);
lcmData(0x2D);
lcmCommand(0xB3);
lcmData(0x01);
lcmData(0x2C);
lcmData(0x2D);
lcmData(0x01);
lcmData(0x2C);
lcmData(0x2D);
lcmCommand(0xB4);
lcmData(0x07);
lcmCommand(0xC0);
lcmData(0xA2);
lcmData(0x02);
lcmData(0x84);
lcmCommand(0xC1);
lcmData(0xC5);
lcmCommand(0xC2);
lcmData(0x0A);
lcmData(0x00);
lcmCommand(0xC3);
lcmData(0x8A);
lcmData(0x2A);
lcmCommand(0xC4);
lcmData(0x8A);
lcmData(0xEE);
lcmCommand(0xC5);//VCOM
lcmData(0x0E);
lcmCommand(0x36);//MX, MY, RGB mode
lcmData(0xC8);
lcmCommand(0xe0);
lcmData(0x02);
lcmData(0x1c);
lcmData(0x07);
lcmData(0x12);
lcmData(0x37);
lcmData(0x32);
lcmData(0x29);
lcmData(0x2d);
lcmData(0x29);
lcmData(0x25);
lcmData(0x2b);
lcmData(0x39);
lcmData(0x00);
lcmData(0x01);
lcmData(0x03);
lcmData(0x10);
lcmCommand(0xe1);
lcmData(0x03);
lcmData(0x1d);
lcmData(0x07);
lcmData(0x06);
lcmData(0x2e);
lcmData(0x2c);
lcmData(0x29);
lcmData(0x2d);
lcmData(0x2e);
lcmData(0x2e);
lcmData(0x37);
lcmData(0x3f);
lcmData(0x00);
lcmData(0x00);
lcmData(0x02);
lcmData(0x10);
lcmCommand(0x2A);
lcmData(0x00);
lcmData(0x02);
lcmData(0x00);
lcmData(0x81);
lcmCommand(0x2B);
lcmData(0x00);
lcmData(0x01);
lcmData(0x00);
lcmData(0xA0);
lcmCommand(0x3A);//65k mode
lcmData(0x05);
lcmCommand(0x29);//Display on
// Set viewport
lcmCommand(0x2A);
lcmData(0 >> 8);
lcmData(0 & 0xFF);
lcmData(128 >> 8);
lcmData(128 & 0xFF);
lcmCommand(0x2B);
lcmData(0 >> 8);
lcmData(0 & 0xFF);
lcmData(160 >> 8);
lcmData(160 & 0xFF);
Прошиваем наш МК и смотрим что получилось. Видим шум на экране? Значит дисплей инициализирован верно!
После инициализации дисплея, мы можем выводить на него данные! Дабы дать возможность процессору заниматься другими делами во время передачи картинки на дисплей, мы настроим один из DMA-каналов. DMA-контроллер занимается пересылкой данных из ОЗУ в другой участок ОЗУ (аппаратный memcpy) или периферию. Как раз для второго случая, т. е. пересылки данных в контроллер SPI, мы и будем использовать DMA!
Аллокейтим фреймбуфер, куда мы будем выводить нашу картинку и настраивает DMA-канал:
int backBufSize = LCM_WIDTH * LCM_HEIGHT * 2 + 1;
backBuffer = (byte*)malloc(backBufSize);
printf("LCM: Setting up DMA channel...\n");
bulkDMAChannel = dma_claim_unused_channel(true);
cfg = dma_channel_get_default_config(bulkDMAChannel);
channel_config_set_transfer_data_size(&cfg, DMA_SIZE_8);
channel_config_set_dreq(&cfg, spi_get_dreq(spi0, true));
Переходим к выводу изображения на дисплей. Для того, чтобы просто установить цвет пикселя в любых координатах экрана, достаточно лишь посчитать смещение от начала указателя на фреймбуфер к определенным координатам экрана. Формула очень простая и понятная: ширина дисплея * Y-координата + x координата и результат предыдущих операций помноженный на число байт в одном пикселе.
__inline void pixelAt(short x, short y, short color)
{
if(x < 0 || y < 0 || x >= LCM_WIDTH || y >= LCM_HEIGHT)
return;
byte* col = (byte*)&color;
*((short*)&backBuffer[(y * 128 + x) * 2]) = color;
}
В функции есть валидация границ дисплея. Если уверены, что не зайдете за границы дисплея — можете убрать проверку, будет шустрее.
Теперь для вывода картинки, нам достаточно лишь скопировать изначальное изображение в наш фреймбуфер и попросить DMA-канал вывести изображение на дисплей. Для прозрачных картинок без альфа-канала (т. е. с цветовым ключом), функция будет выглядеть так:
А вот как с этим всем работать:
stdio_init_all();
sleep_ms(1000);
printf("LCM test by monobogdan\n");
lcmInitInterface();
lcmAllocBackBuffer();
lcmInit();
grDrawBitmapTransparent((void*)&pikabu, PIKABU_WIDTH, PIKABU_HEIGHT, 0, 0);
lcmFlush();
while(1)
{
}
Запускаем и...
Можно сделать чуть комплекснее, добавив альфа-блендинг и аффинные трансформации (возможность поворота и скейла картинок), но пока-что такой задачи не стоит. Ну что, всё очень просто и понятно? :) Пример прошивки можно найти на моём GitHub!
Производительность такого способ на RP2040 можно увидеть вот в этом видосе (на Пикабу не смог залить из-за ограничения на число медиа-элементов). Обратите внимание, что подход предложенный выше больше подходит именно для динамического вывода изображения без dirty-регионов. Он подойдет для игровых консолей, камер, анимаций или устройств с выводом динамической информации по типу осциллографов. Если вам нужно обновлять картинку реже, например, если вы делаете умные часы с плеером, то нет необходимости занимать довольно большой объем ОЗУ фреймбуфером, ведь вы можете писать напрямую в видеопамять. Тут уже решать в зависимости от конкретной ситуации именно вам :)
Вот мы с вами и систематизировали информацию о том, как использовать дисплеи с мобильных телефонов в своих проектах. Надеюсь, информация была достаточно полезной для вас!
Однако, у меня к вам просьба: пожалуйста, не «дербаньте» рабочие девайсы «на запчасти» :(
Это будет не очень гуманно по отношению к нашему «технобалдежу», где мы наоборот стараемся найти применение стареньким девайсам :)
Был ли для вас материал полезен? Пишите в комментариях.
Друзья! Я, как и многие мои читатели, помимо программирования и железа обожаю тачки! Особенно те тачки, где что-то нужно доделывать самому… и речь, конечно-же, о ТАЗах! Я долго думал, но всё же решился: сейчас я коплю на будущий интересный проект, связанный с ультрабюджетным электронным дооснащением автомобиля, который старше меня в полтора раза — скорее всего, речь пойдет о ВАЗ 2108/2109/21099, причём не исключено что карбюраторной! В планах довольно крутой проект, заключающийся в следующем: мы спроектируем очень дешевый бортовой компьютер (т.е панель) для управления автомобилем на базе дешевого Б/У планшета за пару сотен рублей. Планшет будет связан с управляющим МК через UART (о подобной коммуникации через хардварные протоколы я уже писал целых две статьи: сам себе Linux смартфон, превращаем планшет с нерабочим тачскрином в игровую консоль), и с планшета мы сможем не только управлять основными системами машины (стеклоподъемники, центральный замок и соленоид багажника), но и собирать и пытаться примерно посчитать некоторую информацию о расходе, километраже и стабильности работы двигателя на карбюраторной(!) машине без электронных систем с завода!
Если вдруг двигатель машины будет живенький и заводиться с полтычка, то может и удаленный прогрев постараюсь реализовать :)
В наши задачи будет входить не только проектирование аппаратной части такого оснащения, но и разработка симпатичного интерфейса для самой панели, дабы было не хуже чем в BMW :D Всеми схемами, исходным кодом и инструкциями я буду делится с вами в каждой статье и, как обычно, расскажу обо всех деталях реализации во всех подробностях! У меня уже есть некоторые идеи и наработки. Собственно, почему-б и не попробовать? Будет новая рубрика в блоге: апгрейд автомобилей глазами электронщика и прожженного программера.
Фото не моё, из интернета
Если вам нравятся мои статьи, вас интересует развитие такой рубрики и у вас есть желание и возможность — можете помочь проекту копеечкой с помощью формы доната ниже. Пикабу позволяет остаться анонимным и донатить даже без регистрации. Сейчас у меня есть 40 тысяч рублей личных накоплений, на покупку самой машины планирую выделить 70-80 тысяч рублей (я живу в Краснодарском крае, так что здесь ещё есть шансы найти что-то +- живое за такие деньги), так что остаётся собрать около 30-35 тысяч рублей. За каждую копейку я готов отчитаться (по факту покупки машины я сделаю пост с фотографиями авто, ДКП, а также оглашу фронт будущих работ и сразу начну заниматься проектом).
Если сейчас приехать в пункт приема металлолома, то можно обнаружить просто огромные кучи различных телефонов и прочих электронных «отходов», которые стоят под открытым небом и ждут, когда придёт их черёд окончательного разложения. Однако при ближайшем рассмотрении выясняется, что многие девайсы оказываются полностью рабочими даже после недельного лежания под палящим солнцем и проливными дождями, а сдали их в чермет по причинам «не нужен, надоел, купил новый» и т. п. Я не считаю это правильным, ведь даже в простые кнопочные звонилки имеется возможность вдохнуть новую жизнь, если знать один интересный, но малоизвестный факт: для них можно писать нативные приложения на C и использовать железо телефона в своих целях. А это, на минуточку, как минимум: дисплей с подсветкой, вибромотор, динамик, клавиатура и GSM-радиомодуль с возможностью выхода в сеть. Сегодня мы с вами: узнаем, на каких аппаратных платформах работают китайские телефоны, какие существуют программные платформы и где взять для них SDK, а в практической части мы напишем 2D-игру с нуля, которая будет работать на многих китайских кнопочниках. Интересно? Тогда жду вас под катом!
Содержание:
Не J2ME едины
Аппаратные ресурсы
Кроссплатформенный рантайм
Кроссплатформенный рантайм: Win32
Кроссплатформенный рантайм: MRE
Кроссплатформенный рантайм: VXP
Наконец-то пишем игру
Тестируем на реальных девайсах
Заключение
Думаю, многие мои читатели помнят о такой платформе, как J2ME. Java-приложения стали фактически основной возможностью расширения функционала телефонов в 2000-х годах. API для них был достаточно хорошо стандартизировано, программы не зависели от архитектуры процессора и ОС устройства, а порог вхождения для написания собственных приложений был довольно низкий и даже новички могли за пару дней написать свою игрушку или какое-нибудь GUI-приложение!
Однако не одним J2ME мы были едины: существовало множество платформ, которые так или иначе пытались занять нишу Java на рынке. Некоторые из них я упоминал в своей прошлой статье о написании 3D-игры под Sony Ericsson с нуля: например, была такая платформа на телефонах Sony Ericsson серии T, как Mophun, а CDMA-телефонами с чипсетами Qualcomm использовалась нативная платформа BREW. Пожалуй, я не буду упоминать о .sis и .cab — поскольку это форматы нативных приложений для смартфонов, а не простых «фичефонов».
В какой-то момент, ближе к 2006-2007 году, прилавки российских официальных ритейлеров (по большей части это были телефоны Fly) и неофициальных продавцов на рынках заполонили различные китайские телефоны, которые предлагали какой-то немыслимый функционал для тех лет за копейки, да ещё и визуально напоминали флагманские модели известных брендов. Пожалуй, одним из самых популярных таких телефонов была Nokla TV E71/E72 (да, именно «нокла»), вышедшая примерно в 2008 году и производившаяся аж до 2011 года! За 2-3 тысячи рублей (это менее 100 баксов), пользователь получал здоровый 2.4" дисплей с разрешением 240x320 весьма неплохого качества (когда в те годы многие продолжали ходить с 176x220), да ещё и с тачскрином, гироскоп, огромный громкий динамик (пусть и не очень качественный), поддержку SD-карточек до 32Гб, нередко фронтальную камеру, а также премиальный дизайн с вставками из алюминия. Частенько китайцы заботливо клали в коробку ещё чехольчик и дополнительный аккумулятор :)
Были даже полные копии существующих устройств от Nokia. Особенно китайцы любили подделывать массовые модели на S40: они были очень популярными и китайцы хотели откусить свой кусок рынка у Nokia. Пусть и рынка серого импорта — очевидно, в салонах связи подделки никто не продавал:
Но была и ложка дёгтя в этой бочке меда: китайские телефоны очень часто не имели поддержки Java, из-за чего многие пользователи разочаровывались в них из-за отсутствия возможности установить необходимые им приложения. Никакой тебе оперы, аськи, игр… Скорее всего, это связано с необходимостью отчислений Sun, а также разработчикам реализации J2ME-машины (JBed/JBlend) и установки чипа флэш-памяти чуть большего объёма.
Но многие пользователи не знали, что такие девайсы не просто поддерживали сторонние приложения, но и умели выполнять настоящие нативные программы, написанные на полноценном C! Всему помешала китайская костыльность и тотальная закрытость. Платформа предполагалась для работы на внутреннем рынке. Для вызова менеджера нативных приложений необходимо было вводить специальный инженерный код в номеронабирателе, предварительно скопировав приложение в нужную папку, а SDK долгое время было платным и доступно только для компаний из Китая. Кроме того, далеко не все приложения могли запустить на конкретном девайсе — были серьезные проблемы с совместимостью.
Всё как вы любите: HiTech-девайсы на фоне ковра, который старше автора лет на 30 :)
В ранних китайских телефонах использовалась платформа Mythroad (MRP, MiniJ) от китайской компании SkyWorks, которая лицензировала свою технологию производителям чипсетов. Поддержку MRP можно было встретить на телефонах с чипсетами MediaTek, Spreadtrum, а также MStar (и возможно Coolsand). Mythroad предоставлял некоторое API для работы с железом телефона и разработки как UI-приложений, так и игр, кроме того, Mythroad позволял хранить ресурсы в одном бинарнике с основной программой и даже имел какой-то интерпретируемый язык помимо возможности запуска нативного кода. Для работы таких приложений необходимо было скопировать менеджер приложений dsm_gm.mrp и игру в папку mythroad во внутренней памяти устройства или на флэшке, а затем набрать в номеронабирателе код *#220807#, иногда при отключенной первой SIM-карте. Костыльно? Костыльно! Откуда об этом знать среднестатистическому пользователю? Не откуда! Но работало!
Эта платформа поддерживалась на большинстве подделок под брендовые устройства Nokia, Sony Ericsson и Samsung, а также iPhone и на многих китайских кнопочных телефонах 2008-2010 годов.
Ближе к 2010 году MediaTek разработала свою собственную платформу, которая должна была заменить MRP — WRE (VXP). Эта платформа была гораздо шире с точки зрения функционала (например, был доступ к UART) и её API был вполне удобно читаем для программиста, а SDK свободно доступен для всех. Один нюанс всё портил — приложения без подписи привязывались к IMSI (даже не IMEI) симки в девайсе и на некоторых девайсах требовали переподписания под каждую конкретную SIM или патчинг дампа оригинальной прошивки телефона на отключение проверки подписи. Эта платформа поддерживалась на многих кнопочниках и смарт-часиках 2010-2020 годов: к ним относятся новодельные телефоны Nokia, телефоны DNS и DEXP, Explay и т. п. Для запуска приложений достаточно было выбрать файл с разрешением VXP в проводнике и просто запустить его. Но с совместимостью всё равно имелись проблемы: если запустить VXP для версии 2.0 и выше, мы получим лишь белый экран. Ну хоть не софтресет, и на том спасибо!
Далеко не все такие часы поддерживают MRE, смотреть нужно от устройства к устройству
Большинство китайских кнопочных телефонов работает на базе одних и тех же чипсетов. В конце нулевых чаще всего использовались чипсеты MT6225, SC6520 и некоторые чипы от Coolsand. Средние хар-ки девайса были следующими:
Процессор: ARMv5 ядро на частоте ~104МГц, ARM926EJ-S. Нет FPU, есть Thumb. Большую часть процессорного времени программа могла забрать себе.
ОЗУ: ~4Мб SDRAM. Программам было доступно 512Кб-1Мб Heap'а. Это, в целом, довольно немало для большинства применений.
Флэш-память: ~32Мб, пользователю доступно пару сотен килобайт. Да, вы не ослышались, килобайт! Однако можно без проблем использовать MicroSD-флэшки до 32Гб.
Дисплей: от 128x128 до 320x480, почти всегда есть 18-битный цвет (262.000 цветов), в случае TV E71/E72 используется очень неплохая TN-матрица с хорошими углами обзора и яркой подсветкой. Иногда есть тачскрин.
Звук: громкий динамик, наушники.
Аккумулятор: ~800мАч, на некоторых девайсах может быть и 2.000мАч, а то и больше!
Ввод: клавиатура, иногда была поддержка QWERTY.
Внешние шины: почти всегда был доступен UART, причём его можно было свободно взять прямо с платы — он был явно подмечен! Взять GPIO с проца не выйдет (кроме, возможно, вибромотора), SPI и I2C также напрямую недоступны. Внешние шины можно реализовать с помощью UART через GPIO-мост из микроконтроллера.
В итоге мы получаем очень неплохие характеристики для устройства, которое сочетает в себе сразу всё. На базе такого девайса можно сделать и сигнализацию, и HMI-дисплей с интерфейсом для управления каким-нибудь устройством, и игровую консоль с эмуляторами… да на что фантазии хватает! И это за какие-то 200-300 рублей, если мы говорим о б/у устройстве или 600 рублей, если говорим о новом. Это дешевле, чем собирать девайс с подобным функционалом самому из готового МК (например, RP2040) и отдельных модулей. Кстати, дешевые 2.4" дисплеи на алике — это ни что иное, как невостребованные остатки дисплеев для подобных китайских телефонов на складах! А вы думали, откуда там значки на тачскрине снизу?
Однако в рамках данной статьи мы не будем ограничиваться лишь теорией и на практике напишем примитивную 2D-игрушку, которая будет работать сразу на трех платформах без каких-либо изменений в коде самой игры: Windows, MRP (Mythroad) и VXP. Но для того, чтобы достигнуть такого уровня абстракции от платформы, нам необходимо написать рантайм, который оборачивает все необходимые платформозависимые функции для нашей игры.
Игрушка будет простой: 2D скролл-шутер с видом сверху, а-ля Asteroids. Летаем по космосу, и стреляем по враждебным корабликам, стараясь не попасть под вражеские лазеры. Всё просто и понятно :)
Итак, что нам необходимо от абстракции для такой простой игры? Давайте посмотрим:
Графика: очистка экрана, отрисовка спрайтов с прозрачностью (без альфа-блендинга, только колоркей), отрисовка текста. При возможности, желательно использовать нативное API системы для рисования графики, а не городить собственный блиттер. Формат пикселя фиксирован: RGB565 (65к цветов).
Ресурсы: хранятся в одном образе с основной игрой. Фактически, все ресурсы упакованы в виде обычных массивов байт в заголовочных файлах. Я пользуюсь вот этой тулзой для конвертации спрайтов в массивы байтов.
Звук: воспроизведение хотя-бы одного WAV-потока. Почему одного? Потому что далеко не на всех платформах есть доступ к аппаратному микшеру… да и вообще не везде есть прямой доступ к PCM (привет MRP), иногда разработчики ограничиваются лишь одним каналом для WAV-звука без возможности воспроизведения нескольких аудиофайлов одновременно.
Ввод: абстракция от клавиатуры классического моноблока: стрелки, OK, левый и правые софткеи.
Стандартная библиотека: не на всех платформах можно вызывать функции напрямую из stdlib. Как минимум в MRP и, например, «эльфах» для Motorola, нет возможности вызывать аллокатор, rand и некоторые другие функции из обычных заголовочников стандартной библиотеки. На таких платформах, системные инклуды дефайнами подменяют стандартные функции на своих реализации:
#define malloc system_alloc
#define free system_free
Но если у нас игра кроссплатформенная, то и платформозависимые инклуды мы использовать не будем.
Выглядит всё достаточно просто, верно? Примерно такого набора функций хватит для нашей игры:
Давайте же перейдем к реализации рантайма на каждой платформе по отдельности. Начнём с Win32, поскольку адекватно отлаживать игру можно только на ПК.
На десктопе у нас будет фиксированное окно 240x320, в качестве GAPI будет использоваться аппаратно-ускоренный OpenGL, а для обработки ввода будет использоваться классически GetAsyncKeyState. Реализация точки входа, создания окна и инициализации контекста GL и главного цикла приложения у нас такая:
Реализация отрисовки спрайтов очень примитивная — OGL 1.0, полностью FFP, вся отрисовка — это 2 треугольника, формирующие квад. Спрайт заливается при первом использовании в текстуру, последующие кадры реюзается уже готовая текстура. Фактическая реализация всего рендерера — т. е. функций для рисования «просто картинок», без поддержки атласов, блендинга цветов (З.Ы - длинные листинги будут на пастбине, на Пикабу нет нормального тега для кода):
С вводом тоже всё просто. Есть биндинг кнопок клавиатуры к кнопкам на кейпаде телефона. inGetKeyState предполагается вызывать один раз за кадр, поэтому функция опрашивает ОС о состоянии нажатых кнопок на клавиатуре и назначает состояние виртуальных кнопок относительно состояния физических кнопок на клавиатуре.
Результат:
Переходим к реализации рантайма для первой китайской платформы — MRP. Обратите внимание — я использую нативное API платформы для рисования спрайтов. Связано это с тем, что софтварный блиттер работает невероятно медленно даже с прямым доступом к скринбуферу устройства, а в чипсете предусмотрена отдельная графическая подсистема с командбуфером для быстрой отрисовки примитивов и графики:
SDK для MRE можно найти здесь (SKYSDK.zip): оно уже пропатчено от необходимости покупки лицензии. MRP не развивается более 10 лет, поэтому, думаю, его можно считать Abandonware. Компилятор находится в compiler/mrpbuilder.NET1.exe. За китайские SDK в публичном доступе нужно поблагодарить пользователя 4pda AjlekcaHgp MejlbHukoB, который раздобыл их на всяких csdn и выложил в свободный доступ :)
У MRP собственная система сборки, основанная на конфигурациях. Поскольку MRP может работать на устройствах с разными платформами и размерами дисплеев, под каждую можно настроить свой конфиг, который пережмет ресурсы в нужный формат. Дабы ничего не ломать, я заюзал абсолютные пути:
Компиляция приложения:
mrpbuilder.net1.exe game.mpr
Начинаем с функций обработки событий и инициализации, которые вызывает рантайм при старте приложения: mrc_init вызывается при старте приложения, а mrc_event при возникновении события. Вся инициализация очень простая: создаём таймер для обновления и перерисовки состояния игры и вызываем инициализацию игры:
С вводом тоже никаких проблем нет, нажатия кнопок прилетают как события в mrc_event. Переводим кейкоды MRE в наши кейкоды и сохраняем их состояние:
Опять же, отлаживать MRP-приложение под реальным устройством проблематично, поэтому платформозависимый код должен быть минимальным. Кроме того, обратите внимание, что некоторые функции в MRP зависят от библиотек-плагинов. Линкер слинкует вашу программу, но на реальном устройстве их вызов вывалится в SIGSEGV и софтресет устройства. Также нельзя использовать ничего из стандартной библиотеки именно в стандартных заголовочниках (т. е. stdlib.h, string.h и т. д.), часть стандартной библиотеки реализовывается MRP и дефайнится в mrc_base.h
Что интересно, защиты памяти толком нет. Если приложение падает в SIGSEGV или портит память — систему, судя по всему, ребутит Watchdog. Защиты памяти никакой, можно напрямую читать и писать в память ядра, а также писать в регистры периферии чипсета. jpegqs, покумекаем над этим? :)
Переходим к рендереру. Тут буквально две функции, gClearScreen очищает экран, а gDrawBitmap рисует произвольный спрайт с форматом пикселя RGB565. В качестве ROP используется BM_TRANSPARENT — таким образом, mrc_bitmapShowEx будет использовать левый верхний пиксель в качестве референсного цвета для реализации прозрачности без альфа-блендинга.
void gDrawBitmap(CBitmap* bmp, int x, int y) {
mrc_bitmapShowEx((uint16*)bmp->pixels, x, y, bmp->width, bmp->width, bmp->height, BM_TRANSPARENT, 0, 0);
}
Да, всё вот так просто. Рантайм теперь запускается на реальных китайских девайсах и работает стабильно.
Теперь переходим к VXP — платформе не менее неоднозначной, чем MRP. Пожалуй, начать стоит с того, что VXP существует аж в трёх версиях: MRE 1.0, MRE 2.0 и MRE 3.0. В MRE 2.0 и выше появилась поддержка плюсов (в MRE 1.0 только Plain C) и довольно интересного GUI-фреймворка, MRE 1.0 же предлагает реализовывать гуй самому. Платформа распространена на большинстве кнопочных телефонов и смарт-часиков на чипсетах MediaTek, примерно начиная с 6235 и заканчивания 6261D. SDK можно скачать вот здесь (см MRE_SDK_3.0).
VXP сам по себе более функционален чем MRE, поскольку ориентирован исключительно на телефоны с чипсетами MediaTek. Но что самое приятное — есть доступ к уарту без каких либо костылей! То есть, если сделать GPIO-мост на условной ESP32, то мы можем получить готовый мощный МК с клавиатурой, кнопками, дисплеем, звуком и т. д. Звучит не хило, да? Кроме того, у нас есть доступ и к BT, и к GPRS, и к SMS без каких либо ограничений.
Однако в бочке мёда нашлась и ложка дёгтя: для компиляции MRE-приложений необходимо накатывать и крякать довольно старый компилятор ADS, который сам по себе поддерживает только C89 (например, нет возможности объявить переменную в объявлении цикла или середине функции, только в начале, как в Pascal). ADS уже вроде как Abandonware, так что это вроде не наказуемо… но всё равно неприятно.
Кроме того, на некоторых девайсах (в основном, фирменных Nokia а-ля 225), прошивка требует подписи у всех бинарников, либо если бинарник отладочный, то должна быть привязка к конкретному IMSI.
К тому же, каждая программа должна фиксированно указывать в заголовке, сколько Heap-памяти ей необходимо выделить. Оптимальный вариант — ~500Кб, тогда приложение запустится вообще на всех MRE-телефонах.
Зато у VXP есть адекватный симулятор под Windows. Но зачем он нам, если у нас порт игры под Win32 есть? :)
Начинаем с инициализации приложения. В процессе вызова точки входа, приложение должно назначить обработчики системных событий, коих бывает несколько. Для обработки ввода и базовых событий хватает всего три: sysevt (события окна), keyboard (физическая клавиатура. Есть полная поддержка QWERTY-клавиатур), pen (тачскрин).
vm_reg_sysevt_callback(handle_sysevt); vm_reg_keyboard_callback(handle_keyevt); vm_reg_pen_callback(handle_penevt);
Переходим к обработчику системных событий. Обратите внимание, что MRE-приложения могут работать в фоне, из-за чего необходимо ответственно подходить к созданию и освобождению объектов. Что важно усвоить с самого начала — в MRE нет понятия процессов и защиты памяти, как на ПК и полноценных смартфонах. Любая программа может попортить память или стек ОС, более того, программа использует аллокатор остальной системы, поэтому если ваша программа не «убирает» после себя, данные останутся в памяти со временем приведут к зависанию. Впрочем, WatchDog делает свою работу быстро и приводит телефон в чувство (софтресетом) за 1-2 секунды. Но как и в случае с MRE, есть приятный бонус: прямой доступ к регистрам чипсета :)
Переходим к обработке событий с кнопок. Тут всё абсолютно также, как и на MRE, лишь имена дейфанов поменялись :)
И наконец-то, к графике! Пожалуй, стоит сразу отметить, что более 20-30 FPS на большинстве устройств вы не получите даже с прямым доступом к фреймбуферу. Похоже, это связано с тем, что в MRE довольно замороченная графическая подсистема с поддержкой альфа-канала (только фиксированного во время вызова функции отрисовки картинки/примитивов, сам пиксельформат всегда RGB565) и нескольких слоев. Кроме того, похоже есть ограничения со стороны контроллера дисплея.
Изначально, MRE предполагает то, что все картинки в программе хранятся в формате… GIF. Да, весьма необычный выбор. Однако для работы с пользовательской графикой, есть возможность блиттить произвольные картинки напрямую из RAM. Вот только один нюанс — посмотрите внимательно не объявление следующей функции:
void vm_graphic_blt( VMBYTE * dst_disp_buf, VMINT x_dest, VMINT y_dest, VMBYTE * src_disp_buf, VMINT x_src, VMINT y_src, VMINT width, VMINT height, VMINT frame_index );
dst_disp_buf — это целевой RGB565-буфер. Логично предположить, что и src_disp_buf — тоже обычный RGB565-буфер! Но как бы не так. Документация крайне скудная, пришлось посидеть и покумекать, откуда в обычном 565 буфере возьмется индекс кадра. С подсказкой пришёл пользователь 4pda Ximik_Boda — он скинул структуру-заголовок, которая идёт перед началом каждого кадра. В документации об этом не сказано ровным счетом ничего!
Сначала я реализовал софтовый блиттинг, но он безбожно лагал. Мне стало интересно, почему нативный blt быстрее и… вопросы отпали после того, как я поглядел в ДШ чипсета: тут есть аппаратный блиттинг. И даже с ним девайс не может выдать более 20FPS!
Для реализации более-менее шустрого вывода графики, необходимо сначала создать канвас (фактически, Bitmap в MRE), создать и привязать к нему layer, получить указатель на буфер слоя и только потом скопировать туда нашу картинку. Да, вот так вот замороченно:
И только после этого всё заработало достаточно шустро :)
В остальном же платформа довольно неплохая. Да, без болячек не обошлось, но всё же перспективы вполне себе есть.
На данный момент, этого достаточно для нашей игры.
Рантайм у нас есть, а значит, можно начинать писать игрушку. Хоть пишем мы на Plain-C, я всё равно из проекта в проект использую +- одну и ту же архитектуру относительно системы сущностей, стейтов и т. п. Поэтому центральным объектом у нас станет CWorld, который хранит в себе на пулы с указателями на другие объектами в сцене, а также игрока и его состояние:
typedef struct {
CPlayer player;
int nextSpawn; // In ticks
CEnemy* enemyPool[ENEMY_POOL_SIZE];
CProjectile* projectilePool[PROJECTILE_POOL_SIZE];
} CWorld;
Система стейтов простая и понятная — фактически, между состояниями передавать ничего не нужно. При нажатии в главном меню на «старт», нам просто необходимо проинициализировать мир заново и начать геймплей, при смерти игрока — закинуть его обратно в состояние меню. Стейты представляют из себя три указателя на функции: переход (инициализация), обновление и отрисовка.
typedef void(CGameStateCallback)();
Поскольку мы хотим некоторой гибкости при создании новых классов противников, то вводим структуру CEnemyClass, которая описывает визуальную составляющую врагов и их флаги — могут ли они стрелять по игроку или просто летят вниз (астероиды), как они передвигаются (зигзагами например) и т. п.
А также описываем игрока:
typedef struct
{
int health;
int frags;
int score;
int speed;
int nextAttack;
int x, y;
} CPlayer;
Всё! Для текущего уровня реализации игры этого достаточно :)
Переходим к реализации игровой логики. Вообще, динамический аллокатор в играх для китайских платформ лучше использовать как можно меньше. Heap'а довольно мало (~600Кб), да и не совсем понятно, как этот аллокатор реализован, есть вероятность, что используется аллокатор и куча основной ОС.
Начинаем с реализации полёта кораблика. Для этого он должен реагировать на стрелки и не улетать за границы экрана, а ещё для красоты он должен «вылетать» из нижней границы экрана при старте игры:
Переходим к динамическим пулам с объектами. Как вы уже заметили, их всего два — враги и летящие снаряды. Реализация спавна врагов/снарядов простая и понятная: мы обходим каждый элемент пула, если указатель на объект не-нулевой, значит объект всё ещё жив и используется на сцене. Если нулевой — значит ячейка свободна и можно заспавнить новый объект:
При обходе пула во время обновления кадра, мы обновляем состояние каждого объекта и если его функция Think вернула true, значит объект больше не нужен и его нужно удалить:
if (enemyThink(world.enemyPool[i]))
{
sysFree(world.enemyPool[i]);
world.enemyPool[i] = 0;
}
А вот и реализация Think:
bool enemyThink(CEnemy* enemy) {
enemy->y += enemy->_class->speed;
if (enemy->y > gGetScreenHeight() || enemy->health <= 0) return true;
return false;
}
Но кораблики должны же откуда-то появляться! Для этого у нас есть переменная nextSpawn, которая позволяет реализовать самый простой тип спавнера — относительно времени (или в нашем случае тиков):
world.nextSpawn--;
if (world.nextSpawn < 0) {
CEnemy* enemy = spawnEnemy(&enemyClasses[0]);
world.nextSpawn = randRange(40, 70);
}
Результат: мы уже можем полетать, пострелять и поуворачиваться от вражеских корабликов!
Уже что-то напоминающее игру! Осталось лишь добавить подсчет очков, менюшку, разные виды противников, возможно какие-то бонусы и у нас будет готовая простенькая аркада. В целом, выше приведена достаточно неплохая архитектура для простых 2D-игр на Plain C. Фактически, она может быть хорошей базой и для ваших игр: в теме о китах на 4pda я встречал немало людей, которые банально не знали, с чего начать.
Но без тестов на реальных устройствах материал не был бы таким интересным! Поэтому давайте протестируем игру на двух реальных телефонах, как вы уже догадались, один — Nokla TV E71, а второй — клон Nokia 6700, который подарил мне мой читатель Никита.
На TV E71 игра идёт не сказать что очень бодро. Кадров 15 точно есть, что, учитывая разрешение 240x320, весьма неплохо для такого девайса.
а 6700,, даже учитывая более низкое разрешение — 176x220, дела примерно также — ~15FPS! Но поиграть всё равно можно. Уже хотите написать «автор наговнокодил, а теперь ноет из-за низкого FPS»? Ан-нет, я попробовал игры сторонних разработчиков — они идут примерно также :( К сожалению, таковы аппаратные ограничения устройства.
Исходный код игры с Makefile'ами и файлами проектов для Visual Studio и MRELauncher доступны на моём GitHub. Свободно изучайте и используйте его в любых целях :)
Но в остальном же, демка получилась довольно прикольной, как и сам опыт программирования для китайских телефонов. В общем и целом, китайцы пытались максимально упростить API и привлечь разработчиков к своей платформе. Если ради примера взглянуть на API для Elf'ов на Motorola, можно ужаснуться от state-based архитектуры платформы P2K. А тут тебе init, event, draw — и всё!
Но популярности помешала непонятная закрытость платформы, костыльный запуск программ, отсутствие нормального симулятора. А ведь сколько фишек было: даже возможность писать и читать память ядра!
А вы как считаете? Можно ли вдохнуть в китайские кнопочники новую жизнь, узнав о наличии возможности запуска нативного кода на них?
P. S.: Друзья! Время от времени я пишу пост о поиске различных китайских девайсов (подделок, реплик, закосов на айфоны, самсунги, сони, HTC и т. п.) для будущих статей. Однако очень часто читатели пишут «где ж ты был месяц назад, мешок таких выбросил!», поэтому я решил в заключение каждой статьи вставлять объявление о поиске девайсов для контента. Есть желание что-то выкинуть или отправить в чермет? Даже нерабочую «невключайку» или полурабочую? А может, у этих девайсов есть шанс на более интересное существование! Смотрите в соответствующем посте, что я делаю с китайскими подделками на айфоны, самсунги, макбуки и айпады! Да и чего уж там говорить: эта статья уже сама по себе весьма наглядный пример! Найти меня можно в комментариях тут, на Пикабу, и в тг @monobogdan
Понравился материал? У меня есть канал в Телеге, куда я публикую бэкстейдж со статей, всякие мысли и советы касательно ремонта и программирования под различные девайсы, а также вовремя публикую ссылки на свои новые статьи. 1-2 поста в день, никакого мусора!
Материал подготовлен при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud, дабы не пропускать новые статьи каждую неделю!
Одна вакансия, два кандидата. Сможете выбрать лучшего? И так пять раз.
Китайские производители не перестают удивлять: многие видят явные перспективы рынка одноплатных компьютеров и стараются представить целую линейку девайсов на самых разных чипсетах, а разработчики стараются использовать уже привычное и поддерживаемое долгие годы железо. К ним относятся решения на чипсетах AllWinner, RockChip, Tegra. Другие же стараются взять малоизвестный, но дешевый чип для иного круга применений, развести на нем компактную плату и продавать по цене пачки сухарей, подобные решения появляются регулярно. Один из таких одноплатников я недавно купил на AliExpress — некий DongShan Pi Pico W, на базе экзотического чипсета SigmaStar SSD210, всего за 600 рублей. И тут действительно есть на что посмотреть: два ядра Cortex-A7, контроллер TTL матриц, 2D GPU, Wi-Fi, 64Мб ОЗУ и Embedded Linux на борту. Более того, девайс поставляется в виде System on Module с переходной Evaluation-платой, что позволяет использовать это устройство в составе других гаджетов! Что это за красавец и на что он способен? Читайте в статье!!
Думаю, большинство моих читателей когда-либо слышали об одноплатных компьютерах. Это компактные и достаточно мощные устройства, которые можно использовать как в качестве компактных серверов или даже десктопных машин, так и собрать своё устройство на базе готового одноплатного компьютера. Одноплатники используется во многих сферах: вендинговые автоматы, умные экраны, самопальные игровые консоли и смартфоны, DIY-ноутбуки!
Однако чаще всего можно увидеть обзоры и проекты на базе довольно известных устройств: Raspberry Pi, Orange Pi, Olimex. Эти платы, скажем так, достаточно дорогие: и если Orange Pi One/Zero ещё можно ухватить за 1.000 рублей на вторичке (один из таких я купил еще летом. Узнав о моем блоге, продавец стал моим читателем и вместо одного OPi прислал мне целых два — один в подарок!), а за RPi Zero придется выложить как минимум 2.000 рублей. Однако есть ещё один сегмент одноплатных компьютеров: ультра-дешевые, разработанные на базе чипов для конкретного применения. Один из самых известных представителей — MangoPi/CherryPi R3, который работает на базе AllWinner F1C200s — чипа для… электронных книг!
Информации по дешевым, почти неизвестным одноплатникам довольно мало. У них не очень хорошая поддержка (кроме AllWinner, там почти все чипсеты есть в mainline-ветке Linux), в них могут обнаружится аппаратные баги, да и многие люди вообще не замарачиваются с ними, предпочитая переплатить, но купить что-то более стабильное. Но не я! Я просто обожаю различные ультрадешевые девайсики, поэтому недавно по наводке моего активного читателя NutsUnderline, я заказал интереснейший девайс — DongShan Pi Pico-W. Устройство обошлось мне всего в 600 рублей, но в первую очередь, меня привлек форм-фактор устройства и его чипсет. Некий SigmaStar SSD210!
Я заказал сразу два устройства: первую партию очень быстро разобрали, поэтому я взял «с запасом». Сейчас конкретно этот одноплатник пока-что не доступен в магазине продавца, однако у него же продаются другие устройства на базе SSD210. Можете найти их по ключевому слову: «SSD210» (прямые линки публиковать не буду, дабы не сочли за рекламу). Через месяц оба красавца пришли ко мне и я принялся их изучать.
Какое же было моё удивление, когда я обнаружил, что это по сути System on Module, который вручную надо припаять к Evaluation-плате! Вкратце это значит, что на базе таких SoM вы можете развести плату, протравить её, а затем припаять одноплатник поверх нее и сделать своё полноценное устройство, «без соплей»! Производителю плюсик за такую гибкость — я не очень люблю одноплатники с штырьковыми гребенками. Хотя, конечно, это очень сильно помогает при разработке макета устройства.
Но чем он так меня привлек, помимо SoM направленности? Своим крутым чипсетом! Давайте ознакомимся с его характеристиками поближе:
Процессор: SigmaStar SSD210. 2 ядра Cortex-A7, работающие на частоте до 1ГГц. 16Кб кэш инструкций и 16Кб кэш данных, плюс 128Кб L2-кэша. В процессоре есть FPU и поддержка SIMD-инструкций Neon (альтернатива SSE в x86). Нехило, правда?
Поддержка дисплеев: У чипсета есть выделенный модуль для работы с внешними матрицами. Поддерживаются TTL дисплеи (до 1024x768), SPI-матрицы с клоком до 54МГц (480x320), а также прямой RGB аналоговый RGB сигнал (этот интерфейс можно использовать для подключения к ТВ с тюльпанами или аналоговым матрицам). Про типы дисплеев, вы можете прочитать в моей статье.
2D GPU: Поддержка отрисовки линий, прямоугольников, градиентной заливки, BitBLT, клиппинг, дизеринг, автоматическая конвертация формата пикселя (с RGB888 в RGB565). Это серьёзно снимает нагрузку с ЦПУ при рисовании графики, однако поддерживается ли он в Linux — вопрос другой.
ОЗУ: 64Мб DDR2 памяти «бутербродом» прямо с чипсетом, плюс поддержка до 512Мб DDR2 внешней памяти, до 1333Мб/с.
Звук: Один моно-выход DAC, два выходных канала I2S, вход микрофона. Входные каналы поддерживают частоту дискретизации до 96КГц. Можно организовать вывод звука лишь подключив внешний усилитель. Внешний ЦАП не обязателен, если вам не нужен стерео-звук.
Память: Контроллер NOR/NAND SPI-памяти, до двух параллельно подключенных чипов, плюс поддержка SDIO. BootROM поддерживают загрузку с MicroSD карт.
Сеть: Ethernet, на DongShan Pi есть Wi-Fi.
USB: Как хост, так и ведомое устройство
Периферия: 4 канала ШИМ, GPIO, 4 UART, 2 канала SPI, 2 канала I2C
Камера: До двух камер по интерфейсу MIPI CSI
Безопасность: Есть аппаратное шифрование.
Питание: 0.9В ядро, 1.8В ОЗУ, 3.3В I/O
Очень даже бодро, согласитесь? Вообще, производитель подразумевает SSD210 как чипсет для HMI-дисплеев — т. е. умные дисплеи, которые могут, например, служить стендами в музеях, или служить для заказа билетов в кино. Есть внешние HMI-дисплеи, которыми можно управлять используя другие МК: просто посылая команды и реагируя на нажатия кнопок. Тут мы и видим, как китайский производитель решил применить этот чипсет для другой сферы: одноплатный компьютер для DIY!
На SSD210 есть порт Linux, предлагается использовать Embedded Linux в качестве основной системы. Никаких дистрибутивов по типу Ubuntu для устройства нет — предполагается, что вы сами реализуете весь необходимый для ваших программ функционал (отрисовку графики, обработку ввода, звук и т. п.). Есть Build root и исходный код ядра, а также U-Boot.
Помимо этого, вендор предлагает целое SDK для разработки уже готовых устройств на этом чипсете. Но есть один нюанс: документации практически нет :( Такие пакеты предлагаются крупным коммерческим производителям устройств, поэтому и основная поддержка есть только для них. Есть некоторые сэмплы, как, например, использовать графические дисплеи (показан пример с TTL-матрицей 1024x600), но совершенно не ясно как использовать SPI-матрицы, поскольку они требуют отдельной инициализации.
Но сначала наш одноплатник нужно собрать и запустить. И здесь есть множество тонких моментов, которые необходимо знать перед покупкой такого девайса. Переходим к сборке!
Для более удобного процесса разработки нашего устройства, лучше всего заказывать сразу две платы: одну припаять к переходной плате с штырями, а другую использовать на нашем устройстве. Как я уже говорил ранее, одноплатник предлагается в виде System on Module, которые можно при желании распаять на переходной плате:
Честно сказать, я очень люблю такой тип монтажа и топлю за то, чтобы другие одноплатники не форсировали использование штырьков, а позволяли припаять себя «бутербродом» к другой плате. Обычно SoM дороже чем простые одноплатники, один из примеров — Olimex A20 SoM. Припаиваем основную плату к eval-плате. Обратите внимание, что припой должен находится «скосом» с внешней стороны пинов!
После этого, можно распаять гребенку. После окончания процесса сборки, вызваниваем все пятачки на плате и гребенку, дабы исключить непропай в каком-то месте.
Теперь подключаем питание. На плате уже разведены Step-down преобразователи с 5В на 3.3В (основная логика), 1.8В (DDR2), и 0.9В/1.0В (ядро), нам достаточно подключить лишь 5В, либо запитать плату от 3.7В аккумулятора. Устройство стабильно работает и от 0.5А порта ПК (если не юзать Wi-Fi).
Для работы с одноплатником, обязательно нужен COM-преобразователь. Открываем Putty, задаем COM-порт, выставляем бодрейт 115200 и отключаем контроль четности. После подачи питания на устройство, в консоли побегут логи, U-Boot начнет загружать систему… однако, есть один важный нюанс…
Все платы прошиваются на заводе с помощью фирменного флэшера SSD210. Но фирменный флэшер, по каким-то причинам, на некоторых платах не может сохранить U-Boot Environment (переменные окружения, которые в том числе определяют таблицу разделов и коммандлайн ядра).
Поэтому если ваша плата повисла на CRC Error, нужно ввести следующие команды:
setenv mtdids nand0=nand0
setenv mtdparts ' mtdparts=nand0:0x140000(CIS),0x1a0000(BOOT0),0x1a0000(BOOT1),0x40000(ENV),0x40000(ENV1),0x20000(KEY_CUST),0x500000(KERNEL),0x500000(RECOVERY),0x600000(rootfs),0xa0000(MISC),-(UBI)
setenv bootargs ubi.mtd=UBI,0x800 root=/dev/mtdblock8 rootfstype=squashfs ro init=/linuxrc LX_MEM=0x3FE0000 mma_heap=mma_heap_name0,miu=0,sz=0x1E00000 cma=2M highres=off mmap_reserved=fb,miu=0,sz=0x300000,max_start_off=0x3C00000,max_end_off=0x3F00000 ${mtdparts}
setenv bootcmd ' nand read.e 0x22000000 KERNEL ${kernel_file_size}; dcache on ; bootlogo 0 0 0 0; bootm 0x22000000;nand read.e 0x22000000 RECOVERY ${recovery_file_size}; dcache on ; bootm 0x22000000
setenv autoestart 0
setenv sstar_bbm off
setenv ipl_version "##p3##gdf99011IPL_##########
setenv ipl_version "DUALENV=1 SILENT_CONSOLE=1 CFG_SDMMC_DISABLE=n ALK=1 SPINAND=1 CHIP=pioneer3""
saveenv
После этого отправляем плату в ресет и система загружается как ни в чем не бывало!
Поскольку на плате не разведен разъем USB, для прошивки нужно распустить нерабочий кабель для зарядки смартфона, либо купить внешний USB-разъем на плате. VBUS кидаем на вход питания, белый провод на DM-, зелёный на DM+. Не забывайте провести общую землю между UART-преобразователем и основным питанием платы, дабы не потерять логи.
Замыкаем два пина в центре платы пинцетом и жмем RESET. Плата определится как MSDC-флэшка (не удивляйтесь). Прошивальщик глючный и бывает не с первого раза может прошить устройство. Если девайс после прошивки не включается — введите команды в консоль U-Boot выше.
Теперь переходим к самой системе.
Девайс работает на базе ядра Linux 4.9. Тем не менее, производителем заявлена поддержка Mainline-ядра, что даёт надежду на поддержку устройства в будущем.
Таблица разделов устройства организована в виде ubifs. Вообще, предполагается, что для тестов можно будет запускать ваш софт без перезагрузки, однако когда речь заходит о серьезных модификациях, ребут и прошивка устройства глючным софтом — дело неизбежное.
«Из коробки» на устройстве доступен лишь i2cdev, благодаря которому можно свободно общаться с i2c-устройствами из юзерспейса. Хотите получить доступ к SPI? Готовьтесь качать билдрут, вручную включать spidev в конфиге и редактировать DeviceTree, дабы spidev мог получить доступ к физическим spi-устройствам ядра.
Кроме того, конечно же, есть доступ к GPIO из sysfs.
На самой плате, Wi-Fi реализован в виде внешнего USB-хаба + Wi-Fi адаптера. Чипсет также поддерживает Ethernet.
Для разработки устройств, производитель предлагает отдельное SDK для общения с периферией устройства из юзерспейса. С помощью этого SDK, можно получить доступ к камере, аппаратному декодеру, звуку и настроить матрицу. Судя по всему, общение происходит с помощью ioctl к необходимым устройствам. Это сделано для того, чтобы разработчики не копались в низкоуровневых драйверах, ведь например, ALSA, на устройстве нет совсем.
Если включить нужные нам модули в юзерспейс (spidev, i2cdev, gpio), то можно будет проектировать устройства более простым путем. Например, подключить дисплейчик и прямо из юзерспейса выводить на него графическую информацию. Это открывает перспективы для самых разных применений: опрос датчиков и хранение информации в внутренней памяти, умные сигнализации, самодельные часы, DIY игровые консоли, самодельные телефоны и т. п. Применений просто куча!
Вот мы и посмотрели с вами на дешевые одноплатники, где используются чипсеты, которые разработаны для использования в совершенно других сферах. Девайсы весьма своеобразные и для полноценной работы с ними нужно обладать навыками прожженного линуксоида и иметь навыки системного программирования. Но, чего уж точно нельзя отрицать, так это перспектив подобных девайсов для своих проектов. Да, под них нет готовых гайдов, как для Raspberry Pi или Orange Pi, информации по ним минимум… но если захочется — то всегда можно «сварганить» самопальное устройство за минимальный прайс!
Вероятнее всего, я применю один из этих одноплатников для своего проекта немного позже. И конечно же, я напишу об этом отдельный материал — ведь про экзотические чипсеты на Пикабу пишут не так часто!
Чуть позже выйдет материал про Repka Pi. Их одноплатник получился не менее интересным и как раз таки метит в нишу одноплатников с хорошей поддержкой, где есть уже готовые гайды, информация и даже сами разработчики могут помочь с решением некоторых проблем. Без косяков не обошлось: есть пару аппаратных проблем, о которых я расскажу открыто, но в целом девайс выглядит интересным!
Материал подготовлен при поддержке TimeWeb Cloud. Подписывайтесь на меня и @Timeweb.Cloud , дабы не пропускать свежие статьи каждую неделю!
Человек будущего, если ты наткнешься, на эту тему в поисках таймера в подрозетник, знай, что я почти полгода искал для себя и таки нашел решение. Таймер Доктор Сухов SE001. Выполняет все озвученные ТСом функции: включается простым нажатием, время до выключения можно задать до 180 минут колесиком. Можно не задавать каждый раз, просто нажимать кнопку включения. Монтируется в стандартный подрозетник. Стоит копейки. Из минусов только то, что ему для работы нужна нейтраль, т. е. проводку надо заранее предусмотреть.
В моих руках светодиодная лампа In-Home за какие-то удивительные деньги... за 69 рублей. В нашем рейтинге ламп Е27 этот производитель имеет довольно грустные оценки, потому я захотел убедиться в этом сам.
Начинаю с проверки мощности. На входе 220 Вольт, включаю лампу при первом включении 3.6 Ватта.
Нет слов, попытаюсь поднять напряжение до 230 Вольт, до того уровня который и должен быть в нашей сети.
230 Вольт на входе 3,7 Ватт, ну 3,8 Ватт. Потрясающая цифра, такого еще не удавалось увидеть, InHome действительно удивил. 8 Ватт обещал на упаковке, а сейчас я вижу 3,7 Ватт.
Ну что, посмотрим на другие характеристики.
Коэффициент мощности 0,59, в идеале желательно, чтоб здесь была единица, но так никогда не бывает со светодиодными лампами. Здесь типовой для светодиодных ламп коэффициент мощности.
Расходы за год 59 рублей. При работе лампы 8 часов в день и при тарифе 5.38 рублей за киловатт. Оставляю лампу поработать в течение 15 минут.
После 15 минут прогрева, мощность так и осталось 3,7 ватта. При этом такие показатели при 230 Вольтах. При 220 Вольт – 3,6 Ватта.
Цветовая температура 5927 Кельвина. Индекс цветопередачи очень неплохой 87,2. Измерены координаты X и Y и по ним найдена точка на диаграмме цветности. Дельта UV равно 0.0056, что выше порога заметности. Таким образом, присутствует оттенок желто-зеленого цвета, и да, этот оттенок таков, что найдутся люди, которые смогут и невооруженным глазом увидеть его. Не идеально белый свет от этой лампы.
InHome гордо пишет, что никаких пульсаций нет и лампа безопасна для глаз. Давайте это проверим.
1,1% на очень высокой частоте 38 кГц, это очень хороший результат — пульсаций, вредных для глаз, нету.
Какое же количество света может быть от лампы с реальной мощностью 3,7 ватт? Лампа в метре над столом включена и прогрета. В сети 220 вольт. Убираю внешнюю подсветку, 140 Люкс. Немного.
250 Вольт – 139 Люкс,
170 Вольт – 33 Люкса,
150 Вольт и полная темнота.
Ну что можно сказать, это маломощная лампа, еще и обладатель драйвера, который совершенно неустойчив к изменению напряжения питания в розетках. Такую лампу можно использовать только там, где высока стабильность напряжения питания.
Тест с выключателем с подсветкой Лампа InHomeнормально прошла. Работает.
InHome указал размеры своей лампы 60мм на 110мм. Я же измерил 60мм на 108мм.
Очень любопытный вариант нагрева корпуса. Тепло не распространяется вниз к цоколю лампы, а застыло каким-то странным поясом, каким-то кольцом на корпусе лампы. Корпус нагрелся 67 градусов Цельсия. Колба нагрелась 40 градусов Цельсия.
Итого - такого несоответствия заявленной мощности с фактической я еще не видел. Хотя бы поэтому я не советую данную лампу к покупке. Тем не менее, если вы не согласны со мной, то можете перейти на страничку обзора лампочки на сайте нашего проекта (ссылка будет ниже) и проголосовать в соответствии с вашим мнением, это повлияет на конечный рейтинг лампы.
Теперь про разбор лампы. И тут довольно интересно, публика разделилась на две когорты. Одной - разбор интересен, другой - нет. В прошлом обзоре я пробовал просто оставить ссылку на первоисточник, с одной стороны был негатив, а с другой - переходов по ссылке особо не увидел. Собственно в этом посте также сошлюсь на свой обзор лампы inHome. Но кроме этого оставлю видео с разбором лампочки, посмотрим как это зайдет вам.
На этом все, буду стараться выпускать обзор лампы каждую неделю для вас!
К огромному сожалению, старые смартфоны всё чаще и чаще находят своё пристанище в мусорном баке. К прошлым, надежным «друзьям» действует исключительно потребительское отношение — чуть устарел и сразу выкинули, словно это ненужный мусор. И ведь люди даже не хотят попытаться придумать какое-либо применение гаджетам прошлых лет! Отчасти, это вина корпораций — Google намеренно тормозит и добивает довольно шустрые девайсы. Отчасти — вина программистов, которые преследуют исключительно бизнес-задачи и не думают об оптимизации приложений совсем. В один день я почувствовал себя Тайлером Дёрденом от мира IT и решил бросить вызов проприетарщине: написать свою прошивку для уже существующего смартфона с нуля. А дабы задачка была ещё интереснее, я выбрал очень распространенную и дешевую модель из 2012 года — Fly IQ245 (цена на барахолках — 200-300 рублей). Кроме того, у этого телефона есть сразу несколько внешних шин, к которым можно подключить компьютер или микроконтроллер, что даёт возможность использовать его в качестве ультрадешевого одноплатника для DIY-проектов. Получилось ли у меня реализовать свои хотелки? Читайте в статье!
Честно сказать, идея попытаться реализовать свою прошивку мне пришла ещё давно. Однако, дабы не завлекать опытного читателя кликбейтом, я сразу поясню, в чём заключается «прошивка с нуля»:
Мы всё ещё используем Linux: в качестве ядра мы продолжаем использовать образ Linux, предоставленный нам производителем. Написание прошивки полностью с нуля заняло бы очень много времени (особенно без схемы на устройство). Однако, мы вообще не загружаем Android никаким образом.
Мы не используем библиотеки AOSP: наша прошивка без необходимости не использует никаких библиотек уже имеющегося образа Android. Вся работа с железом происходит с помощью низкоуровневого API Linux. Это значит, что отрисовка графики, звук, управление ресурсами и питанием ложится полностью на нас.
Прошивка может запускать только нативные программы: да, это тоже камень в сторону Android. Изначально, наша прошивка умеет запускать только нативные программы, написанные на C. Причём она экспортирует собственное C API — дабы приложения могли использовать всю мощь нашего смартфона в виде простого и понятного набора методов.
Проектов по выкидыванию Android из, собственно, Android-смартфонов как минимум несколько: UBPorts — бывший Ubuntu Touch, FireFox OS и его наследник Kai OS и конечно же, postmarketOS. Отчасти можно сюда отнести и Sailfish OS — но там образы имеются в основном на смартфоны от Sony. Все эти проекты объединяет сложность портирования и невозможность их завести на устройствах без исходного кода ядра. Даже если у вас есть исходный код ядра, но, например, устройство использует ядро 2.6 — навряд-ли вы сможете завести современный дистрибутив на нём.
Другой вопрос в том, что можно использовать полу-baremetal подход, когда от Linux берется практически минимальный функционал. Всё, что мы имеем — busybox, libc и низкоуровневый доступ к железу, благодаря API самого ядра. Как под это всё программировать — я рассказывал впрошлойстатье. Этот же подход мы будем использовать и сейчас — как иллюстрация реального применения подобного способа.
Итак, что наша прошивка должна уметь:
Отрисовывать произвольную графику: графическая подсистема нашей прошивки должна работать с фиксированным форматом пикселя, уметь загружать прозрачные и непрозрачные изображения, отрисовывать картинки с альфа-блендингом и т. п.
Уметь звонить и работать с модемом: общение с модемом происходит посредством AT-команд — общепринятого в индустрии стандарта. Однако в случае нашего устройства, есть м-а-а-а-ленький нюанс, о котором я расскажу позже.
Иметь механизм приложений: мы ведь не будем хардкодить все «экраны» в прошивке в виде кучи стейтов, верно? Для этого у нас должен быть простой и понятный механизм слинкованных с прошивкой приложений.
Обрабатывать ввод: обработка тачскрина и жестов — это задача подсистемы ввода.
Реализовывать анимированный UI: здесь всё очевидно, наша прошивка должна иметь готовые элементы пользовательского интерфейса для будущих приложений: кнопки, текстовые поля и т. д. О деталях реализации этой подсистемы, я расскажу ниже (а реализовал я её очень необычно для такой системы).
Начинаем мы с хардварной части. Именно здесь я покажу вам, как использовать внешние шины вашего устройства.
В качестве смартфона для нашего проекта, я выбрал популярную бюджетную модель из 2012 года — Fly IQ245 Wizard. Это простенький китайский смартфон, который работал на базе популярного в прошлом 2G-чипсета: MediaTek MT6573, да и стоил около 2х тысяч рублей новым. Однако вот в чём суть: мне удалось заставить работать «медиатековский» модем и даже позвонить с него на свой основной телефон, но… только ввод и вывод данных из звукового тракта модема происходит через звуковую подсистему Android — к которой доступа у нас нет!
Именно поэтому, мы идём на очень хитрый и занимательный костыль: мы распаяем внешний модем сами! В качестве радиомодуля у нас выступит модуль SIM800 от компании SIMCOM. И даже он очень близок к нашему смартфону в аппаратном плане: ведь в основе этого модуля лежит популярнейший чипсет из кнопочников тех лет: MediaTek MT6261D. Преимущество SIM800 в его цене — он стоит пару сотен рублей, так что по карману выбор модема не влияет.
На весу паять крайне неудобно. В финальном варианте перепаяю нормально.
Но как его подключать? SIM800 общается с другими устройствами посредством протокола UART — универсальный асинхронный приемо-передатчик. И вот тут мы включаем смекалочку. Разбираем устройство и видим то, что я пытаюсь долгое время донести до моих читателей — аж два канала UART: один практически посередине, второй справа. Нам нужны пятачки TXD4 и RXD4:
Обычно на этот канал UART летят логи ядра, которые можно без проблем отключить минорной правкой U-Boot в HEX-редакторе. Впрочем, модем никак не реагирует на «мусор» из консоли и просто отвечает ошибками — хватит лишь очистить буфер сообщений для того, чтобы все работало нормально. Подпаиваемся к UART'у с помощью преобразователя — у меня оным выступает ESP32 с выпаянным чипом.
Увидели логи? Замечательно, пора попытаться что-то отправить на ПК и с ПК. UART работают без тактовых сигналов и зависит исключительно от старт/стоп битов и бодрейта, который на устройствах MediaTek равен 921600. TXD4 и RXD4 обнаруживаются в системе на консоли/dev/ttyMT3. Пробуем что-то отправить: всё работает!
Вот теперь-то можно подключить наш внешний модем и попытаться пообщаться с ним, отправив тестовую командуAT. Модем отвечаетOK! На этот раз я работаю с смартфоном из режимаFactory mode— практически тоже самое, что и режим recovery, но позволяющий, например, получить доступ к камере устройства. Простая и понятная схема, поясняющая что и куда подключать:
На этом модификация аппаратной частипоказакончена. Пора переходить к реализации софта! Я решил разделить материал на каждый модуль, который я реализовывал — дабы вам был понятен процесс разработки и отладки прошивки!
На этот раз я решил загружать смартфон из режима рекавери. Однако никто не мешает в будущем просто прошить раздел recovery вместо boot и получить прямую загрузку прямо в нашу прошивку. Время такой загрузки будет заниматься ~3-4 секунды с холодного старта. Очень даже ничего.
Я взял уже готовый образ TWRP для своего смартфона и пропатчил его, дабы сам рекавери не мешал своим интерфейсом. Для этого я распаковал образ recovery.img с помощью MtkImgTools и убрал в init.rc запуск службы /sbin/recovery. После этого, я залил прошивку обратно на устройство и получил подобную свободу действий — консоль через USB и чистый холст в виде смартфона! Старые смартфоны на чипсетах MediaTek шьются через USB только после замыкания тест-поинта — на моем аппарате его местонахождение очевидно. Замыкаем контакты между собой, подключаем смартфон без АКБ к ПК и ждем прошивки:
Теперь можно деплоить программы! Важный нюанс: в отличии от Makefile из прошлой статьи, для Android 2.3 параметр -fPIE нужно убрать — иначе динамический линкер (/sbin/linker) будет вылетать в segmentation fault.
В комментариях под прошлой статьёй меня похвалили за то, что я делюсь достаточно профильными знаниями касательно эффективной отрисовки 2D-графики. Собственно, к реализации графической подсистемы я подошёл ответственно и постарался реализовать достаточно шустрый рендерер, к которому затем можно подключить другие модули.
Как я уже говорил ранее, графическая подсистема должна уметь загружать картинки, выводить некоторые примитивы, выводить картинки с прозрачностью и без, загружать и отрисовывать заранее подготовленные шрифты, а также управлять отрисовкой бэкбуфера на экран.
В случае с этим устройством (и большинством старых устройств), формат пикселя оказался RGB565 — т. е. 5 бит красный, 6 бит синий, 5 бит зеленый. Конвертация форматов пикселей всегда была занозой в заднице для программных рендереров, поскольку занимает дополнительное время, которое обратно зависимо от размера дисплея. Изначально я решил выделить буфер в том же формате, что и фреймбуфер, но затем решил сделать классический и самый портативный формат — RGB888 (24х-битный цвет), а при копировании кадра на экран, на лету делать преобразования цвета:
Очень важный нюанс, который я не упомянул в предыдущей статье: на устройствах прошлых лет для обновления фреймбуфера необходимо послать структуру var_screeninfo, где хотя бы что-то изменено, иначе никаких изменений мы не увидим. Этот же костыль используется в родном recovery для отрисовки, а судя по исходникам драйвера fb, «правильный» способ обновить экран — послать драйверу ioctl (который я пока что не пробовал).
После того, как я смог управлять дисплеем, я решил загрузить и отобразить какую-нибудь картинку. Пусть это будут обои для нашей прошивки:
Загрузчик TGA сильно не поменялся: я таскаю его в неизменном виде из проекта в проект. Он поддерживает любые форматы пикселя, кроме палитровых, но я его искусственн ограничиваю на RGB888 и RGBA8888 — для поддержки обычных картинок и картинок с альфа-каналом. После этого, я написал не очень шустрые, но достаточно универсальные методы для отрисовки картинок. Для больших участков кода, я буду использовать pastebin, поскольку на Пикабу до сих пор не добавили ни подсветки синтаксиса, не нормальный перенос форматирования табов :(
PutPixel желательно заинлайнить в будущем. В целом, сама отрисовка работает достаточно быстро, но поскольку рендеринг выполняется на ЦПУ — рано или поздно мы упремся в количество картинок на экране. Есть некоторые оптимизации: например, непрозрачные картинки можно просто коприовать сканлайнами прямо в задний буфер.
Сразу же реализовываем методы для рисования шрифтов: они у нас будут совсем простенькими — только моноширинные (все символы имеют одинаковую ширину) и растровыми (для каждого размера придется «запекать» несколько шрифтов). Для этого я написал маленькую программку, которая рисует виндовые шрифты прямо в наш самопальный формат:
Формат примитивнейший:
1 байт говорит нам о размере шрифта и далее идут 255 изображений символов. Да, это не очень эффективно т.к попадают пустые символы из ASCII-таблицы, но в будущем это можно поправить.
Прозрачность в символах обеспечивает фоновый цвет Magena — ярко-розовый. Я не стал делать дополнительный альфа-канал, т. к. иначе будут серьезные лаги при выводе большого количества текста.
Теперь у нас есть отображение картинок и текста! Что с этим можно сделать?
Пока что здесь не хватает обработки «хардварных» кнопок — домой, меню, назад и т. п. Однако в будущем это всё можно реализовать!
Не забыл я и про анимации. Ну кому с такими ресурсами нужен неанимированный топорный интерфейс? Пусть лучше будет анимированный, пусть и примитивный!
Аниматор напоминает оный из ранних версий Android: он имеет фиксированный набор свойств, которые умеет интерполировать в промежутках определенного времени. Если простыми словами: то он оперирует линейными отрезками времени a и b, в промежутке которых мы имеем значение «прогресса» — которое даёт нам результат от 0.0f (начало анимации) до 1.0f (конец анимации). Пока время тикает до необходимого интервала (duration), аниматор интерполирует заранее назначенные ему поля до нужных значений.
Именно так и получается плавность! Похожим образом реализованы анимационные системы во многих играх и мобильных ОС, только там они гораздо более комплексны: есть сериализация/десериализация из файлов, поддержка кейфреймов (несколько последовательных состояний на одном промежутке времени), поддержка кастомных свойств и т. п.
Как я уже говорил раннее, работа с модемом происходит посредством AT-команд. Лучше всего обрабатывать ввод-вывод модема из отдельного потока, поскольку он может отвечать довольно медленно и тормозить UI-поток основной программы, вызывая лаги. В SIM800 уже реализован весь GSM-стек, в том числе декодирование и вывод звука через встроенный усилитель с фильтром — остается только подключить динамики и микрофон от нашего телефона. Пока что я подсобрал аудиотракт на том, что было под рукой — микрофон от нерабочего смартфона и динамик от планшета, но для проверки этого хватает:
Важный нюанс: по умолчанию, tty-устройства в Linux работают по терминальному принципу — т. е. дробят транзакции по символу окончания строки (\n), имеют ограниченный буфер и т. д. Для нормальной работы в условиях модема — когда фактически длина ответа неизвестна, а в сам ответ могут «вклиниваться» Unsolicited-команды (своеобразные флаги о состоянии от модема, которые могут прийти в произвольное время — т. е. при входящем звонке, модем начнёт флудить RING в терминал), необходимо иметь возможность точно прочитать весь буфер до конца и парсить данные «по месту». Для этого используется raw-режим терминала:
tcgetattr(modemFd, &tio);
tio.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
tio.c_oflag &= ~(OPOST);
tio.c_cflag |= (CS8);
tio.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
tcsetattr(modemFd, TCSAFLUSH, &tio);
После чего можно запросить состояние модема:
И продолжить работу дальше. После этого, можно переходить к реализации самой прослойки между модемом и вашей программой:
Пытаемся позвонить с помощью метода Dial и видим, что всё работает! Это очень круто! А теперь, конечно же, самое время переходить к реализации того, чего вы ждали — пользовательского интерфейса!
К выбору концепции для интерфейса, я поступил максимально просто — «слизал» дизайн первых версий iOS. Как по мне, это одни из самых красивых версий iOS вообще — все эти приятные градиенты и переливания. Конечно, я не так крут, как инженеры Apple, да и мощного UI-фреймворка у меня пока что нет, поэтому я приступил к реализации с «минимальным» функционалом.
Начал я с разделения главного экрана на модули и продумывания архитектуры основного «лаунчера». У нас есть статусбар, который рисуется поверх всех приложений, полка с приложениями — AppDrawer и сами экраны приложений, унаследованные от суперкласса CScreen.
На данный момент, отрисовка достаточно примитивная: сначала рисуются фоновые обои, затем, если нет никаких активных экранов — AppDrawer и в самом конце рисуется статусбар и всевозможные оверлеи.
Практически сразу я решил обкатать анимационную «систему» и добавить первые анимашки — выезжающий статусбар и анимация а-ля айфон:
animator = new CAnimator();
animator->SetTranslation(0, -imFiller->Height, 0, 0);
animator->Run();
Выглядит симпатичненько. Если я смогу поднять хардварный GLES, то это получится сделать в разы плавнее и шустрее — не хуже айфонов тех лет! Реализация самого статусбара примитивненькая, но вполне рабочая:
gLauncher->Graphics->DrawImage(imFiller, animator->X, animator->Y);
gLauncher->Graphics->DrawImage(imBattery[(int)gLauncher->PowerManager->GetBatteryLevel()], imFiller->Width - imBattery[0]->Width - 5, animator->Y + 5);
char timeFmt[64];
time_t _time = time(0);
tm* _localTime = localtime(&_time);
strftime((char*)&timeFmt,
sizeof(timeFmt), "%R", _localTime);
gLauncher->Graphics->DrawString(gLauncher->Font, (char*)&timeFmt, 0, 0);
Кроме этого, я сразу же реализовал предварительный механизм приложений в системе — пока что они слинкованы статически с основным лаунчером. Для этого есть структура CAppDesc, которая содержит минимально-необходимую информацию для показа информации о приложении и фабрику для создания его основного экрана.
Обратите внимание на удобство примененного подхода Immediate GUI. Нам понадобился новый элемент интерфейса, который описывает кнопку номеронабирателя? Мы просто реализовываем ещё один метод, который берет за основу стандартную кнопку и дорисовывает к ней текст. Всё крайне просто и понятно, хотя на данный момент слишком захардкожено. :)
Пришло время совершить первый звонок с нашей по настоящему кастомной прошивки. Набираем номерок и…
Да, всё работает и мы без проблем можем дозвониться :)
Конечно же, это далеко не весь функционал, необходимый любому современному смартфону. Здесь много чего еще нужно реализовать хотя бы для соответствия уровню бюджетных кнопочных телефонов: телефонную книгу, поддержку СМС/ММС, мультимедийный функционал с играми. Однако начало уже положено и самая необходимая часть модулей реализована. Этот проект очень занимательный для меня и я горд, что смог не на словах, а на деле показать вам, моим читателям, возможности моддинга совершенно NoName-устройств, без каких либо опознавательных знаков…
Моя задача заключается в том, чтобы показать вам возможности использования старых телефонов не только в потребительских, но и в гиковских DIY-сферах. Судите сами: огромный классный дисплей, емкостной тачскрин, готовый звук, камера — и всё это за каких-то пару сотен рублей. Главное показать людям, как всю эту мощь использовать в своих целях и делать совершенно новые устройства из существующих, а не выбрасывать их на помойку!
Сейчас смартфоны, подобные Fly из этого поста стоят копейки, а портировать на них прошивку можно без каких-либо трудностей. Я очень надеюсь, что после этого поста читатели попытаются сделать что-то своё из старых смартфонов, благо свои наработки я выкладываю на GitHub!
Справились? Тогда попробуйте пройти нашу новую игру на внимательность. Приз — награда в профиль на Пикабу: https://pikabu.ru/link/-oD8sjtmAi
Сейчас появилось достаточно много различных дешевых одноплатников с очень достойными характеристиками, которые вполне можно назвать экономичными и портативными. Однако очень часто встает вопрос вывода изображения на дисплей: к сожалению, в подобные устройства обычно ставят урезанные версии чипсетов без видеовыхода на обычные матрицы. Конечно в них практически всегда есть HDMI, но это совершенно не выход для портативного устройства: прожорливый чип скалера будет очень негативно влиять на время работы от АКБ. Да и сами подобные дисплеи очень дорогие: почти 2.000 рублей за матрицу со скалером — это действительно бьет по карману. Сегодня я расскажу Вам о существующих протоколах для дисплеев, подскажу, как применить экранчики от старых навигаторов/мобильников и мы подключим с вами SPI-дисплей к одноплатнику без видеовыхода. Причем мы реализуем как просто библиотеку, которая позволяет выводить произвольную графику из ваших программ, так
и службу, которая будет напрямую копировать данные из фреймбуфера и преобразовывать в формат для нашего дисплея. Интересно? Тогда жду вас в статье!
На самом деле, существует достаточно много различных физических протоколов для общения с дисплеями. На программном уровне, общение с ними относительно стандартизированно, однако на аппаратном уровне различий довольно много. Самые распространенные из них:
MIPI DSI — дифференциальный многоканальный LVDS протокол. Если говорить совсем условно — то это эдакий быстрый низковольтный SPI, который для передачи одного байта использует минимум 4 линии — D+, D-, CLK+, CLK-, где фактических линии две, но для подавления помех используются доп. линии инвертированной полярности, из которых затем вычитаются положительные. Этот протокол позволяет подключать дисплеи очень высокого разрешения и используется практически во всех современных смартфонах. Насколько мне известно, такие дисплеи имеют собственную видеопамять размером с буфер кадра (т.е для 1920х1080х3 дисплея — ~5мб).
TTL/RGB — относительно простой для реализации протокол, очень похож на VGA, но по сути является цифровым: для передачи пикселей используются отдельные линии — например, 5 битов красного, 6 битов синего и 5 битов зеленого (RGB565). Не требует инициализации и обычно не имеет системы команд — пиксели синхронизируются с помощью тактовых сигналов HSYNC/VSYNC. Эти крайне дешевые дисплеи можно встретить на старых китайских игровых консолях, планшетах (до 720p) и автомобильных навигаторах (о них ниже), а также КПК (но на них даташиты найти сложнее). На МК и одноплатниках их использовать можно, но для этого нужно большое кол-во пинов (~18). У таких дисплеев нет собственной памяти, поэтому обновлять картинку нужно всегда, иначе будет белый дисплей. Есть еще аналоговая разновидность, практически 1 в 1 похожая на VGA, используется в ранних автомобильных телевизорах — но ей управлять сложнее из-за кучи различных тактовых сигналов.
8080 — 8 или 16-битная параллельная шина, именно этот протокол использовали большинство телефонов в середине-конце нулевых, а его 16-битная разновидность использовалась в ультрадешевых китайских смартфонах начала 2010-х (Fly Jazz, Explay N1, Fly Era Nano 1, Fly Wizard — дисплеи всех этих копеечных на вторичке телефонов можно использовать и в своих проектах!). Занимает минимум 11 пинов — 8 на данные, 2 на сигналы RD/WR (он определяет, хотим ли мы сейчас что-то прочитать или записать) и 1 DC (определяет, куда мы пишем данные — в регистры, или в видеопамять). Такие дисплеи имеют собственную ОЗУ, поэтому необязательно гонять в них данные постоянно.
SPI — популярный протокол, который используется и в DIY-проектах и возможно в китайских старых MP3-плеерах (информация пока не точная). Отличается тем, что требует всего 3 пина для подключения — MOSI (данные), CLK (тактовая частота) и DC (имеет ту же роль, что и в 8080 дисплеях). Он гораздо предпочтительнее для использования в домашних проектах, поскольку хардварный SPI есть во многих микроконтроллерах/одноплатниках, а частенько к нему в комплект идёт DMA, позволяя разгрузить процессор. Кроме того, эти дисплеи использовали в телефонах начала нулевых — Nokia и Siemens точно использовала именно их. Причём у Siemens сами пины не на шлейфе, а «прижимаются» — бери да подпаивайся, только бустер подсветки до 12в придётся сделать.
I2C — редкий протокол для дисплеев из-за медлительности. Сейчас используется в недорогих OLED-модулях низкого разрешения, использовался в мобильниках самого начала нулевых (Ericsson) и Motorola C350.
Я не стал упоминать «большие» протоколы типа HDMI или eDP — они так или иначе, в физическом плане близки к MIPI DSI. Как видите — протоколов много и самых разных, соответственно и дисплеи нужно искать в разных местах. Дешевые DIY-дисплеи можно найти за довольно разумные деньги на алике — 1.8" матрицы на момент написания статьи стоили ~200 рублей, 2.4 — ~400 рублей, 3.5 и выше — от 700 рублей и выше. Пичем Вы вольны выбирать интерфейс — кому-то удобнее SPI, кому-то удобнее 8080. Я лично выбрал SPI — поскольку он есть в «хардварном» виде на большинстве одноплатников и доступен для программирования как из обычного пользовательского режима (т.е можно пользоваться шиной из обычной программы), так и из драйверов.
Однако есть способ найти дисплеи «бесплатно» — из старых и нерабочих устройств. Например, из автомобильных навигаторов. Недавно читатель с DTF предложил заслать с 10-ок подобных девайсов, я конечно же согласился! Что самое приятное в них — так это то, что дисплеи там обычно стандартизированы — как по размерам, так и по шлейфу. Суть вот в чем: китайские компании довольно долго производили 4" дисплеи с разрешением 480x232 и резистивным тачскрином.
Поэтому Вы практически на 100% можете быть уверены, что один дисплей подойдет к другому навигатору и покажет картинку (а если нет — то открываем даташит на дисплей и корректируем тайминги). Эти дисплеи используютTTL/RGBпротокол, поэтому для того, чтобы с ними работать, вам понадобится либо много свободных пинов, либо превратить микроконтроллер в видеоконтроллер (Raspberry Pi Pico/ESP32 должен с этим справиться без проблем). Большинство из этих дисплеев работает в 16-битном режиме, т.е до 65536 цветов. Ниже прилагаю распиновку к ним:
Для более удобно подключения, можно использоватьтакиеbreakout-платы для 40-пин шлейфов. Я себе заказал несколько, в том числе и для паябельных шлейфов от старых мобилок. Стоят на алике копейки — в среднем, 100 рублей за 5 плат (берите 40 пин/0.5мм).
На некоторых одноплатниках уже есть готовый 40-пин коннектор для подключения ваших дисплеев. Большинство из них базируется на базе чипсетов AllWinner F1C100s/F1C200s/V3s и экран работает там «из коробки», за исключением тачскрина (с ним надо повозиться), известные мне — Lctech Pi, MangoPi (извиняюсь за плохое качество фото, это с моего сайд-проекта):
Если Вам нужен маленький дисплей, то можно взять оный от старого нерабочего кнопочного телефона. Из самых простых — Siemens C65, S65, M65, A55, A65. Эти дисплеи работают по протоколу SPI и к ним легко подпаяться. Как еще один из вариантов — дисплей от «народного» Motorola C350, который работает через интерфейс SPI, но требует 12-битного формата на цвет:
Обратите внимание, что для этих дисплеев нужно самому мастерить бустер подсветки: от 3.7в они не заведутся. Сименсовским дисплеям нужно 12в — связано это с тем, что светодиоды в подсветке подключены последовательно, дабы уменьшить потребление. Если есть желание — можно разобрать модуль и перепаять светодиоды параллельно, но «кушать» такая сборка будет ощутимо, проще взять step-up преобразователь до 12В с алика за пару соток.
MIPI дисплеи можно достать из копеечных старых смартфонов ZTE/Lenovo/МТС/Билайн и.т.п. Предпочтительнее здесь именно именитые бренды, поскольку и ZTE и Lenovo делятся исходниками прошивки — так что можно будет найти команды инициализации и самому запустить дисплей. Кроме инициализации дисплея, там же можно будет найти и драйвер тачскрина — обычно они общаются по протоколу I2C и при очень большом желании, можно будет заставит работать и его.
Для работы с ними, я также рекомендую Breakout-платы, а схему на коннектор дисплея можно найти в сервисмануале или схеме устройства (если таковой имеется для вашего смартфона). Для Lenovo подобные ищутся без проблем, но для топовых Samsung S2/S3/S4 с крутыми OLED-дисплеями за MIPI-дисплеи придётся забыть, т.к схем в открытом доступе нет.
8080 дисплеи можно достать из старых китайских «кнопочников». Ищите те модели, на которые есть сервис-мануал (Fly DS124 и другие модели, некоторые Explay), тогда Вы сможете прочесть ID дисплея из регистра 0x0 (вида 0x9325/0x7739 и.т.п), найти даташит на интересующий вас контроллер и использовать его в своем проекте. В этих дисплеях самое приятное — паябельный шлейф и подсветка 5в, которая будет работать и на 3.7в, но немного тусклее.
Если же Вам хотелось бы экранчик побольше, с разрешением 480x320, то смотрите в сторону очень дешевых мобильников из начала 2010х — Explay N1, Fly Jazz, Fly Wizard. Вполне может быть так, что у Вас лежит подобный девайс будучи разбитым или утопленным, а дисплей остался. Кстати, если вдруг у вас лежит один из подобных ультрадешевых китайчиков, но вам они не нужны — пишите в ЛС, есть идеи для проектов с ними.
Обратите внимание, что эти дисплеи используют 18-битный физический интерфейс, но для программного доступа должно хватать 16-бит. Кроме того, на этом шлейфе есть пин IM0 — он отвечает за установку режима работы контроллера дисплея. Если бы у нас был еще IM1 и IM2, то мы могли бы хоть режим SPI установить, но в данном случае, мы можем установить либо 8-битный режим, либо 16-битный. Можете отследить пин IM0 на шлейфе и если он идет к обвязке, где предположительно разрывается/соединяется IM1/IM2, то можете попробовать разорвать/кинуть на них высокий уровень. Насчет подсветки на таких дисплеях пока что не знаю. Если распиновки на телефон нет, то поищите диагностические пятачки под коннектором, с осциллографом или даже просто тестером можно попытаться найти распиновку.
На этом предлагаю перейти к практической реализации нашего драйвера дисплея. Как я уже говорил, реализовать его можно двумя способами: в виде user-space библиотеки для вывода картинки из обычных программ, так и kernel-mode драйвер, который будет реализовать framebuffer, что позволит выводить туда и X Window System, и SDL — что душе угодно.
У каждого подхода есть плюсы и минусы. Перечисляю их:
Универсальность: Библиотека сможет выводить только ту картинку, которая формирует для нее программа. Однако, она может это делать максимально эффективным для этого образом, да и никто не мешает написать сервис, который будет копировать из /dev/fb0 картинку на наш дисплей (однако это лишняя нагрузка на процессор), китайцы так и делают.
Производительность: Kernel-mode драйвер может быть теоретически быстрее, хотя по факту вся SPI-подсистема Linux выделен в удобный spidev.
Стабильность: По понятным причинам, User-space библиотека будет куда стабильнее драйвера и не крашнет систему в случае ошибки.
Работать мы будем с простеньким 1.8" дисплеем, который имеет разрешение 128x160, работает на контроллере ST7739.
В качестве одноплатника я взял Orange Pi One. Брал я его на вторичке за 1.000 рублей, однако продавец меня порадовал и положил не один, а два девайса — в благодарность за статьи о Orange Pi 3G IoT :) Сейчас старые модели RPi и Orange Pi (но не их Mini и Zero версии) стоят копейки.
Накатываем систему на флэшку (я выбрал Debian с ядром 3.4 — то которое еще не имело поддержки DeviceTree) и идем изучать гребенку:
Видим SPI? Он нам и нужен! Подключаем питание дисплея (3.3В на VCC, 5В на LED и не забываем землю), подключаем сигнальные линии (SCK — CLK, SDA — MOSI, A0 и RESET — цепляем на произвольный GPIO, на котором «ничего нет», я выбрал PA10 и PA20 пины). Если SPI Вам нужен только для дисплея, то можно просто поставить перемычку между CS и землей. Оставлять его «в воздухе» нельзя — иначе дисплей не будет работать.
Если подключили все верно, то при включении одноплатника, Вы увидите подсветку.
Теперь для того, чтобы им управлять, нам нужно получить доступ к шине SPI и проинициализировать контроллер. Для этого убеждаемся в том, что у нас есть spidev в каталоге /dev/, где spidev0.0 — первый контроллер SPI с первой линией CS, spidev0.1 — первый контроллер SPI с второй линией CS. У OrangePi One в стоке он только один — а для CS предлагается использовать sysfs. Кроме этого, нам нужно «экспортировать» из задать направлением пинам, которые мы будем использовать для сигналов RESET и DC. Для этого пишем номера пинов на гребенке прямо в устройство /sys/class/gpio/export, например так:
echo 10 > /sys/class/gpio/export
echo 20 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio20/direction
echo out > /sys/class/gpio/gpio10/direction
Обратите внимание, что в свежих версиях ядра появилось нормальное API для доступа к GPIO из userspace, управлять пинами через sysfs — в какой-то степени считается плохим тоном.
Открываем устройство как обычный файл:
fd = open("/dev/spidev0.0", O_RDWR | O_NONBLOCK);
dcFd = open("/sys/class/gpio/gpio10/value", O_RDWR);
resetFd = open("/sys/class/gpio/gpio20/value", O_RDWR);
И отправляем контроллер дисплея в RESET:
gpHelperSetState(resetFd, 0);
usleep(250000); // 250ms
gpHelperSetState(resetFd, 1);
После этого, реализовываем методы для передачи данных через SPI. В Linux, общение через эту шину идёт посредством транзакции, причем размер одной транзакции ограничен конкретным SPI-контроллером. В случае AllWinner, тут от 64, до 128 байт. Для каждой транзакции можно установить тактовую частоту — AllWinner поддерживает до ~100мгц.
void CLCM::Command(unsigned char cmd) {
spi_ioc_transfer tf;
memset(&tf, 0, sizeof(tf));
tf.bits_per_word = 8;
tf.len = 1;
tf.speed_hz = 64000000;
tf.tx_buf = (unsigned long)&cmd;
gpHelperSetState(dcFd, 0);
if(ioctl(fd, SPI_IOC_MESSAGE(1), &tf) < 0) LOG("SPI transfer failed\n");
}
void CLCM::Data(unsigned char data) {
spi_ioc_transfer tf;
memset(&tf, 0, sizeof(tf));
tf.bits_per_word = 8;
tf.len = 1;
tf.speed_hz = 64000000;
tf.tx_buf = (unsigned long)&data;
gpHelperSetState(dcFd, 1);
if(ioctl(fd, SPI_IOC_MESSAGE(1), &tf) < 0) LOG("SPI transfer failed\n");
}
Теперь нам нужно инициализировать дисплей. Для этого, нужно передать ему несколько команд, которые задают настройки развертки, поворота, внутренние настройки цветности и.т.п:
Для передачи фреймбуфера, мы реализовываем отдельный метод, который разобьёт его на транзакции. В нашем случае, фреймбуфер занимает 128 * 160 * 2 = 40960 байт, делим на 64, получаем 640 транзакций на передачу одного кадра
void CLCM::Bitmap(void* data, int len) {gpHelperSetState(dcFd, 1);
for(int i = 0; i < len / 64; i++) {
spi_ioc_transfer tf; memset(&tf, 0, sizeof(tf));
tf.bits_per_word = 8;
tf.len = 64;
tf.speed_hz = 32000000;
tf.tx_buf = (unsigned long)data;
data += 64;
if(ioctl(fd, SPI_IOC_MESSAGE(1), &tf) < 0) LOG("SPI transfer failed\n");
}
}
Компилируем нашу программу, запускаем и видим: на дисплее появился мусор, а это значит, что он успешно проинициализирован. Если у Вас всё равно белый дисплей — смотрите подключение и убедитесь, что подключили сигнальные линии RESET/DC куда надо. После инициализации, на DC должен быть логический 0 (0В), на RESET — логический 1 (3.3В).
Пишем простенький загрузчик TGA и выводим картинку на экран:
CImage* img = CImage::FromFile("test.tga");
if(img) Bitmap(img->RGB, img->Width * img->Height * 2);
Всё работает и у нас есть картинка на дисплее! Производительность системы, скажем так, оптимальная, но учтите: чем выше разрешение, тем выше нагрузка на ядро!
Это всё конечно замечательно, однако зачастую есть необходимость отображать картинку, которые рисуют другие программы — X Window System, или, например, порт эмулятора денди на SDL1.2. Для этого, нам нужен способ выводить на наш дисплейчик то, что рисуется в главный фреймбуфер — /dev/fb0. И для этого, у нас есть целых два способа:
Реализация kernel-mode драйвера фреймбуфера: Это правильный вариант, однако при условии отсутствия dts, придется «подвигать» родной драйвер на другой фреймбуфер, либо перенастраивать уже имеющееся окружение на /dev/fb1.
Служба-прослойка, которая копирует фреймбуфер и вручную рисует на наш дисплей Этот способ я подсмотрел у китайцев: именно он реализован в драйвере дешевых дисплеев для Raspberry Pi. В целом, если так подумать, то это действительно довольно простой, портативный (не зависящий от версии ядра) и шустрый метод.
Именно второй способ мы и выберем в силу его некоторой диковинности. Фреймбуфер Linux имеет одну очень приятную особенность: он способен сам выполнять преобразования формата пикселей и динамически менять размер рабочего пространства. Мы можем просто попросить драйвер установить комфортный для нашего дисплея режим (128x160), цветность (RGB565) и читать уже готовые битмапы, по необходимости пересылая их на дисплей.
Давайте напишем простенькую службу для этого. Наша служба должна быть универсальной, дабы уметь выводить картинку на несколько разных дисплеев, в зависимости от статически слинкованных с ней драйверов. Для этого мы сразу определяем структуру, описывающую процедуру инициализации и вывода уже готовой картинки на экран:
struct CLCM {
char* name;
int width,
height;
void(*init)();
void(*presentBuffer)(void* buf);
};
CLCM lcm7735
{
.name = "ST7735",
.width = 128,
.height = 160,
.init = &st7735Init,
.presentBuffer = &st7735Bitmap
};
CLCM* lcmList[] = { &lcm7735 };
Теперь у нашей службы есть некоторая гибкость. Захотели — поставили дисплей на базе ILI9341, захотели — на базе ILI9325, достаточно лишь портировать код инициализации.
Открываем всем необходимые устройства и назначаем нашему фреймбуферу желаемое разрешение. Обратите внимание, что мы можем весь буфер кадра отобразить в наш процесс с помощью mmap: это гораздо быстрее и экономичнее к памяти, чем выделять отдельный буфер под read/write.
bool setupFrameBuffer() {
LOG("Open framebuffer device");
fbDevice = open("/dev/fb0", O_RDWR);
if(!fbDevice) {
LOG("Failed to open primary framebuffer");
return false;
}
ioctl(fbDevice, FBIOGET_VSCREENINFO, &fbVar);
fbVar.xres = lcm->width;
fbVar.yres = lcm->height;
if(ioctl(fbDevice, FBIOPUT_VSCREENINFO, &fbVar) < 0) {
LOG("Unable to set framebuffer size :c");
return false;
}
ioctl(fbDevice, FBIOGET_VSCREENINFO, &fbVar); // Get yet another time for test
LOGF("Parent FB: %ix%i %i-bits", fbVar.xres, fbVar.yres, fbVar.bits_per_pixel);
ioctl(fbDevice, FBIOGET_FSCREENINFO, &fbFix);
fbMem = (char*)mmap(0, fbFix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fbDevice, 0);
buf = (unsigned short*)malloc(lcm->width * lcm->height * 2);
if(!fbMem) {
LOG("mmap failed");
return false;
}
return true;
}
К сожалению, в случае с OrangePi, мне не удалось запросить драйвер обрабатывать картинку в формате RGB565, поэтому для вывода пришлось выделять внешний буфер, где мы на лету конвертируем картинку из 32х-битного RGB в 16-битный.
__inline unsigned short lcmTo565(unsigned int r, unsigned int g, unsigned int b) {
short ret = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3);
return bswap_16(ret);
}
Ну и переходим, собственно, к копированию фреймбуфера на наш дисплей:
void lcmCopyFramebuffer() {
int bpp = fbVar.bits_per_pixel / 8;
for(int i = 0; i < lcm->width; i++) {
for(int j = 0; j < lcm->height; j++) {
unsigned char* rgbData = (unsigned char*)&fbMem[(j * fbFix.line_length) + (i * bpp)];
buf[j * lcm->width + i] = lcmTo565(rgbData[0], rgbData[1], rgbData[2]);
}
}
lcm->presentBuffer(buf); }
Да, это вся программа. Тестируем наш результат:
Работает! Теперь если мы захотим запустить, например, эмуляторы, или вывести иксы на внешний экранчик — то мы смоежм сделать это без каких либо проблем.
Как видите, даже из казалось бы, из неактуальных и нерабочих гаджетов можно вытащить дисплеи для собственных проектов. Документация по протоколам доступна в свободном доступе в сети, да и со схемами уже не так сложно, как в нулевых.
Даже с практический точки зрения нет никаких проблем в том, чтобы подключить дисплейчик даже к устройствам, где подобный видеовыход и не предусмотрен. Надеюсь этот подробный материал окажется полезным моим читателям. Само собой, я создал репозиторий на гитхабе и запушил туда все наработки из сегодняшней статьи.