LLM после работы: интерактивная книга на ESP32-P4. Часть 2: ESP-IDF, дисплей и первая анимация


В предыдущей статье мы с дочкой начали собирать собственную интерактивную книгу на ESP32-P4, где дочь придумывает основных персонажей и является источником вдохновения, а я с помощью нейросетей превращаю её фантазии в реальность в виде изображений, звуков и анимации. Делаем несколько сюжетных веток, где решение принимает ребенок, получаем 16 различных концовок, и всё это на английском языке для пущего образовательного эффекта.
Смысл затеи: провести вместе время за созиданием, так, чтобы было интересно и дочери (придумать, повозиться с результатом), и мне (освоить новую панель и ИИ), а также получить крутую развивающую штуку (логика, память, английский язык). Один из комментаторов к прошлой статье назвал это новой настоящей формой технологического искусства – не буду с ним спорить.
Делаем книгу на отдельной тач-скрин панели (Elecrow CrowPanel Advanced ESP32-P4), т.к. хотелось сделать самостоятельное устройство, а не просто приложение на планшете или телефоне. Одно устройство – одна функция: без дополнительных соблазнов в виде ютьюба. Кроме того – сами панели весьма интересные, они не так давно появились, и чего на них только сейчас не делают – это такой очень бюджетный вариант сделать полноценный HMI для чего угодно.
В этой статье настроим среду разработки и сделаем первые шаги: запустим дисплей, сенсор, разберемся с LVGL и научимся выводить свою графику. Исходники – выложены в репозиторий, берите, делайте похожее или что-то совершенно новое, буду рад, если поделитесь результатом, можем выкладывать ваши работы в тот же репозиторий.
Настройка ESP-IDF и PSRAM
Для работы с MIPI DSI и тяжелой графикой мы используем официальный фреймворк ESP-IDF 5.5.2. Эта версия выбрана не случайно: официальные BSP от Elecrow для панелей требуют версию не ниже 5.4.2, а в ветке 5.5 были исправлены критические баги с таймерами и энергопотреблением для чипов ESP32-P4.
Первое, что нужно сделать перед сборкой – включить внешнюю память. Без этого при попытке загрузить картинку разрешением 1024×600 (которая занимает больше мегабайта) система упадет с ошибкой Out of Memory.
В menuconfig переходим в раздел Component config -> ESP PSRAM. Включаем Support for external PSRAM.

На плате CrowPanel распаяно 32 МБ PSRAM. Мы выбираем режим 16-Line-Mode PSRAM и устанавливаем частоту 200 МГц. Такой режим работы обеспечивает высокую пропускную способность, необходимую для быстрой подгрузки кадров анимации в буфер дисплея.
Инициализация BSP и выделение задачи для LVGL
Плата использует высокоскоростной интерфейс MIPI DSI для связи с дисплеем. Настраивать его тайминги вручную – долго и чревато ошибками, поэтому мы используем готовый компонент espressif__esp32_p4_function_ev_board от Elecrow.
Вся инициализация железа происходит в функции app_main:
extern "C" void app_main(void)
{
bsp_display_cfg_t display_cfg = {
.lvgl_port_cfg = ESP_LVGL_PORT_INIT_CONFIG(),
.buffer_size = BSP_LCD_H_RES * BSP_LCD_V_RES,
.double_buffer = BSP_LCD_DRAW_BUFF_DOUBLE,
// ... флаги настройки буферов
};
// Привязываем задачу LVGL к конкретному ядру
display_cfg.lvgl_port_cfg.task_affinity = kLvglTaskCore;
if (!bsp_display_start_with_config(&display_cfg)) {
ESP_LOGE(TAG, "display init failed");
return;
}
}Здесь мы видим, как настраивается порт для графической библиотеки LVGL. Ключевой момент – параметр task_affinity.
ESP32-P4 двухъядерный, и мы можем распределить выполнение задач между ядрами. В частности, задачу LVGL, которая отвечает за обработку графического интерфейса и анимаций, закрепляем за конкретным ядром (в нашем случае kLvglTaskCore). Пока это ядро занято пересчетом пикселей и анимаций, второе ядро ESP32-P4 остается свободным для выполнения фоновых задач: чтения файлов с SD-карты или обработки логики.
Сам BSP внутри себя поднимает esp_lcd, настраивает MIPI DSI и инициализирует тач-контроллер GT911.
Анимация заставки на LVGL
На экране отображается сама книжка, а вокруг неё появляются и перемещаются графические элементы. Для движения облаков и звёзд используем встроенный механизм анимаций LVGL.
В LVGL для этого есть lv_anim_t. Он позволяет менять свойства графических объектов во времени: например, перемещать их из одной точки в другую.
В нашем случае элементы заставки движутся по вертикальной оси Y.
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, obj);
lv_anim_set_exec_cb(&anim, title_screen_anim_set_y_cb);
lv_anim_set_values(&anim, group[i].final_y + start_offset_y, group[i].final_y);
lv_anim_set_time(&anim, duration_ms);
lv_anim_set_path_cb(&anim, lv_anim_path_ease_out);
lv_anim_start(&anim);В этом фрагменте мы создаем объект анимации, задаем ему начальное и конечное положение по оси Y (заставляя элементы «выезжать» на экран) и применяем функцию плавности lv_anim_path_ease_out. Это позволяет облакам и звездам на заставке двигаться естественно, без резких рывков.
В принципе, для первых экспериментов с анимацией этого достаточно. Можно создавать несколько графических объектов и запускать для них разные движения, используя стандартные средства LVGL.
Дальше эту же графическую библиотеку будем использовать и для остальных элементов книги.
Аппаратные нюансы: SD-карта и питание
В процессе выяснились две особенности, которые могут вас затормозить, когда вы будете делать свой вариант книги, если о них не знать.
Карта памяти
Так как ресурсов (картинки, звук) много, а памяти на самой панели – мало, их приходится хранить на внешней microSD карте.

