Pull to refresh

Прошивка для фотополимерного LCD 3D-принтера своими руками. Часть 3

Reading time 23 min
Views 5.2K


В предыдущих двух частях я рассказал о том как делал GUI, заводил управление шаговым двигателем и организовывал работу с файлами на USB-флэшке.

Сегодня я напишу о процессе печати, выводе печатаемых слоев на экран засветки и об оставшихся, не таких существенных вещах:

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


4. Вывод изображений слоев на дисплей засветки


4.1 Вывод изображений на УФ-дисплей


Как вообще микроконтроллер, у которого нет специализированной периферии, смогли заставить рефрешить изображение на матрице высокого разрешения со скоростью 74 миллиона пикселя в секунду (разрешение 2560х1440, 20 кадров в секунду) по интерфейсу MIPI? Ответ: с помощью FPGA с подключенной к ней 16-мегабайтной SDRAM и двух микросхем интерфейса MIPI — SSD2828. Две микросхемы стоят потому, что дисплей логически разделен на две половины, каждая из которых обслуживается по своему отдельному каналу, получается два дисплея в одном.

Изображение для вывода на дисплей хранится в одном из 4 банков SDRAM, микросхема FPGA занимается обслуживанием SDRAM и выводом изображения из нее в SSD2828. FPGA генерирует для SSD2828 сигналы вертикальной и горизонтальной синхронизации и гонит
непрерывный поток значений цвета для пикселей по 24 линиям (8R 8G 8B) в каждую из SSD2828. Частота кадров получается около 20 Гц.

FPGA соединена с микроконтроллером последовательным интерфейсом (SPI), через который микроконтроллер может передавать изображение. Передается оно пакетами, каждый из которых вмещает одну строку изображения (строки считаются по короткой стороне дисплея — 1440 пикселей). В пакете кроме этих данных указываются так же номер банка SDRAM, номер строки и контрольная сумма — CRC16. FPGA принимает этот пакет, проверяет контрольную сумму и если все в порядке, сохраняет данные в соответствующую область SDRAM. Если CRC не совпадает, FPGA выставляет сигнал на одном из своих выводов, так же соединенном с микроконтроллером, по которому микроконтроллер понимает, что данные не дошли нормально и может повторить отправку. Для полного изображения микроконтроллер должен отправить в FPGA 2560 таких пакетов.

Данные изображения внутри пакета представляются в битовом формате: 1 — пиксель светится, 0 — пиксель затемнен. Увы, это полностью исключает возможность организации полутонового размытия краев печатаемых слоев — антиалиасинга. Чтобы организовать такой способ размытия необходимо переписывать конфигурацию (прошивку) FPGA, к чему я пока не готов. Слишком давно и не очень долго я работал с FPGA, придется практически заново все осваивать.

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

Микросхемы SSD2828 так же подключены к микроконтроллеру по SPI. Это нужно для того, чтобы при включении сконфигурировать их регистры, перевести их в спящий или активный режим.
Имеются еще несколько линий между микроконтроллером и FPGA/SSD2828 — сигнал сброса (Reset) и сигналы выбора активного чипа (Chip Select) на каждую из микросхем.

Вообще, эта схема работы довольно далека от оптимальной, на мой взгляд. Было бы, например, логичнее подключить FPGA к микроконтроллеру по параллельному интерфейсу внешней памяти, данные передавались бы гораздо быстрее, чем по SPI с ограничением по частоте в 20 МГц (при повышении частоты FPGA уже перестает нормально принимать данные). Плюс ко всему сигнал сброса заведен не на физический вход Reset FPGA, а как обычный логический сигнал, то есть аппаратного сброса по нему у FPGA не происходит. И это тоже сыграло злую шутку, о которой будет ниже.

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

4.2 Чтение слоев из файла для печати


Ок, с выводом готового изображения более-менее разобрались, теперь я расскажу немного про то как эти изображения добываются из файлов, подготовленных к печати. Файлы форматов .pws, .photons, .photon, .cbddlp — это, по сути, куча изображений слоев. Такой формат пошел, насколько я знаю, от китайской компании Chitu, которая и придумала делать платы с такой схемой (мкроконтроллер — FPGA — SDRAM — SSD2828). Предположим, нужно напечатать модель высотой 30 мм с толщиной каждого слоя 0.05 мм. Программа-слайсер нарезает эту модель на слои указанной толщины и для каждого из них формирует его изображение.

Таким образом получается 30/0.05=600 изображений разрешением 1440х2560. Эти изображения упаковываются в выходной файл, туда же вписывается заголовок со всеми параметрами и такой файл уже и попадает в принтер. Изображения слоев имеют глубину цвета 1 бит и сжимаются алгоритмом RLE по одному байту, в котором старший бит указывает значение цвета, а семь младших битов — число повторов. Такой способ позволяет сжимать изображение слоя с 460 КБ до примерно 30-50. Принтер считывает сжатый слой, разжимает его и отправляет построчно в FPGA.

У производителя это происходит следующим образом:

  1. Читается один байт из файла и распаковывается в байтовый массив — если очередной бит равен 1, то и очередному байту присваивается значение 1, иначе значение 0. Так повторяется пока не будет заполнен весь байтовый массив, размер которого равен числу пикселей в строке дисплея (1440), то есть все значения для строки дисплея.
  2. Этот байтовый массив передается в функцию, которая упаковывает его опять в битовый массив размером 1440 бит (180 байт).
  3. Полученный битовый массив передается в FPGA как данные для строки в составе пакета.

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

Сейчас у меня используется этот же способ, хотя и оптимизированный. Чтобы пояснить в чем заключалась оптимизация, мне нужно пояснить еще один момент. Данные для строки дисплея идут не сплошным массивом полезных данных. Посередине присутствуют несколько лишних «нерабочих» пикселя из-за того, что два контроллера дисплея стыкуются именно на короткой стороне, и у каждого из них есть по 24 «нерабочих» пикселя по краям. Таким образом, реальные передаваемые данные для одной строки изображения состоят из 3 частей: данные для первой половины (первого контроллера), промежуточные «нерабочие» 48 пикселей, данные для второй половины (второго контроллера).

Так вот, китайцы при формировании байтового массива внутри цикла проверяли достигнут ли конец первой половины, если не достигнут, то значение писалось по указателю *p, а иначе по указателю *(p+48). Эта проверка для каждого из 1440 значений, да еще и модификация указателя для половины из них, явно не способствовали скорости работы цикла. Я разбил этот один цикл на два отдельных — в первом заполняется первая половина массива, после этого цикла указатель увеличивается на 48 и начинается второй цикл для второй половины массива. В оригинальном исполнении слой читался и выводился на дисплей за 1.9 секунды, одна только эта модификация снизила время чтения и вывода до 1.2 секунд.

Еще одно изменение касалось передачи данных в FPGA. В оригинальных исходниках она происходит через DMA, но после старта трансфера по DMA функция ожидает его завершения и только после этого начинает декодировать и формировать новую строку изображения. Я убрал это ожидание, так что следующая строка формируется пока данные предыдущей строки передаются. Это уменьшило время еще на 0.3 сек, до 0.9 на слой. И это при компиляции без оптимизации, если скомпилировать с полной оптимизацией, то время уменьшается до примерно 0.53 сек, что уже вполне приемлемо. Из этих 0.53 сек примерно 0.22 сек занимает вычисление CRC16 и около 0,19 сек — формирование битового массива из байтового перед передачей. А вот сама передача всех строк в FPGA занимает около 0.4 секунды и с этим, скорее всего, уже ничего не сделать — тут все упирается в ограничение максимально допустимой для FPGA частоты SPI.

Если бы самому заняться написанием конфигурации FPGA, то можно было бы отдать ей и разжатие RLE, и это могло бы на порядок ускорить вывод слоя, но как сделано так сделано…

И да, я же собирался написать о косяке, связанном с тем, что FPGA не сбрасывается аппаратно по сигналу сброса от микроконтроллера. Так вот, когда я уже научился выводить изображения слоев, доделал сам процесс печати, то столкнулся с непонятным багом — один раз из 5-10 печать запускалась с полностью засвеченным дисплеем. Я вижу в отладчике, что слои читаются корректно, данные в FPGA отправляются какие надо, FPGA подтверждает корректность CRC. То есть все работает, а вместо рисунка слоя — полностью белый дисплей. Явно виноваты или FPGA или SSD2828. Еще раз перепроверил инициализацию SSD2828 — все нормально, все регистры в них инициализируются нужными значениями, это видно при контрольном чтении значений из них. Тогда я уже полез в плату осциллографом. И выяснил, что когда происходит такой сбой, FPGA никакие данные в SDRAM не пишет. Сигнал WE, разрешающий запись, стоит в неактивном уровне как вкопанный. И я бы, наверное, долго бился с этим глюком, если бы не знакомый, который посоветовал попробовать перед сбросом дать в FPGA явную команду отключения вывода изображения, чтобы в момент сброса гарантированно не было обращений от FPGA к SDRAM. Я попробовал — и все заработало! Больше этот баг ни разу не проявил себя. В конечном итоге мы с ним пришли к выводу, что корка (IP-core) контроллера SDRAM внутри FPGA имплементирована не совсем правильно, сброс и инициализация контроллера SDRAM происходит нормально не во всех случаях. Что-то мешает правильному сбросу если в этот момент происходит обращение к данным в SDRAM. Вот так…

4.3 Пользовательский интерфейс во время печати файла


После того как пользователь выбрал файл и запустил его печать появляется вот такой экран:



Это довольно стандартный экран для подобных фотополимерных принтеров.

Самую большую область экрана занимает картинка засвечиваемого в данный момент слоя.
Отображение этой картинки синхронизировано с засветкой — когда засветка включается, картинка отображается, при выключении засветки и картинка стирается. Формируется картинка как и для УФ-дисплея — по короткой стороне изображения. Я не стал метаться с указателями по смещениям строк этой картинки, а просто перед ее выводом даю контроллеру дисплея команду изменить направление вывода для заливаемых данных, т.е. область этой картинки получается «повернутой» на бок.

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

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



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

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

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

5. Всякая мелочь типа управления засветкой и вентиляторами, загрузки и сохранения настроек и т.п.


На плате есть 3 коммутируемых через мощные MOSFET выхода — один для УФ-светодиодов засветки и два для вентиляторов (охлаждение диодов засветки и охлаждение дисплея, например). Тут ничего интересного — выходы микроконтроллера подключены к затворам этих транзисторов и управлять ими так же просто, как мигать светодиодом. Для высокой точности выдерживаемого времени засветки она включается в основном цикле через функцию, задающую время работы:

UVLED_TimerOn(l_info.light_time * 1000);

void		UVLED_TimerOn(uint32_t time)
{
	uvled_timer = time;
	UVLED_On();
}

А выключается из миллисекундного прерывания таймера по достижению счетчика работы засветки нуля:

...
	if (uvled_timer && uvled_timer != TIMER_DISABLE)
	{
		uvled_timer--;
		if (uvled_timer == 0)
			UVLED_Off();
	}
...

5.1 Настройки, загрузки из файла и сохранение в EEPROM


Настройки хранятся в имеющейся на плате микросхеме EEPROM at24c16. Тут, в отличии от хранения ресурсов в большой флэш-памяти, все просто — для каждого типа сохраняемых данных жестко задано смещение адреса внутри EEPROM. Всего в ней сохраняются три блока: настройки оси Z, общие настройки системы (язык, звук и т.д.) и счетчики времени работы основных компонентов принтера — засветки, дисплея и вентилятора.

В структурах сохраняемых блоков присутствуют текущая версия прошивки и примитивная контрольная сумма — просто 16-битная сумма значений всех байтов блока. При считывании настроек из EPROM проверяется CRC и если он не соответствует реальному, то параметрам этого блока присваиваются значения по умолчанию, высчитывается новый CRC и блок сохраняется в EPROM вместо старого. Если у считанного блока не совпадает с текущей версия, то должно произойти его обновление до текущей версии и уже в новом виде он будет сохранен вместо старого. Это пока не реализовано, но будет в будущем сделано для корректного обновления прошивки.

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

Структура такого файла стандартна: имя параметра + знак равенства + значение параметра. Одна строка — один параметр. Пробелы и символы табуляции в начале строки и между знаком равенства и именем и значением игнорируются. Так же игнорируются пустые строки и строки, начинающиеся с символа решетки — "#", этот символ определяет строки с комментариями. Регистр букв в именах параметров и разделов значения не имеет.

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

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

Содержимое конфигурационного файла
# Stepper motor Z axis settings
[ZMotor]

	# Изменяет направление движения платформы.
	# Допустимые значения: 0 или 1. По умолчанию: 1.
	# Измените этот параметр если платформа двигается в неверном направлении.
	invert_dir = 1

	# Направление движения платформы при поиске домашней позиции.
	# Допустимые значения: -1 или 1. По умолчанию: -1.
	# Если этот параметр равен -1, то при поиске домашней позиции
	# платформа будет двигаться вниз, к нижнему концевику. При значении 1
	# платформа будет двигаться к верхнему концевику.
	home_direction = -1

	# Значение оси Z после поиска домашней позиции. Как правило, для нижнего
	# домашнего концевика это 0, для верхнего - максимальная высота оси.
	home_pos = 0.0

	# Ограничение на минимальную допустимую нижнюю позицию платформы в миллиметрах.
	# Допустимые значения: число в диапазоне от -32000.0 до 32000.0.
	# По умолчанию: -3.0
	# Это ограничение действует только после нахождения домашней позиции. Если
	# поиск домашней позиции не производился, то движение ограничивается концевиками.
	min_pos = -3.0

	# Ограничение на максимальную допустимую верхнюю позицию платформы в миллиметрах.
	# Допустимые значения: число в диапазоне от -32000.0 до 32000.0.
	# По умолчанию: 180.0
	# Это ограничение действует только после нахождения домашней позиции. Если
	# поиск домашней позиции не производился, то движение ограничивается концевиками.
	max_pos = 180.0

	# Работа нижнего концевика.
	# Допустимые значения: 0 или 1. По умолчанию: 1.
	# Если при срабатывании концевика напряжение на его выходе пропадает, то поставьте
	# значение 1, если наоборот - поставьте 0.
	min_endstop_inverting = 1

	# Работа верхнего концевика.
	# Допустимые значения: 0 или 1. По умолчанию: 1.
	# Если при срабатывании концевика напряжение на его выходе пропадает, то поставьте
	# значение 1, если наоборот - поставьте 0.
	max_endstop_inverting = 1

	# Количество шагов двигателя на 1 мм движения платформы.
	steps_per_mm = 1600

	# Скорость первого, быстрого движения к концевику при поиске домашней
	# позиции, мм/сек. По умолчанию: 6.0.
	homing_feedrate_fast = 6.0

	# Скорость второго, медленного движения к концевику при поиске домашней
	# позиции, мм/сек. По умолчанию: 1.0.
	homing_feedrate_slow = 1.0

	# Ускорение платформы в режиме печати, мм/сек2.
	acceleration = 0.7

	# Скорость движения платформы в режиме печати, мм/сек.
	feedrate = 5.0

	# Ускорение платформы в режиме свободного движения (движение кнопками из интерфейса,
	# подъем по окончании печати и т.п.), мм/сек2.
	travel_acceleration = 25.0

	# Ускорение платформы в режиме свободного движения (движение кнопками из интерфейса,
	# подъем по окончании печати и т.п.), мм/сек. На высоте менее 30 мм платформа
	# двигается в три раза медленнее заданной в этом параметре скорости, но не менее
	# 5 мм/сек.
	travel_feedrate = 25.0

	# Ток двигателя для интегрированного в плату драйвера, мА.
	current_vref = 800.0

	# Ток двигателя для интегрированного в плату драйвера в режиме удержания, мА.
	current_hold_vref = 300.0

	# Время с момента последнего движения двигателя, после которого включается режим
	# удержания с пониженным током. Задается в секундах. Значение 0 отключает режим
	# удержания с пониженным током.
	hold_time = 30.0

	# Время с момента последнего движения двигателя, после которого мотор полностью
	# отключается. Задается в секундах. Значение этого параметра должно быть не меньше
	# значения параметра hold_time. Значение 0 отключает этот режим.
	# Следует учесть, что при отключении мотора теряется домашняя позиция.
	off_time = 10.0



