Как стать автором
Обновить
1513.17
Timeweb Cloud
То самое облако

Разработка самой маленькой в мире книги на e-ink дисплее

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров9.9K
Приветствую, Хабр!



Хоть название и громкое, но тут почти нет преувеличения. Моя разработка не предполагает ежедневного использования и сделана больше просто ради забавы, но тем не менее она довольно интересная. Владимир Анискин из Новосибирска, например, создал книгу на лавсановой пленке размерами 70х90 мкм, а почему бы и нет? На занесение в книгу рекордов Гиннеса я не претендую, но, если Вы остались заинтересованы, заходите под кат. Не хотел делить статью на части, так что наберитесь терпения.

Идея создания маленькой книги на электронных чернилах родилась уже довольно давно. Для этого даже был приобретен прямоугольный e-ink дисплей 2,13”, но по незнанию заказал версию, которая не поддерживала частичного обновления экрана. Смена страниц происходит только через полное стирание, а это занимает много времени и выглядит некрасиво. Поделка была заброшена на долгое время, но как-то на глаза попался мне дисплей 1,54” (200х200 pix), и я вновь загорелся этой идеей. После недолгих раздумий был заказан уже нужный дисплей. Пока посылка шла положенные три недели я решил сделать макетную плату и запустить старый дисплей, сил терпеть больше не было.


Рис.1. e-ink дисплей 2,13” на макетной плате

На плате не обошлось без ошибок. Я неверно выбрал ножки SPI для дисплея и чуть-чуть ошибся со схемой подключения CP2102 в режиме автоматического программирования. Несколько перерезанных дорожек и три проводка решили почти все мои проблемы.

Почему именно ESP32.
Во-первых, есть возможность закидывать файлы через Wi-Fi. Во-вторых, для этого модуля есть множество примеров скетчей, в том числе и для работы с e-ink. Также у меня есть одна не реализованная идея, которая потребует наличия беспроводной связи, но об этом не в этой статье (интрига). На самом деле я долго метался между микроконтроллером и модулем, но последний все-таки победил.

Немного по схемотехнике. Так как на момент написания статьи у меня уже была готова схема и pcb новой платы, я буду ориентироваться на нее. Схему включения/выключения книги я реализовал на CD4013.


Рис.2. Схема включения/выключения на CD4013

Ток потребления микросхемы в состоянии покоя порядка микроампера, что меня вполне устраивает. IO4 от ESP32 позволит программно выключать устройство, например, при долгом бездействии. U/D – кнопки для перелистывания страниц. Сигнал OUT_KEY включает общее питание, что видно на следующем куске схемы.


Рис.3. Схема зарядки и питания

Тут все просто. Питанием управляет CD4013 через полевик, до этого устройство выключено. Сигнал ADC_EN от ESP32 подает напряжение для измерения заряда аккумулятора. Сам зарядник LTC4054. Может стоило поставить что-то более навороченное с измерением заряда и шиной I2C, типа BQ25895, но как уж есть. Вместо LDO (как было на прототипе) для питания и использовал DC/DC. Нога CHARGE нужна для отслеживания окончания зарядки.

Так как используемые мной дисплеи pin-to-pin, схемотехника одна и та же. Все внутренние питания формируются из основного источника 3,3В.


Рис.4. Схема включения e-ink дисплея

Из таблицы на схеме видно, что можно подключить различные дисплеи. DIP переключатель я убрал и оставил резисторы. Сделано это ради экономии места, так как в данном проекте габариты решают. По этой же причине я не стал ставить SD карту. Памяти для хранения одной книги в txt должно хватить на внутренней SPIFlash.

Аккумулятор буду использовать LiPo 582728 3.7В 400 мАч. Этого достаточно и для недолгой работы в режиме Wi-Fi (проверено), но в крайнем случае, есть разъем microUSB. Из интересного по схемотехнике, пожалуй, все.

После того, как я латиницей вывел свой ник на дисплее, решил попробовать кириллицу. Библиотеки для работы с e-ink использовались «E-Paper_code» — так их можно найти на github. И да, там нет русских букв. Мои руки уже практически опустились. Отдельная благодарность Алексею a3x за помощь в подсчете offset букв (и не только в этом) в таблице ASCII, для правильной интерпретации.

if (char_offset > 1140) char_offset -= 780; // функция DrawCharAt в epdpaint.h

