The Jerusalem PostIDF reinforces West Bank troop deployment amid concern of terror attacks over holidaysESPNTaylor Swift 12, Purdue 0: Boilermakers' album release curse continuesInquirerMarcos meets with Eastern Visayas troops; lauds security, peace effortsBollywood HungamaAnubhav Sinha launches music label ‘Benaras Beat’ with exclusive songs to be unveiledPunchHaaland leads Ronaldo, Mitrović, Gyokeres in Nations League all-time scoring chartUN NewsThe Takeaway: UN General Assembly debate Day 5CNN TürkSON DAKİKA... Antalya Milletvekili Erdem hakkında 'rüşvet' suçundan soruşturma한겨레추석 연휴 마지막날 한강공원 흉기소지 40대 검거…다친 사람은 없어UOLMulher é agredida com soco e ameaçada de morte por namorada em Jandaia do SulColliderTom Hardy’s 8-Episode 'Peaky Blinders' Follow-Up Officially Lands on Free StreamingCBS NewsSeveral arrested, bomb disposal deployed near air base used by U.S. forcesAnime News NetworkMonster Strike Franchise Gets New Mera×Death: Shinigami to Boku no Ijō na Koi TV Anime Starting on January 5
The Daily Newsstand · Free, Always
Sunday, September 27, 2026

[Перевод] Как я запустил Bad Apple с частотой 20 fps на E-Ink дисплее за $71

Translate

Мне удалось запустить полноценное чёрно-белое видео на стоковой плате ESP32-S3 с экраном на электронных чернилах: никакой ПЛИС и никакого ПК — панель управляется напрямую по параллельной шине. В статье я подробно расскажу, как работает этот драйвер. Если коротко, то в нём используется векторный блок, принимающий по 64 пикселя за раз; управляющие сигналы (waveforms), которые хранятся в регистрах, а не в таблицах, и дельта-движок, который обрабатывает только изменения. При этом точность всей системы контролировалась с помощью планшетного сканера.

Рис. 1 · Воспроизведение ролика Bad Apple на 20 к/с:

Flatbed scan of the e-paper panel showing a clean white silhouette with detailed hair outline on a solid black background, a paused frame of the Bad Apple video

Рис. 2 · Стоп-кадр, полученный путём сканирования панели на планшетном сканере Canon с разрешением 300 dpi. Глубокий чёрный, чёткие края силуэта, и текстура дизеринга на заднем фоне полностью сохранена. Это реальная картинка с панели, а не скриншот.

Что это за панель

В качестве драйвера для параллельных E-Ink панелей, не имеющих встроенного контроллера (такие используются в Kindle) и работающих на ESP32-S3, применяется опенсорсный EPD_Painter. На данный момент он поддерживает платы M5PaperS3 и LilyGo T5. Эти панели представляют собой голое стекло — у них нет ни контроллера, ни буфера кадров, ни набора команд. Только сдвиговый регистр драйвера столбцов вдоль одного края и драйвер строк вдоль другого.

На них вы потактово загружаете данные управления для 960 пикселей в сдвиговый регистр, подаёте импульс на защёлку, переключаетесь на следующую строку и повторяете это 540 раз. Это один проход. Но электронные чернила не сдвинутся за один проход; электрофоретические частицы перемещаются только пока подано напряжение, поэтому изображение формируется за семь проходов, на каждом из которых каждому пикселю подаётся команда потемнеть, посветлеть или перейти в режим float.

То есть серый — это не какое-то значение, которое вы записываете, а определённая доза энергии: она зависит от того, сколько выполняется проходов и как долго длится каждый из них. Большинство драйверов микроконтроллеров выдают таким способом всего несколько кадров в секунду, и типичный путь к получению полноценного видео на электронных чернилах — это использование ПЛИС с прописанными вручную сигналами управления. В свою очередь, драйвер EPD_Painter делает ставку на то, что возможностей кремния на готовой плате ESP32-S3 за $71 хватит ровно впритык при условии, что ему не придётся делать лишнюю работу. И в остальной части статьи я покажу, что конкретно эта экономия усилий означает на практике.

Общая структура конвейера

Три буфера, три задачи — каждая в наиболее подходящей области памяти:

Кадр из Adafruit GFX / LVGL / готовые кадры с SD-карты
      │  отрисовка
