Автор текста: the_bat
Приветствую всех энтузиастов!

Хотя название может показаться громким, в нём нет преувеличения. Этот проект — скорее увлекательный эксперимент, чем устройство для повседневного использования, но он демонстрирует возможности миниатюризации. Например, существуют микрокниги, созданные на плёнке, так почему бы не сделать свою версию на электронных чернилах? Я не стремился попасть в Книгу рекордов Гиннеса, но если присмотреться, можно найти кота на изображении. Эта статья — единый подробный рассказ, так что наберитесь терпения.
Истоки идеи и выбор компонентов
Идея использовать электронную бумагу для создания крошечной книги витала в воздухе давно. Изначально я приобрёл прямоугольный E-Ink дисплей с диагональю 2,13 дюйма, но, по незнанию, выбрал модель без поддержки частичного обновления экрана. Смена страницы требовала полного её стирания, что было долго и неэстетично. Проект заморозился, но позже моё внимание привлёк дисплей размером 1,54 дюйма (200x200 пикселей), и энтузиазм вернулся. После долгих раздумий нужный дисплей был заказан. Пока шла трёхнедельная доставка, я решил собрать макет на макетной плате, используя старый дисплей, чтобы не терять время.

Рисунок 1. Дисплей E-Ink на макетной плате (2,13 дюйма)
На плате были ошибки: неправильно выбраны контакты SPI для дисплея и допущена неточность в схеме подключения CP2102 в режиме автопрограммирования. Несколько перерезанных дорожек и три проволочные перемычки решили почти все проблемы.
Выбор платформы: почему ESP32?
Выбор пал на модуль ESP32 по нескольким причинам. Во-первых, он позволяет загружать файлы по Wi-Fi. Во-вторых, для него существует множество готовых примеров скетчей, в том числе для работы с E-Ink дисплеями. Кроме того, у меня есть нереализованная идея, требующая беспроводного соединения (но это уже другая история). Я долго колебался между микроконтроллерами и готовыми модулями, но последние всё же победили.
Разработка схемотехники
Поскольку на момент написания статьи у меня уже были готовая схема и печатная плата для новой версии устройства, я сосредоточусь на ключевых моментах. Для реализации схемы включения/выключения книги я использовал микросхему CD4013.

Рисунок 2. Схема включения на CD4013
Потребление тока микросхемой в режиме ожидания составляет порядка микроампер, что меня полностью устраивает. Вывод IO4 ESP32 позволит программно отключать устройство, например, после длительного бездействия. U/D — это кнопка для перелистывания страниц. Сигнал OUT_KEY включает основное питание, как показано на следующей части схемы.

Рисунок 3. Схема зарядки и питания
Здесь всё относительно просто. Питание управляется через CD4013 с помощью полевого транзистора до момента выключения устройства. Сигнал ADC_EN от ESP32 подаёт напряжение для измерения заряда аккумулятора. В качестве зарядного устройства используется LTC4054. Можно было бы установить что-то более продвинутое, например, BQ25895, с измерением заряда по шине I2C, но я решил остановиться на этом. Вместо линейного стабилизатора (LDO), как в прототипе, я использовал импульсный преобразователь DC/DC. Контакт зарядки (CHRG) необходим для отслеживания окончания процесса зарядки. Поскольку используемый дисплей имеет контактное соединение (FPC), схема его подключения стандартна. Все внутренние источники питания формируются от основного напряжения 3,3 В.

