Эмуляция сотового телефона… на сотовом телефоне
О чём только не пишут на Хабре. Например, о создании J2ME-игры в 2024 году, о написании программ на ассемблере под Nokia 3310. Вдохновлённый этими статьями, я тоже решил создать нечто подобное. Это «нечто» должно удовлетворять следующим критериям:
а) Быть не слишком простым в техническом плане;
б) Быть, вероятно, бесполезным в практической эксплуатации;
в) Иметь ностальгические элементы.
Многие помнят линейку телефонов Siemens 65–75 серии, которые были в ходу около 20 лет назад. Мы попробуем провести необычный эксперимент на их основе, для чего напишем небольшую управляющую библиотеку на C, а также изменим код одной из встроенных в ОС Linux утилит. Подробнее в статье.
❯ 1. Аппаратная часть
В серии телефонов «Siemens 75» существовали такие модели, как C75, ME75, CX75 и прочие. Они различались между собой дизайном корпуса и некоторым другим функционалом. Но их основные элементы были, во многом, схожи. Например, дисплей разрешением 132×176. Для работы с данным проектом в коробке с различным электронным мусором были обнаружены останки подобного телефона. Состояние изделия за 20 с лишним лет очень плохое, надписи стёрлись, но я предполагаю, что это был экземпляр телефона C75 — BenQ, позднего выпуска. Родной процессор телефона давно уже умер — какое-то время назад удавалось оживить его, прогрев плату паяльным феном, но теперь осталась лишь безжизненная плата и оболочка. Поэтому я решил перехватить информацию на уровне дисплея.
Нас интересует, в первую очередь, этот самый дисплей. Поэтому, для начала, разберём устройство:
Поскольку любая наука стоит «на плечах гигантов», я не буду повторять материал о видах дисплеев в данном телефоне. Ознакомиться с ним вы можете самостоятельно, это описал ещё в 2013 году Кизим Игорь, в своей статье.
У меня уже был некоторый опыт работы с данным дисплеем. Используя библиотеку, взятую из статьи Игоря, я подключал данный дисплей к микроконтроллеру ATmega8. Однако, это было более 5 лет назад, поэтому, для начала, я решил проверить, а работает ли вообще данный дисплей. Для чего собрал схему Игоря. Её ключевые особенности: установлен стабилитрон по питанию дисплея (5V → 2.9V), установлены резисторы для понижения уровней логики (5V → 3.3V). Однако, нам необходимо ещё одно напряжение — 12V, для питания диодной ленты подсветки. Поскольку я не хотел городить дополнительный инвертор, я расковырял подсветку и заменил диоды на те, которые могут работать напрямую от 5V, подсоединив их не последовательно, а параллельно. Таким образом я избавился от лишнего провода питания, хотя и качество подсветки несколько пострадало.
Теперь я имел 2 провода питания (+5V, GND), и 5 сигнальных проводов с уровнем логики 3.3V.
Однако, контроллера ATmega8 под рукой не оказалось, но был китайский клон Arduino Uno на ATmega328. Я попытался поискать библиотеку Arduino под этот дисплей, и нашёл такую. Но она была заточена под ESP8266. Хотя идею замены SPI в функции Send_to_lcd с программного на аппаратный я намотал на ус:
void Send_to_lcd (uint8_t RS, uint8_t data)
{
static uint8_t old_RS = 0;
if ((old_RS != RS) || (!RS && !old_RS)) {
digitalWrite(LCD_RS, RS);
}
SPI.transfer(data);
}
В итоге я взял оригинальную библиотеку Игоря (Igoryosha), и портировал её под Uno, просто заменив в текстовом редакторе функции присваивания (напр. LCD_CLK=0) на ардуиновские digitalWrite (LCD_CLK, LOW). Библиотека запустилась без особых проблем.
Так как в ней используется программная имитация SPI, выводы GPIO можно назначить произвольно (кроме тех, что заняты RX TX). Библиотеку под Arduino Uno для данного дисплея в итоге я оставил у себя на github, в ветке main.
Результат запуска программы и скорость работы такого SPI можно увидеть наглядно на видео:
Теперь можно подсобрать телефон для удобства работы. Я вытащил динамик, чтобы вывести провода через отверстие.
Проверено: дисплей работоспособен, а это означает, что наша задумка в конечном итоге получится.
❯ 2. Подключаем Orange Pi GPIO
Как можно было увидеть в предыдущем фрагменте, скорость отрисовки картинки просто черепашья. Оно и неудивительно: контроллер работает всего лишь на 16 МГц, ещё и использует программный SPI для вывода картинки. Ситуацию нужно исправлять, взяв более мощный и производительный контроллер. Тут я натыкаюсь на статью хабровчанина Hoshi.
В ней он данный дисплей подключает к Raspberry Pi, и использует его как «монитор». Поскольку мы стоим на плечах гигантов, было решено проследовать его примеру, и не изобретать ещё какие-либо методики. Но Raspberry Pi у меня не оказалось, нашёлся только Orange Pi PC.
Поэтому запустить код Hoshi без переделки у меня бы не вышло: в своей статье он оперирует GPIO через библиотеки чипсета bcm2835. Так как данного чипсета на моей плате нет, и библиотеку подключить не выйдет. Я начал поиски способа, как подключить GPIO на моём чипсете h3. Выяснилось, что для этого нужна библиотека wiringPi (на текущий момент уже не поддерживается официальными авторами). Однако, просто установить её через apt-get оказалось мало: устанавливался оригинал под Raspberry. Спустя некоторое время была найдена модификация этой библиотеки от zhaolei. Я собрал её через build (ветка h3), и именно она оказалась рабочей для моей платы. После выполнения build библиотека установилась в систему, и стало возможным вызвать её из требуемого места (например, проверить пины с помощью sudo gpio readall).
Назначение выводов GPIO для «апельсина» назначается через define по аналогии с Arduino:
#define LCD_RS 21 // CD - тип передаваемых данных - СЕРЫЙ
#define LCD_RESET 22 // Сброс - КОРИЧНЕВЫЙ
#define LCD_CS 23 // Выбор чипа - ЗЕЛЁНЫЙ - ИСПОЛЬЗУЕМ SPIDEV0.0 - PIN 24
#define LCD_CLK 24 // Синхронизация - СИНИЙ - ИСПОЛЬЗУЕМ SPIDEV0.0 - PIN 23
#define LCD_DATA 25 // Данные - БЕЛЫЙ - ИСПОЛЬЗУЕМ SPIDEV0.0 - PIN 19
Также следует отметить следующий момент: в схеме Igoryosha для AVR использовался логический уровень +5В, а на Orange Pi разъем GPIO сразу оперирует уровнями +3.3В. Поэтому резисторные делители я убрал. В итоге получилась следующая конструкция:
Однако, у меня сходу не получалось завести аппаратный SPI. Поэтому для повторной проверки работоспособности дисплея я взял, опять же, библиотеку Игоря из-под AVR, портировав её теперь и под wiringPi. К счастью, особых замен не понадобилось, только поменять в коде LOW и HIGH на 0 и 1 соответственно. Даже функции delay в wiringPi аналогичны ардуиновским. Также на данном этапе библиотека лишилась практически всех графических функций отрисовки примитивов, кроме отрисовки, непосредственно, одного кадра из буфера.
После чего я собрал программу через gcc (gcc –o example example.c –lwiringPi), и запустил из терминала.
Результат можно увидеть на видео:
Однако разницы, по сравнению с Arduino, практически нет. Отрисовка стала шустрее, но лишь немножко. Оно и неудивительно: чаще всего вызывается метод передачи по SPI (Send_to_lcd), а так как он у нас всё ещё программный, прироста в скорости мы не видим, сам GPIO работает достаточно медленно, по скорости сопоставим с обычным Arduino. Поэтому нам нужно исправлять ситуацию, задействовав аппаратный SPI.
❯ 3. Подключаем аппаратный SPI
Для замены программного SPI на апаратный можно, также, задействовать библиотеку wiringPi, а именно, из wiringPiSPI.h использовать функции wiringPiSPISetup и wiringPiSPIDataRW. Функции эти несколько хитрые. Но перед тем, как их использовать, нужно включить этот самый SPI. Информации о том, как это сделать конкретно на Orange Pi PC также в интернете нет, но удалось найти направление, в котором нужно искать. Немного погуглив, я выяснил, что spi включается правкой файла /boot/armbianEnv.txt (актуально для моей версии системы Armbian_23.11.1_Orangepipc_jammy_current_6.1.63_xfce_desktop.img).
В него нужно добавить следующие строки:
overlays=spi-spidev
param_spidev_spi_bus=0
param_spidev_spi_cs=0
После чего сделать sudo reboot, и у нас в /dev/ появляется spidev0.0. Проверить это можно, выполнив команду ls /dev | grep spi. Если spidev0.0 появился, дальше библиотека wiringPiSPI подхватит его. Теперь контакты дисплея CS, CLK, DATA нужно подключить к пинам SPI0, как это сделано у Hoshi. Распиновка (40-pin) полностью соответствует Raspberry Pi. Пины RS и RESET оставляем на попечении обычного GPIO.
Однако, просто заменив SPI на аппаратный, я заметил, что FPS отрисовки практически не поменялся. Проблема заключалась в том, что если отправлять по одному байту, то с каждой посылкой SPI будет заново открываться и закрываться, а эта процедура отнимает очень много времени. Поэтому было решено отправлять данные пачками максимально возможной длины.
Возникла следующая проблема: буферизация пакетов SPI. На данном устройстве мы можем отправить только 4 килобайта данных за одну посылку. Наша страница же занимает порядка 44 Кб: 132×176*2, так как используется 16-битная цветность. В качестве решения можно было либо увеличить буфер SPI, что возможно, однако, мне не хотелось прибегать к данной методике. Поэтому я просто в своём коде раздробил страницу на 11 пачек:
for (unsigned char i = 0; i < 11; i += 1)
{
*myPointer = &myScreenBuffer[(i * 2048)];
memcpy(mySendBuffer, myPointer, 4096);
wiringPiSPIDataRW(0, dataPointer, 4096);
}
И используя memcpy, копировал перед отправкой каждую пачку в буфер. К слову, это необходимо ещё и потому, что буфер побайтово очищается в процессе передачи, заменяя выходные данные на входные с буфера RX (MOSI pin).
Также библиотека позволяет регулировать скорость: от 500 КГц до предела в 32 МГц. Делается это в момент инициализации: int fd = wiringPiSPISetup (0, 32000000); мы выставляем канал 0, и скорость в 32 МГц.
После вышеописанных процедур мне удалось получить скорость кадров в 60 FPS. Я не уверен, способен ли дисплей отрисовать данные с такой скоростью, но таймер рапортовал именно так. Можно увидеть это на видеозаписи:
На первой половинке видео можно увидеть кусочек области дисплея, который передаётся за одну отправку (4096 байт), это примерно одна десятая всей экранной области. Скорость шины выставлена в 500 КГц. На второй половинке отправка всех 11 областей, и скорость шины в 32 МГц. То есть, скорость передачи примерно порядка 20 Мбит/сек. В данном случае, я считаю, достигнут потолок пропускной способности всей нашей сборки.
❯ 4. Выводим статичный bmp кадр
Следующее, что сделал Hoshi в своей статье — вывел статичную картинку, получив проблемы с цветностью. Поскольку я иду по его стопам, я попробовал вывести картинку из буфера, используя частично его код, поменяв только сдвиг (offset, так как мой заголовок занял другое количество байт). Однако, сначала я получил такую картинку, как показана на левой части изображения, и, лишь потом, такую, как на правой:
Я взял такую же картинку, как у Hoshi, и получил примерно такой же результат. Подводный камень заключался в следующем:
- Два байта нужно поменять местами, этот момент был в коде Hoshi. Либо же добавить/убрать один лишний байт в начале пакета, чтобы вызвать сдвиг всего массива.
- Я сохранил исходную картинку через Adobe Photoshop в формат BMP 16-bit. Однако, как выяснилось после просмотра в HEX-редакторе, белый цвет у меня получился не FF FF, а FF 7F, вследствие чего он отображался, как бирюзовый, и остальные цвета также имели искажения:
Произошло это из-за того, что редактор сохранил BMP файл в режиме X1R5G5B5 (с альфа-каналом), а у нас в дисплее используется R5G6B5, то есть, зелёный цвет занимает на один бит больше. Поэтому, при скармливании картинки дисплею, мало того, что один из старших битов пропадает, так ещё и происходит бинарный сдвиг одного цветового канала на единицу, из-за чего вся палитра оказывается искажена. После сохранения картинки в нужном режиме значения белого заменились на FF FF, потери одного бита данных больше не было, и она отрисовалась с нормальной цветностью.
На этом построение библиотеки для работы с дисплеем было закончено, и началось самое интересное — попытка вывести на него живой видеопоток.
❯ 5. Пишем ПО для захвата экрана
Далее вышеупомянутый автор для рендеринга использует интерполяцию из фрейм-буфера ОС Linux /dev/fb0. Попытка запустить его код не привела ни к чему хорошему: в моём случае фреймбуфер отображается как чёрная сетка из-за несоответствия данных, да и мне не нужно было проводить интерполяцию картинки всего рабочего стола, а нужна была конкретная область экрана.
Слева: отрисовка из /dev/fb0. Справа: отрисовка из скриншота.
Так как моя GUI в Armbian работает на графической оболочке XFCE, у меня возникла идея выдрать требуемые пиксели непосредственно через неё. Для этого используются средства gdk и x11. Добавление всех необходимых библиотек сильно усложнило бы программу, поэтому мне пришлось прийти к костыльному решению проблемы.
Я решил копнуть в сторону встроенной в ОС утилиты xfce4-screenshooter. Данная утилита позволяет снять скриншот, в том числе, через командную строку. Однако, функции сохранения заданной области в ней нет, либо требуется задавать каждый раз область мышкой, что было мне неудобно. Поэтому я сделал форк кода данной утилиты. И добавил в опции командной строки, помимо FULLSCREEN, WINDOW и REGION ещё и аргумент FIXED, который сохранял в файл конкретно прописанную в коде область экрана.
Для удобства разработки пришлось поставить xubuntu на виртуальную машину с x86, после чего изменить исходный код, а затем собрать его же, но под armbian непосредственно на своём Orange Pi. Сборка утилиты осуществляется помощью xdt-autogen: сначала ./autogen.sh, далее установить библиотеки по требованию (через apt-install) затем с помощью make, и make install для замены установленного в систему скриншоттера на изменённый вариант. После этого готовый скриншот нужного размера стало можно выводить в файл с помощью одной лишь команды терминала — для определения файлового пути, формата и прочего используется оригинальный код скриншоттера.
Однако, данное решение имеет и недостаток: программа-скриншоттер работает, вероятно, таким образом, что сначала делает скриншот всего root-окна (рабочего стола), затем обрезает его до требуемого размера. При этом, на какой-то момент, отрисовка даже приостанавливается. Вся процедура съемки занимает порядочное количество времени: на десктопе xubuntu она осуществлялась примерно за 50–100 миллисекунд. На Orange Pi она же стала занимать порядка 100–400 миллисекунд. Видеозахват — в целом тяжелая процедура для ЦПУ. Поэтому уменьшение разрешения рабочего стола помогло, но незначительно. В идеале нужно выдирать изображение через низкоуровневый код непосредственно из экранной памяти, а не из пользовательской среды через функции gdk. Более того, в самом коде скриншоттера написано, что рекомендуемая задержка между скриншотами должна быть не менее 200 мс, то есть, это уже ограничивает нас до 5 FPS. В случае, если нужно просто проверить консольный вывод, этого достаточно, а вот для видеопотока оказалось маловато.
Прим. авт.: через некоторое время после написания статьи, мне удалось решить данную проблему, используя вместо скриншоттера ПО jsmpeg-vnc. С ним я получил 50 FPS и выше, плюс имеются встроенные функции обрезки кадра до нужного размера.Данное ПО передаёт MPEG-поток через WebSocket протокол (выполняя трансляцию видеосигнала), позволяет закодировать только нужную область, чтобы не передавать весь рабочий стол. Далее кадр можно расшифровать и передать непосредственно в контроллер, минуя костыль в виде отображения его на экране и снятия скриншота. Если это будет интересно, можно показать подробнее в следующей статье.
Также, поскольку сохранение в bmp происходит через встроенные средства gdk, мне не удалось заставить программу сохранять в 16-битный формат. Она сохраняла в 24-битный формат, поэтому для преобразования цветовой палитры мне пришлось написать фрагмент кода на бинарных сдвигах:
newColorByte =
(inputScreen[cell] & 0b11111000)
| ((inputScreen[cell + 2] & 0b11100000) >> 5)
| ((inputScreen[cell + 2] & 0b00011100) << 11)
| ((inputScreen[cell + 1] & 0b11111000) << 5);
screen[y][x] = newColorByte;
Изначально содержимое нашего скриншота копируется в массив байтов (uint8_t) в порядке очередности. Для конвертации цвета 24bit → 16 bit (ещё и с перевёрнутым порядком байтов) использованы сдвиги: мы сравниваем первый байт красного с пятью единицами, далее сравниваем второй байт зелёного с тремя единицами и сдвигаем результат в самое начало, далее сравниваем следующие разряды байта зелёного и сдвигаем в самый конец, далее сравниваем синий и сдвигаем его в середину. Таким образом, из цветности КККККККК ЗЗЗЗЗЗЗЗ ССССССС мы привели палитру к цветности ЗЗЗССССС КККККЗЗЗЗ, которую и принимает наш дисплей. Для отладки я использовал цветные картинки, после чего смотрел, корректно ли отображается цвет согласно своему описанию, или же цветовой канал требуется сдвинуть ещё на какое-то количество ячеек.
После чего я в цикле запустил скриншоттер через вызов терминала, а далее отрендерил картинку на экран. Это можно увидеть на видео:
Конечно, данный пункт программы нуждается в доработке — ссылки на заголовочные файлы библиотек gdk и x11 следует внести в общий файл программы, в котором происходит работа с дисплеем чтобы избежать костыля в виде сохранения картинки в кэш на жестком диске. Возможно, это несколько улучшит производительность. А для идеальной работы требуется переписать это всё на уровне ядра ос, чтобы превратить самодельную библиотеку в драйвер для устройства. Но на текущий момент ход программы получился такой:
- Через терминал вызывается скриншоттер, который сохраняет кадр в cache.bmp;
- Файл cache.bmp открывается, после чего отправляется его содержимое на дисплей.
Причём, основная потеря скорости идёт на этапе снятия скриншота, а не записи/чтения его с диска. Для увеличения FPS выше 5 необходимо заменить xfce4 скриншоттер на какое-то другое ПО. Тем не менее, мы движемся дальше.
❯ 6. Эмуляция сотового телефона
Эмулятор телефона CX75 был написан лет 20 назад, и входил в официальный пакет программ для разработки java-приложений через WTK/JDK 2.0. Он пролежал у меня на жестком диске лет 15, после чего я запустил его для данной работы. Если кому-то интересно также запустить его на своём компьютере, делюсь файлами.
Для работы требуется JDK 6u45 и Windows XP. Насколько я помню, даже при запуске на Windows 7 эмулятор вылетал, на Win 10, тем более, работоспособность я не проверял. Поэтому запускать я его буду через виртуальную машину с WinXP.
Эмулятор полностью реализует функционал прошивки телефона 75-й серии, в том числе, можно устанавливать java-игры, подключать веб-камеру для съемки фото, и так далее. Единственное, вряд ли будет работать интернет, по причине того, что WAP технологии уже не получится использовать.
Вот так эмулятор выглядит в системном окне. Управлять можно с клавиатуры (джойстик — стрелки и enter, клавиши — цифры или тачпад), либо нажатием на виртуальные кнопки.
Выдаёт он картинку чётко размера 132×176, поэтому интерполяция не потребуется. Теперь нужно прокинуть картинку из виртуальной машины в Linux машину. Можно было использовать wine, но я не уверен, будет ли эмулятор адекватно работать на нём. Поэтому он запущен в XP. Для передачи картинки, опять же, ничего нового изобретать я не буду, использую TightVNC.
На Windows мы устанавливаем сервер, на armbian«е устанавливаем клиент через apt-get xtightvncviewer.
После чего запускаем клиент-сервер, и выставляем на экране требуемую зону отображения. FPS в данном случае также примерно равен 5 кадрам, поэтому дисплей будет рендерить с точно такой же скоростью.
И вот момент, ради которого всё затевалось: помещаем эмулятор телефона в экранную область на самом телефоне
Видео:
Таким образом, наш видеопоток проходит через следующие уровни:
- Эмулятор CX75 (x86 C-программа, но порт ARM-совместимой прошивки);
- Windows XP (виртуальная машина);
- Windows 10 (через виртуализацию, но можно пропустить, выведя в VNC напрямую с XP);
- Armbian xfce4 gui (через VNC);
- Изображение cache.bmp (через xfce4-screenshooter);
- Дисплей C75 (через wiringPi + wiringPiSPI).
Для эмуляции же клавиатуры достаточно просто припаять контактные площадки к контроллеру от USB-клавиатуры в соответствии со схемой их разводки:
Делать этого сейчас я не буду по причине того, что это несколько монотонное занятие, и вся задача заключается лишь в правильном сопоставлении таблиц кнопок и таблиц на контроллере от USB клавиатуры. После чего клавиатура подключается к виртуальной машине, и можно испытать полное погружение в эмулятор.
❯ 7. Заключение
Результат проекта:
В ходе работы были изучены особенности работы с GPIO, SPI, GTK3, VNC, преобразованием цветности и некоторым другим функционалом компьютерных и микроконтроллерных систем.
Готовые файлы проекта под wiringPi.
Спасибо за внимание.