8-битный холст ......................... PSRAM, 518 KB
      │  SIMD-расчёт дельты относительно состояния панели (64 px на операцию)
2-битные слои чернил + маски строк ....... внутренняя SRAM, 130 KB
      │  ×7 проходов: преобразование в сигналы (только через регистры)
240-байтовые буферы строк .............. внутренняя SRAM, двойная буферизация
      │  LCD_CAM DMA
Драйвер столбцов панели ............... стекло

Приложения могут рисовать в любой области обычного 8-битного холста с помощью Adafruit GFX или LVGL. Видеоплеер же обходит этот холст полностью и сразу передаёт кадры, уже упакованные по два бита на пиксель (их генерирует конвертер на Python: оттенки серого с линейным коэффициентом отражения, упорядоченный дизеринг, сжатие PackBits RLE — всего выходит около 4,4 КБ на один кадр Bad Apple, передаваемый напрямую с SD-карты). Всё, что находится ниже указанной черты, уже ложится на плечи самого драйвера и построено вокруг одного принципа: минимум считываний каждого байта и их выполнение в самой быстрой памяти, в которой он только может находиться.

Платим только за изменения

Драйвер хранит в памяти зеркальную копию того, что физически отображается на стекле, в формате 2 бита на пиксель. При поступлении нового кадра запускается один SIMD-проход, который сравнивает его с этим «зеркалом» и генерирует команды для чернил. В итоге получается слой затемнения (пиксели, проступающие на белом фоне) и слой осветления (стираемые пиксели), а также битовая маска для каждой строки, куда записывается, какие 64-пиксельные блоки включают изменения. Эти два слоя по своей структуре на уровне пикселей не пересекаются: изменившийся пиксель может двигаться либо в сторону затемнения, либо в сторону осветления.

Все последующие процессы пропускаются через эти маски. Строка с пустой маской вообще не преобразуется и больше не считывается ни в одном из семи проходов. Отрисовка статичного экрана не стоит практически ничего, а показ видео с «говорящей головой» тратит ресурсы только на отображение этой головы. Именно это свойство и делает воспроизведение видео на электронных чернилах возможным в принципе: самая медленная особенность панели (необходимость совершать семь проходов) умножается только на те пиксели, которые реально изменились.

Проход вычисления дельты — это ещё один этап, на котором мы тратим PSRAM, причём ровно один раз. Сравнение происходит в момент чтения нового кадра. Копирование данных в рабочий буфер драйвера объединено в такой же 64-пиксельный цикл, поэтому за один кадр каждый байт холста передаётся по шине PSRAM ровно один раз. После этого семь проходов выполняются исключительно внутри SRAM.

По 64 пикселя на инструкцию

Процессор ESP32-S3 таит в себе реально полезный секрет — векторный сопроцессор PIE со 128-битными регистрами и возможностью выполнять операции со строками байтов (byte lane) за один такт. Весь путь обработки пикселей в драйвере написан под него на ассемблере. Формат «2 бита на пиксель» выбран не ради сжатия, а ради выравнивания данных: 64 пикселя заполняют ровно один векторный регистр, поэтому сравнение, выборка, маскирование и упаковка выполняются для 64 пикселей за одну команду. Определение дельты для всего экрана (оба слоя, оба буфера, генерация маски и обновление «зеркала») для полумиллиона пикселей занимает всего 6,4 мс.

Некоторые могут возразить, что использовать здесь SIMD бессмысленно, так как всё упирается в пропускную способность памяти. Но именно поэтому буферы и распределены таким образом: векторный код снабжается данными из внутренней SRAM на частоте ядра, а PSRAM задействуется всего раз при описанном выше совмещённом проходе. Векторный блок никогда не упирается в пропускную способность памяти — специально созданная схема размещения данных полностью исключает такую возможность.

Сигналы управления хранятся в регистрах, а не в таблице

Теперь опишу изменение модели, которое даёт самый большой выигрыш. Логика сигналов управления сопоставляет 2-битное значение пикселя с кодом управления для текущего прохода. В классической реализации из учебника для этого используется таблица соответствия в RAM, к которой обращаются для каждого пикселя на каждом проходе. При разрешении 960×540 и 7 проходах это даёт 3,6 миллиона чтений из памяти на один кадр ещё до того, как хоть один байт дойдёт до панели.

