В этой статье я хочу поделиться опытом разработки электронной книги с использованием недорогого контроллера STM32H750VB, распространенных дискретных компонентов и относительно недорогого дисплея E-Ink. Статья будет большой, так как приведены будут все процессы от постановки задачи до получения первой версии устройства, способного выполнять поставленную задачу. Все будет снабжено схемами, трассировками, кодом и комментариями. Почему в названии от «от А до Э»? Потому что нельзя просто так взять и сделать конечный продукт без ошибок и недоделок.


Постановка задачи


Первый логичный вопрос – а зачем? Все уже выпускается китайцами за относительно небольшие деньги.

Все просто. Главное — потому что интересно, иногда хочется в свободное время изобрести велосипед.
Если люди перестанут периодически изобретать велосипеды, то велосипеды превратятся в непознаваемое наследие предков.

Дополнительно к велосипеду – хочется сделать книжку, которая не будет разряжаться за месяц, валяясь в шкафу, а будет тратить энергию только на обновление экрана (пара книг из бюджетного сектора, которыми я пользовался, грешила именно этим). Исполнение книги можно сделать под свои руки (я имею в виду кнопки под пальцами руки, которая держит саму книгу). Ну и как ни странно, бюджет самоделки вышел ниже, чем продаваемые электронные книги из сектора 8 дюймов (если не считать затраченных человеко-часов, но хобби на то и хобби, что время бесплатно).

Итак


  • Необходимо разработать электронную книгу с экраном около 8 дюймов (по моему личному мнению это самый удобный для чтения размер).
  • Книга не должна требовать предварительной конвертации файлов электронных книг перед загрузкой, то есть отображать формат .FB2 как есть.
  • Файлы читать с SD-карты.
  • Воспринимать кириллицу.
  • После обновления страницы уходить в режим потребления сравнимым с саморазрядом аккумулятора.
  • Навигацию по пунктам меню сделать светодиодами. Имеется в виду, что отображенные на экране списки для выбора (файлы, список действий) будут статичными, и навигация по ним не будет заставлять перерисовывать экран. Выбор производится включением светодиода напротив списка.

    фото реализации для наглядности
  • для управления книгой 4 кнопки — «вперед(вверх)», «назад(вниз)», «меню», «функция»
  • различать на кнопках длинное и короткое нажатие. Кнопку «вперед(вверх)» разместить под пальцем левой руки держащей книгу, чтобы для перехода на следующую страницу не требовалось перехватывать книгу (это я совсем под себя делаю)

    фото реализации для наглядности

  • корпус из ореха

  • Железо для всего этого.

    Экран E-ink — Waveshare 7.5inch E-Ink raw display 800?480. Самый дорогой компонент. Покупал на Алике примерно за 2700 руб.
    Контроллер — STM32H750VBT6. Несмотря на топовую серию, это самая бюджетная модель микроконтроллера в своем сегменте ( у того же Алика – 240 руб). Отличается малым количеством FLASH (128 Кбайт), но в остальном соответствует параметрам всей серии.
    Также потребуется энергонезависимое хранилище для запоминания служебной информации. Их будет два — EEPROM — AT24C02D и FLASH — W25Q64 (стоимостью 60 руб за обе).
    Надо предусмотреть возможность подключения дополнительных блоков, чтобы использовать книгу в задачах не свойственным другим электронным книгам (погодной станции, логического анализатора…).