Рисунок 4. Схема подключения E-Ink дисплея
Как видно из таблицы на рисунке, можно подключать различные дисплеи. Я убрал DIP-переключатели и оставил только резисторы. Это сделано для экономии места, что критично в этом проекте. По той же причине не стал добавлять слот для SD-карты. Встроенной флеш-памяти SPIFlash должно хватить для хранения книги в формате TXT. В качестве источника питания используется литий-полимерный аккумулятор (LiPo) 582728 на 3,7 В и 400 мАч. Этого достаточно для кратковременной работы в режиме Wi-Fi (проверено), но на крайний случай есть разъём microUSB для зарядки. На этом самое интересное в схемах заканчивается.
Битва за кириллицу и создание шрифта
Когда на дисплее на латинице отобразился мой никнейм, я решил попробовать вывести кириллицу. Библиотеки для E-Ink дисплеев часто используют кодировку "E-Paper_code", которую можно найти на GitHub. И, как часто бывает, поддержки русских букв там не оказалось. Мои руки почти опустились. Отдельная благодарность Алексею (a3x) за помощь в расчёте смещений символов в таблице ASCII для корректного отображения кириллицы.
Для отображения русских букв потребовалось две вещи. Первое — конвертер utf8rus (в качестве примера). Второе — доработка библиотеки для отрисовки символов. Здесь было немного сложнее. В примере было несколько размеров шрифтов: 8, 12, 16, 20, 24. Шрифт размером 8 имел размер 8x5 пикселей. Его большое преимущество — 25 строк на странице. Выглядело это впечатляюще, но читать было практически невозможно. Я решил, что размер 12 будет оптимальным. Максимальная высота символа — 10 пикселей. На дисплее можно разместить 15 строк по 28 символов в каждой.
Некоторые буквы можно было позаимствовать из английского алфавита (например, «К» и «K»). Я попробовал рисовать буквы вручную, но результат был кривым и неразборчивым. Тогда я решил воспользоваться программой GLCD Font Creator, что значительно ускорило процесс.

Рисунок 5. Создание шрифта в GLCD Font Creator на основе "Comic Sans MS"
Эта программа позволяет конвертировать системные шрифты в нужный массив точек. Поскольку между строками нужны отступы, высота символа для библиотеки составила 12 пикселей (шрифт 12). Многие буквы оказались обрезанными, а некоторые стали нечитаемыми, но исправить это было гораздо проще, чем рисовать с нуля. В какой-то момент я также вспомнил, что букв стало в два раза больше (прописные и строчные). Программа позволяет экспортировать шрифт в массив, но структурно у меня это не заработало. Я морально не был готов писать конвертер из одного формата массива в другой. Пришлось пересчитывать всё вручную с помощью калькулятора — это была отдельная задача. При внесении правок важно было оставлять минимум по 1 пикселю слева и справа от символа, чтобы буквы в тексте не слипались. В коде символ выглядит примерно так:
// Символ 'f' (ширина 7 пикселей)
0x00, //
0x00, //
0x38, // ###
0x10, // #
0x7C, // #####
0x92, // # # #
0x92, // # # #
0x7C, // #####
0x10, // #
0x38, // ###
0x00, //
0x00, //
Не могу сказать, что все буквы получились идеальными, но читать их вполне можно. Если кому-то понадобится эта библиотека, готов поделиться. После подготовки всех символов я попробовал отобразить их на новом дисплее.

Рисунок 6. Тест кириллицы на дисплее 1,54 дюйма, 200x200 пикселей
Это был первый вариант, который я позже доработал. Почему я до сих пор пишу твёрдый и мягкий знаки заглавными? Хотя... оказывается, есть, например, слово «ЪМХ» — так назывался советский паровоз. Конечно, можно читать болгарские книги, где есть слова, начинающиеся на «Ъ».
Работа с текстом и вывод на экран
С помощью скетча "ESP32_SPIFFS_test" я научился помещать текстовый файл книги во флеш-память SPIFlash. Первая же попытка вывода текста разочаровала: текст, как и следовало ожидать, "рвался" по краям экрана. Прежде чем начать нормально форматировать текст программно, я воспользовался онлайн-сервисом для предварительной подготовки.

Рисунок 7. Форматирование текста до 28 символов в строке
Помимо расстановки переносов, сервис также выполнял выравнивание текста. Результат был очень хорош! Но возникла новая проблема. В текстовых файлах для переноса строки используются символы CR (Carriage Return) и LF (Line Feed). Мой базовый код искал только «\n» (LF). Мне пришлось вычислять смещение для CR и заменять его пробелами, иначе в конце каждой строки появлялись "мусорные" символы. Это костыль, но полезный.