Для отрисовки русских букв нужно две вещи. Первое – это конвертер utf8rus (как пример). И тут все понятно, идем снова на github и находим скетч. Второе – нарисовать буквы в библиотеке. Здесь немного сложнее. В примере есть несколько размеров шрифтов: 8, 12, 16, 20, 24. Шрифт 8 имеет размеры 8х5 pix. Есть один существенный плюс – это 25 строк на странице. Смотрится шикарно, но читать почти невозможно. Шрифт 12 думаю будет самое то. Максимальная высота 10 pix. На дисплее можно отобразить 15 строк по 28 символов.

Какие мог буквы я перенес из английского алфавита (я понимаю, что похожие разноязычные буквы отличаются, как например «К» и «K»). Дальше просто пытался рисовать, но буквы оказывались кривыми и нечитаемыми. Решил воспользоваться программой «GLCD Font Creator». Дело пошло быстрее.


Рис.5. «GLCD Font Creator» шрифт «Comic Sans MS»

Программа позволяет сконвертировать шрифт в нужное количество точек. Так как для соседних строк нужен отступ библиотека работает с 12 пикселями высоты (12 шрифт). Многие буквы подрезались, некоторые стали нечитаемы, но исправить это куда проще, чем рисовать с нуля. В какой-то момент я еще вспомнил что букв в два раза больше, чем ожидалось (заглавные и строчные). Программа позволяет выгрузить шрифт в массив, но мне он не подошел по структуре. Писать преобразование одного массива в другой я был не готов морально. Пришлось преобразовать все в калькуляторе – то еще занятие. При корректировке нужно не забывать оставлять не менее 1 пикселя справа/слева, чтобы буквы не слипались в тексте. В коде буква выглядит примерно так:

// @ 'ф' (7 pixels wide)
  0x00, //        
  0x00, //        
  0x38, //    ###      
  0x10, //     #      
  0x7C, //  ##### 
  0x92, // #  #  # 
  0x92, // #  #  # 
  0x7C, // ##### 
  0x10, //   # 
  0x38, //  ###      
  0x00, //        
  0x00, //   

Не скажу, что все буквы идеальны, но вполне читаемы. Если кому-то понадобится библиотека, могу поделиться. После того, как все буквы готовы пробуем вывести на новый дисплей.


Рис.6. Кириллица на 1,54” дюйма 200х200 pix

Это самый первый вариант, и я его дорабатывал. Для чего вообще я рисовал заглавные твердый и мягкий знаки? Хотя … оказывается есть, например, слово «Ьмх» — это название советского локомотива. Ну и, конечно, можно будет почитать книги на болгарском языке, там есть слова, начинающиеся на «Ъ».

Используя скетч «ESP32_SPIFFS_test» я научился на SPIFlash класть текстовый файл книги. Когда я в первый раз вывел текст, я был огорчен. Как и следовало ожидать, все слова «рвались» на границе экрана. Пока я не сделал нормального форматирования текста пришлось воспользоваться online сервисом.


Рис.7. Форматирование текста по 28 символов в строке

Помимо переносов по словам, тут есть выравнивание текста по ширине. Результат превосходен! Но и тут появилась новая проблема. Для переноса текста используется символы «CR»«LF». Мой недокод умеет искать только «\n» = «LF». Пришлось находу вычислять offset «CR» и заменять его на пробел, в противном случае на каждой строке справа появлялся символ в виде мусора. Это костыль, но рабочий.


Рис.8. Первый вывод текста книги 16 строк

Тут еще некоторые буквы кривые и слипаются. Также в тексте много ошибок в виде пропущенных букв и лишних пробелов в середине слова. Мне нравится читать электронную книгу, у меня для этого есть полноценная версия, но вот ошибки в тексте раздражают.

Ниже функция вывода страницы текста из файла на экран. Строго не судите – пишу код как могу.

void DrawPage(int page) {
  if (!SPIFFS.begin(true)) {
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  if(EEPROM.read(3) == 0) {
    file = SPIFFS.open("/book.txt", "r");
  if (!file) {
    Serial.println("Failed to open file for reading");
    return;
  }
  char temp_qs[56];
  while (file.available()) { 
    file.readBytesUntil('\n', temp_qs, sizeof(temp_qs));
    Quantity_string++; 
  }
  Quantity_page = (Quantity_string / 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 = SPIFFS.open("/book.txt", "r");
  if (!file) {
    Serial.println("Failed to open file for reading");
    return;
  }
  if (Quantity_page == 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("Page number:");
    Serial.println(page);
    Serial.println("File Content:");
    for (int i = 0; i < page * 15; i++) {
      int l = file.readBytesUntil('\n', one_string, sizeof(one_string));
    }
    paint.SetWidth(200);
    paint.SetHeight(36);
    //PAGE NUMBER
    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, COLORED);
    epd.SetFrameMemoryPartial(paint.GetImage(), 2, 183, paint.GetWidth(), paint.GetHeight());
    //PAGE
    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 строк. Следом формируется сама страница. Тут нужно отдельное пояснение.

    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++;

Как я писал ранее – этот дисплей поддерживает частичное обновление фреймами. Максимальный размер фрейма в памяти 8192 pix. Заполнение одного фрейма занимает порядка 0,8 с. Сначала я сделал построчно и это занимало около 15 с. После некоторых расчетов стало понятно, что формировать можно по 3 строки за раз: 200х36=7200 (каждая строка 200х12). Тогда это займет порядка 5 с – это намного лучше (4 на текст и 1 на строку состояния). Поэтому внутренним циклом формируются три строки, а внешним вся страница. Здесь нет функции «epd.DisplayPartFrame()» (вывод на дисплей) не случайно. После включения книги пользователь видит последнюю читаемую страницу (ее я стартую в «void setup()»), а я тем временем готовлю следующую (над процессом чтения предыдущей страницы еще нужно подумать).

void loop()
{
  if (((digitalRead(KEY_UP) == LOW) && (page_count <= Quantity_page + 2))) {
    Quantity_page = (EEPROM.read(2)*100)+EEPROM.read(3);  
    //SAVE PAGE
    Serial.println(page_count);
    if(page_count > 255) {
      EEPROM.write(0, page_count/100);
      EEPROM.write(1, page_count%100);      
      } else {
        EEPROM.write(0, 0);
        EEPROM.write(1, page_count);  
      }
    EEPROM.commit();
    //LOAD PAGE
    epd.DisplayPartFrame();
    //PRELOAD NEXT PAGE
    DrawPage(page_count);
    delay(50);
    //LIGHT SLEEP
    esp_light_sleep_start();
  }
}

В результате этого получается, что пока мы наслаждаемся лого загрузки и чтением первой страницы (5 с + 5 с) у меня готова следующая. По нажатию кнопки я моментально делаю вывод и начинаю формирование следующей, которая готова через ~5 с. Запись закладки в один и тот же адрес EEPROM ни к чему хорошему не приведет, но 10000 страниц я прочитаю. Вопрос в том, сколько страниц я успею прочитать на аккумуляторе 400 мАч. Без режима сна все-таки не обойтись, но это оказалось просто. Добавляем в конец «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.

Пока я «писал код» в производстве была вторая итерация платы. Габариты получились 40х57мм.


Рис.10. 3D модель платы v0.2

Я особо не старался уменьшить размеры, так как решающую роль тут играет аккумулятор и сам модуль ESP32. Также, чем меньше будут габариты, тем нелепее буде выглядеть толщина устройства (аккумулятор 5,8мм). Я широким жестом удалил индикационные светодиоды (осталась только индикация процесса/окончания зарядки), хотя место для них есть, а для отладки они бы пригодились.


Рис.11. Плата в сборе


Рис.12. Устройство в сборе

Из нереализованного:

  1. Переделать запись в EEPROM.
  2. Реализовать отправку книги по Wi-Fi.
  3. Сделать перелистывание страницы назад.
  4. Отслеживать и выводить заряд аккумулятора.
  5. Поддержка epub.
  6. Научиться писать код.

Не люблю использовать видео в статье, но тут без него не обойтись. Оно наиболее полно отображает работу устройства.


В целом поделкой я остался доволен. Практическое применение, конечно, минимально, но, как говориться, опыт бесценен, а также присутствует моральное удовлетворение. Думаю, я найду в себе силы прочитать пару книг на моем устройстве.

P. S.: Первая книга саги «Ведьмак» занимает 1654 страницы (24810 строк).

Чтение – к мудрости движение.
Спасибо за внимание и успехов!



Читайте также:

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+96
Комментарии77

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud