Русский
Русский
English
Статистика
Реклама

3d принтеры

Умный дом как хобби

20.08.2020 12:11:50 | Автор: admin

Когда-то вблоге мырассказывали, как наш руководитель отдела обучения изамечательный программист Евгений создал марсианские часы изаодно выиграл вконкурсе. Апотом как онже сконструировал систему Умный дом своими руками. Так вот, сегодня снова поговорим оDIY-электронике ипринципиально новой системе контроля задомом, которую Евгений создал снуля взамен предыдущей.


image



Язанимаюсь всем, что связано сэлектроникой ипрограммированием электронных устройств, суниверситета. Это моя профессия яучился наприборостроителя. Иполучилось так, что работаю яразработчиком, амоё хобби связано спрофессией.


Евгений, разработчик




Идея умного дома недавала покоя инженерам ещё в50-е: тогда появился первый дом скнопками, где все бытовые задачи решались нажатием конкретной кнопки. В1966 году спомощью единой системы уже можно было настраивать климат впомещении ивключать/выключать приборы.


Полноценные умные дома появились назападе в70-е сизобретением передачи сигнала поэлектросети ибеспроводного управления нарадиочастоте 433МГц. Нодля управления системой нужен был специальный пульт: сего помощью можно было включать ивыключать приборы, менять яркость света иинформировать отекущем состоянии бытовой техники. Одна беда работали такие пульты позакрытым протоколам. Нокогда появился Интернет иWi-Fi, всфере умных домов всё изменилось.



Счего всё началось


Эпопея сУмным домой началась уЕвгения давно ещё 5лет назад, когда онвпервые узнал про Arduino (аппаратно-программную платформу для создания каких угодно устройств). Эта тема так его заинтересовала, что спустя пару месяцев онзаказал собственный набор Arduino иначал сним ковыряться, изучать возможности идокументацию. Ауже спустя 4месяца сделал первое устройство сенсорную игру нареакцию. Нопосле этого, пособственному признанию Евгения, его посетил творческий кризис: унего была база для проектирования идаже время, авот идеи, что сотворить, небыло.


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


первая версия умного дома


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




Новая система была построена намикрокомпьютере, иунеё был клёвый сенсорный экран (интерфейс которого, кстати, нарисовал один издизайнеров Сибирикс). Она умела разговаривать, распознавать речь ипонимать данные свнешних датчиков, которые кней можно было подключить. Свнешними устройствами система общалась поWi-Fi спомощью JSON (напомним, это текстовый формат обмена данными).



Восстание машин


Естественно, все комплектующие для системы заказывались изКитая, поскольку вРоссии найти что-то подобное заадекватные деньги проблематично. Вкакой-то момент дисплей, как всё китайское (простите заштамп), начал глючить: стал неадекватно реагировать нанажатия ижить своей жизнью.


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


Евгений, разработчик




Третья версия системы построена насамописной системе центрального управления. Нозачем, спроситевы, ведь естьже готовые решения?! Ага, есть такие.


Первая MajorDomo, придуманная разработчиком изБелоруссии. Сама посебе штука прикольная иочень популярная, кней есть множество плагинов имодулей, кней даже можно подключить датчики отсупер-брендовых производителей. Ноесть нюанс она построена наязыке PHP сиспользованием MySQL (свободной реляционной системы управления базами данных). Евгений пробовал еёвделе, нонасервере дважды терялась база данных, потому что MySQL при запуске сервера почему-то нестартовал.


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


Евгений, разработчик




Второе распространённое готовое решение система Home Assistant, написанная наPython иотлично работающая наUnix-машинах (оборудовании спереносимой операционкой). Можно просто записать образ программы управления нафлешку, вставить еёвсвою систему ивсё заработает. Да, для Home Assistant есть плагины для быстрой настройки, носними непросто. Поэтому чаще конфигурация такой системы делается вручную (приходится писать код): проще укусить себя залокоть. Аесли ужнужно писать код, так лучше свой собственный.



Всё заново: система умного дома своими руками


Вновой, написанной ручками, системе есть три узла:


  1. Вещи осязаемые элементы системы: датчики, сенсоры ивообще всё, что можно взять ипотрогать руками.
  2. Сервисы виртуальные модули, пока ихтри: видео-камеры, прогноз погоды иданные самой системы.
  3. Центр управления сдвумя основными компонентами: MQTT-брокером исервером наNode.JS.

Сердце системы микрокомпьютер версии Raspberry PI3b+. Стоит он23 тысячи рублей, авзамен выполучаете полноценный компьютер размером спачку сигарет. Единственный нюанс: его файловая система базируется накарте памяти формата MicroSD, апостоянные записи данных быстро выводят еёизстроя. Из-за этого приходится заботиться оминимизации записываемых данных или подключать HDD-носитель вместо MicroSD. Нопринцип вобоих случаях одинаковый: нужно скачать образ Raspbian, установить его накарту памяти ивставить вмикрокомпьютер ивуаля, увас готов полноценный Linux-компьютер, накотором можно делать всё что угодно. Внашем случае сервер умного дома.


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


Arduino-модуль

Arduino всем хорош, номодуль изкоробки умеет общаться только скомпьютером итолько через USB-порт, адля системы умного дома это серьёзный минус. Зато модуль NodeMCUV3, который Евгений использовал для своей системы, имеет наборту Wi-Fi азначит, ненужно заморачиваться сдополнительными периферийными устройствами, иданные легко передавать.


Всистеме также есть несколько датчиков:


  • BME280
    Один изсамых крутых погодных сенсоров, отвечает заданные температуры, влажности идавления. Стоит порядка 200рублей, авзамен предоставляет пользователю полноценную метеостанцию.
  • PIR-сенсор, или датчик движения
    Работает онхитро: сканирует инфракрасное излучение помещения, иесли вдва момента времени температура сканированного участка изменилась, значит, какой-то тепловой объект попал вполе зрения датчика. Если вас бесит, что онпостоянно реагирует накошку, можно снизить порог чувствительности устройства.
  • RC522
    Датчик, считывающий электронный ключ. Отэтой маленькой штучки зависит, например, пройдёте выдальше пропускного пункта или навас налетит группа быстрого реагирования:)

    Такие стоят везде, где выприкладываете RFID ключ-карту, чтобы пройти через турникет или войти вкакое-то охраняемое помещение. Удатчика есть катушка, иесли подключено питание, тонаней создаётся электромагнитное поле. Когда выподносите ключ-карту ктакому датчику, онпопадает под действие этого электромагнитного поля фактически происходит беспроводная передача энергии. Ключ начинает посылать свой сигнал надатчик, итот распознаёт, подходит тот или нет.
  • SIM800L
    Пожалуй, самая интересная вещь всистеме. Это GSM/GPRS-модем. Иными словами, телефон размером споловинку спичечной коробки. Внего устанавливается мини-SIM-карта, изасчёт этого система может звонить, принимать звонки, отправлять SMS-сообщения идаже гуглить винтернетах. Так что, если хотите замутить себе хенд-мейд мобилку, тонабазе этой штуки увас точно получится. Несмартфон, конечно, нозвонить будет.
  • MH-Z19
    Инфракрасный датчик концентрации углекислого газа, самый дорогой всистеме. Зачем оннужен, спроситевы? Если впомещении много CO2 (высокая концентрация, говоря научным языком), человек начинает зевать истановится вялым. Если очень много может заболеть голова, аесли слишком много можно исознание потерять совсеми вытекающими. Так что контроль этого параметра необходимый элемент любой метеостанции.

    Утакого датчика есть 2окошка, под которыми спрятаны 2капсулы: водной азот, вдругой проба воздуха впомещении. Также внутри есть источник инфракрасного излучения (попросту лампа): когда инфракрасное излучение проходит через газ вкапсулах, оно начинает преломляться засчёт молекул газа, идлина волны меняется. Сенсор, который установлен навходе датчика, сравнивает эталонные значения длины волны стекущими иделает вывод оконцентрации CO2в помещении.

Чтобы система работала как одно целое, центральный узел ипериферийные устройства должны как-то между собой общаться, причём общаться максимально стабильно. Изначально Евгений сделал JSON-обмен: элементы обменивались данными поhttp-запросу. Способ стабильный, нопроблема втом, что так никто неделает. Поэтому втекущей системе заобмен данными отвечает протокол MQTT, заточенный специально под обмен информацией между узлами системы. Его создали в2016-м исейчас активно используют набольших серьёзных заводах, где требуется автоматизация производства.


Упротокола есть центральная часть Broker, который отвечает заприём иотправку сообщений всем элементам системы. Элементыже делятся надве группы: Publisher (публикуют данные вBroker) иSubscriber (получают эти данные). Причём, Subscriber может заранее подписаться накакой-то топик (скажем, данные отемпературе), которого всистеме пока нет. Как только топик появится, данные сразу придут.


Как устроен MQTT-протокол


Конфигурация системы


Физические модули: метеостанция


Метеостанция своими руками

Всердце системы NodeMCU, окотором мыуже говорили выше. Кнему подключены три узла:


  1. датчик температуры ивлажности BME280;
  2. сенсор CO2 MH-Z19;
  3. OLED-дисплей, который выводит данные датчиков пользователю.

Данные между устройствами исервером передаются поинтерфейсу I2С или UART это интерфейсы для общения. Внашем случае, между микроконтроллерами.



Физические модули: охрана


Система охраны своими руками

Изначально модуль планировался без узла Arduino, ноконтроллер NodeMCU просто неуспевал отслеживать все изменения всистеме ипередавать ихнабольшой тачскрин-дисплей. Также вмодуль охраны включены датчик движения исканер RFID ключа-карты. НаArduino-модуль приходится обработка всех параметров стачскрина. Фактически контроллер запрашивает уArduino данные сдатчиков иотправляет ихнадисплей через интерфейс UART.



Физические модули: GSM


Система Умный дом» своими руками

Этот модуль неимеет корпуса из-за пандемии.


Все платы Евгений заказывает накитайском заводе просто потому что так дешевле. Сотрудничает сними давно, качество плат устраивает. Нокогда онвочередной раз заказал уних плату для охранной системы, спересылкой вБарнаул начались проблемы: пандемия, границы закрыты, все дела. Деньги вернуть могут только задоставку, азасами платы нет ведь они ихуже изготовили. Всего 2$, конечно, новсё равно как-то жалко. Правдами инеправдами выяснилось, что вНовосибирск доставка возможна ичерез друзей Евгений получил-таки свои заветные железки. Правда, при стоимости плат в150 рублей доставка вышла в1000:)


Из-за этого другие платы онзаказал наAliExpress, ноитам всё пошло непоплану: заказ дважды отменялся, аденьги занего долго висели нахолдировании. Собственно, поэтому плату для GSM пришлось делать самостоятельно. Акорпуса нет, потому что его ещё напечатать на3D-принтере надо.



Сервисы внутри системы


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


  • Openweather отвечает запрогноз погоды натекущую дату инанеделю, атакже график температур вближайшие дни.
  • Macroscope отвечает завидеонаблюдение (вдоме, где живет Евгений, Управляющая компания установила 18камер, изасчёт крутого API ковсем 18не составило труда подключиться).
  • Сервис всамой системе позволяет получить текущее состояние всей системы: дата ивремя, продолжительность непрерывной работы для отслеживания сбоев, состояние батареи питания, атакже функционал перезагрузки ивыключения сервера.


Центр управления


Примерная структурная схема того, как устроена система:


Как сделать умный дом структурная схема

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


Вещи (физические датчики) публикуют данные вMQTT-брокер, аоттуда они попадают насервер, где происходит ихвалидация, обработка, проверка ипреобразование. После эти данные попадают вмодуль Public, который отвечает запубликацию всех собранных данных для внешних подключений. Ауже оттуда данные переходят вSocket IO библиотеку Java Script, засчёт которой обеспечивается двусторонняя связь между веб-клиентами исерверами врежиме реального времени. Навыходе данные получает клиент (например, вбраузере). Сервисы работают схожим образом, нопередача данных для них обходится без MQTT информация передаётся сразу через Public иSocket IOнаружу.


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


Когда задаётся сценарий, нужно выбрать целевую точку: либо отправить команду вузел вещи, либо всервис, либо отправить http-запрос для webhook функционал для уведомления сторонних сервисов. Иконечно, каждой изних можно дать название сценария иприкрепить кней иконку.


Запуск набора сценариев поконкретному событию происходит засчёт автоматизации. Например, если выоткрыли дверь, система должна включить свет. Чтобы задать автоматизацию, сначала указывается событие запуска: дата, параметр или автоматизация. Например, 3-го числа каждого месяца, если идёт дождь, нужно напоминать взять ссобой зонт :) Внутри автоматизации можно указывать идругие автоматизации. Например, увас есть два сценария: для будней идля выходных дней. Так вот, вторая автоматизация проверит, какой сегодня день, итолько потом запустится автоматический сценарий выходного или рабочего дня. Такая механика даёт неограниченную вложенность автоматизаций, чтобы создавать максимально сложные сценарии, комбинируя типы событий и сили.


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


Ключевые характеристики системы


  • Софт реализован наNuxt.js(сиспользованием Vue.jsиMongoDB).
  • Автономная работа сервера врайоне 5часов (если отключится внешнее питание, система проработает примерно столько засчёт батареи).
  • Запуск приложения происходит через утилиту PM2 process manager (рабочий диспетчер процессов для приложений Node.js) работает максимально просто: устанавливается всистему, выпишете команду старт, ивсё работает. Может одновременно запустить столько процессов, сколько есть ядер всистеме. Например, если ядра4, можно запускать параллельно 4сервера, разделяя между ними трафик наслучай, если какой-то изних упадёт.
  • Подключение происходит поWi-Fi, причём каждый узел системы способен самостоятельно восстанавливать подключение, если оно отвалится.
  • Каждый элемент системы может работать как отдельное устройство это было принципиальное условие настарте.
  • Разработка этой версии заняла полгода Евгений инженерил всвободное время: зачас доработы ивобеденный перерыв побудням.



Как происходит обмен данными


Обмен данными в системе Умный дом

Есть два основных канала, которые пофакту являются топиками вMQTT:


  1. Канал данных понему все элементы системы шлют данные насервер. Данные делятся натипы: строка, справочник (набор предопределённых данных вроде вкл ивыкл для охранной или метеосистемы), число, да/нет.
  2. Канал управления понему сервер отдаёт команду какому-либо устройству. Данные также передаются, делясь натипы: либо кнопка, либо строка. Вответ накоманду устройство, сервис или вещь возвращает результат выполнения статус-код: 1 всё хорошо; 2 сейчас выполнить нельзя (скажем, нельзя включить тревогу, если помещение непоставлено наохрану); 3 всё плохо.