Но EPD_Painter обрабатывает сигнал управления как состояние, а не как данные. Вся колонка сигналов для одного прохода занимает лишь несколько байтов. В начале каждого вызова функции преобразования она однократно дублируется во все векторные регистры. С этого момента перевод упакованных пикселей в коды управления происходит исключительно с помощью регистров: через сдвиги, сравнения и выборку — по 64 пикселя за раз и без единого обращения к таблицам в памяти. Строка из 960 пикселей обрабатывается всего за 15 ассемблерных инструкций. Преобразование всех семи проходов цельного кадра таким образом занимает 15,7 мс, а за счёт масок строк это время сокращается, и кадр с минимумом движений обрабатывается в разы быстрее.

Вывод строки через DMA с одновременным преобразованием следующей

Параллельная шина панели управляется через периферийный блок LCD_CAM процессора S3. Изначально этот блок предназначен для дешёвых TFT-экранов, но он отлично справляется и с потактовой передачей 240 байт данных строки в сдвиговый регистр E-Ink панели. Для буферов строк используется двойная буферизация во внутренней SRAM: пока DMA-контроллер передаёт на панель строку n, векторный блок уже преобразует строку n+1. В этом случае процессор не использует программное управление линиями (bit-banging) для вывода пикселей и никогда не ждёт окончания передачи по шине — время передачи полностью скрывается временем преобразования. Единственное, к чему прикасается процессор — это управляющие сигналы строк (стартовый импульс, тактирование строк, защёлкивание), поскольку именно на их фронтах и завязана вся физика панели, что я наглядно показываю в следующем разделе на собственном нелёгком опыте.

Эксперимент, который панели не зашёл: двойная активация одной строки

Один коварный сценарий поставил под угрозу всю схему: когда в 64-пиксельный блок попадает движущийся край силуэта, в ходе одного обновления кадра на передней границе движения часть пикселей нужно затемнить, а на задней — осветлить. В своей ранней версии движок мог обрабатывать только одно направление изменения цвета для блока за кадр, а второе откладывал на потом. При непрерывном воспроизведении видео это приводило к появлению размытых, тянущихся шлейфов.

И здесь напрашивалось очевидное аппаратное решение. Оно опиралось на то, что драйвер строк удерживает строку выбранной до тех пор, пока не будет подан тактовый импульс CKV, а контакт защёлки (LE) в течение этого времени можно активировать неоднократно. Алгоритм в этом случае простой: защёлкнуть данные затемнения для строки, затем задвинуть данные осветления и применить их к той же строке, после чего уже перейти к следующей. Так мы получаем оба направления изменения цвета для каждой строки за один проход. Эту схему мы и реализовали: создали двухслойный детектор дельты, формирующий раздельные данные для затемнения и осветления, и настроили в драйвере строк режим защёлкивания без перехода к следующей строке.

Photo of the e-paper panel showing a barely visible, washed-out sketch-like image with scattered horizontal black dashes

Рис. 3 · Результат работы механизма с двойным защёлкиванием: блёклые края, похожие на карандашный набросок, и хаотичные штрихи, длиной в целый пиксельный блок. Я ошибся, но ошибка вышла информативной: слою затемнения повсеместно не хватало энергии, и доминировал слой осветления.

Дисплей работал. Картинка выглядела, как на рис. 3: едва заметные тёмные области и блёклые края, напоминающие наброски карандашом. Отлаживать это дело, просто глядя на фото с телефона, было бесполезно, поэтому мы не стали гадать и решили всё измерить. Мы уже использовали планшетный сканер Canon в качестве фотометра, поэтому просто написали тестовый шаблон: сырые коды управления, никакого дельта-движка и три зоны однопиксельных линий, выводимых с интервалом в пять строк. В первой зоне каждая линия формируется как обычно (контрольный образец). Во второй формируется линия, а затем на той же строке без перехода к следующей все пиксели защёлкиваются в состояние float. В третьей зоне порядок меняется на противоположный: сначала float, а чёрный цвет подаётся только при втором защёлкивании без перехода к следующей строке. Прошиваем плату, сканируем панель с разрешением 300 dpi и измеряем положение линий. 