При самой первой загрузке панель может не увидеть карту памяти из-за особенностей инициализации контроллера. В этом случае достаточно перезагрузить панель (выключить и включить питание), и карта прочитается корректно. А, ну и карта должна быть отформатирована в FAT32. Ещё замечание (столкнулся с этим уже в следующем проекте, там была существенно выше скорость чтения с карты) – не все карты одинаковые. Могут отваливаться, если карта у вас старая (ниже Class 10), или неоригинальная (заказывал на озон несколько разных карт одинакового класса у разных продавцов, пока не пришло то, что работает). В этом проекте вряд ли эта ошибка появится, но для будущих проектов – имейте в виду.
Питание
Ещё одна странность, не претендую на правильность решения, но у меня сработало.
Когда запускается тяжелая анимация и громкий звук, нагрузки на шину питания возрастают. Случается, что панель внезапно перезагружается, или гаснет экран.
Опытным путём установили, что можно подключить второй кабель USB во второе гнездо (USB 2.0), и этот эффект пропадает.

Логика работы приложения: Story Loop и Full Story
Сейчас в прошивке реализовано два режима работы: демо и интерактивный.
Демо-режим (Story Loop): панель сама крутит историю, если лень принимать решения. Также это инструмент для отладки – можно запустить сюжет по кругу и проверить, не утекает ли память при долгой работе.
Интерактивный режим (Full Story). Это тот самый режим для дочки, ради которого всё затевалось. История останавливается на развилке и ждет, пока ребенок сам нажмет на кнопку.
Для реализации автоматического перехода в режиме Story Loop используется программный таймер LVGL – lv_timer_create(). После появления экрана выбора запускается таймер на 10 000 мс (STORY_LOOP_CHOICE_DELAY_MS). Если за это время касания не произошло, таймер срабатывает и программно эмулирует нажатие кнопки.
В выложенных исходниках в режиме Full Story доступна только одна сюжетная ветка. Недоступные варианты выбора блокируются: кнопка получает состояние LV_STATE_DISABLED, поверх нее отрисовывается полупрозрачный слой, а на уровне логики координатора блокируется обработка события выбора. Это защищает от случайного перехода в незаконченные ветки сюжета.
Работа с настройками и состояние паузы
Экран настроек (где регулируется громкость и яркость) можно открыть прямо во время чтения. Чтобы анимации и звук не конфликтовали с меню, реализована система паузы.

При открытии настроек функция settings_opened() сохраняет текущую фазу сюжета, а затем удаляет все активные таймеры через lv_timer_del() (включая 10-секундный таймер ожидания выбора). Блокировка мьютексами здесь не используется, так как удаление таймеров безопасно останавливает логику. После закрытия настроек сценарий запускается с сохраненной фазы, но отсчет времени на выбор начинается заново.
Что получилось и что дальше
Пока мы рассмотрели графическую часть – у нас есть настроенная среда, работающий дисплей, LVGL. Сделали заставку с несколькими анимированными объектами и стали понятны некоторые особенности панели.
Дальше – покажу, как устроен сюжетный движок и хранятся данные, и покажу, как генерили картинки, звуки, тексты и речь, поделюсь промптами (там хоть и не «рокет сайенс», но не будет лишним, особенно если вы не генерили раньше много картинок в одном стиле, сюжете, с одинаковыми героями).
Если у вас такая же панель (подойдут и 7 и 9 и 10.1 дюйма от Elecrow на P4), можете попробовать запустить нашу книгу и посмотреть, как она работает, взять исходники и собрать свой собственный вариант. Повторюсь, буду рад – если кто-то попробует и поделится своим вариантом, очень интересно, что получится, можно будет добавить в тот же репозиторий, получим «библиотечку» таких DIY интерактивных книжек.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.