Почему ничего неполучилось сGoogle Assistant


Google Assistant тот самый сервис, который отвечает зазапросы окей, Гугл.... Аналог Siri отApple. Иконечно, унего есть своё API исреда для подключения умных устройств вэкосистему Google Assistant, чтобы можно было сказать окей, Гугл, включи охранную систему или окей, Гугл, какая погода сегодня?. Запрос пройдёт через сервера Гугла, вернётся квам наустройство, ионо выдаст результат.


Яподумал круто!, готовое нативное приложение для телефона наAndroid икак раз годится для системы умный дом. Начал подключать. Если кто-то видел документацию сервисов Гугла, теменя поймут: яофигел ихчитать. Во-первых, наанглийском. Во-вторых, многобукаф инаписано очень мудрёно.


Яподписан наютуб-канал одного разработчика, которому удалось подключиться кэкосистеме Google Assistant. После своих неудачных попыток ярешил написать ему онответил идаже помог мне сподключением. Новпервыйже день яразочаровался: приложение Google Home сырое, аего русская версия иподавно. Носамое главное система заточена под управление брендовыми устройствами: например, умной рисоваркой, которой можно управлять через Wi-Fi. Или умной колонкой Google Station, спомощью которой реально можно управлять хоть всем домом, нокоторая по-русски непонимает. Моиже устройства кастомные, ипод них вэкосистеме Google Assistant ничего нет. Так что всё было зря.


Евгений, разработчик




Пробовал Евгений ияндексовскую Алису она лучше из-за адекватного русского языка, ноиеёконфигурация ущербная. Витоге онпонял, что сготовыми решениями просто несудьба:)



Сколько стоило создание системы


Самое дорогое микрокомпьютер Raspberry PI3b+ивообще весь блок центрального управления. Онвышел постоимости чуть больше 4тысяч рублей. Аобщий бюджет проекта составил 9540 рублей без учёта расходов напластик для печати корпусов имелких деталек, которые уже были варсенале уЕвгения.


Сколько стоит создать умный дом

Планы


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


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


Среди замков нарынке есть интересное решение замки Sherlock, управляемые через Wi-Fi сосмартфона. Евгений хочет собрать нечто подобное, только чтобы состороны подъезда вместо электронного дисплея было гнездо для RFIDметки. Носамое главное замок должен сигнализировать отом, что оннезакрыт (признайтесь, увас нераз было такое, что выпроспали всю ночь соткрытой дверью, правда?).


современная люстра и замок Sherlock

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


Носамая крутая штука, которую Евгений хочет воплотить, это подключение ZigBee-устройств. ZigBee это стандарт общения между гаджетами или попросту протокол передачи данных. Грубо, аналог подключения поWi-Fi. Основное преимущество такой технологии низкое потребление энергии. Ноиз-за маломощности идальность работы тоже меньше, чем уWi-Fi (хотя всвежих версиях этот недостаток, вродебы, пофиксили).


Организовать сеть ZigBee можно тремя вариантами конфигураций:


Конфигурации ZigBee

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


Китайцы быстро поняли, что эта технология быстрая, классная идешёвая, поэтому выпускают все свои устройства уже стаким функционалом. Нокаждый гаджет может функционировать только сосвоим координатором: тоесть, если увас чайник Xiaomi, тоикоординатор должен быть этойже фирмы (выглядит онкак круглое устройство, которое вставляется врозетку). Нотеже самые китайцы быстренько нашли, как обойти это ограничение: сделали USB-свисток, накоторый через USB-порт компьютера можно скачать специальноеПО, итогда кнему можно подключить любое устройство любого производителя.


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


Нояпонимаю, что мне никогда непереплюнуть Xiaomi, IKEA или Aqara банально поцене: даже если янайду китайцев, которые сделают партию пооптовым ценам сготовыми платами, это всё равно получится дороже, чем устройство оттогоже Xiaomi. Нуивдобавок моё устройство небудет таким красивым, как уних.


Евгений, разработчик




Как система выглядит для пользователя


Физические устройства:








Что пользователь видит вбраузере:


Основной экран

Экран Вещи

Данные метеостанции

Сервис Прогноз погоды

Данные сервиса системы видеонаблюдения

Сервис системы

Список сценариев

Добавление сценария

Список автоматизаций

Добавление автоматизации

Нувот, теперь вывсё или практически всё знаете осистеме умных домов идаже сможете поддержать разговор синженером-электронщиком. Апока стэй тюнед, как говорится: впереди много интересного!

Подробнее..

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

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


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


Предыстория


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

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

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

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

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


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

Материнская плата построена на основе микроконтроллера STM32F407. Для управления дисплеем засветки на плате стоит FPGA китайского производителя GW1N-LV4LQ144ES, SDRAM и две микросхемы MIPI-интерфейса SSD2828. Микроконтроллер загоняет в FPGA изображение слоя, FPGA сохраняет его в SDRAM и оттуда рефрешит дисплей через SSD2828. Конфигурацию (прошивку) FPGA производитель, кстати, не предоставляет в исходниках :( Кроме этого, на материнской плате есть:
  • вход питания 12-24 вольта
  • USB A разъем для подключения флэшки/картридера
  • коммутируемые выходы питания для засветки и двух вентиляторов
  • драйвер шагового двигателя A4988 и разъем для подключения двигателя
  • два разъема для подключения концевиков оси Z верхнего и нижнего
  • разъем для подключения модуля WiFi
  • микросхема FLASH-памяти W25Q64
  • микросхема EEPROM-памяти AT24C16

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

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

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

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

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


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

1.1 Шрифты


В сети множество библиотек работы со шрифтами для микроконтроллеров, но абсолютное большинство из них работают с моноширинными шрифтами, а мне это не очень нравится. Это когда у всех символов одинаковая ширина, что у буквы ж, что у буквы i. Когда-то для одного из своих пет-проектов я написал библиотеку пропорциональных шрифтов. В ней для каждого шрифта используются два массива массив с битовыми данными самих символов и массив с указанием ширины каждого символа. И небольшая структура с параметрами шрифта указатели на массивы, высота шрифта, количество символов в шрифте:
typedef struct{uint16_t*width;uint8_t*data;uint8_theight;uint16_tsymcount;} LCDUI_FONT;


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

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

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

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

Вот процедура нахождения в массиве данных символа:
uint8_t*_lcdui_GetCharData(char c){if (c < 32)return 0;if (c > 126)c -= 65;c -= 32;if (c >= lcdui_current_font->symcount)return 0;uint16_t c1 = lcdui_current_font->width[c];if (c1 & 0x8000)c = (c1 & 0x7FFF);uint16_t ch = lcdui_current_font->height;int32_t i = 0, ptr = 0, bits = 0, line_bits = ch;for (i = 0; i < c; i++){if (lcdui_current_font->width[i] & 0x8000)continue;bits = lcdui_current_font->width[i] * line_bits;ptr += bits >> 3;if (bits & 0x07)ptr++;}return &(lcdui_current_font->data[ptr]);}


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

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

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

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

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

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


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

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

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

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

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

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

1.3 Основа GUI


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

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

Структура экрана
typedef struct{void*addparameter;char*bgimagename;void*prevscreen;LNG_STRING_IDname;TG_RECTnameposition;TG_TEXTOPTIONSnameoptions;uint8_tbtns_count;TG_BUTTON*buttons;LCDUI_FONT_TYPEfont;LCDUI_FONT_TYPEnamefont;uint16_ttextcolor;uint16_tnametextcolor;uint16_tbackcolor;struct {paintfunc_callpaint;// repaint screenprocessfunc_process;// screen process handling (check for changes, touch pressed, etc)} funcs;} TG_SCREEN;


Кнопки по сути могут быть не только кнопками, но и текстом, иконкой, каким-то изменяющимся элементом типа счетчика или часов. Просто оказалось удобным объединить все в одном типе, а поведение каждой конкретной кнопки задавать через ее свойства.
Свойства кнопок:
  • координаты на экране
  • цвет фона
  • фоновое изображение для свободного состояния
  • фоновое изображение для нажатого состояния
  • фоновое изображение для выключенного состояния (disabled)
  • фоновое изображение для активного состояния (для активного элемента группы переключателей, например)
  • способ отрисовки изображением или фоновым цветом
  • перерисовывать ли кнопку при ее нажатии и отпускании
  • текст кнопки
  • шрифт текста кнопки
  • координаты области вывода текста кнопки
  • цвета текста и фона для всех состояний
  • флаги отключенного и активного состояний
  • флаг необходимости перерисовать (изменилось состояние)
  • опции вывода текста (выравнивание, прозрачность)
  • идентификатор кнопки
  • идентификатор группы (для группы переключателей)
  • указатели на процедуры отрисовки, обработки событий и нажатия
  • указатели на дочерний экран, вызываемый при нажатии на эту кнопку

Структура кнопки
typedef struct{void*addparameter;uint8_tbutton_id;int8_tgroup_id;// for swithed options buttons, >0 - single selection from group (select), <0 - multiple selection (switch)TG_RECTposition;void*parentscreen;void*childscreen;char*bgimagename_en;char*bgimagename_press;char*bgimagename_dis;char*bgimagename_act;// for swithed options buttonsLNG_STRING_IDtext;TG_RECTtextposition;LCDUI_FONT_TYPEfont;uint16_ttextcolor_en;uint16_ttextcolor_press;uint16_ttextcolor_dis;uint16_ttextcolor_act;// for swithed options buttonsuint16_tbackcolor_en;uint16_tbackcolor_press;uint16_tbackcolor_dis;uint16_tbackcolor_act;// for swithed options buttonsstruct {uint8_tactive:1;// for swithed options buttonsuint8_tneedrepaint:1;uint8_tpressed:1;uint8_tdisabled:1;uint8_trepaintonpress:1;// repaint or not when pressed - for indicate pressed stateBGPAINT_TYPEbgpaint:2;} options;TG_TEXTOPTIONStextoptions;struct {paintfunc_call_paint;// repaint buttonpressfunc_call_press;// touch events handlingpressfunc_call_longpress;// touch events handlingprocessfunc_call_process;// periodical processing (for example text value refresh)} funcs;} TG_BUTTON;


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


Структура окна сообщений
typedef struct{MSGBOXTYPEtype;void*prevscreen;charcaption[128];chartext[512];TG_RECTboxpos;uint8_tbtns_count;TG_BUTTONbuttons[TG_BTN_CNT_MSGBOX];uint16_tcaption_height;LCDUI_FONT_TYPEfont_caption;LCDUI_FONT_TYPEfont_text;uint16_ttext_color;uint16_tbox_backcolor;uint16_tcapt_textcolor;uint16_tcapt_backcolor;} TG_MSGBOX;


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

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




Мультиязычность стояла в задачах изначально. Но сначала я пошел по глупому пути при инициализации всех элементов я присваивал им тексты из той языковой таблицы, которая являлась текущей. Переключение языка означало переинициализацию всех текстовых элементов и когда экранов в интерфейсе стало больше двух, а кнопок и надписей больше 20, я понял, что дальше так жить нельзя. После чего сделал все обращения к текстам через процедуру. Процедуре дается параметром идентификатор текста, а она возвращает указатель на текст в текущем языке:
char *mshortname = LANG_GetString(LSTR_SHORT_JANUARY);

При изменении языка происходит просто изменение указателя с массива текстов на старом языке на массив с текстами на новом языке:
voidLANG_SetLanguage(uint8_t lang){lngCurrent = lngLanguages[lang].strings;return;}

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

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


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



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


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

Ссылки


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

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

25.09.2020 02:06:03 | Автор: admin


Продолжение статьи о написании своей прошивки для фотополимерного LCD 3D-принтера. Первая часть лежит тут. В ней было описан первый этап создание графического пользовательского интерфейса для дисплея с сенсорной панелью.
В этой части продолжу описывать этапы своего проекта:
2. Работа с USB-флэшкой и файлами на ней
3. Управление шаговым двигателем для движения платформы.

2. Работа с USB-флэшкой и файлами на ней


До этого я никогда не работал с USB-хостом на микроконтроллерах. Как USB-device делал прошивки и с классом CDC (эмуляция COM-порта) и с классом HID, но вот с хостом не работал. Поэтому для ускорения процесса я создал всю инициализацию этой периферии в STM32CUBE. На выходе я получил работающий в режиме USB FS хост, поддерживающий устройства хранения данных (Mass storage). В том же кубе я сразу подключил и библиотеку FatFS для работы с файловой системой и файлами. Дальше оставалось просто скопировать полученные исходники в свой проект и разобраться как с ними работать. Это оказалось несложно и описывать тут особо нечего. В файле usb_host.c из Куба имеется глобальная переменная Appli_state с типом ApplicationTypeDef:
typedef enum {  APPLICATION_IDLE = 0,  APPLICATION_START,  APPLICATION_READY,  APPLICATION_DISCONNECT}ApplicationTypeDef;

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

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

А вот для работы FatFS с кириллицей в именах файлов и каталогов пришлось немного повозиться. Для того, чтобы FatFS корректно читала кириллические имена, нужно в ее конфигурации включить работу с Unicode, и после этого все строки, связанные с FatFS, должны быть только в этой кодировке имена дисков, имена файлов и т.д. При этом текстовый редактор в IDE и FatFS поддерживают Юникод с разным расположением старшего байта один с Little Endian, другая с Big Endian, так что просто писать исходники с текстами в Юникоде не получится. Да и не хочется, если честно. Вот тогда и пришлось писать конвертеры из ANSI и UTF-8 в Unicode и обратно, плюс несколько функций по работе со строками разных кодировок в разных сочетаниях. Например, скопировать UTF-8-строку в Unicode-строку, или добавить к Unicode-строке ANSI-строку. Впрочем, ANSI-строк, кажется, нигде уже и не осталось, все исходники полностью перешли в кодировку UTF-8.
Так, открытие файла с заданным именем выглядит сейчас примерно вот так:
tstrcpy(u_tfname, UsbPath);// задаем полному пути (Unicode) имя диска в (Unicode)tstrcat_utf(u_tfname, SDIR_IMAGES);// добавляем к пути (Unicode) имя каталога (UTF-8)tstrcat_utf(u_tfname, (char*)"\\");// добавляем к пути (Unicode) слэш (UTF-8)tstrcat(u_tfname, fname);// добавляем к пути (Unicode) имя файла (Unicode)

Когда все это быстренько заработало, захотелось проверить скорость чтения файлов с флэшки. Чтение 10-мегабайтного файла блоками по 4 КБ показало скорость около 9 Мбит/сек, что, в общем-то, довольно неплохо и меня устроило.

Сунулся было изучить вопрос по переводу этого дела на DMA, но оказалось, что периферия USB-хоста просто не имеет доступа к DMA. Ну или я не нашел его :) Поэтому показалось логичным все буферы чтения/записи для файлов USB организовать в CCM (Core Coupled Memory) области оперативной памяти размером 64 КБ, которая так же не имеет выход на DMA. В этой же области памяти имеет смысл размещать и другие переменные/массивы, которые не работают с DMA, просто чтобы больше памяти оставить в обычной оперативке. Кстати, мне показалось, что само ядро работает с этой памятью чуть быстрее, чем с обычной.

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


Принтер Anycubic Photon S, который у меня имеется, выводит список файлов в виде значков предпросмотра, 4 штуки на экран. И в принципе, это достаточно удобно видно имя файла, в картинке предпросмотра видно примерно что за модель. Поэтому и я пошел по тому же пути файлы выводятся по 4 штуки на страницу в виде картинок предпросмотра с именем файла.

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

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


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


Кстати, по поводу картинок предпросмотра, которые рисуются для файлов в режиме иконок тут никакой интриги нет. Прошивка не анализирует весь файл, чтобы построить изображение по 3D-модели, как некоторые думают :) Эта картинка сохраняется в файле печати самим слайсером, в формате, схожем с BMP массив 16-битных значений цвета пикселей. Размеры картинки предпросмотра хранятся в специальных полях внутри файла. Так что все очень просто.

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

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

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

2.2 Просмотр информации о файле перед началом печати


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


Имя файла, его размер, время последней модификации и практически все параметры печати. Тут, правда, мне на руку сыграл еще тот факт, что дисплей у MKS DLP имеет разрешение 480х320, тогда как у Эникубиков он поменьше 320х240, на таком особо не размахнешься с кучей текста.

2.2.1 По поводу расчета времени печати напишу отдельно.

Этот показатель не хранится в файле, в отличии от всех остальных параметров. Его принтер должен рассчитать самостоятельно, исходя из известной ему информации. Тот же Anycubic Photon S имеет обыкновение промахиваться с этим расчетом, причем в меньшую сторону например, обещает 5 часов печати, тогда как в реальности печатает 6 часов. А Longer Orange 30 вообще во время печати меняет это время туда-сюда чуть ли не в два раза. Я решил подойти к этому моменту максимально тщательно. Из чего складывается это время?
  1. Время, за которое платформа опустится с заданной скоростью на высоту очередного слоя.
  2. Время паузы перед началом засветки.
  3. Время засветки слоя.
  4. Время, за которое платформа поднимется на заданную высоту с заданной скоростью после засветки слоя.


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

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

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

Если честно, у меня голова кругом шла когда я писал функцию расчета времени печати :) И в результате все равно получил небольшую погрешность. Например, реальное время печати 07:43:30 вместо расчетных 07:34:32.


Или 05:48:43 вместо расчетных 05:43:23.


Но в принципе, меня такая погрешность устроила. Я пытался найти ошибку в расчетах, но там вроде бы все верно. Скорее всего, реальное ускорение слегка не соответствует задаваемому из-за особенностей управления шаговым двигателем. Вот так плавно мы подошли к следующему этапу :)

3. Управление шаговым двигателем для движения платформы.


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

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

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

Сначала я взял из исходников Марлина файл stepper.cpp непосредственно управление шаговым двигателем. Однако оказалось, что его работа очень сильно зависима от планировщика движений из файла planner.cpp, пришлось взять и его. Ну и до кучи я взял оттуда еще и файл endstops.cpp обработка концевиков осей, так как мне все равно нужно было обрабатывать события от них, а тут и планировщик и управление двигателем уже были связаны с этим файлом для концевиков.

Я очень долго провозился с тем, чтобы убрать из этих файлов все ненужное и отвязать их от остальной экосистемы Марлина. Дело в том, что Марлин заточен под управление 6 или 7 шаговиками одновременно, при этом их работа может зависеть от температуры нескольких нагревателей, от параметров пластика и т.д. Система там на самом деле сложная. Мне пришлось очень многое переделать, в основном удаляя лишние оси и ненужные экструдеры и избавляясь от целой толпы макросов, полезных в оригинальной версии, но очень мешающих в моей. Просто для понимания размер взятых мною из Марлина исходников сократился с 346 до 121 КБ. И каждую строку приходилось удалять с оглядкой.

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

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

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

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

Поясню на примере. Планировщик свободен, ему приходит задание на движение оси вперед на 20 мм со скоростью 30 мм/сек. Планировщик формирует первый пакет, в котором описывает ускорение с нуля до 30 мм/сек, прямолинейное движение с этой скоростью и замедление от этой скорости до нуля. Если до того как stepper заберет у планировщика этот пакет планировщику будет выдано новое задание на движение этой оси еще на 50 мм вперед, но уже со скоростью 40 мм/сек, то планировщик не просто создаст новый пакет с ускорением от нуля, а изменит первый пакет, удалив замедление и продлив на его расстояние прямолинейное движение, а в созданном втором пакете ускорение уже будет начинаться не с нуля, а от скорости предыдущего пакета.

В результате получится одно движение, в котором ось ускорится до 30 мм/сек, проедет 20 мм, затем еще раз ускорится уже до 40 мм/сек и проедет еще 50 мм, замедлившись в конце до нуля. Но это только если stepper еще не успел забрать в работу предыдущий пакет, иначе эти два задания будут отработаны как два отдельных движения с нулевой начальной и конечной скоростью в каждом из них. Поэтому, кстати, в принтерах при ручном управлении платформой если несколько раз подряд нажать подъем с шагом 10 мм, то платформа после первых 10 мм подъема остановится и потом уже продолжит движение без остановок на всю высоту, нащелканную кнопкой.

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

3.1 Интерфейс управления движением платформы




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

Кнопка Уст. Z=0 предназначена для калибровки высоты платформы над дисплеем. Такая система калибровки используется, например, в принтерах Anycubic, когда нулевая точка платформы (оптимальная ее высота над дисплеем) находится на 1-2 мм ниже срабатывания домашнего концевика. И эта система калибровки видится мне более правильной, чем становящиеся в последнее время популярными системы, когда высота срабатывание концевика является одновременно и нулевой высотой платформы.

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

3.2 Другие моменты по движению платформы


Вот в Anycubic Photon меня жутко раздражают несколько вещей.

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

Второй момент почему при постановке печати на паузу платформа поднимается на высоту паузы с той скоростью, которая задана в параметрах печати? Черт возьми, ждать 15 секунд пока платформа поднимется на два (всего лишь) сантиметра это за гранью добра. Но спасибо, что хоть поднимается. У Orange 30 пауза вовсе не подразумевает подъем платформы хоть на миллиметр, так что даже непонятно для чего она там вообще есть.