Рисунок 8. Первый вывод 16 строк текста книги
Некоторые буквы всё ещё были кривыми и слипшимися. Также в самом тексте оказалось много ошибок: пропущенные буквы, лишние пробелы. Я люблю читать электронные книги, но ошибки в тексте всегда раздражают.
Ниже представлена функция, выводящая на экран страницу текста из файла. Не судите строго — я старался сделать код как можно лучше.
void DrawPage(int page) {
if (!SPIFFS.begin(true)) {
Serial.println("Произошла ошибка при монтировании SPIFFS");
return;
}
if(EEPROM.read(3) == 0) {
File file = SPIFFS.open("/book.txt", "r");
if(!file) {
Serial.println("Не удалось открыть файл для чтения");
return;
}
char temp_qs[56];
while (file.available()) {
file.readBytesUntil('\n', temp_qs, sizeof(temp_qs));
Quantity_line++;
}
Quantity_page = (Quantity_line/15);
if (Quantity_page > 255) {
EEPROM.write(2, Quantity_page/100);
EEPROM.write(3, Quantity_page%100);
} else {
EEPROM.write(2, 0);
EEPROM.write(3, Quantity_page);
}
EEPROM.commit();
file.close();
Serial.println(Quantity_page);
} else {
Quantity_page = (EEPROM.read(2)*100)+EEPROM.read(3);
Serial.println(Quantity_page);
}
File file = SPIFFS.open("/book.txt", "r");
if(!file) {
Serial.println("Не удалось открыть файл для чтения");
return;
}
if (number_pages == 0) {
paint.SetWidth(200);
paint.SetHeight(24);
paint.Clear(UNCOLORED);
paint.DrawStringAt(2, 2, utf8rus("Файл пуст...").c_str(), &Font12, COLORED);
epd.SetFrameMemoryPartial(paint.GetImage(), 50, 20, paint.GetWidth(), paint.GetHeight());
epd.DisplayPartFrame();
} else {
Serial.print("Номер страницы:");
Serial.println(page);
Serial.println("Содержимое файла:");
for (int i = 0; i < page*15; i++) {
int l = file.readBytesUntil('\n', one_string, sizeof(one_string));
}
paint.SetWidth(200);
paint.SetHeight(36);
// Номер страницы
paint.Clear(UNCOLORED);
paint.DrawStringAt(2, 2, utf8rus(String(page_count + 1)).c_str(), &Font12, COLORED);
paint.DrawStringAt(30, 2, utf8rus("из").c_str(), &Font12, COLORED);
paint.DrawStringAt(52, 2, utf8rus(String(Quantity_page + 1)).c_str(), &Font12, COLORED);
paint.DrawRectangle(167, 5, 194, 15, COLORED);
paint.DrawStringAt(168, 5, "'''''", &Font8, UNCOLORED);
epd.SetFrameMemoryPartial(paint.GetImage(), 2, 183, paint.GetWidth(), paint.GetHeight());
// Страница
int k_line = 2;
for (int i = 0; i < 5; i++) {
paint.Clear(UNCOLORED);
for (int s = 0; s < 3; s++) {
int l = file.readBytesUntil('\n', one_string, sizeof(one_string));
one_string[l] = 0;
//Serial.println(one_string);
paint.DrawStringAt(0, s * 12, utf8rus(one_string).c_str(), &Font12, COLORED);
}
epd.SetFrameMemoryPartial(paint.GetImage(),2,k_line,paint.GetWidth(),paint.GetHeight());
k_line += 36;
}
file.close();
page_count++;
}
}
Здесь мы читаем текстовый файл из памяти ESP32. В переменную "page" я передаю номер страницы из (псевдо)EEPROM — это своеобразная закладка. Для перемещения указателя в файле на нужную позицию необходим первый цикл for. Над этим ещё предстоит поработать.
Далее я решил сформировать служебную строку, чтобы показать общее количество страниц и номер текущей. Чтобы в будущем отображать уровень заряда батареи, я нарисовал в рамке заглушку шрифтом размера 8 (чтобы не было пересечений при выводе). Таким образом, для основного текста осталось 15 строк. Далее формируется сама страница. Это требует отдельного пояснения.
Дисплей поддерживает частичное обновление кадра. Максимальный размер кадра в памяти — 8192 пикселя. Заполнение этого кадра занимает около 0,8 секунды. Сначала я делал это построчно, и это занимало около 15 секунд. После некоторых расчётов стало ясно, что можно формировать сразу 3 строки: 200x36 = 7200 пикселей (каждая строка — 200x12). В этом случае процесс занимает около 5 секунд — что гораздо лучше (4 секунды на текст, 1 секунда на строку состояния). Таким образом, внутренний цикл формирует три строки, а внешний — всю страницу (5 блоков по 3 строки). По какой-то причине здесь нет функции «epd.DisplayPartFrame()» (непосредственный вывод на экран). При открытии книги пользователь видит последнюю прочитанную страницу (я запускаю её вывод в "void setup()"), а в фоне я готовлю следующую страницу (нужно ещё подумать о процессе чтения предыдущей страницы).
В итоге получается, что к моменту, когда мы наслаждались загрузкой логотипа и чтением первой страницы (5 секунд + 5 секунд), следующая страница уже была готова. Нажатием кнопки я моментально вывожу её на экран и начинаю формировать следующую, которая будет готова примерно через 5 секунд. Запись закладки в EEPROM при каждом перелистывании не лучшая практика, но 10 000 страниц я точно прочитаю. Вопрос в том, сколько страниц можно прочитать от аккумулятора на 400 мАч? Я также реализовал режим лёгкого сна (light sleep), что оказалось довольно просто. Нужно добавить в конец "void setup()" настройку пробуждения и вызывать функцию «esp_light_sleep_start()» после нажатия кнопки. Для пробуждения по кнопке используется RTC_GPIO: «esp_sleep_enable_ext0_wakeup(GPIO_NUM_14, 0)». Итого: в момент "перелистывания страницы" устройство потребляет около 38 мА, затем — 5,0 мА. Мой мультиметр (UNI-T UT70A) показывает 0 в режиме измерения микроампер, когда устройство выключено кнопкой (возможно, он врёт).

Рисунок 9. Вывод 15 строк текста + служебная строка
К плате добавляется ещё один провод для кнопки. Оказывается, не все контакты ESP32 можно использовать как GPIO.
Создание печатной платы и финальная сборка
Пока я "писал код", в производстве находилась вторая версия печатной платы. Итоговый размер получился 40x57 мм.

Рисунок 10. 3D-модель платы версии 0.2
Сильно уменьшать размер я не пытался, так как решающую роль здесь играют габариты аккумулятора и самого модуля ESP32. Кроме того, чем меньше плата, тем нелепее выглядит общая толщина устройства (батарея имеет толщину 5,8 мм). Я убрал светодиоды для отладки по жесту смахивания (оставив только индикаторы процесса зарядки), хотя они, безусловно, полезны при разработке.

Рисунок 11. Сборка платы

Рисунок 12. Устройство в сборе
Что осталось реализовать?
Из нереализованных идей:
Оптимизировать запись в EEPROM.
Реализовать отправку книг по Wi-Fi.
Добавить перелистывание страницы назад.
Внедрить мониторинг и отображение уровня заряда батареи.
Добавить поддержку форматов электронных книг (например, FB2, EPUB).
Научиться писать более эффективный и чистый код.
Я не люблю использовать видео в статьях, но здесь без него не обойтись. Оно наиболее полно отражает работу готового устройства.
В целом, я доволен результатом. Практической пользы, конечно, немного, но, как говорится, опыт бесценен, а моральное удовлетворение от завершённого проекта огромно. Думаю, найду в себе силы прочитать на этом устройстве несколько книг.
P.S.: В первой книге "Саги о Ведьмаке" оказалось 1654 страницы (24810 строк).
Чтение — движение к мудрости.
Благодарю за внимание и желаю удачи в ваших проектах!

Статья написана специально для читателей Timeweb Cloud и Пикабу. Ещё больше интересных материалов вы найдёте в нашем блоге и Telegram-канале на Хабре.
Если вы автор (или хотите им стать) и хотите поделиться чем-то интересным в нашем блоге, напишите об этом здесь.
Облачный сервис Timeweb Cloud — рекомендуемая ссылка, которая может помочь поддержать проект автора.
[My] TimewebDIY Мастер сборки гаджетов с электронными чернилами на электронных чернилах Техническое шоу Домашнее видео Длинный пост на YouTube 27Больше интересных статей здесь: Гаджеты.
Источник статьи: Разработка самой маленькой в мире книги на e-ink дисплее.