The LilyGo T5 e-paper board lying face-down on a flatbed scanner's glass, USB-C cable running out from under the lid

Рис. 4 · Измерения. Тестируемое устройство лежит экраном вниз на стекле сканера и USB-кабелем подключено к компьютеру. Цикл полностью замкнут: прошивка памяти, управление панелью, сканирование результата, программное измерение пикселей. Человеческая оценка в цепочке отсутствует

Flatbed scan of the panel showing three horizontal bands: fine stripes at top, a blank middle band, fine stripes at bottom

Рис. 5 · Результат сканирования тестового шаблона. Верхняя группа (контрольная): линии на месте. Средняя группа (чёрный цвет, затем float): пусто. Нижняя группа (чёрный цвет подаётся только при втором защёлкивании): линии на месте, причём их смещение составляет не более 0,4 пикселя относительно сетки контрольной группы, продлённой на 57 периодов вниз. (Первая попытка закончилась полной пустотой на экране, так как мы перепутали полярность управляющих сигналов. Разобраться помогли их таблицы: конкретно на этой панели код 01 отвечает за затемнение).

Один скан ответил сразу на два вопроса. Нижняя группа линий показала, что сам механизм работает: повторное защёлкивание активирует ту же самую строку затворов без какого-либо смещения. А вот средняя группа наглядно продемонстрировала, что это всё равно тупиковая идея: подача сигнала float сразу после чёрного цвета почти полностью его стирала.

Эта пустая зона заставила нас пересмотреть логику панели. Время передачи данных для строки составляет всего около 30 мкс — слишком мало, чтобы сдвинуть чернила с места. По факту же пиксель работает иначе: каждая ячейка сохраняет защёлкнутое напряжение за счёт собственной ёмкости и продолжает двигать чернила до тех пор, пока в эту строку не будут защёлкнуты новые данные. А это, как правило, занимает время полного прохода — то есть в сотни раз дольше. Именно время удержания заряда определяет дозу энергии. И именно благодаря этому 540 строк фактически работают параллельно, управляясь от одного общего драйвера столбцов.

Получается, что при двойной активации строки за один проход практически вся доза энергии отдаётся тому слою, который обрабатывается последним. Первому же достаётся лишь «утешительный приз» продолжительностью 30 мкс. И это особенность самой панели, а не ошибка кода, и никакие теоретические рассуждения о защёлках, развалившись в кресле, не помогли бы это обнаружить.

Оба направления за одно защёлкивание

Эта же физика панелей и подсказала правильную схему. Формат данных при передаче составляет 2 бита на пиксель: 00 — float, 01 — затемнение, 10 — осветление. Направление изменения цвета никогда не было ограничением самого железа на уровне блоков пикселей — оно зависело лишь от кодирования в буфере. К тому же, слои чернил по своей структуре на уровне пикселей не пересекаются. Значит, оба направления можно объединить в одну передачу. Сначала мы преобразуем слой затемнения в буфер строки, а затем накладываем на него данные слоя осветления с помощью побитовой операции «ИЛИ» через тот же работающий в регистрах конвертер в варианте «загрузка—ИЛИ—запись». Одно защёлкивание и полное удержание заряда для каждого пикселя.

// Для каждой строки при каждом проходе: оба направления в одной передаче
memset(dma_buf, 0x00, row_bytes);                     // Float по умолчанию
convert_packed_fb_to_ink   (darkPlane,  dma_buf, row_bytes, darker_wf,  maskD);
convert_packed_fb_to_ink_or(lightPlane, dma_buf, row_bytes, lighter_wf, maskL);
sendRow();                                            // Одно защёлкивание, полная доза заряда

Всё, что раньше драйвер тащил на себе в попытках обойти эту проблему (очереди отложенных изменений, приоритеты направлений движения чернил и даже «режим чередования строк», который запускал три полных цикла управления для сглаживания артефактов), на самом деле только тормозило систему. Теперь от этого не осталось и следа. Правильный движок оказался одновременно и самым простым.

Настройка сигналов управления с помощью планшетного сканера

Панель отображает четыре уровня: белый, два оттенка серого и чёрный. Какую именно дозу энергии должны получать серые оттенки? В спецификации об этом ни слова. Партии панелей отличаются между собой, их свойства меняются в зависимости от температуры, а человеческий глаз склонен сглаживать любую картинку, которую ему показывают. Поэтому таблицы сигналов управления в драйвере мы калибровали оптическим методом по результатам измерений самой панели. Дисплей выводит тестовые карты, на которых сплошные серые области перемежаются с узорами упорядоченного дизеринга из его собственных чёрных и белых пикселей. Планшетный сканер считывает и то, и другое, а скрипт корректирует длительность импульсов для каждого прохода до тех пор, пока коэффициент отражения каждой сплошной серой области в числовом выражении не совпадёт с коэффициентом отражения дизеринга.

Именно сопоставление серых областей с сетками дизеринга, а не абстрактными целевыми показателями яркости, делает видеопоток бесшовным. Формируемые конвертером оттенки упорядоченного дизеринга и сплошные серые области панели попадают в одну и ту же оптическую шкалу. Благодаря этому, передаваемые через дизеринг градиенты остаются плавными и не распадаются на полосы. Каждая поддерживаемая плата поставляется с собственными настроенными таблицами. Режимы качества различаются в основном дозировкой энергии: в более качественных режимах задержки на стабилизацию между проходами являются частью самой откалиброванной дозы, тогда как в быстром режиме ради достижения 20 кадров в секунду ими приходится жертвовать.

Итоговые цифры и честное признание

На кадр (960×540, быстрый режим из 7 проходов)

Затраты

Вычисление дельты + загрузка из PSRAM (SIMD, для обоих слоёв)

6.4 мс

Преобразование управляющих сигналов на всех проходах (выполняется в регистрах)

15.7 мс

Передача строк на дисплей

Скрыта за преобразованием (через DMA)

Стабильная частота воспроизведения

20.0 к/с

И здесь я добавлю одно неловкое примечание, чтобы поделиться полезным опытом. На поздних этапах разработки моя сборка выдавала всего 17 к/с, и мы долго с уверенностью грешили на нестабильность конвейера DMA. Истинной же причиной оказалась забытая экспериментальная задержка 2 мс в цикле прохода. В итоге удаление буквально одной строчки вернуло заветные 20 кадров в секунду. В этом проекте любая причина замедленной работы по факту оказывалась чем-то осязаемым и измеримым, а совсем не тем, в чём мы себя убеждали. Обычный планшетный сканер и узор из линий на одном единственном снимке решили проблему, о которой мы до этого впустую рассуждали несколько дней.

Flatbed scan of another paused video frame: a figure with an outstretched hand, individual fingers resolved, on a dark patterned background

Рис. 6 · Ещё один стоп-кадр, отсканированный прямо во время воспроизведения (в плеер пришлось специально добавить команду паузы по последовательному порту, чтобы сканер мог сделать снимок для проверки видео). Здесь на движущемся силуэте различимы отдельные пальцы рук, а кружащаяся текстура фона отчётливо сохраняет глубокие чёрные тона.

Предыдущие эксперименты

Уже много лет как различные демонстрации видео на E-Ink экранах с использованием ПЛИС доказали, что сами панели вполне на такое способны. Проекты EPDiy и FastEPD первыми подтвердили, что микроконтроллеры могут управлять голыми панелями напрямую. Наш же проект находится на стыке их достижений: здесь и частота обновления на уровне полноценного видео, и поддержка оттенков серого, и оптическая калибровка, и работа через вычисление дельты. Причём всё это на доступном железе с использованием открытой библиотеки Arduino. Сама панель никогда не была узким местом — ей просто был нужен драйвер, который не делает лишней работы.

Код и документация лежат на GitHub. Подробный разбор конвейера обработки и физики удержания заряда ищите в файле How_It_Works.md, а плеер и видео-конвертер в папке examples/other/bad_apple.

Проект реализован Тони Вестоном в тандеме с Claude, который помогал с программированием. Анализ работы аппаратной части (эксперимент с двойным защёлкиванием, осознание принципа дозирования энергии за счёт задержек и его влияние на качество) произвёл Тони. Вклад ИИ — это тестовый шаблон, ассемблерные инструкции и та самая ошибочная теория про нестабильность DMA. Сканер не давал соврать нам обоим.

Характеристики панели: 4.7″ с разрешением 960×540 px, применяется на платах LilyGo T5 E-Paper S3 Pro ($71) и M5PaperS3.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.