И третий момент, который просто бесит после окончания печати платформа поднимается на самый верх. С той же скоростью, которая была задана в параметрах печати 1 мм/сек. Это 100 секунд на подъем наверх с высоты 5 см!

Поэтому в своей прошивке я сделал настраиваемыми скорости и ускорения отдельно для режима печати и отдельно для ручного управления платформой. Но с двумя ограничениями:
  1. Пока ось не будет обнулена кнопкой Домой скорость движения будет снижена втрое. Это сделано потому что пока принтер не знает точную текущую высоту платформы, существует опасность раздавить дисплей, не успев остановиться с высокой скорости (инерция, чтоб ей) или повредить верхний упор оси. После обнуления оси принтер уже точно знает положение платформы и в силу вступают программные ограничения высоты, которые так же задаются в настройках.
  2. На высоте меньше 30 мм скорость так же снижается втрое, независимо от того обнулена ось или нет. Это сделано для предотвращения разбрызгивания фотополимера из ванны при слишком быстром опускании в него платформы. Или при слишком быстром подъеме из него.


Конечно же, в настройках присутствуют и другие стандартные параметры оси количество шагов на 1 мм, направление движения, работа концевиков и т.п. Если кому-то интересно, то под спойлером приведен текстовый конфигурационный файл со всеми поддерживаемыми параметрами. Такой файл с расширением .acfg кушается прошивкой прямо из списка файлов, загружая параметры, сохраняя их в 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



И на этом я закончу эту часть, и так уже получилось слишком много текста :)
Как и раньше с удовольствием отвечу на вопросы и приму замечания.

Ссылки


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

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

25.09.2020 20:20:19 | Автор: admin


В предыдущих двух частях я рассказал о том как делал 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);voidUVLED_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 spaceswhile (*string != 0 && maxlen > 0 && (*string == ' ' || *string == '\t' || *string == '\r')){string++;maxlen--;}// until first space symbolwhile (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 spaceswhile (*string != 0 && (*string == ' ' || *string == '\t' || *string == '\r'))string++;if (*string == 0)return;// check param if it numericif ((*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;}//==============================================================================voidCFG_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_tcnt = 0;uint32_treaded = 0, totalreaded = 0;char*string = msg;charlexem[128];PARAM_VALUEpval;CFGREAD_STATErdstate = CFGR_GENERAL;int16_tnumstr = 0;while (1){// read one stringcnt = 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 endstrtrim(string);// if string is emptyif (*string == 0){// if end of fileif (readed == 0)break;elsecontinue;}// skip commentsif (*string == '#')continue;// upper all lettersstrupper_utf(string);// get parameter name_cfg_GetParamName(string, lexem, sizeof(lexem));// check if here section nameif (*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 parameterswitch (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;}} elseif (*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;}} elseif (*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;}} elseif (*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;}} elseif (*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;}} elseif (*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;}} elseif (*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;}} elseif (*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;}} elseif (*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;}} elseif (*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;}} elseif (*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;elsecfgConfig.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 и счетчики наработки. Внизу еще притулилась информация об авторе интерфейса и адрес репозитория на Гитхабе. Автор интерфейса это задел на будущее. Если я все же сделаю возможность конфигурации интерфейса через простой текстовый файл, то там будет возможность указать имя автора.

Конец


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

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

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

Ссылки


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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru