У меня есть склонность к реализации глупых и/или бессмысленных проектов. Перед вами один из них, который появился в результате беседы, закончившейся словами: «Слушай, а ведь технически, возможно…», — не вопрос, давай сделаем.
DDC (канал данных дисплея) – это протокол для считывания информации о том, какие разрешения и в целом параметры поддерживает монитор. Позднее он был расширен до версии DDC/CI, которая позволяет настраивать яркость и прочие атрибуты, но суть начальной идеи заключалась в установке на каждое устройство дешёвой EEPROM с интерфейсом I2C, на которой бы хранилась некая базовая информация. (Технически изначальная идея была даже проще, но мы не станем заострять на этом внимание).
Внедрение этой технологии началось ещё во времена VGA, но она настолько закрепилась, что поддерживается даже в современном оборудовании с HDMI и DisplayPort. Всё верно – в HDMI-кабеле среди высокоскоростных дифференциальных пар скрывается чрезвычайно медленная шина I2C.
В крохотных OLED-дисплеях с точечной матрицей зачастую присутствует I2C-контроллер, поэтому у меня возникла идея подключить такой дисплей прямиком в порт HDMI. Смешно, не так ли? Давайте пробовать.
Схема соединения
Я обрезал нерабочий HDMI-кабель и отыскал интересующие нас контакты: SCL, SDA, 5V, DDC-GND и HPD (обнаружение активного соединения). Распиновку я нашёл в Google:
На этой схеме показан разъём HDMI. При подключении к контактам проводов его нужно развернуть слева направо.
Номер вывода HDMI | Сигнал |
---|---|
1 | TMDS Date 2+ |
2 | TMDS Data 2 shield |
3 | TMDS Data 2- |
4 | TMDS Data 1+ |
5 | TMDS Data 1 shield |
6 | TMDS Data 1- |
7 | TMDS Data 0+ |
8 | TMDS Data 0 shield |
9 | TMDS Data 0- |
10 | TMDS Clock+ |
11 | TMDS Clock shield |
12 | TMDS Clock- |
13 | CEC |
14 | HEC Data- |
15 | SCL (Serial Clock for DDC |
16 | SDA (Serial Data Line for DDC |
17 | DDC / CEC / HEC Ground |
18 | +5 V Power (50 mA max) |
19 | Hot Plug Detect (1.3) / HEC Data+ (1.4) |
Когда дело доходит до переделки оборудования, обычно я стараюсь придерживаться наиболее безопасных вариантов – никто не хочет наблюдать синий дым, особенно если речь идёт о дорогой плате. Однако сегодня я решил испытать судьбу и собираюсь припаять этот дисплей прямо к распотрошённому кабелю HDMI, подключённому к моему относительно новому ноутбуку. Какой накал страстей! Если я напортачу, то этот дурацкий эксперимент мне дорого обойдётся.
Для скачивания спецификации HDMI необходимо зарегистрироваться, что никак не вписывается в мои сжатые планы реализации, но название вывода Hot Plug Detect (обнаружение активного соединения) довольно описательно. Я предположил, что для подачи сигнала о подключении кабеля этот контакт нужно подтянуть либо вверх, либо вниз. Установка резистора 20кОм на линию 5В решила эту задачу. С помощью осциллографа теперь можно регистрировать активность на линиях SCL/SDA, когда кабель подключён к ноутбуку.
После этого я жирно припаял к разъёму гребёнки четыре интересующие нас линии. Для данного эксперимента я заказал пару OLED-экранов, которые оснащены контроллерами SSD1306 и поставляются на печатных платах с четырехконтактной гребёнкой.
I2C и SMBus
В Linux можно организовать доступ к устройствам I2C, загрузив модуль
i2c-dev
(modprobe i2c-dev
), который отобразит в /dev/i2c-*
кучу устройств I2C. Мой ноутбук показывает девять таких устройств.Некоторые из них на деле относятся к протоколу SMBus, основанному на I2C. Вроде как это тот же I2C, но с кучей дополнительных ограничений вроде ограничения транзакции размером 32 байта.
Также желательно установить пакет
i2c-tools
, который поставляется с утилитой i2cdetect
и устанавливает для групповых разрешений правило udev
. Для доступа к устройствам I2C без sudo
добавьте себя в группу I2C (sudo usermod -G i2c -a username
) и снова авторизуйтесь, чтобы изменения вступили в силу. Мне также пришлось выполнить для этого udevadm trigger
. Может показаться, что проще выполнить перезагрузку, но так делать ни в коем случае не надо.Имейте ввиду: именуются устройства I2C не последовательно. Я узнал, что линией HDMI DDC, к которой я припаялся, была/dev/i2c-3
, но после выгрузки и повторной загрузки модуля ей стала уже/dev/i2c-4
. Здесь нужно быть очень внимательным, так как запись (или даже считывание) из неверного устройства I2C может запросто привести к порче оборудования ноутбука.
Я установил ещё один пакет,
ddcutil
, только чтобы иметь возможность выполнять ddcutil detect
. Эта команда выводит список дисплеев и связанную с ними шину I2C. Также можно выполнить i2cdetect -l
, которая выведет список устройств I2C с их описанием. В моём случае три линии I2C содержали в своём описании i915 gmbus
, где i915 – это графический драйвер Intel. Однако проще всего такие вещи выяснять всё же с помощью ddcutil
.Первые тесты
Осциллограф показал, что линии SCL/SDA уже подтянуты к верхнему уровню, значит, у нас должно получиться подключить экран без использования дополнительного оборудования. Очевидно, что линия 5В на порту HDMI может подавать до 50мА, значит, нам даже не потребуется блок питания. Чудесно!
i2cdetect
может сканировать шину I2C на предмет подключённых устройств. Как и ожидалось, без подключённого кабеля она ничего не обнаружила. Однако, когда я подключил свой распотрошённый провод с впаянным резистором, отобразилось очень много ответов. Я не совсем понимаю, что здесь происходит, но важно то, что когда я подключил дисплей, по адресу 0x3c
появилось дополнительное устройство.Самый быстрый способ взаимодействия с дисплеем – это скрипт Python. Библиотека SMBus позволяет всё ловко наладить.
import smbus
bus = smbus.SMBus(4) # для /dev/i2c-4
i2caddr = 0x3c
bus.write_i2c_block_data(i2caddr, 0, [0xaf] ) # включает дисплей
Прежде чем мы сможем что-либо вывести на дисплей, нужно выполнить кучу команд, в том числе активировать генератор подкачки заряда. Обратите внимание, что спецификация SSD1306, по крайней мере, найденная мной копия, в своём конце содержит указания по применению, которые объясняют процесс инициализации яснее, чем основной документ (некоторые команды в таблице отсутствуют). Как обычно, самый быстрый способ начать работу – это посмотреть исходный код существующих библиотек, поэтому я нашёл чей-то пакет для SSD1306 и скопировал оттуда команды инициализации. Дисплей ожил!
Я также нашёл скрипт для отрисовки текста на SSD1306, куда сразу встроил код для SMBus. Всё прошло успешно!
Никакого микроконтроллера или прочего оборудования, только SSD1306, подключённый прямо в HDMI-порт. Для меня это выглядит весьма удовлетворительно.
Вывод на дисплей данных
Придерживаясь пока что скрипта Python, я хочу иметь возможность брать изображение 128х64 и передавать его на дисплей. Применяемая мной утилита отрисовки текста с помощью команд SSD1306 управляет адресом столбца и страницы, куда записываются данные, благодаря чему один символ можно отрисовывать без влияния на остальную часть дисплея (отсюда и неинициализированные фоновые пиксели на изображении выше).
Здесь доступно очень много режимов адресации памяти, сдобренных мутной терминологией. SEG или COL – это координата X, COM – это координата Y, но группируются они по страницам. В спецификации приводятся кое-какие схемы.
Сам дисплей монохромный, каждая страница состоит из 8 строк (COM), и когда мы передаём данные на дисплей, каждый байт занимает одну страницу, один столбец пикселей. Было бы логичнее настроить дисплей в режим вертикальной адресации, чтобы биты располагались по порядку, но всё же быстрее оказалось просто перетасовать биты на нашей стороне.
С помощью библиотеки PIL можно преобразовать изображение в монохромное, используя команду
.convert(1)
, после чего сериализовать его командой .tobytes()
. В результате каждый байт будет представлять 8 горизонтальных пикселей, но нам нужно, чтобы он представлял 8 вертикальных. Вместо реализации утомительной побитовой логики, проще будет исправить это, повернув изображение перед сериализацией на 90 градусов, а затем загрузив полученные байты в матрицу NumPy и транспонировав её. Такой метод либо заработает идеально с первого раза, либо выведет полную чушь. В последнем случае нужно будет изменять порядок операций, пока всё не заработает. На деле гораздо проще, чем в теории.Как я говорил, SMBus не позволит отправлять более 32 байт за раз, несмотря на то, что это простое устройство I2C. Этот нюанс можно обойти, обратившись к устройству напрямую из скрипта Python. Хитрость в том, чтобы использовать
ioctl
для настройки адреса слэйва. В заголовочном файле ядра i2c-dev.h
есть определения констант, среди которых нас интересует лишь I2C_SLAVE
.import io, fcntl
dev = "/dev/i2c-4"
I2C_SLAVE=0x0703 # из i2c-dev.h
i2caddr = 0x3c
bus = io.open(dev, "wb", buffering=0)
fcntl.ioctl(bus, I2C_SLAVE, i2caddr)
bus.write(bytearray([0x00, 0xaf]))
Поочерёдно отправляя 1024 байта нулей или
0xFF
, я смог оценить, насколько быстро при этом обновлялся дисплей. Быстрее всего обновление происходило при одновременной отправке 256 байт. Не уверен, является ли это ограничением оборудования I2C (может, есть какой-то дополнительный уровень буферизации?)В таких условиях я смог добиться скорости в районе 5-10 кадров в секунду (против 2 кадров, которыми ограничивается SMBus). Думаю, что DDC работает на 100кГц, но всё равно он превосходит те возможности, ради которых создавался.
Превращение в монитор
Можно просто заставить приложение делать отрисовку прямо на этом экране, но меня такой вариант не устроит – хочу, чтобы это был монитор.
(Я даже не уверен, что у нас тут конкретно за приложение, но это не играет роли. Хочу, чтобы у меня получился монитор!)
Можно также написать собственный видеодрайвер. Несмотря на богатый обучающий потенциал, это бы означало невероятный объём усилий, а я планирую закончить сегодня вечером.
В природе существует множество dummy-видеодрайверов, которые используются в консольных машинах для активации VNC и прочего. В нашем случае должен сгодиться
xserver-xorg-video-dummy
, но есть у меня неприятное чувство, что он не сработает как надо, поскольку у нас также присутствуют и реальные выходы на дисплей. Есть ещё Xvfb
, виртуальный буфер кадров, но он тоже не особо сгодиться, если мы хотим расширить на него рабочий стол. Так как я использую
xorg
, то наиболее правильным способом имитировать монитор, не тратя на это несколько дней, будет xrandr
.xrandr
– это и библиотека, и утилита командной строки пользовательского пространства.На знакомство с терминологией этого инструмента у меня ушло немало времени, так как объясняется она не лучшим образом.
-
framebuffer
– это весь рабочий стол, то есть то, что сохраняется во время скриншота. -
output
– это физический видеовыход. -
monitor
– это виртуальная область, которая обычно отображается на часть или весь буфер кадров и, как правило, соответствует одному выходу. Если окно развернуть, то оно заполнит весь монитор.- Однако можно настроить один выход на дисплей под несколько мониторов. Например, чтобы разделить широкий экран на два монитора.
- И наоборот, несколько выходов могут относиться к одному монитору, то есть несколько физических экранов могут рассматриваться как один. В таком случае развёрнутое окно охватит их все.
-
mode
– это формат видео, состоящий из как минимум ширины, высоты и частоты кадров. В частности, здесь используются моделайны VESA CVT, которые можно генерировать с помощью утилитыcvt
.-
addmode
иdelmode
вxrandr
служат для связывания имеющегося режима с выходом на дисплей. -
newmode
иrmmode
вxrandr
служат для добавления на сервер нового режима, который затем можно связать с выходом.
-
Обратите внимание, что этот список относится исключительно к
xrandr
. В других контекстах Linux термины output
, display
, monitor
и screen
используются иначе.На моём ноутбуке вызов
xrandr
показывает пять видеовыходов: eDP-1, являющийся основным экраном с огромным множеством доступных режимов, и четыре отключённых (HDMI-1, HDMI-2, DP-1, DP-2), три из которых предположительно доступны через Thunderbolt или другие интерфейсы.Имитация монитора, попытка 1
Если подумать, то лучше всего для этого будет убедить
xrandr
, что один из неиспользуемых видеовыходов подключён. Для инструментов вроде VNC есть целый рынок «заглушек», которые заставляют видеокарту думать, что монитор подключён. Естественно, нам этого делать не нужно, а нужно заставить xrandr
вести себя должным образом программно.Для того чтобы вывести наше аномально низкое разрешение 128х64 по HDMI, в теории сначала нужно сгенерировать моделайн CVT:
$ cvt 128 64
# 128x64 39.06 Hz (CVT) hsync: 3.12 kHz; pclk: 0.50 MHz
Modeline "128x64_60.00" 0.50 128 136 144 160 64 67 77 80 -hsync +vsync
Затем этот режим добавить на сервер X:
$ xrandr --newmode "128x64_60.00" 0.50 128 136 144 160 64 67 77 80 -hsync +vsync
На этом этапе
xrandr
отображает в конце своего вывода неиспользуемые режимы. Здесь может показаться, будто режим является частью последнего выхода из перечисленных, но это не так (пока). Вот теперь мы добавляем этот режим к одному из выходов:xrandr --addmode HDMI-1 128x64_60.00
И пробуем использовать:
xrandr --output HDMI-1 --mode 128x64_60.00 --right-of eDP-1
Добавлю, что у меня на ноутбуке есть сочетание клавиш, которым я переключаю режимы дисплея, поэтому я вполне могу пробовать здесь всё что угодно. В противном же случае есть вероятность, что вы ничего не увидите. Хотя всё равно должна быть возможность получить доступ к другим виртуальным терминалам через Ctrl+Alt+F2 и так далее, поскольку они настраивают дисплей при помощи KMS (механизма смены видеорежимов средствами ядра), который расположен на уровень ниже сервера X.
Я попробовал использовать и HDMI-1, и HDMI-2. Оба выводятся в списке как отключённые. Наш кабель, подключённый к HDMI-1, подтягивает контакт Hot Plug Detect вверх, но на стандартные запросы DDC не отвечает.
Возможно, я использовал не все возможности, но добиться результата так и не смог. Подозреваю, что видеодрайвер просто не может справиться с этим смехотворным разрешением, а моделайн – это просто мусор. Частота 39.06Гц определённо меня заинтересовала, но когда я снова попробовал установить конкретно это значение, ничего не получилось.
Честно говоря, подобное издевательство над видеовыходами в любом случае кажется плохим решением.
Чтобы зачистить весь этот беспорядок, сначала отвяжите режимы от любых выходов командой
--delmode
, а затем командой --rmmode
удалите их с сервера X.Имитация монитора, попытка 2
Когда вы изменяете настройки дисплея,
xrandr
обычно выставляет все связанные параметры автоматом, но если углубиться, то их можно подстроить и вручную. Как пишут в интернете, для создания виртуального монитора можно просто расширить буфер кадров и определить этот монитор в нём, не заморачиваясь связыванием его с выходом.Интересно, что если сделать буфер кадров больше необходимого, то по умолчанию при приближении курсора мыши к краю он будет автоматически панорамироваться. Это полезно знать, но здесь нам необходимо такую возможность исключить. Опция
--panning
получает до двенадцати параметров: для panning area
(области панорамирования), tracking area
(области отслеживания) и border
(границ).Область отслеживания – это та, которой ограничено перемещение курсора мыши. Как правило, для панорамирования, отслеживания и буфера кадров устанавливается одинаковый размер. Я не уверен, что именно представляет в данном контексте «граница», так как при корректировке этого параметра никаких изменений не заметил.
При установке панорамирования на
0x0
оно отключается, но это также ограничивает и область отслеживания, так что мышь новой части буфера кадров достичь не сможет. Вместо этого, мы ограничим панорамирование до размера основного монитора, по сути, его отключив, и расширим область отслеживания, добавив наш новый участок буфера кадров. Вот вся соответствующая команда:xrandr --fb 2048x1080 --output eDP-1 --panning 1920x1080/2048x1080
Затем можно определить новый монитор, который будет находиться в этом новом участке буфера:
xrandr --setmonitor virtual 128/22x64/11+1920+0 none
Размер выставляется в пикселях и мм. Предполагаю, что это где-то 22х11мм.
virtual
– это имя нового монитора, его можно назвать как угодно. none
– это выход. Просмотреть мониторы можно командой xrandr --listmonitors
, а впоследствии удалить всё это безобразие командой xrandr --delmonitor virtual
.Теперь я могу указать скрипту выводить эту часть буфера кадров на OLED-экран. Ура! Правда, есть у этого метода один нюанс – отслеживание здесь не L-образное, и мышь может попадать в полосу буфера, которая не соответствует ни одному монитору. Не знаю, есть ли для этого какое-то простое решение, но если уж будет очень напрягать, то можно установить допустимые положения курсора в скрипте через
Xlib
.Считывание буфера кадров
Я предполагал, что на этом этапе мне придётся выбросить свой скрипт Python, но есть библиотека
python-xlib
, которая даёт доступ почти ко всему, что нам нужно. Немного напрягает отсутствие какой-либо документации к ней, да и имена методов не совпадают, к примеру XGetImage
— теперь называется root.get_image
.Любопытный факт. Знаете ли вы, что курсор мыши отображается аппаратно? Думаю, в этом есть смысл. Это также объясняет, почему он обычно не попадает на скриншоты. Но нам нужно захватывать буфер кадров вместе с курсором поверх него, так что здесь потребуется намного больше работы.
Получить изображение курсора, как правило, можно с помощью
XFixesCursorImage
, но в python-xlib
ещё не реализовали все возможности из XFixes
. Я уже был готов начать заново в Си, но обнаружил, что кто-то всю эту работу проделал до меня, реализовав привязывание к X11/XFixes
при помощи ctypes
специально для получения информации о курсоре.Теперь у нас есть всё необходимое для захвата изображения нового виртуального монитора, наложения курсора в нужном месте (не забывая об
xhot
и yhot
, смещения изображения указателя/курсора), преобразования результата в монохромное изображение с необходимой перетасовкой бит и непрерывного вывода этого изображения на дисплей.Это четвёртое рабочее пространство i3 с отсутствующим
i3status
и странным искажением в верхней части фона. Красота!Демо
Итоги
Для повышения частоты кадров можно доработать скрипт, чтобы вместо полной перерисовки изображения для каждого кадра отправлять только его изменения. Но как бы фантастически это ни звучало, учитывая абсолютную бесполезность этого второго крохотного экрана, реализовывать подобную затею я не собираюсь.
Если вы вдруг по какой-то безумной причине решите повторить это сами, то скрипт лежит на GitHub.
Комментарии (20)
fforthuser
08.04.2022 14:10-4Bright_Translate Автор
08.04.2022 14:11+15В той статье ничего практически нет от оригинала. Сравните ради интереса содержание. У нас полная версия со всеми деталями, а не приблизительный пересказ, как в статье по вашей ссылке.
fforthuser
08.04.2022 17:26-2Ок,
но факт остаётся фактом даже в полном его изложении. :)Bright_Translate Автор
08.04.2022 17:29+6:) Хорошо, если говорить по фактам, то там не перевод статьи, а ее описание;)
daniilshat
09.04.2022 12:16+2По вашей ссылке новостная заметка, а тут полноценный перевод материала. Это разные жанры))
tigreavecdesailes
08.04.2022 17:44+14Пришлось посмотреть исходники, чтобы понять что на самом деле (перевод механический, увы) этот братюня хотел сделать с монитором. Для тех, кто тоже дочитал до конца с туманом в голове после раздела с xranrd, объясняю:
Чувак запустил питон-скрипт, который в бесконечном цикле без задержек делает скриншот экрана, затем его конвертирует (цвет/разрешение), рендерит на нем изображение курсора левым пакетом и загоняет полученную картинку на свой OLED экранчик. Показания LA из top предусмотрительно не приводятся ))
Но в целом, молодец, конечно. Как-то всегда факт наличия физического I2C наружу на борту любого компа выпадает из внимания.
dlinyj
08.04.2022 22:33+2Меня статья порадовала тем, что это удобный способ вывести айтуси себе на стол для экспериментов.
А по поводу механического перевода, достаточно сравнить выхлоп гуглопереводчика и этой статьи, чтобы понять что вы ошибаетесь. Переводчики тоже живые люди, могут не так сформулировать. Или в попытке более литературно передать, получить искажение смысла.tigreavecdesailes
08.04.2022 23:14+3Reading the framebuffer = Считывание буфера кадров ?
I was prepared to start over in C = Я уже был готов начать заново в Си ?
which binds to X11/XFixes = реализовав привязывание к
X11/XFixes ?
Мы (не переводчики, а программисты) же знаем, что фреймбуфер и биндинги как пишутся, так и произносятся :) А программируют НА Си, а не в Си. И вся статья в таком. Перевод механический в том смысле, что без понимания предметной области на нужную глубину. И по факту плохой (моя личная оценка), потому что я, зная всё что написано в статье, не сразу вкурил детали реализации)
Но, тем не менее, спасибо и за такой, на русском читать всё равно менее лениво))
Bright_Translate Автор
09.04.2022 04:13+2Спасибо за более предметную оценку, возьму на заметку. Так по крупицам понимание нюансов и копится.
Ясно, что программируют НА, а не В том или ином языке. Просто именно в этом контексте В прозвучало лично для меня уместнее.
Насчет привязки в оригинале дословно получается, что «репозиторий привязывается к X11/XFixes» — так было бы правильнее?
Бесспорно, профессионалы в области во многих случаях выразятся лучше. Но ведь в каждой области профессионалом не станешь. Да и авторы в статьях иной раз формулируют и называют вещи так, что не каждый профессионал понять может. Да и у программистов что, ошибок в коде не бывает? ))
Intolerambler
08.04.2022 21:04+1Неплохо. Встроить этот дисплей в клавиатуру и выводить на него нужную информацию, не отрываясь от основного занятия.
А вот это певеселило: i915gm + относительно новому ноутбуку )) HDMI-то чем там реализован, в чипе нет поддержки HDMI.
AllexIn
08.04.2022 21:18+1Встроить этот дисплей в клавиатуру и выводить на него нужную информацию, не отрываясь от основного занятия.
И тащить еще HDMI провод к клаве? Не нужно. Композитный USB - и имеем через один провод несколько устройств. Одно - клава. Второе - наше устройство для отображения информации.
Intolerambler
08.04.2022 21:29Так и поступают производители. А я пишу про применение автором, он уже всю работу сделал практически.
iiiytn1k
08.04.2022 23:06+6Занимательный троллейбус из буханки получился. Честно говоря, даже не предполагал, что можно вот так просто рулить I2C-шиной HDMI порта.
FirstEgo
10.04.2022 06:15+1С удовольствием читаю про подобные троллейбусы! И, более того, хотел бы рассмотреть такой внешний монитор на цветном дисплее. Правда, fps там будет явно ниже((
le2
10.04.2022 02:08+215+ лет назад слышал байку, как уже сделали изделие для военных и оказалось что заказчику нужен еще один порт. Технари сказали — «все пропало», а заказчик сказал что «я в технике не разбираюсь, но вижу свободный порт VGA».
В результате также вытащили сигнал I2C из VGA.
ramiil
Мсье, уважаю.