# General settings
[General]

	# Длительность звука зуммера в миллисекундах (0.001 сек) при окончании печати
	# или при выводе сообщений об ошибках.
	# Допустимые значения: от 0 до 15000. По умолчанию: 700 (0.7 сек).
	buzzer_msg_duration = 700

	# Длительность звука зуммера в миллисекундах (0.001 сек) при нажатии
	# на активную зону сенсорного дисплея, например на кнопку.
	# Допустимые значения: от 0 до 15000. По умолчанию: 70 (0.07 сек).
	buzzer_touch_duration = 70

	# Переворачивает изображение на интерфейсном дисплее на 180 градусов.
	# Служит для возможности переворота дисплея в принтере для более удобного его размещения.
	# Допустимые значения: 0 или 1. По умолчанию: 0.
	rotate_display = 0

	# Время перехода дисплея в режим скринсейвера с отображением времени и даты, задается в минутах.
	# Скринсейвер эмулирует настольные LCD-часы. Переход обратно в рабочий режим - нажатие в любом
	# месте дисплея.
	# Допустимые значения: от 0 до 15000. По умолчанию: 10. Значение 0 отключает режим скринсейвера.
	screensaver_time = 10


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



В случае обнаружения ошибки будет выведено сообщение о ней с указанием типа ошибки и номера строки. Обрабатываются следующие ошибки:

  • неизвестное имя раздела
  • неизвестное имя параметра
  • неверное значение параметра — когда, к примеру, числовому параметру пытаются присвоить текстовое значение

Если кому будет интересно - тут полная простыня трех главных функций парсера
void			_cfg_GetParamName(char *src, char *dest, uint16_t maxlen)
{
	if (src == NULL || dest == NULL)
		return;
	
	char *string = src;
	// skip spaces
	while (*string != 0 && maxlen > 0 && (*string == ' ' || *string == '\t' || *string == '\r'))
	{
		string++;
		maxlen--;
	}
	// until first space symbol
	while (maxlen > 0 && *string != 0 && *string != ' ' && *string != '\t' && *string != '\r' && *string != '\n' && *string != '=')
	{
		*dest = *string;
		dest++;
		string++;
		maxlen--;
	}
	
	if (maxlen == 0)
		dest--;
	
	*dest = 0;
	return;
}
//==============================================================================




void			_cfg_GetParamValue(char *src, PARAM_VALUE *val)
{
	val->type = PARAMVAL_NONE;
	val->float_val = 0;
	val->int_val = 0;
	val->uint_val = 0;
	val->char_val = (char*)"";
	
	if (src == NULL)
		return;
	if (val == NULL)
		return;
	
	char *string = src;
	// search '='
	while (*string > 0 && *string != '=')
		string++;
	if (*string == 0)
		return;
	
	// skip '='
	string++;
	// skip spaces
	while (*string != 0 && (*string == ' ' || *string == '\t' || *string == '\r'))
		string++;
	if (*string == 0)
		return;

	// check param if it numeric
	if ((*string > 47 && *string < 58) || *string == '.' || (*string == '-' && (*(string+1) > 47 && *(string+1) < 58) || *(string+1) == '.'))
	{
		val->type = PARAMVAL_NUMERIC;
		val->float_val = (float)atof(string);
		val->int_val = atoi(string);
		val->uint_val = strtoul(string, NULL, 10);
	}
	else
	{
		val->type = PARAMVAL_STRING;
		val->char_val = string;
	}
	
	return;
}
//==============================================================================




void			CFG_LoadFromFile(void *par1, void *par2)
{
	sprintf(msg, LANG_GetString(LSTR_MSG_CFGFILE_LOADING), cfgCFileName);
	TGUI_MessageBoxWait(LANG_GetString(LSTR_WAIT), msg);

	UTF8ToUnicode_Str(cfgTFileName, cfgCFileName, sizeof(cfgTFileName)/2);
	if (f_open(&ufile, cfgTFileName, FA_OPEN_EXISTING | FA_READ) != FR_OK)
	{
		if (tguiActiveScreen == (TG_SCREEN*)&tguiMsgBox)
			tguiActiveScreen = (TG_SCREEN*)((TG_MSGBOX*)tguiActiveScreen)->prevscreen;
		TGUI_MessageBoxOk(LANG_GetString(LSTR_ERROR), LANG_GetString(LSTR_MSG_FILE_OPEN_ERROR));
		BUZZ_TimerOn(cfgConfig.buzzer_msg);
		return;
	}

	uint16_t		cnt = 0;
	uint32_t		readed = 0, totalreaded = 0;
	char			*string = msg;
	char			lexem[128];
	PARAM_VALUE		pval;
	CFGREAD_STATE	rdstate = CFGR_GENERAL;
	int16_t			numstr = 0;
	
	while (1)
	{
		// read one string
		cnt = 0;
		readed = 0;
		string = msg;
		while (cnt < sizeof(msg))
		{
			if (f_read(&ufile, string, 1, &readed) != FR_OK || readed == 0 || *string == '\n')
			{
				*string = 0;
				break;
			}
			cnt++;
			string++;
			totalreaded += readed;
		}
		if (cnt == sizeof(msg))
		{
			string--;
			*string = 0;
		}
		numstr++;
		string = msg;
		
		// trim spaces/tabs at begin and end
		strtrim(string);
		
		// if string is empty
		if (*string == 0)
		{
			// if end of file
			if (readed == 0)
				break;
			else
				continue;
		}
		
		// skip comments
		if (*string == '#')
			continue;
		
		// upper all letters
		strupper_utf(string);
		
		// get parameter name
		_cfg_GetParamName(string, lexem, sizeof(lexem));
		
		// check if here section name
		if (*lexem == '[')
		{
			if (strcmp(lexem, (char*)"[ZMOTOR]") == 0)
			{
				rdstate = CFGR_ZMOTOR;
				continue;
			}
			else if (strcmp(lexem, (char*)"[GENERAL]") == 0)
			{
				rdstate = CFGR_GENERAL;
				continue;
			}
			else
			{
				rdstate = CFGR_ERROR;
				string = LANG_GetString(LSTR_MSG_UNKNOWN_SECTNAME_IN_CFG);
				sprintf(msg, string, numstr);
				break;
			}
		}
		
		// get parameter value
		_cfg_GetParamValue(string, &pval);
		if (pval.type == PARAMVAL_NONE)
		{
			rdstate = CFGR_ERROR;
			string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
			sprintf(msg, string, numstr);
			break;
		}
		
		// check and setup parameter
		switch (rdstate)
		{
			case CFGR_ZMOTOR:
				rdstate = CFGR_ERROR;
				if (*lexem == 'A')
				{
					if (strcmp(lexem, (char*)"ACCELERATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						cfgzMotor.acceleration = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'C')
				{
					if (strcmp(lexem, (char*)"CURRENT_HOLD_VREF") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val < 100)
							pval.uint_val = 100;
						if (pval.uint_val > 1000)
							pval.uint_val = 1000;
						cfgzMotor.current_hold_vref = pval.uint_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"CURRENT_VREF") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val < 100)
							pval.uint_val = 100;
						if (pval.uint_val > 1000)
							pval.uint_val = 1000;
						cfgzMotor.current_vref = pval.uint_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'F')
				{
					if (strcmp(lexem, (char*)"FEEDRATE") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						if (pval.float_val > 40)
							pval.float_val = 40;
						cfgzMotor.feedrate = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'H')
				{
					if (strcmp(lexem, (char*)"HOLD_TIME") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val == 0)
							pval.uint_val = TIMER_DISABLE;
						else if (pval.uint_val > 100000)
							pval.uint_val = 100000;
						cfgzMotor.hold_time = pval.uint_val * 1000;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOME_DIRECTION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val != -1.0 && pval.int_val != 1.0)
							pval.int_val = -1;
						cfgzMotor.home_dir = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOME_POS") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						cfgzMotor.home_pos = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOMING_FEEDRATE_FAST") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						if (pval.float_val > 40)
							pval.float_val = 40;
						cfgzMotor.homing_feedrate_fast = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOMING_FEEDRATE_SLOW") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						if (pval.float_val > 40)
							pval.float_val = 40;
						cfgzMotor.homing_feedrate_slow = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'I')
				{
					if (strcmp(lexem, (char*)"INVERT_DIR") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val < 0 || pval.int_val > 1)
							pval.int_val = 1;
						cfgzMotor.invert_dir = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'M')
				{
					if (strcmp(lexem, (char*)"MAX_ENDSTOP_INVERTING") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val < 0 || pval.int_val > 1)
							pval.int_val = 1;
						cfgzMotor.max_endstop_inverting = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"MAX_POS") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						cfgzMotor.max_pos = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"MIN_ENDSTOP_INVERTING") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val < 0 || pval.int_val > 1)
							pval.int_val = 1;
						cfgzMotor.min_endstop_inverting = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"MIN_POS") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						cfgzMotor.min_pos = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'O')
				{
					if (strcmp(lexem, (char*)"OFF_TIME") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 100000)
							pval.uint_val = 100000;
						else if (pval.uint_val < cfgzMotor.hold_time)
							pval.uint_val = cfgzMotor.hold_time + 1000;
						else if (pval.uint_val == 0)
							pval.uint_val = TIMER_DISABLE;
						cfgzMotor.off_time = pval.int_val * 60000;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'S')
				{
					if (strcmp(lexem, (char*)"STEPS_PER_MM") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val < 1)
							pval.uint_val = 1;
						if (pval.uint_val > 200000)
							pval.uint_val = 200000;
						cfgzMotor.steps_per_mm = pval.uint_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'T')
				{
					if (strcmp(lexem, (char*)"TRAVEL_ACCELERATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						cfgzMotor.travel_acceleration = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"TRAVEL_FEEDRATE") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						cfgzMotor.travel_feedrate = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				}

				string = LANG_GetString(LSTR_MSG_UNKNOWN_PARAMNAME_IN_CFG);
				sprintf(msg, string, numstr);
				break;

			case CFGR_GENERAL:
				rdstate = CFGR_ERROR;
				if (*lexem == 'B')
				{
					if (strcmp(lexem, (char*)"BUZZER_MSG_DURATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 15000)
							pval.uint_val = 15000;
						cfgConfig.buzzer_msg = pval.uint_val;
						rdstate = CFGR_GENERAL;
						break;
					}
					if (strcmp(lexem, (char*)"BUZZER_TOUCH_DURATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 15000)
							pval.uint_val = 15000;
						cfgConfig.buzzer_touch = pval.uint_val;
						rdstate = CFGR_GENERAL;
						break;
					}
				} else
				if (*lexem == 'R')
				{
					if (strcmp(lexem, (char*)"ROTATE_DISPLAY") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 0)
						{
							cfgConfig.display_rotate = 1;
							LCD_WriteCmd(0x0036);
							LCD_WriteRAM(0x0078);
						}
						else
						{
							cfgConfig.display_rotate = 0;
							LCD_WriteCmd(0x0036);
							LCD_WriteRAM(0x00B8);
						}
						rdstate = CFGR_GENERAL;
						break;
					}
				} else
				if (*lexem == 'S')
				{
					if (strcmp(lexem, (char*)"SCREENSAVER_TIME") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 15000)
							cfgConfig.screensaver_time = 15000 * 60000;
						else if (pval.uint_val == 0)
							pval.uint_val = TIMER_DISABLE;
						else
							cfgConfig.screensaver_time = pval.uint_val * 60000;
						rdstate = CFGR_GENERAL;
						break;
					}
				}

				string = LANG_GetString(LSTR_MSG_UNKNOWN_PARAMNAME_IN_CFG);
				sprintf(msg, string, numstr);
				break;

		}
		
		if (rdstate == CFGR_ERROR)
			break;
		
		
	}
	f_close(&ufile);
	
	
	if (tguiActiveScreen == (TG_SCREEN*)&tguiMsgBox)
	{
		tguiActiveScreen = (TG_SCREEN*)((TG_MSGBOX*)tguiActiveScreen)->prevscreen;
	}

	if (rdstate == CFGR_ERROR)
	{
		TGUI_MessageBoxOk(LANG_GetString(LSTR_ERROR), msg);
		BUZZ_TimerOn(cfgConfig.buzzer_msg);
	}
	else
	{
		CFG_SaveMotor();
		CFG_SaveConfig();
		TGUI_MessageBoxOk(LANG_GetString(LSTR_COMPLETED), LANG_GetString(LSTR_MSG_CFGFILE_LOADED));
	}
}
//==============================================================================


После успешного парсинга файла новые настройки сразу же применяются и сохраняются в EPROM.

Счетчики часов наработки компонентов принтера обновляются в EPROM только по окончании или прерывании печати файла.

6. Дополнительные возможности для комфорта и удобства


6.1 Часы с календарем


Ну, просто чтобы было. Зачем пропадать добру — встроенным в микроконтроллер автономным часам реального времени, которые умеют работать от литиевой батарейки при выключенном общем питании и потребляют так мало, что CR2032 по расчетам должно хватать на несколько лет. Тем более, что производитель даже предусмотрел на плате требующийся этим часам кварц на 32 кГц. Осталось только приклеить на плату держатель батарейки и припаять от него проводки на общий минус и на специальный вывод микроконтроллера, что я у себя и сделал.

Время, число и месяц отображаются слева вверху на главном экране:



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

6.2 Блокировка экрана от случайных нажатий во время печати


Это было сделано по просьбе одного из знакомых. Ну, почему бы и нет, может быть полезным в некоторых случаях. Блокировка включается и отключается длительным нажатием (~2.5 сек) на заголовке экрана печати. Когда блокировка активна, в правом верхнем углу отображается красный замочек. При окончании печати блокировка автоматически отключается.

6.3 Уменьшение тока двигателя в режиме удержания, отключение двигателя по простою


Сделано для уменьшения общего нагрева внутри корпуса принтера. Двигатель может быть переведен в режим удержания с уменьшенным током после заданного в настройках времени отсутствия движения. Такая возможность, кстати, широко распространена во «взрослых» драйверах шаговых двигателей типа TB6560. Кроме того, в настройках можно задать время по истечении которого при отсутствии движения мотор будет полностью обесточен. Но это приведет и к тому, что обнуление оси, если оно проводилось, станет недействительным. Обе эти возможности можно и полностью отключить в тех же настройках.

6.4 Скринсейвер


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



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



В настройках можно задать время срабатывания скринсейвера или отключить его.

6.5 Проверка засветки и дисплея




В этот экран можно попасть из меню «Сервис» и это будет полезно при проверке диодов засветки или УФ-дисплея. Вверху выбирается одно из трех изображений, которое будет отображено на УФ-дисплее — рамка, полная засветка всего дисплея, прямоугольники. Внизу две кнопки, включающие и отключающие засветку и дисплей. Включенная засветка автоматически отключится через 2 минуты, обычно такого времени достаточно для любой проверки. При выходе из этого экрана автоматически будут выключены и засветка и дисплей.

6.6 Настройки




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

Конечно же, здесь можно выставить время и дату (раз уж есть часы) в открывающемся отдельно экране:



Можно настроить высоту подъема платформы на паузе и включить и выключить звук нажатий дисплея и сообщений. При изменении настроек новые значения будут действовать только до выключения питания и не будут сохранены в EPROM. Чтобы они сохранились нужно после изменения параметров нажать в меню кнопку сохранения (с иконкой дискеты).

Ввод числовых значений производится в специальном экране:



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

  1. Кнопки "±" и "." работают только если редактируемый параметр может быть отрицательным или дробным соответственно.
  2. Если после входа в этот экран первой будет нажата любая цифровая кнопка, то старое значение заменится соответствующей цифрой. Если кнопка ".", то заменится на «0.». То есть нет необходимости стирать старое значение, можно сразу начинать вводить новое.
  3. Кнопка «АС», обнуляющая текущее значение.

    При нажатии кнопки «Назад» новое значение не применится. Чтобы его применить, нужно нажать «ОК».

6.7 И последнее — экран с информацией о принтере




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

Конец


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

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

Жду вопросы и замечания, и спасибо за интерес к этим статьям.

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

Ссылки


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

Articles