image

… или как я изобретал собственные велосипеды с преферансами и гейшами на свой вкус — писал с нуля прошивку для фотополимерного принтера. На данный момент прошивка уже вполне работоспособна.

За основу была взята продающаяся на Алиэкспресс плата MKS DLP, для которой производитель дает схему и исходные коды прошивки, которые я отверг в пользу написания всего с нуля.
Статья получается очень уж большой, поэтому я решил разбить ее на две части. В этой части будет предыстория и описание самодельного GUI для сенсорного дисплея. В конце будут ссылки на сам предмет издевательств и на репозитории Гитхаба.


Часть 1: 1. Пользовательский интерфейс.
Часть 2: 2. Работа с файловой системой на USB-флэшке. 3. Управление шаговым двигателем для движения платформы.
Часть 3: 4. Вывод изображений слоев на дисплей засветки. 5. Всякая мелочь типа управления засветкой и вентиляторами, загрузки и сохранения настроек и т.п. 6. Дополнительные возможности для комфорта и удобства.

Для лучшего понимания дам очень короткое описание работы фотополимерных LCD 3D-принтеров для тех, кто с ними не знаком:

Короткое объяснение принципа работы большинства 'бытовых' фотополимерных принтеров
Главная часть такого принтера — LCD-дисплей высокого разрешения (как правило, это матрица с диагональю 5.5" и разрешением 2560х1440 (размер пикселя — 47.25 мкм). Под этим дисплеем находится источник УФ с длиной волны 405 нм. Над дисплеем находится ванна с фотополимером, у которой в качестве дна тонкая прозрачная FEP-пленка. В ванну опускается платформа, на которой «выращивается» модель. В начале печати платформа опускается на высоту одного слоя от пленки, на дисплей выводится изображение первого слоя и на заданное время включается УФ-засветка. Засветка, попадая через «открытые» пиксели дисплея и пленку на фотополимер отверждает его, так получается затвердевший слой. Первый слой прилипает к платформе. Затем засветка выключается, платформа приподнимается на высоту следующего слоя, на дисплей выводится изображение этого слоя и включается засветка. Второй слой отверждается, свариваясь с предыдущим слоем. И так повторяется раз за разом, пока не будет напечатана вся модель.

Предыстория


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

Предыстория получилась длинной, поэтому убрал ее под спойлер
Лет 5 назад я заинтересовался 3D-печатью. Не в профессиональном плане, а просто стало любопытно что же это такое, что она может и как работает. Сначала был приобретен FDM-принтер, один из самых бюджетных на тот момент — Anet A8. И в общем-то мне понравилось, учитывая, что чудес от него я не ждал. На нем я до сих пор иногда печатаю что-то утилитарное — какие-нибудь крепления, подставки, корпуса. А затем мне стало интересно пощупать фотополимерную печать с ее потрясающей детализацией, но тогда фотополимерные принтеры назвать бюджетными было никак нельзя. И вот пару лет назад я все-таки созрел на покупку одного из них — Anycubic Photon S. Уже и цены были не такими высокими, и я смог себе позволить потратить энную сумму просто для удовлетворения любопытства.

Сначала, конечно же, был эффект «вау» — он печатает такие мельчайшие детали, да так аккуратно. Никаких слоев, прыщей и т.п., присущих FDM-принтерам. Область печати, конечно, не ахти — всего примерно 115х65 мм, но фигурки и модельки получаются очень хорошо :) Когда эффект «вау» прошел, я понял, что детализация у него не такая хорошая, какая могла бы быть. После чего я по примеру знакомого его слегка модернизировал. Пришла новая волна «вау» — детализация повысилась в разы. Правда, стали четко видны границы пикселей, но только если рассматривать модель на расстоянии 20-30 см. Кстати, последующая покраска напечатанных моделей оказалась довольно неплохим способом отдохнуть от работы — мозг отдыхает, руки возятся. Результат дарится знакомым как интересный сувенир.

Но по мере освоения принтера я начал замечать недостатки в работе принтера. Нельзя настроить это, сложно изменить то, не работает так как хотелось бы и т.д. В частности, например, мой принтер не умел работать с каталогами на флэшке, не поддерживал кириллические имена файлов, скорость движения платформы в определенных случаях была не той, что бы меня устроила. Я даже дизассемблировал прошивку и начал разбираться с ее внутренностями. Реализовал работу с кириллицей в именах файлов, изменил процесс вывода на интерфейсный экран (ускорил), переделал работу с языками. Но все это было несерьезно, нужно было иметь исходники, чтобы можно было нормально переделать все что хотелось. А исходники никто из производителей почему-то не дает. И вот несколько месяцев назад я узнал, что есть такой набор для фотополимерного принтера от довольно известного в сфере 3D-печати производителя — MKS DLP. В набор входят: сама материнская плата, дисплей засветки с защитным стеклом (5.5", 2560х1440) и интерфейсный дисплей с сенсорной панелью (3.5", 480х320). И для этого набора идут открытые исходники и схема — бери и переделывай как угодно! И я приобрел этот набор, рассчитывая изменить в исходниках то, что мне не нравится.

Когда я получил комплект и скачал с гитхаба исходники, приготовившись их слегка модифицировать, у меня случился легкий шок. Ну, во-первых их родная прошивка оказалась в принципе работоспособна, но это и все, что можно сказать о ней хорошего. Недостатков в ней полно и печатать с ней было бы очень не комфортно. Уже на этапе проб родной прошивки у меня начала закрадываться мысль, что модифицировать придется не так уж слегка. А когда я открыл их проект с исходниками… Во-первых, это жуткая мешанина Ардуины и библиотек CMSIS и HAL от ST (плата построена на микроконтроллере STM32F407). Во-вторых, в проект впихнута полная версия Marlin 3D. Кто не знает — Marlin 3D — это проект для управления FDM 3D-принтерами. Он поддерживает работу до 6 шаговыми двигателями, несколькими нагревателями с контролем температуры, кучи концевиков, парсинг G-кода с построением траекторий движения осей и много-много чего еще. Больше 3 МБ исходников. И сюда он был целиком впихнут только ради управления одним шаговым двигателем. Причем это управление было сделано совершенно без заморочек — в текстовой строке формировался G-код движения оси и эта строка передавалась на вход парсера Мерлина. Ну это как если бы взяли целиком автомобиль для того, чтобы использовать одну из его фар для освещения. Вообще создалось впечатление, что производитель взял исходники от своих плат для FDM-принтеров и просто сверху прикостылял код для работы с фотополимерной частью.

Кроме того, там была еще GUI-библиотека в бинарнике, без исходников. И я понял, что проще будет написать свою прошивку с нуля, чем пытаться что-то сделать с родными исходниками.

Итак, что мы имеем:

  • комплект MKS DLP, в который входят: материнская плата, интерфейсный дисплей 3.5" 480х320 и дисплей засветки 5.5" 2560х1440
  • родные исходники от производителя
  • схема материнской платы (без названий активных и номиналов пассивных компонентов)

Материнская плата построена на основе микроконтроллера STM32F407. Для управления дисплеем засветки на плате стоит FPGA китайского производителя GW1N-LV4LQ144ES, SDRAM и две микросхемы MIPI-интерфейса SSD2828. Микроконтроллер загоняет в FPGA изображение слоя, FPGA сохраняет его в SDRAM и оттуда рефрешит дисплей через SSD2828. Конфигурацию (прошивку) FPGA производитель, кстати, не предоставляет в исходниках :( Кроме этого, на материнской плате есть:

  • вход питания 12-24 вольта
  • USB A разъем для подключения флэшки/картридера
  • коммутируемые выходы питания для засветки и двух вентиляторов
  • драйвер шагового двигателя A4988 и разъем для подключения двигателя
  • два разъема для подключения концевиков оси Z — верхнего и нижнего
  • разъем для подключения модуля WiFi
  • микросхема FLASH-памяти W25Q64
  • микросхема EEPROM-памяти AT24C16

Интерфейсный дисплей с резистивной тач-панелью подключается плоским 40-пиновым шлейфом. Контроллер дисплея — ILI9488, контроллер тач-панели — HR2046 (аналог TSC2046).

Для инициализации периферии я использовал программу STM32CUBE MX. Но не использовал напрямую полученный из него результат, а вставлял нужные куски в свои исходники. При работе с периферией использовал библиотеки HAL от ST, а там, где нужно было получить максимальную скорость — работал с регистрами напрямую.

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

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

1. Пользовательский интерфейс


Сначала была инициализация дисплея. Тут ничего интересного, стандартная последовательность для контроллера ILI9488. Ее я выдрал из родных исходников, вырезав оттуда код инициализации других видов дисплеев (которые, вероятно, остались там еще от FDM-жизни этих исходников). Дальше я занялся шрифтами.

1.1 Шрифты


В сети множество библиотек работы со шрифтами для микроконтроллеров, но абсолютное большинство из них работают с моноширинными шрифтами, а мне это не очень нравится. Это когда у всех символов одинаковая ширина, что у буквы «ж», что у буквы «i». Когда-то для одного из своих пет-проектов я написал библиотеку пропорциональных шрифтов. В ней для каждого шрифта используются два массива — массив с битовыми данными самих символов и массив с указанием ширины каждого символа. И небольшая структура с параметрами шрифта — указатели на массивы, высота шрифта, количество символов в шрифте:

typedef struct
{
	uint16_t	*width;
	uint8_t		*data;
	uint8_t		height;
	uint16_t	symcount;
} LCDUI_FONT;

Казалось бы, такая организация шрифта должна занимать в памяти больше места, чем просто битовый массив моноширинного шрифта, однако это не совсем так. Во-первых, сама моноширинность дает избыток хранящихся данных. Например, если в шрифте высотой 8 и шириной 5 пикселей для буквы «i» было бы достаточно 1 байта (1 бит ширины и 8 бит высоты), то она все равно будет занимать 5 байт данных (5 бит ширины и 8 бит высоты), т.к. ширина фиксирована. Во-вторых, как правило в таких шрифтах делается выравнивание по границам байта каждой строки или каждой колонки, смотря как организованы данные.

Например, взять тот же шрифт 5х8. Если битовые данные хранятся по строкам, то для каждой строки получается избыток 3 бита. Или 3 байта на символ:

image

Или шрифт 7х12 с хранением данных по колонкам, тогда получается избыток данных 4 бита на колонку или 3.5 байта на символ:

image

В моей библиотеке битовые данные непрерывны для символа и выравнивание по границе байта идет только в конце символа.

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

Вот процедура нахождения в массиве данных символа:

uint8_t*	_lcdui_GetCharData(char c)
{
	if (c < 32)
		return 0;
	if (c > 126)
		c -= 65;
	c -= 32;
	if (c >= lcdui_current_font->symcount)
		return 0;
	uint16_t c1 = lcdui_current_font->width[c];
	if (c1 & 0x8000)
		c = (c1 & 0x7FFF);
	uint16_t ch = lcdui_current_font->height;
	int32_t i = 0, ptr = 0, bits = 0, line_bits = ch;
	for (i = 0; i < c; i++)
	{
		if (lcdui_current_font->width[i] & 0x8000)
			continue;
		bits = lcdui_current_font->width[i] * line_bits;
		ptr += bits >> 3;
		if (bits & 0x07)
			ptr++;
	}

	return &(lcdui_current_font->data[ptr]);
}

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

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

Например, на фото ниже голубой текст и верхняя белая строка отрисованы моей библиотекой, а белая нижняя — стандартной ардуино-подобной библиотекой из родных исходников:

image

Голубой текст отрисовался в несколько раз быстрее, чем нижняя белая строка.

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

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

1.2 Вывод изображений интерфейса


Для пользовательского интерфейса понадобится выводить на дисплей изображения — фон, иконки, кнопки. Сначала я решил сильно не заморачиваться и хранить все изображения в формате .bmp в 8-мегабайтной флэш-памяти, имеющейся на плате. И даже уже написал для этого процедуру. Файл сохраняется в 16-битном формате (R5 G6 B5) с прямым или обратным порядком строк, и может уже быть напрямую скормленным процедуре отрисовки. Но размер фоновой картинки размером 480х320 выходит более 300 Кбайт. С учетом того, что часть этой флэш-памяти будет отводиться под обновление прошивки, 30 фоновых изображений займут всю память. Вроде и немало, но все же меньше, чем хотелось бы иметь на всякий случай. А ведь должны быть еще кнопки, иконки и т.п. Поэтому было решено преобразовывать изображения в какой-то сжатый формат.

Со сжатием вариантов немного — все более-менее хорошо сжимающие изображения алгоритмы требуют или прилично оперативки (по меркам микроконтроллера) или прилично времени на разжатие. Картинки же должны выводиться, разжимаясь на лету, и желательно чтобы картинка при выводе не уподоблялась ползущему прогресс-бару. Поэтому я остановился на RLE-сжатии — 1 байт кодирует количество повторов, а два следующих за ним — цвет. Для этого так же была написана утилита, преобразующая файлы .bmp в сжатые таким образом изображения. Заголовок состоит всего из 4 байт — по 2 байта на ширину и высоту изображения. В среднем фоновые изображения сжимаются таким способом в 5-7 раз, сильно зависит от размера одноцветных участков (чего и следовало ожидать). Например вот такая картинка сжалась с исходных 307 КБ до 74 КБ:

image

А вот такая — до 23 КБ с тех же 307:


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

Меня такой результат устроил. Декодирование и вывод изображений происходит очень быстро — примерно 40 миллисекунд на полное фоновое изображение. Так что на таком варианте я и остановился.

И, кстати, переход на режим DMA для вывода данных в дисплей не дал почти никакого ускорения вывода. Дисплей подключен по внешней 16-битной шине данных как внешняя память, но вот его тайминги довольно печальны, что и сводит почти на нет преимущества DMA-вывода перед попиксельным выводом «вручную».

1.3 Основа GUI


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

С тач-панелью все просто — микроконтроллер по прерываниям постоянно опрашивает контроллер тач-панели и усредняет последние 4 полученных результата, переводя их в координаты дисплея. Таким образом в любой момент известно состояние сенсора — нажат он или нет и если нажат, то в каком именно месте. Еще одна прослойка между тач-панелью и основной частью программы — процедура обработки нажатий кнопок, которая уже довольно давно кочует у меня из проекта в проект с небольшими адаптациями под конкретные условия.

Вот вкратце ее принцип работы
Изначально всем кнопкам присваивается статус «СВОБОДНА». По прерыванию таймера вызывается процесс опроса кнопок (100-150 раз в секунду). Если кнопка оказывается нажата, то ей присваивается статус «ПРЕДНАЖАТА». При следующем опросе если она все еще остается нажатой, ее счетчик увеличивается на единицу. Если оказывается, что счетчик достиг определенного значения, то кнопке присваивается статус «НАЖАТА», а счетчик обнуляется. Если при очередном опросе кнопка оказалась не нажатой, имея статус «ПРЕДНАЖАТА», то ее статус меняется на «СВОБОДНА». Когда оказывается отпущенной кнопка со статусом «НАЖАТА», ей дается статус «ОТПУЩЕНА». Основная программа просто опрашивает когда может статус кнопок и если у какой-то кнопки статус оказывается «НАЖАТА» или «ОТПУЩЕНА», то вызывается процедура обработки нажатия или отпускания этой кнопки. Тут реализуется и программный фильтр дребезга контактов (статус «ПРЕДНАЖАТА»), и срабатывание нажатия кнопки даже если основная программа во время нажатия была чем-то занята. Кроме того, там еще есть статусы и для длительного нажатия, и для повторяющегося ввода ни длительном нажатии.

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

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

В конечном итоге я пришел к такой схеме: интерфейс состоит из двух основных типов элементов — экранов и кнопок.

Экран — это своего рода полноэкранный контейнер для кнопок. У экрана есть следующие свойства:

  • фоновое изображение
  • цвет фона
  • способ отрисовки фона — заливка фоновым цветом или вывод изображения
  • текст заголовка
  • цвет текста заголовка
  • шрифт текста заголовка
  • указатель на родительский экран (в который нужно вернуться при закрытии этого)
  • массив указателей на кнопки
  • указатель на процедуру обработки событий (вызывается периодически в основном цикле программы)
  • указатель на процедуру отрисовки экрана

Структура экрана
typedef struct
{
	void				*addparameter;

	char				*bgimagename;
	
	void				*prevscreen;
	
	LNG_STRING_ID		name;
	TG_RECT				nameposition;
	TG_TEXTOPTIONS		nameoptions;
	
	uint8_t				btns_count;
	TG_BUTTON			*buttons;
	
	LCDUI_FONT_TYPE		font;
	LCDUI_FONT_TYPE		namefont;
	uint16_t			textcolor;
	uint16_t			nametextcolor;
	uint16_t			backcolor;

	struct {
		paintfunc		_callpaint;	// repaint screen
		processfunc		_process;	// screen process handling (check for changes, touch pressed, etc)
	} funcs;
} TG_SCREEN;


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

Свойства кнопок:

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

Структура кнопки
typedef struct
{
	void				*addparameter;
	
	uint8_t				button_id;
	

	int8_t				group_id;		// for swithed options buttons, >0 - single selection from group (select), <0 - multiple selection (switch)
	
	TG_RECT				position;
	
	void				*parentscreen;
	void				*childscreen;

	char				*bgimagename_en;
	char				*bgimagename_press;
	char				*bgimagename_dis;
	char				*bgimagename_act;	// for swithed options buttons

	LNG_STRING_ID		text;
	TG_RECT				textposition;
	LCDUI_FONT_TYPE		font;
	uint16_t			textcolor_en;
	uint16_t			textcolor_press;
	uint16_t			textcolor_dis;
	uint16_t			textcolor_act;	// for swithed options buttons
	uint16_t			backcolor_en;
	uint16_t			backcolor_press;
	uint16_t			backcolor_dis;
	uint16_t			backcolor_act;	// for swithed options buttons
	
	struct {
		uint8_t				active:1;		// for swithed options buttons
		uint8_t				needrepaint:1;
		uint8_t				pressed:1;
		uint8_t				disabled:1;
		uint8_t				repaintonpress:1;		// repaint or not when pressed - for indicate pressed state
		BGPAINT_TYPE		bgpaint:2;
	} options;
	
	TG_TEXTOPTIONS	textoptions;

	struct {
		paintfunc		_call_paint;	// repaint button
		pressfunc		_call_press;	// touch events handling
		pressfunc		_call_longpress;	// touch events handling
		processfunc		_call_process;	// periodical processing (for example text value refresh)
	} funcs;
} TG_BUTTON;


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

А вот на что не хватило возможностей этой схемы — так это на модальные окна с сообщениями или вопросами (типа MessageBox в Windows API), поэтому для них я сделал отдельный тип экранов. Без фоновых изображений и с размером, определяющимся заголовком или самим сообщением. Эти сообщения могут быть созданы в четырех вариантах — с кнопками «Да/Нет», с кнопками «Ок/Отмена», с одной кнопкой «Ок» или вообще без кнопок (типа «Подождите, идет загрузка данных...»).



Структура окна сообщений
typedef struct
{
	MSGBOXTYPE			type;
	
	void				*prevscreen;
	
	char				caption[128];
	char				text[512];
	TG_RECT				boxpos;
	
	uint8_t				btns_count;
	TG_BUTTON			buttons[TG_BTN_CNT_MSGBOX];
	
	uint16_t			caption_height;
	
	LCDUI_FONT_TYPE		font_caption;
	LCDUI_FONT_TYPE		font_text;
	uint16_t			text_color;
	uint16_t			box_backcolor;
	uint16_t			capt_textcolor;
	uint16_t			capt_backcolor;
} TG_MSGBOX;


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

1.4 Мультиязычность




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

	char *mshortname = LANG_GetString(LSTR_SHORT_JANUARY);

При изменении языка происходит просто изменение указателя с массива текстов на старом языке на массив с текстами на новом языке:

void		LANG_SetLanguage(uint8_t lang)
{
	lngCurrent = lngLanguages[lang].strings;
	
	return;
}

Все тексты в исходниках — в кодировке UTF-8. С этими кодировками тоже пришлось повозиться. Тексты — в UTF-8, кириллица файлов — в Unicode-16, некоторые строки — в обычном ANSI. Тянуть в прошивку целый набор библиотек для поддержки многобайтовых кодировок не хотелось, поэтому было написано несколько функций для преобразований из кодировки в кодировку и для операций с текстами в разных кодировках, например, добавить к концу строки Unicode16 строку в UTF-8.
Добавление нового языка теперь свелось к созданию таблицы текстов на нем и к изменению значения константы LNG_LANGS_COUNT. Правда, остается вопрос со шрифтами, если в новом языке используются символы помимо кириллицы и латинницы… Сейчас я поддерживаю в исходниках русский и гуглопереведенный английский.

1.5 Хранение изображений и прочих ресурсов


Для хранения больших ресурсов на плате имеется SPI-флэш на 8 мегабайт W25Q64. Изначально я хотел поступить как всегда — задать смещение для каждого ресурса внутри флэши и сохранять их туда как просто бинарные данные. Но потом понял, что проблемы с таким способом мне гарантированно обеспечены как только количество сохраняемых ресурсов перевалит за пару десятков и мне захочется изменить, например, какую-то картинку, которая сохранена шестой по порядку. Если ее размер увеличится, то придется сдвигать адреса всех следующих ресурсов и перезаписывать их заново. Или оставлять после каждого ресурса запасное пространство неизвестного размера — кто его знает как может измениться какой-то из ресурсов. Да в гробу я видал эту возню :) Поэтому я плюнул и организовал на этой флэши файловую систему. К тому времени у меня уже работала файловая система для USB на основе библиотеки FatFS, так что мне было достаточно просто написать отдельные низкоуровневые функции чтения/записи секторов. Одно только меня слегка расстроило — размер стираемого сектора в этой микросхеме аж целых 4 КБ. Это во-первых приводит к тому, что файлы будут занимать место порциями по 4 КБ (записал файл 200 байт — он занял 4 КБ флэши), а во-вторых буфер в структуре каждого файлового указателя будет отъедать те же 4 КБ оперативки, которой в микроконтроллере не так уж много — 192 КБ. Можно было бы, конечно, извратиться и написать низкоуровневые функции так, чтобы они могли писать и читать и меньшими порциями, рапортуя о размере сектора, например, 512 байт. Но это замедлило бы работу с флэш, так что оставил размер сектора 4 КБ. Так что обращение к любому ресурсу осуществляется просто по имени его файла, что оказалось очень удобным. На данный момент, например, количество хранимых ресурсов перевалило уже за 90. И их обновление я сделал максимально простым — обновляемые (или новые) ресурсы записываются на USB-флэшку в определенный каталог, флэшка вставляется в плату, плата перезагружается в сервисный режим (во время включения или перезагрузки нажать и держать правый верхний угол дисплея) и автоматически копирует все найденные в этом каталоге файлы с USB-флэшки в SPI-флэш.



Продолжение следует...


По интерфейсу вышла, пожалуй, самая объемная часть. Если эта статья окажется интересной сообществу, то во вторую часть постараюсь вместить все остальное.

Ну и буду рад вопросам и замечаниям.

Часть 1: 1. Пользовательский интерфейс.
Часть 2: 2. Работа с файловой системой на USB-флэшке. 3. Управление шаговым двигателем для движения платформы.
Часть 3: 4. Вывод изображений слоев на дисплей засветки. 5. Всякая мелочь типа управления засветкой и вентиляторами, загрузки и сохранения настроек и т.п. 6. Дополнительные возможности для комфорта и удобства.

Ссылки


Комплект MKS DLP на Алиэкспресс
Исходники оригинальной прошивки от производителя на Гитхабе
Схемы от производителя двух версий платы на Гитхабе
Мои исходники на Гитхабе