Тестом работоспособности первой версии книги (а также логическим финалом этой статьи и материалов к ней) будет возможность прочитать на устройстве трилогию «Властелин колец». К моменту написания статьи я был на середине и первом месяце использования девайса.
Если DIY`щик берет в руки контроллер, то у него получается либо погодная станция, либо будильник
Вопрос «почему не взять для этого Малину, Апельсинку… и не париться», рассматриваться не будет.

Разработка схемы


Подключение дисплея

Дисплей выглядит следующим образом:


Работает по SPI (3 или 4 линии). Схема включения доступна в мануале. Мануал лежит на сайте продавца дисплея. Делаем все по мануалу:



Есть изменение относительно мануала — катушка L1 по мануалу 10мкГн, но с такой индуктивностью пиксели в дисплее включаются нестабильно и дисплей идет непропечатанными пятнами. Индуктивность 47-68мкГн всё исправляет. Информация взята из описания универсального шильда под e-Paper для одноплатников от этого же производителя. Схема содержит транзистор SI1308, это оказался самый «уникальный» компонент, который пришлось отдельно заказывать и ждать месяц.

Подключается E-Ink к микроконтроллеру по SPI1:

PA5 – SCK
PA7 — DIN
и линии управления
PA1 — BUSY
PA2 — RST
PA3 — DC
PA4 — CS

В мануале утверждается, что при обновлении картинки матрицы дисплея, а это около 2 секунд, дисплей потребляет 12mA. На самом деле оказалось чуть-чуть не так – 38mA. С другой стороны, там же указано время обновления: 4-8 секунд, что на самом деле составляет 2, максимум 3 секунды.
Китайская система мер и весов понятие растяжимое, как и улыбка
Подключение SDкарты, и микросхем энергонезависимой памяти типовое.

общая схема


FLASH — W25Q64
подключена к SPI2
и PD8 в качестве сигнала CS

EEPROM -AT24C02D
подключена I2C2

SD-card
подключена к SDMMC1
и PD5 — SD-card insert

Представленная схема доступна в конце статьи. Схема разработана в Proteus. Файл для Куба (CubeMX) — аналогично.

Система питания. На ней остановлюсь подробнее


Основное состояние электронное книги – выключенное. Включение производится только для обновления экрана. Изначально рассматривалось использование режимов сна на всех компонентах схемы, Но когда начал делать замеры потребления, оказалось, что у микросхем FLASH памяти W25QXX есть проблемы с паспортным режимом Standby/ Power-down.

Потребление, в нем должно быть несколько десятков микроампер. Фактически из 11 микросхем W25QXX, которые были у меня, только две соответствовали паспорту (одна в корпусе SOIC-16, а вторая откуда-то выпаянная) остальные (недавно купленные) кушали минимально 5-8 миллиампер. STM`ка, кстати, тоже не блещет соответствием цифрам, которые показывает Куб в расчетах потребления. В итоге предварительно собранная схема кушала 12 mA в режиме сна. В основном из-за потребления FLASH`ки и SD карты.

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

Использование готовых менеджеров питания было отклонено, так как под необходимые функции подходят только специфичные от BQ, но они все в BGA корпусах, а плату (как будет видно ниже) я буду делать под ЛУТ.

В итоге реализация:



Питание (5вольт) подается на точку OUT_POWER, питая контроллер заряда АКБ MAX1555 и закрывая транзистор Q6. Q6 – транзистор MOSFET P-канальный, то есть положительным напряжением (относительно истока), он закрывается. Закрываясь он отключает АКБ. При отсутствии питания на OUT_POWER, Транзистор открыт и АКБ подключена к схеме питания. На схеме стрелками показано движение тока. После транзистора, мы получаем питание забыв откуда оно пришло. На схеме есть два резисторных делителя напряжения. R47 и R48 дает логическую единицу (3.3V), если есть внешнее питание и логический ноль если его нет.

Номинал резисторов не позволяет получить ток, способный запустить обесточенный контроллер через схему защиты от перенапряжения, поэтому сигнал «ADC1» можно смело кидать на свободную ногу контроллера. Делитель напряжения R50 и R49 служит для измерения напряжения на АКБ. Коэффициент деления = 2. Сигнал ADC2 можно подавать на АЦП процессора.

Я вижу лес рук, у тех кто читает эти три предложения про пару R50 и R49.

Действительно, это относительно тонкое место на схеме.

Первое – это постоянная нагрузка на аккумулятор. Расчет с использованием закона Ома дает около 45 микроампер это 0.045 миллиампер. Сколько часов нужно чтобы разрядить АКБ емкостью 600 миллиампер можно посчитать самим. Меня эта цифра устроила. По желанию сопротивления можно взять до 90Ком

Цифры фактического потребления всей схемы с номиналами, указанными на схеме


Второе – Это процесс запуска АЦП микроконтроллера через делитель напряжения. Автор статьи знает про применение операционных усилителей, читал статьи про «демонов АЦП», но в данном конкретном случае считает оправданным такое упрощение схемы замера напряжения. Замер напряжения из-за больших номиналов резисторов, естественно, поплывет (физику никто не отменял, емкость УВХ в АЦП может не успеть получить нужный заряд за квант времени, а еще остаточный заряд…), но коррекцию я буду делать программно на основании тестовых замеров. В результате при номиналах R50 и R49 до 91Ком на каждый, получить прогнозируемый замер можно, а вот выше 100Ком уже нет.
Главное в физике — это умение пренебрегать!
-Лев Ландау
Продолжу по самому менеджеру питания. Мы дошли до точки «Mid_Power» на схеме, где у нас либо ток от внешнего питания, либо с АКБ. Далее идет линейный преобразователь напряжения (LDO) LP2985 у которого есть сигнал ON\OFF который, соответственно, разрешает или запрещает преобразование, если на него подать выше 1,4 вольт, то LDO выдает 3.3 вольта на выходе, если прижать к земле, то ничего не выдает. На схеме этот сигнал «On_Power». Необходимо сделать так, чтобы внешний импульс держал не ниже 1,4 вольта несколько секунд на сигнале «On_Power». За это время микроконтроллер инициализируется и сам будет управлять своим выключением, держа логическую 1 на этом сигнале. Решение в лоб с использованием конденсатора на «On_Power» и землю не подошло по габаритам. Конденсатор нужен слишком большой емкости, LP2985 на этом сигнале относительно много потребляет и емкости 1000мкФ хватает меньше чем на секунду. В связи с этим будем использовать ключ на транзисторе — N канальном MOSFET. Сигнал «Mid_Power» замыкается ключом с сигналом «On_Power». Открывает транзистор внешний импульс от того же «Mid_Power» и «держится» парой C36 и R30. при номиналах, указанных на схеме транзистор открыт порядка 4 секунд, при C36 = 10мкФ – порядка 8 секунд. Резистор R29 служит для, скажем так, стабилизации процесса. Дело в том что, при закрытие транзистора происходит дребезг, то есть отключились, потом опять включение на несколько миллисекунд, а потом уже полное выключение. Причина тому может быть в прыжках напряжения при переходе АКБ на холостой ход, либо в том, что резистор имеет паразитную индуктивность и получается колебательный контур, а может еще в чем-то. Но R29 выправляет ситуацию тем, что обратная связь при открытом транзисторе чуть замедляет разряд C36, а при первом отключении помогает быстрее высаживать заряд C36 на сигнал «On_Power».

В завершении необходимо организовать четыре независимых линии запуска менеджера питания. Три линии для кнопок на корпусе (нажатие листать вперед, назад и кнопка меню), которые будут пробуждать всю схему, и одну линию для микроконтроллера, который после пробуждения первым делом подаст 3.3 вольта на нее, чтобы его не отключили и прижмет к земле, когда закончит обновлять экран. Развязка сделана на диодах. Линии имеют контактные площадки J11, J8, J6,J5

Отдельно необходимо сделать схему управления светодиодами пунктов меню. Всего будет 12 вариантов выбора, следовательно, 12 светодиодов. Чтобы не забирать под это 12 ног контроллера, сделано будет соединением (я не знаю, есть ли для такого соединения отдельное название), когда между каждой парой выводов есть два светодиода, соединенных анод к катоду:


соединяются с
PE2 — J1
PE3 — J2
PE4 — J3
PE5 — J4
(W5 — это перемычка)
Для включения отдельного светодиода, необходимо на пару выводов подать «1» и «0» соответственно полярности включения, а остальные два перевести в режим входа.

План В

Забегая вперед скажу, что вариант с одноцветными светодиодами мне не подошел. Не подошел по цвету свечения :) под корпус из ореха, нужен теплый ламповый цвет свечения, и желтый или оранжевый не подходит. Поэтому в итоге я сделал на адресных RGB светодиодах в корпусе 2020. В исходниках есть функции для обоих вариантов.

Нога PE5 – является выходом канала таймера TIM15_CH1
Нога PE3 – управляет включением шины питания адресных светодиодов
Отключать питание понадобилось из-за того, что диоды WS2812 от 3.3 вольт работают нестабильно и их пришлось подключить напрямую к сигналу «Mid_Power» через ключ

Схема


Три кнопки, которые «будят» менеджер питания, также ведут к ногам (через резистор 1К) контроллера. Плюс четвертая кнопка — только к контроллеру
PB0 — key «вперед»
PB1 — key «назад»
PB2 — key «меню»
PC5 — key «функция»

В архиве, по ссылке внизу статьи схемотехника в трех файлах: схема устройства, схема LED линейки меню и схема линейки на WS2812. Схема устройства на трех листах (отдельно это указываю, так как в Протеусе явно не видно количество листов схемы)
В конце описания схемотехники попрошу прощения за вольные изображения УГО, почти все они делались руками под себя, и именно из-за большой базы смоделированных компонентов пока не хочу уходить с Протеуса.

Разводка и изготовление печатной платы


Как я упоминал выше, разводить плату я буду с учетом того, что изготовлять ее буду в домашних условиях, то есть по древней технологии ЛУТ (Лазер, Утюг, Травление). На данной технологии останавливаться не буду (когда то писал статью об этом habr.com/ru/post/451314 ).

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



Вверху разъем для дисплея, контактные площадки везде подписаны (медью), выведены дополнительные шины (UART,SPI,I2C) и выводы под дополнительное оборудование. Слева площадки под программатор (обычный свисток ST-LINK).

Там где пресечения дорожек не избежать, использовал резисторы-перемычки в корпусе 0805, чтобы перепрыгнуть 1-2 дорожки и в корпусе 1206, чтобы перепрыгнуть 2-3 дорожки. На разводке их видно по зеленым линиям.

Линии запуска менеджера питания K0-K3. метка MPW соответствует сигналу «Mid_Power». Vin – выход от резисторного делителя входного питания BAT_LVL – выход резисторного делителя от АКБ, соединяется с площадкой PA0. Остальные подписи – как на схеме.

Берем текстолит и убираем все лишнее


Текстолит черного цвета (это не маска)

Сборка:



Бонус :
Я делал сразу две платы (на всякий случай), обе получились хорошо и работают. И готов одной штукой поделиться. Если кому-то нужно, кто в Питере может в рабочее время на Петроградке забрать, тот может написать в личку. Отдам безвозмездно, то есть даром (плата со всеми компонентами и проверена в работе, на фото она самая).

Проверка на дым, проверка соединений и можно сказать что 80% работы сделано
Не так страшны первые 80% проекта, как вторые 80% проекта
Девиз PM

Кодим


Первый этап — настройка контроллера и драйвера для устройств


До того как приступить непосредственно к работе с текстом книги, необходимо подготовить для этого все железо и вывести кучку функций по работе с этим самым железом.

Конфигурацию делаю в «CubeMX», проект генериться под «TrueSTUDIO for STM32», соответственно в оболочке у нас HAL. К HAL я отношусь вполне спокойно, ужасов расписанных про него на форумах не встречал, хотя иногда необходимо выходить из его рамок (ниже это будет упомянуто).

Начнем с микроконтроллера.

Частоту я поставил 300Mhz (исходя из соображений производительность\потребление), тактирование начинается от кварца в 16Mhz. Порты настраиваются согласно описанной выше схемотехнике. В ресурсах к статье файл куба в наличии, поэтому подробнее останавливаться не буду.

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

Приступаем к распределению памяти.

В STM32H750 доступно 1 мегабайт ОЗУ, вот только она лежит не одним куском, а разложена по разным банкам:



DTCMRAM и ITCMRAM «близкая» ОЗУ с нулевыми таймингами доступа, то есть банки с быстрой памятью, а RAM_D1,RAM_D2,RAM_D3 просто ОЗУ. Причем HAL в проекте создал только адресное пространство для этих банков и подключил использование DTCMRAM.

Нам 128 килобайт DTCMRAM не хватит.

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

Итак, идем в файл * _FLASH.ld с трассировкой памяти (в приложенном проекте это STM32H750VB_FLASH.ld) он генерируется в корень проекта.

Добавляем туда конструкцию с объявлением переменных, указывающих на наши банки:

SECTIONS
{
…
 .bank1 : {} >RAM_D1
 .bank2 : {} >RAM_D2
 .bank3 : {} >RAM_D3 
…
}

Выравнивание (ALIGN) или еще какие-то действия с этой памятью нам не потребуется, она нужна просто как набор свободных к использования байтов. Есть нюанс, файл _FLASH.ld не имеет User секций в разметке кода, поэтому после новых генераций проекта через Куб (если вдруг они понадобятся), наша запись удалится. В связи с этим не забываем это проверять и заново добавлять (На самом деле TrueSTUDIO умеет игнорировать перезапись, но это опасные настройки, поэтому советовать не буду).

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

Дефайним команду компилятора(исключительно для удобства):

#define _BANK1 __attribute__((section(".bank1")))
#define _BANK2 __attribute__((section(".bank2")))
#define _BANK3 __attribute__((section(".bank3")))

а далее объявляем массивы:

_BANK3 uint8_t BufferEink[48000]; // экранный буфер
_BANK1 char Out_Text[327680];    // буфер текста
_BANK2 char CurrFileDir[256];
_BANK2 char CurrFileName[256];
_BANK2 char Curr_Buffer_Info[1024];

Теперь у нас в первом банке 320килобайт для текстов, в Третьем экранный буфер, а второй банк под все остальные массивы. DTCMRAM под текущие переменные, а также будет немного динамического выделения памяти (стек и куча осталась в DTCMRAM)



Дисплей


Задача к драйверу дисплея: Инициализация, рисование точки, рисование символа и строки символов, рисование линии и передача экранного буфера в сам дисплей

Из библиотеки продавца дисплея я взял алгоритм инициализации дисплея и передачу экранного буфера, как есть.

В экранном буфере 48 Килобайт, в каждом байте бит отвечает за точку (точка либо белая, либо черная, без градаций). Чтобы зажечь конкретную точку и не погасить рядом стоящие в байте биты(точки) сделана функция DrawPoint и дальнейшая графика идет через оперирования точками, отойдя от байт и бит.

Код функции


	  uint16_t x = y1;
	  uint16_t y = 480-x1;
	  uint8_t p_x = (x>>3); // (разделить на 8 по целому = позиция в байтах)
	  uint8_t px_x = x-(p_x<<3);  // (номер точки в байте)
	  uint16_t IM_addres = y*100+p_x; // адресс точки в массиве
	  uint8_t point = 0b10000000>>px_x; // точка в байте
	  uint8_t lastPoint = BufferEink[IM_addres];
	  BufferEink[IM_addres] = lastPoint|point; // ИЛИ

Вначале идет переворачивание координат, так как по умолчанию дисплей не в портретном режиме, а дальше я все действия прокомментировал в коде.

Функции работы с дисплеем собраны в файле EInk7inch.c (и описаны в EInk7inch.h)
Функция печати символов и строки использует массив с битовыми описаниями изображения каждого символа, каждый символ 8x8 точек. Kодировка символов win-1251.

Это будет служебный шрифт, читать им – глаза сломаешь.

Для удобного чтения необходим шрифт в два раза больше, чем системный, у каждого символа нужно знать ширину, чтобы строка «iiiiiii» занимала меньше места чем «шшшшшшшш» при одинаковом количестве символов. С векторными шрифтами связываться не буду (не те масштабы, не те ресурсы), поэтому рисуем



Матрица для шрифта 16x17 точек, то есть описание символа занимает 34 байта, плюс 1 байт с позицией конца символа (слайдер на рисунке). Для генерации пришлось быстро набросать программку, но приводить ее в ресурсах к статье не буду, так как там все просто, а написана она на коленке левой пяткой с массовой копипастой вместо функций.
Если долго жать на Ctr+C, то Ctr+C нажмет тебя
Таким образом готовится:

  • Стандартный шрифт
  • Курсив
  • Жирный
  • Крупный для заголовков
  • Мелкий для вспомогательных целей

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

Чтобы начать работать, надо хорошенько заскучать, чтобы ничего больше не хотелось.
Стругацкие
В результате появилось несколько типовых функций собранных в файле my_fonts.с и my_fonts.h

uint8_t My_char_standart(uint16_t f_x , uint16_t f_y , uint8_t type, uint8_t ascii );
uint8_t My_char_italic(uint16_t f_x , uint16_t f_y , uint8_t type, uint8_t ascii );
uint8_t My_char_bold(uint16_t f_x , uint16_t f_y , uint8_t type, uint8_t ascii );
uint8_t My_char_big(uint16_t f_x , uint16_t f_y , uint8_t type, uint8_t ascii );
uint8_t My_char_14px(uint16_t f_x , uint16_t f_y , uint8_t type, uint8_t ascii );

Указываются координаты, символ, и тип вывода: 1 — печать, 0 -не печатать а только вернуть ширину (нужна будет позже).

Возвращает ширину символа.

К ним всем есть однотипные функции печати строки
пример

Print_standart(uint16_t x, uint16_t y, char* string);

для печати имен файлов функция:

void Print_big_scr_DOS(uint16_t x, uint16_t y, char* string);

которая печатает в кодировке DOS. кодировка DOS мапиться через массив на кодировку WIN-1251

Результат:



Подключение энергонезависимой памяти и SD-карточки

Все типовое, не один раз описано на просторах интернета.

для выведения результатов сделал страницу с системной информацией:

страница системной инфы


LED Указатели меню


Помните, я выше писал про то, что спокойно отношусь к HAL? Так, вот, к управлению GPIO это не относится! Я очень не понимаю, зачем на самую простую и быструю функцию навесили валидаторы входных значений (тем более они предифайнены) и создали монструозные структуры. Поэтому я буду дрыгать ножками по старому, регистрами.

Для работы с 12 LED диодами нужно выдавать на 4 GPIO комбинации типа:
1-0-Z-Z
0-1-Z-Z
0-Z-Z-1
Где Z – нога сконфигурирована как вход, 1 – как выход с логической «1», 0 – соответственно логический «0»

Делаем для каждого GPIO типовые функции:

 void PE2_1()
{
	GPIOE->MODER |= GPIO_MODER_MODE2_0;  // выход
	GPIOE->PUPDR &= ~GPIO_PUPDR_PUPD2;  // без подтяжки
	GPIOE->BSRR = GPIO_BSRR_BS2;       // HIGHT
}
void PE2_0()
{
	GPIOE->MODER |= GPIO_MODER_MODE2_0;  // выход
	GPIOE->PUPDR &= ~GPIO_PUPDR_PUPD2;  // без подтяжки
	GPIOE->BSRR = GPIO_BSRR_BR2;       //LOW
}
void PE2_Z()
{
	GPIOE->MODER &= ~ GPIO_MODER_MODE2; // вход
	GPIOE->PUPDR &= ~GPIO_PUPDR_PUPD2;  // без подтяжки
}

и выводим общую:

void Set_LEDs(uint8_t key)

в которую подается пункт меню, а она зажигает нужный диод.

В случае использования адресных диодов WS2812 есть два варианта реализации- через таймер TIM15 — ШИМ на первый канал (PE5) и через дрыганье ногами с равными промежутками времени. Конечно любой STM`шик скажет что – таймером, таймеры наше все! Но я, видно плохой STM`шик, так как таймер у меня периодически дает сбой. Изредка загорается произвольно последний в цепочке диод. Хотя на осциллографе картинка сигнала одинакова относительно реализации на задержках. Но об этом я подумаю позже, так как обходное решение есть, функция не критичная, а значит баг минорный.

В приложенном проекте реализация на задержках, хотя схемотехника позволяет использовать таймер на выделенных под диоды пинах.

Три функции:

void WS2812_LOW();
void WS2812_UP();
void WS2812_LED_Set(uint8_t key);

Последняя аналогично void Set_LEDs(uint8_t key) зажигает пункт меню, первые две формируют сигнал. Необходимая задержка организована обычным циклом с математическим выражением. Задержка определялась методом подбора счетчика цикла и созерцания кривой на осциллографе.

кривая


Ну и перед началом передачи сигнала на адресные диоды, выключаем прерывания, а потом опять включаем. (В HAL создается системный таймер для delay и остальных функций, который прерывает наш код и может сместить тайминги).

Файлы


Подключение SD-карты и библиотеки «fatfs» делает HAL, единственное, что нужно указать, что использоваться будут длинные имена файлов (LFN).

Работа с SD-карточкой типовая. Но хочу описать – как храниться список файлов, и выбранный файл.

Работа с файлами и их содержимом собрана в FileManager.с и соответственно в FileManager.h, также подключается файл «MyExtention.h».

Функция:

uint8_t FM_GetDIR(char* st_dir)

читает указанную директорию и возвращает в случае успеха 0 или код ошибки.

Для хранения состава директории определена структура:

struct NData
{
    int menu_cnt; //номер файла
    char* Name;   //имя файла или каталога
    unsigned long size;  //размер файла
    uint8_t type;   //директория =1 файл=0 
};

на базе которой построена структура для стека:

typedef struct Node
{
    struct NData value;
    struct Node *next;
} Node;

А также функции для работы с этим стеком


Основные:

void push(Node **head, struct NData data );
struct NData pop(Node **head);

При запросе директории, заполняется структура NData в количестве, равной количеству файлов.

При этом для сохранения имени файла вызывается malloc для массива char в количестве, равному длине имени файла + 1 (завершающий ноль, для представления в виде строки)
malloc пишет в банк DTCMRAM размер которого 128 килобайт и он примерно на 20% занят нужными данными. Поэтому у нас получается тонкий момент по памяти. Если в директории будет 100500 файлов, да еще с именами длинной близкой максимальной, то девайс уйдет в закат.

Проверку у malloc на доступность выделения памяти я не использую, так как слабо могу представить, что буду по 200 книг хранить без разбивки по директориям.

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

Функция:

void FM_CurrListFile(int startpunkt)

печатает на экран список файлов. Ей надо указать на каком пункте изначально зажечь диод
Общая переменная Select_line_menu хранит выбранный пункт, который равен номеру файла+1 в структуре NData.

Функция:

FM_SelectFile(Select_line_menu-1);

запоминает выбранное имя файла, после чего считается что книга для чтения выбрана.

Кнопки


Функция работы с кнопками синхронная, то есть пока идет ожидание нажатия, в девайсе ничего не происходит (кроме прерываний).

Функция возврата нажатой кнопки:

 uint8_t FB_WaitKey()
{
uint32_t key_count = 0;
uint8_t key = 0;

while (key==0)
{
	if((GPIOC->IDR&(1<<5))==(1<<5))   {key=1;}   //dwn
	if(GPIOB->IDR&1)                  {key=2;}   // up
	if((GPIOC->IDR&(1<<4))==(1<<4))   {key=3;}   // меню
	if((GPIOB->IDR&(1<<1))==(1<<1))   {key=4;}   // функция
}

while ( ((GPIOC->IDR&(1<<5))==(1<<5)) |(GPIOB->IDR&1)|((GPIOC->IDR&(1<<4))==(1<<4))|((GPIOB->IDR&(1<<1))==(1<<1)))
{
	key_count++;
	HAL_Delay(1);
}
if (key_count>500) {key=key+10;};
return key;
}

Из кода видно, что после вызова мы падаем в бесконечный цикл до нажатия кнопки, выход из цикла по факту нажатия и тут же попадаем во второй цикл, который ждет когда мы отпустим кнопку.

Если кнопка нажималась на время меньшее полусекунды, то это короткое нажатие, если больше, то длинное. Соответственно возвращается код от 1 до 4 при коротком нажатии и +10 к номеру, если нажатие длинное.

На этом с подготовкой все.

Все готово для непосредственной работы с файлом FB2.

Второй этап — реализация непосредственной функции девайса


Структура файла FB2 является структурой похожей на XML. Поля ограничиваются открывающими и закрывающими тегами, есть вложенные структуры. Теги маркируются угловыми скобками (это я объяснил своими словами, подробнее в сети). Весь файл грузить в ОЗУ ресурсов контроллера не хватит, поэтому вложенность раскручиваться не будет. В процессе разбора FB2 необходимо бежать по файлу, регистрировать текстовые теги и выводить текст из них. То есть будет представлена некая упрощенная модель разборки формата, для которой много памяти не требуется.

Определю общие правила:

  • Текст в тегах «р» является основным текстом, выводится основным стилем №1 – начало тега с красной строки, далее вывод текста с учетом формата символов, определяемым внутри тега «p»
  • Текст в тегах «v» является подачей стихов, песен и подобного, выводится стилем№3 – каждая строка с отступом от левого края. Вывод текста с учетом формата символов, определяемым внутри тега «v»
  • Текст в серии тегов заголовков, выводится стилем №2 – печатается крупным шрифтом, формат символов игнорируется.
  • Теги, содержащие информацию, которая не является текстом книги – игнорируются или выводятся как исключение (автор, название книги, пустые линии)

Алгоритм следующий.

Курсор бежит по файлу, фиксирует теги, определяя угловые скобки.

Зафиксированный текстовый тег копируется в буфер «Out_Text[]» и зафиксированным стилем в переменной «FB2Tag_Style». Внутри текста тега возможно переключение на курсив или жирный шрифт, которое тоже определяется тегами. Эти теги удаляются, а вместо них ставится символ с кодом ниже 32.
28 – курсив
29 – жирный
30 – обычный

Функция отвечающая за этот алгоритм (файл FileManager.c, FileManager.h):

unsigned long FM_Into_file_FB2(unsigned long Cursor)

на вход подается позиция курсора, откуда надо начать искать очередной (или первый) тег, на выход – позиция курсора конца тега. Результат работы функции – текст первого встреченного тега, содержащего текст, который положен в «Out_Text[]» и стилем в «FB2Tag_Style». Файл из которого читаем, закрывается.

Да, имя файла и каталог лежат в общих переменных, в которые были записаны функцией, которую я привет в разделе Файлы выше.

После чего тег можно напечатать на экране.

Печатает функция:

uint8_t FB2_Printing_Tag_on_Screen(int deltatag)

на входе смещение относительно начала текста, которое необходимо вывести на экран, на выходе флаг был ли напечатан текст полностью не хватило экрана.

В ходе печати меняется общая переменная FB2_CurrentTagStep, которая хранит позицию в массиве «Out_Text[]», то есть в тексте из тега.

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

По сути общая переменная «FB2_CurrentTagStep» и «deltatag» несут одинаковый смысл, но в ходе обработки текста могут указывать в разные места, поэтому и разделены, я допустил новую сущность в угоду меньшим вычислениям.
Если долго множить сущности, то сущности объединятся и пожрут твой мозг
Через последовательный вызов двух вышеуказанных функций проходиться вся книга.

FB2_Printing_Tag_on_Screen – делает серию вызовов вспомогательных функций:

uint8_t Estimation_String(int deltatag)

– служит для оценки длинны строки. Отсекает по целым словам и дает длину пробела для выравнивания строки по правому краю (мы же привыкли, чтобы текст был ровным и справа и слева). Каждый символ имеет разную ширину, поэтому функция фактически проходит алгоритм печати, но без рисования точек на экране (для этого в функциях My_char_хххх есть параметр uint8_t type, о котором я говорил в разделе шрифтов).

void PrintFormatString(int deltatag)

– проводит уже настоящую печать с учетом данных от Estimation_String.
int PrintUnicodeChar(int tagstep,uint8_t CurrFormatChar, uint8_t type)

выводит символ на экран используя кодировку Юникода, с учетом типа шрифта (жирный, стандартный, курсив).

По факту, последняя функция должна иметь сестру для печати в кодировке WIN-1251, так как такая кодировка допускается в FB2.

int PrintWIN1251Char(int tagstep,uint8_t CurrFormatChar, uint8_t type)

функция реализована, но в общий алгоритм не включена, так как пока у меня нет книги в такой кодировке.

Вывод текста целой страницы выглядит следующим образом

uint8_t Y_End = 1;
if (FB2_CurrentTagStep>0) // выбор - продолжить печатать  незаконченный тег или начать новый
{
	FBCursor = FM_Into_file_FB2(FBCursorLastTag);
}
else
{
	FBCursor = FM_Into_file_FB2(FBCursor);
}
Y_End = FB2_Printing_Tag_on_Screen(FB2_CurrentTagStep);
while (Y_End == 1)   // допечатать страницу до конца
{
	FBCursorLastTag = FBCursor;
	FBCursor = FM_Into_file_FB2(FBCursor);
	Y_End = FB2_Printing_Tag_on_Screen(FB2_CurrentTagStep);
}

Переменные которые использует и меняет данный алгоритм
FBCursor — позиция курсора в файле на начале тега
FBCursorLastTag — сдвиг по тексту внутри тега
FB2_CurrentTagStep — сдвиг по тексту уже записанному в буфер
FB2Tag_Style — стиль текста.

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

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

Так выглядит результат печати листа:

Вспомним кто такие Хоббиты


Пример вывода разных стилей текста


Осталось совсем чуть-чуть (а то я утомил уже, наверное).

Сохранение информации и общий цикл работы


Для сохранения данных при отключении книги, воспользуемся установленными FLASH и EEPROM
Почему я решил дополнительно их припаивать, а не использовать ресурсы STM`ки или SD? Хранить во FLASH микроконтроллера это зверство, там циклов перезаписи не так много, да и этой самой FLASH кот наплакал. Использовать SD не хочу просто потому, что считаю некрасивым использовать не свой ресурс, да и в итоге будут доп. задачи, которые должны работать без карточки.

В чем характерное отличие EEPROM и FLASH для поставленной задачи:

FLASH – имеет ограниченное число перезаписей на конкретную ячейку (5-20тысяч) записывает и читает быстро
EEPROM – можно сказать не имеет ограничений на перезапись (1-2млн), записывает медленно (5 msek), читает относительно быстро.

Вывод: долбить во FLASH статистику каждой страницы не следует.

Положа руку на все места, одной FLASH обойтись можно, используя смещение ячеек или другие алгоритмы которые используются уже давно, но на базе этой книжки у меня еще будет логгер сигналов (этакий вариант анализатора с длительным ожиданием запрограммированной формы сигнала) и там мне Епромка будет нужна. Поэтому и тут я ее все же использую :)

Алгоритм работы следующий:

  1. При открытии новой книги FLASH стирается вся (ограничиваем себя 5-20 тысячами книг, потом замена FLASH) можно стирать и по одному блоку (их 128 по 64кбайт), но пока так не делал.
  2. В первую станицу пишется имя файла (256 байт), во вторую директорию (256 байт).
  3. С 512 ячейки начинаем запись текущих данных. Каждая открытая страница записывает 28 байт и смещает указатель текущей страницы для записи следующих 28 байт. Указатель записан в EEPROM. 1000 прочитанных страниц займет 28000 байт.
  4. при включении книги идем в EEPROM и получаем адрес, по нему идем в FLASH и получаем данные, как отразить текущую страницу или следующую (в зависимости от нажатой кнопки).

Описание данных:

struct SaveData   // 28 байт
{    // маркеры текущей страницы
	unsigned long CurrFileCursor;    
	unsigned long CurrFileCursorLastTag;
      unsigned long CurrTagStep;
      uint8_t CurrTagStyle;
    // маркеры следующей страницы
      unsigned long NextFileCursor;
      unsigned long NextFileCursorLastTag;
      unsigned long NextTagStep;
      uint8_t NextTagStyle;
    uint8_t EPointer; // указатель для поиска текущей страницы, в случает отказа от использования EEPROM 
    uint8_t Mark;  // номер закладки
};

С маркерами мы уже встречались выше, EPointer – резерв, Mark – номер закладки, тоже пока как резерв.

Запись в EEPROM — одно значение типа uint32_t

Работа с EEPROM по умолчанию в HAL поддержана, работа с FLASH, через библиотеку «w25qxx.h» взятую с github. Останавливаться на особенностях не буду, так как все в сети есть.
Упомяну только то, что при сохранении данных указываются массивы, а у нас структура. Их можно перекидывать одно в другое через указатели, а можно через объединения.

Для записи во FLASH:

typedef union
   {
    struct SaveData my_Var;
    uint8_t bytes[28];
} DataChang;

Для EEPROM
 typedef union
   {
    uint32_t my_Var;
    uint8_t bytes[4];
} Four_Bytes;

Поля в union имеют общую память, так что переводить одно в другое удобно.

Старт книги


В main.c

Первым делом после инициализации поддерживаем питание через PD7, определяем с какой кнопки мы включились, если мы нажали кнопку и очень быстро отпустили, то есть после инициализации она уже будет отпущена, то просто обновим текущую страницу. Включаем питание на WS2812 и еще один диодик, который показывает что процессор уже не спит. После уходим в Book_Start(start_key), где отрисовывается вышеуказанными методами страница.

После гасим питание:

 /* USER CODE BEGIN 2 */
       GPIOD->BSRR = GPIO_BSRR_BS7;// вкл поддежку питания
    //При включении определяем с какой кнопки включились
       uint8_t start_key = 0;
	if((GPIOC->IDR&(1<<5))==(1<<5))   {start_key=1;}   //dwn
	if(GPIOB->IDR&1)                  {start_key=2;}   // up
	if((GPIOC->IDR&(1<<4))==(1<<4))   {start_key=3;}   // меню
	GPIOE->BSRR = GPIO_BSRR_BS3;// вкл светодиоды меню
	GPIOE->BSRR = GPIO_BSRR_BS4;// вкл индикатор на панели
       Book_Start(start_key);
       GPIOD->BSRR = GPIO_BSRR_BR7;// by-by
  /* USER CODE END 2 */

Наверное все и наверное ничего не забыл


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

Ссылка на файлы (там все по папочкам)