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



    В предыдущих двух частях я рассказал о том как делал 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 на Алиэкспресс
    Исходники оригинальной прошивки от производителя на Гитхабе
    Схемы от производителя двух версий платы на Гитхабе
    Мои исходники на Гитхабе
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 35

      0
      А вот бонусом было-бы неплохо сборку и демонстрацию работы, самой аппаратной части. :-)
        +1
        Ну, пока есть только промежуточные видео, снятые в процессе работы над проектом :) В принтер я ее пока еще не интегрировал, жду второй такой комплект, чтобы одну плату можно было вставить в принтер, а со второй продолжать возиться на столе около компа, подключив отладчик.
        Вот видео с тестом движения оси — www.youtube.com/watch?v=lr3uIkM4YtM
        А вот видео с демонстрацией эмуляции печати — www.youtube.com/watch?v=ob9bVc12w_o
        И 20-минутный ролик с демонстрацией интерфейса и пояснениями голосом, по состоянию на начало сентября — www.youtube.com/watch?v=jGwTHx8fFaE :)
          0
          Отлично! Ждём продолжения!
        0
        Счетчики часов наработки компонентов принтера обновляются в EPROM только по окончании или прерывании печати файла.

        В случае пропадания питания данные потеряются. Можно добавить сохранение раз в N минут.

        Статьи потрясающие! Огромная и очень интересная работа проделана, и не менее интересно и тщательно описана. Спасибо за такое увлекательное чтение!
          0
          Спасибо за отзыв :)
          В случае пропадания питания данные потеряются.

          Это да, но я намеренно проигнорировал этот момент. При пропадании питания во время печати потеря пары часов счетчиков будет наименьшей из бед :) Да и случается это достаточно редко, чтобы оказать какое-то влияние, превышающее погрешность :)
          Но в принципе можно сделать сохранение во время печати, например, раз в час. Ресурс перезаписи этой EPROM вроде бы позволяет не слишком стесняться в частоте сохранения данных.
            0
            Ресурс же только при стирании расходуется. Есть умные алгоритмы записи, уменьшающие его частоту.
              0
              Да, есть, но что-то мне претит писать кольцевую запись для этой бздюльки с несколькими сотнями байт данных :)
              Наверное, действительно сделаю сохранение счетчиков во время печати каждый час.
                +1
                Разве нельзя 8 раз писать по одному (разному) биту в один и тот же байт?
                  0
                  Получится та же кольцевая запись, только с битовым, а не байтовым сдвигом :)
                    +1
                    Ей проще управлять. Выделяешь 3 байта под локальный счетчик на 24 часа. Когда заполняется — инкриментируешь глобальный. В итоге перезапись памяти не чаще, чем раз в сутки вне зависимости от сценария использования.
                      0
                      Да, о таком не подумал. Спасибо, возьму на заметку на будущее :)
                      Хотя у той же 24c16 нет отдельной операции стирания, оно производится автоматически во внутреннем цикле записи байта или страницы.
                        0
                        Ну да, решение аппаратно зависимое. Но если честно, я вообще плохо понимаю, как в таком случае сделать «размазывание износа». Нужно же как-то обновлять актуальные адреса или хотя бы инвалидировать старые записи.
                          0
                          Есть метод записи структур (записей) данных по «кругу». В начале каждой записи идет байт (ну или два/четыре) с идентификатором записи. В EPROM выделяется какая-то область, кратная размеру записи. Программа прыгает по записям в EPROM, пока не находит максимальный идентификатор записи, после которого идет пустой блок или запись с меньшим идентификатором (кроме случая перехода от MAX к MIN, например от 255 к 0 при однобайтовом номере). В новой записи идентификатору присваивается значение найденного максимального + 1 и эта запись сохраняется после найденной. И так оно все пишется по кругу в выделенной области.
                          Ну и чтение актуальной записи происходит так же — ищется запись с максимальным идентификатором, она и считается актуальной.
                            0
                            Как-то отбросил эту идею. Но, видимо, для маленькай памяти преемлема, когда есть ресурсы просканировать всю флэшку.
                              0
                              Ну, всю флэшку и не обязательно сканировать :) Можно хранить «круг» на 5 записей, например. Или на 50. Смотря насколько хочется растянуть ресурс флэшки.
                              ЗЫ: это не мое изобретение, это давно известный способ экономии ресурса перезаписываемой памяти :)
                                0
                                В какой-то момент возникает желание организовать ротацию целых страниц или их групп, и тогда дело приобретает некрасивый оборот)
                                  0
                                  Не совсем понял о чем Вы :)
                                  От размера сохраняемых записей скорость работы этого алгоритма не зависит, только от количества в ротации :)
                                    0
                                    Это я затупил.
                                      0
                                      Я пользовался таким способом, вполне быстро работает :) Но смысл в нем есть только если данные пишутся очень часто. Ну, например, если я буду делать возможность продолжения печати после сбоя питания, когда текущее состояние должно сохраняться каждые несколько секунд, то скорее всего этот способ и использую.
                                        0
                                        И если данные имеют заранее известный константный размер.

                                        Можно поставить на проц микроаккумулятор и делать запись только при пропадании питания (ну и спокойно спать ложиться. может быть, даже получится вообще обойтись без сброса оперативы, если архитектура материнки позволяет радикально урезать потребление). В теории может даже и конденсатора в блоке питания хватить, если быстро все мощные потребители отрубать при просадке глубже определенной, но это не очень надежно.
                                          0
                                          Даже можно небольшой объем критичных данных (80 байт) хранить в бэкап-регистрах часов, они будут сохраняться и при питании от часов батарейки :)
                                          В данном случае схемотехника материнки совершенно не предусматривает никакого управления питанием, увы.
                  0
                  Но вообще, я согласен, что это всё блажь. В идеале уметь детектировать и корректно обрабатывать power fail.

                  p.s. Также помимо времени печати я бы вел статистику времени простоя.
                    0
                    Вообще есть мысль попытаться сделать продолжение печати после сбоя питания. Сначала я считал, что это невозможно. Если на платформе уже имеется частично напечатанная модель, то платформа просто не сможет опуститься к концевику — она упрется напечатанной частью в дисплей. А значит не сможет и определить свое реальное положение, чтобы продолжить печать ровно с того места, где она была прервана. Но потом мне подсказали мысль, что в случае сбоя по питанию платформу можно хомить по верхнему концевику. Это, конечно, не так точно, но в большинстве случаев должно позволить продолжить прерванную печать :)

                    А зачем статистика времени простоя? Наработку компонентов я решил считать чтобы потом при замене, например, дисплея можно было узнать сколько часов он отработал перед тем как откинуться :)
                      0
                      Блок питания, дисплей и кулеры работают при простое. Ну и просто для интереса.
                        0
                        Нет, кулеры при простое не работают. Что касается дисплея, то он в простое находится в спячке, да и ресурс его заметно подсаживается только от сильного потока УФ (и от нагрева им же) во время засветки, так что подсчитываемое время работы дисплея равно времени работы засветки.
                        Вообще, выход дисплея из строя — обычное дело в таких принтерах. Смерть кулера из-за истирания втулок — тоже не редкость. А вот блоки питания и электроника работают, как правило, годами :)
                        Ну и просто для интереса.

                        Вот это уже достаточно хорошая причина, согласен :)
                          0
                          А. Я назвал дисплеем тачскрин. Хотя вряд ли он должен ломаться.
                            0
                            А-а, ну интерфейсный дисплей тоже живет очень долго. Может только уплывать тач, но его можно перекалибровывать.
                        0
                        Есть драйвера tmc2209 и похожие, там есть опция определения фазы обмотки в которой находится мотор.
                        При хоминге и срабатывании датчика определяется фаза мотора и мотор доворачивается в заданную целую фазу. Получается что при хоминге вал мотора всегда в одном положении в полной фазе.
                        Повторяемость максимально допустимая и определяется точность мотора. Это позволяет снять проблему рестарта печати при потере питания.
                          0
                          Тут драйвер впаян в плату, так что заменить его без паяльника и перерезания дорожек не выйдет.
                          Но главное — проблема в том, что после того, как часть модели уже напечаталась, хомиться стандартно по нижнему концевику нельзя, иначе платформа раздавит напечатанным куском модели дисплей засветки. А хомиться после отключения питания придется в любом случае, т.к. неизвестно в каком положении остался двигатель после пропадания питания.
                            0
                            Ну на будущее, будете знать что есть удобные драйвера.
                            А вобще не обязательно хомиться по нижнему.
                            При старте можно хомиться по верхнему, а потом опускаться в рабочее положение.

                            Вот для примера повторяемость положения после парковки в полной фазе обмоток. Тоесть парковка всегда в одном и том же положении шага.

                            Парковка между измерениями безсенсорная, не люблю концевики :)

                            Ну и просто видео про безсенсорную парковку при упирания каретки в палец :) но с винтом по Z и экраном так не прокатит, с винтом слишком большое усилие в экран будет.

                              0
                              Ну на будущее, будете знать что есть удобные драйвера.

                              Я знаю о них :)
                              При старте можно хомиться по верхнему, а потом опускаться в рабочее положение.

                              Можно, конечно, но верхний получается не таким точным. Температура изменилась на 5 градусов и стальной винт оси Z при длине 200 мм удлинился на почти 0.02 мм из-за теплового расширения — а это уже почти толщина слоя.
                              но с винтом по Z и экраном так не прокатит, с винтом слишком большое усилие в экран будет.

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

                  Раз на схеме стоит батарейка, то можно использовать набор регистров, которые на ней висят для таких случаев :)

                    0
                    Да, я выше писал об этом. Просто это не слишком критичные данные, редкая потеря части данных не страшна. А батарейка изначально на плате не стоит, это я на свою плату сам ее прилепил :)
                0
                Отличная статья! В настройки можно вынести такой параметр, как время засветки слоя. При замене полимера на более или менее светочувствительный, эта фича сможет здорово помочь
                  0
                  Все настройки печати — время паузы, время засветки, высота и скорость подъема/опускания платформы — будут доступны в настройках из окна печати файла :) Я даже думаю добавить туда отключение антиалиасинга и сохранение всех внесенных изменений обратно в файл :)

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

                Самое читаемое