Что мы любим в «Hello world» задачах для любого программного языка? Такое упражнение позволяет пройти через базовые этапы установки средств языка, сборку проекта и непосредственно лицезрение результата. Если нужно углубиться чуть больше, идеальным заданием может быть создание клиент‑сервер приложения. Для достижение этой цели уже, как правило, требуется линковка с внешними зависимостями, асинхронное программирование и прочие ключевые направления, необходимые для полноценной работы с языком.
Но что, если мы хотим сделать некий «Hello world» в робототехнике — направление на стыке программирования, механики и многого чего еще? Такое упражнение, которое проведет через ключевые проблемы, с которыми сталкивается конструктор сложных приборных систем? Поиграть с Arduino всегда весело, но встроенная разработка это лишь малая часть от желаемого скоупа работ. Сборка манипулятора или дрона тоже была бы интересна, но как будто это требует слишком специфичных деталей (если не брать уже готовые наборы). И тут милое красное сердечко из пластика, приобретенное на 14 февраля, немедленно дало мне ответ — это должен быть 3D принтер.
Почему это казалось крутой идеей
Проект по собственному дизайну и сборке принтера для 3D печати с одной стороны требуют большого количества навыков, применяемых в робототехнике, с другой — реально выполнимо из «подручных» (то есть AliExpress + Ozon) средств без требований надежных расчетов обратных кинематик, баланса полета и прочих емких программных решений. По сути, все, что нужно от конструкции картезиан 3D принтера — перемещение печатной головки по XYZ координатам. Этого можно достичь при помощи обычных и винтовых валов, моторчиков и тривиальных креплений, что легко доступно на маркетплейсах. Помимо основ встроенной разработки, среди компетенций нужны в общем случае школьные знания по физике, а также базовое понимание CAD (на уровне 1 часа youtube + 2 часов практики).
Таким образом, проект охватывает широкий спектр компетенций и при этом абсолютно выполним в домашних условиях. Так что решено, давайте построим наш родной отечественный 3D принтер!
Для специалистов в этой области работа вероятно покажется наивной и от силы вызовет умиление от очевидных проблем. Для таких же любителей, как я, это пример пути, который будет необходимо пройти для достижения схожей цели. В статье я попытался кратко отразить составляющие всех компонентов итоговой машины, опуская многие детали. Надеюсь, что по прочтению станет понятен масштаб проблем, с которыми можно столкнутся, выполняя похожие проекты в кустарных условиях.
Но даже если для вас эта работа звучит как тривиальная, не спешите переключать канал! В работе было разработано ПО на достаточно свежем фреймворке — Embassy проекта для Rust. В конце статьи вы найдете живой пример программирования принтера на этом прекрасном фреймворке, о котором вы, вероятно, ранее не слышали.
Реализация
Для планирования конструкции я изучил основные составляющие принтера, однако не стал привязываться или изучать конкретные архитектуры. Мне было интересно из абстрактного понятия «что такое принтер» создать конркетный дизайн и свои решения сопутствующих проблем. Поэтому принтер, описанный здесь, вполне воможно, уникальная снежинка с авторскими косяками конструкционными решениями.
Итак, обозначим ключевые элементы нашего принтера:
-
Корпус — скелет нашей машины. Размеры напрямую зависят от целевой печатной области.
-
Печатная область — нагревающаяся платформа со стеклянным покрытием.
-
Горячая часть печатной головки — платформа с соплом, нагревателем филамента и охлаждающей системой.
-
Шаговые двигатели — 4 для XYZ движения + 1 для экструдера.
-
Система движения
-
Логическая электроника — непосредственно микроконтроллер (далее — MCU), драйверы для моторов, концевые выключатели и датчики температуры.
-
Электропитание — нам нужно два контура питания: 5V для логической электроники + 24V для шаговых моторов, нагревающих элементов и системы охлаждения.
-
Непосредственно ПО для MCU.
В целом звучит логично, давайте посмотрим, как это можно реализовать на практике.
Корпус
Итак, мы хотим сделать картезиан принтер. Для этого необходимо обеспечить движение в трех степенях свободы нашей горячей головы. Однако, как часто делают, мы можем сократить это число до двух, если сама горячая кровать так же будет двигаться по одной из осей. Таким образом, ось движения кровати обозначим X. Горячая голова таким образом будет двигаться по оси Y и вверх‑вниз по оси Z. Как указано на картинке ниже, конструкция будет представлять собой квадратное основание и Z арку.
Мы хотим печатную площадь хотя бы 20×20×20 см, соответственно внутренняя площадь оси XY должна быть вдвое больше (ведь ось X должна подстраиваться под головку и предоставить оба своих края).
Ось Z должна быть высотой 20 см + высота платформы с головкой. Я с запасом условился на 50 см. Таким образом, мы имеем основание с внутренней площадью 40×40 см и высотой 50 см.
В качестве материала корпуса выберем классический алюминиевый профиль 20×40 мм, который часто используется, например, для конструкций стеллажей. Материал легкий, дешевый и для наших целей более чем прочный. Более важно, что на такой профиль очень легко крепить необходимые элементы. На практике, однако, почти невозможно найти профиль нужной длины с точностью до сантиметра, а прибегать к радикальным работам по типу резки профиля не хотелось. Поэтому я взял 4 профиля 40 см, и у меня получилась внутренняя площадь 38×38 см.
Печатная область
Платформа, на которой мы хотим печатать, должна обладать двумя ключевыми характеристиками — способностью подогревать филамент до 90 градусов и не быть слишком липкой для филамента. Для подогрева можно использовать платформу на основе нагревающего элемента — 24V с нагревом до 100 градусов. Поверх нагревающего элемента расположим стекло — просто стекло правильного размера. Филамент едва прилипает к нему и оно хорошо проводит тепло. Чем бы его закрепить? Думаю офисные зажимы сойдут.
Наконец, в качестве вишенки на торте, добавим регулирующие пружины для выравнивания платформы. Нам важно, чтобы XY оси шли идеально по поверхности, но этому часто мешают лишние миллиметры неидеальности конструкции, поэтому быстрая регулировка поверхности сохранит много времени на подстраивание платформы.
Горячая часть печатной головки
Тут все было относительно просто для меня — готовых головок на выбор большое множество. Выбрал головку с 24V нагревающим элементом и 0.3 мм соплом. К нему рядом добавил несколько вентиляторов для охлаждения «на глаз». Взял помощнее — 24V. Расположил вентиляторы по бокам с опцией прикрепления третьего с лицевой стороны (спасибо двухстороннему скотчу). Интенсивность охлаждения будет контролировать MCU. Все это чудо крепится на 150×150×3 мм алюминиевой платформе.
Да, платформа у меня оказалась громоздкая — только треть несет полезную нагрузку, остальное место использовано для крепления на Y ось. Но все в итоге поместилось и свою задачу выполняет. Игнорируйте разные болты на креплении и ни разу не ошибочно просверленные отверстия…
Шаговые двигатели и система движения
И вот мы дошли до наших мышц — источника подвижности нашей машины. Шаговые моторы должны обеспечить движение по осям XYZ, а также подачу филамента в горячую головку. Выбрал для этих целей моторы Nema 17 — как понимаю, немного перебор, ну и ладно.
Главный вопрос (для меня, как чайника) — как превратить простой крутящийся штырь в движение всех платформ и филамента? Начнем с самого простого — подача филамента.
Вопрос этот простой, так как широко распространены экструдеры. Взял филамент, вставил в экструдер и с другой стороны присоединил штырь мотора. И оно просто работает.
Ось X немного менее тривиальна. Нам нужно закрепить нашу печатную платформу на резиновый ремень, который будет приводится в движение нашим мотором. Для этих целей хорошо подошли алюминиевые валы и соответствующие линейные подшипники. По краям корпуса закрепим валы, закрепим линейные подшипники с печатной платформой и закрепим ремень к платформе. Это даст нам движение по оси X.
С осью Z все относительно просто — мы можем использовать винтовой вал и нужный кронштейн. Это позволит двигать прикрепленный к кронштейну груз вверх и вниз. Для этого использовал два мотора с каждой стороны — для стабилизации движения. Тут опытный специалист сразу подметит, что для надежности конструкции надо или использовать второй кронштейн, или поместить внутрь второй винт, чтобы вал находился в вертикальном положении.
Ось Y самая противная, так как мотор должен двигаться вместе с горячей головой по оси Z. В связи с этим подход был схожий с осью X. Однако вместо корпуса, крепление сделано на подвижные платформы, закрепленные на кронштейны оси Z. Для этого использовал алюминиевые листы 100×100×4 мм для стороны мотора и 100×50×4 мм с другой стороны. Принцип остается таким же, как на оси X — на линейные подшипники крепим платформу, через подшипники проводим алюминиевый вал и закрепляем по сторонам на подвижных на оси Z платформах. К платформе так же крепится резиновый ремень, который двигается шаговым мотором. Штырь был проведен сквозь платформу через отверстие.
Скрытый текст
Обратите внимание, что (на первый взгляд) пустые просверленные отверстия играют важную роль в облегчении конструкции и обеспечении оптимальной аэродинамики и точно не были сделаны из‑за неправильного расчерчивания, или дизайна, или кривых рук.
Логическая электроника
И вот от механики и ручной работы мы переходим в электронику и логику поведения нашей машины. Как мы уже показали, у нас есть ряд компонентов, требующих (как сейчас модно говорить) оркестрации. Общий вид схемы представлен ниже.
Финальная схема имеет некоторые изменения, но концепция отражена верно. Разберем группы компонентов.
Группы слева отражают драйверы для моторов. Пять моторов — пять драйверов A4988. Через драйвер проходит 24V ток для питания мотора и 5V логические сигналы. Для безопасности добавил пару накопителей. Пинов кажется, что многовато, но интересного в них не так много — просто надо подключить, как указано в документации. Только важно, что в один из пинов нужно подавать пульс шириной хотя бы 1 микросекунду, в другом надо держать высоко/низко в зависимости от направления.
Для удобства я использовал небольшой щит для упаковки четырех из пяти драйверов. Позволило сильно сэкономить в пайке. Также сигнальный провод для двух драйверов оси Z — один и тот же, так как они должны выполнять одни и те же движения. Из важного еще стоит отметить, как показал мой личный опыт, около трети заказанных на AliExpress драйверов или бракованные, или ломаются в первые минут 5. Рекомендую заказывать с запасом.
T_FUSE и T_BED компоненты отвечают за считывание температуры горячей головы и печатной кровати, соответственно. Происходит это за счет обычного делителя напряжения с 100k резистором. Делитель проходит через небольшие накопителя для сглаживания вспышек напряжения. Сигнал считывается MCU через аналоговый выход, и при помощи несложной формулы (например, такой, как тут) мы получаем оценку температуры.
X_BRAKE, Y_BRAKE, Z_BRAKE компоненты отвечают за концевые выключатели. Всего их шесть — по паре на каждую ось. Роль у них критическая — собщить в MCU, что движение по одной из осей выходит за рамки допустимого. Ещё они служат ориентирами для первичной калибровки (центрирования) осей. Принцип максимально прост — это кнопка которая пропускает ток, если нажата. Крепить мы это, конечно же, будем простым советским двухсторонним скотчем.
Три модуля MOSFET отвечают за интенсивность трех компонентов — нагревателя горячей головы, нагревателя печатной кровати и охлаждающей системы. MOSFET, по сути, определяет, какой % времени оборудование должно быть активным.Таким образом мы можем задать сигналом MCU некий слабый прогрев вместо нулевого или «на полную».
Можно отметить совесть поставщиков таких модулей. На странице продажи, модуль должен выдерживать около 50V тока. Однако, маркировка MOSFET транзистора стерта (как видно на верхнем модулe) и подтвердить наличие заявленных компонентов невозможно. В комментариях писали, что транзистор отличается от заявленного, и держит до 30V. Для меня это так же было ок, однако, на предельных мощностях могли бы быть проблемы.
Ну и, собственно, всё. Остаётся добавить непосредственно MCU и понижающий регулятор напряжения 24V → 5V. Все это чудо я через боль и страдания спаял на прототипную схему, и, в целом, готово. Очень важно валидировать пайку и убедится, что всё, что надо, изолировано. Так как в пайке не очень опытен, часто сигналы мешали друг другу, и надо было всё переделывать. Также рекомендую убедиться в качестве проводов.
Электропитание
Перечислив все компоненты, давайте подумаем, как мы будем это всё снабжать жизненной силой электрического тока. Мы обозначили, что у нас будет два контура питания — 24V и 5V. При этом, для второго контура у нас есть понижающий регулятор питания. Таким образом, нам нужен блок питания, конвертирующий переменный ток 220V в постоянный 24V. Помимо этого, надо рассчитать мощность нашей махины.
— 5 моторов X 1.7A^2 X 2 фазы X 1.5Ом сопротивление фазы = 43.35W
— Нагреватель кровати: 150W
— Нагреватель горячей головы: 40W
— Охлаждающая система: 0.1A Х 24V Х 3 вентилятора = 7.2W
Таким образом, получается примерно 240W необходимой мощности. Так как ни к своим расчетам, ни к документации комплектующих нет абсолютного доверия, решил взять со значимым запасом блок 400W. Прикрутить адаптер к C13 220V было немного заморочно и страшно, но в итоге с питанием проблем не возникало.
Программное обеспечение
Добрались до (для многих) самого интересного. Разобрав все компоненты, мы готовы приступить к программному обеспечению. Весь код есть в репозитории. В качестве инструментов разработки использовал Rust фреймворк Embassy — относительно свежий продукт с максимальным (из того, что я исследовал) комфортом для пользователя.
По инициализации проекта буду краток — если нужны подробности, следуйте официальным туториалам. Что нам надо знать — мы используем специальную утилиту для инициализации проекта под наш MCU (наподобии Cube IDE, но из консоли и сильно легковеснее). Главной задачей нашего MCU будет выполнение команд G‑code, которые будут передаваться по usb из хоста.
Но для начала рассмотрим архитектуру драйверов для наших компонентов. Для этого создадим класс (ой какой класс, у нас же тут структуры) PrintMngr. Для начала обозначим ряд наших пинов. Для каждой оси + экструдера нам надо (1) пин для сигнала (2) пин для направления (3) пин концевого тормоза. Так же для наших MOSFET нам нужны PWM (пульсовые) пины. Наконец, для считывание температуры нам нужны ADC пины. Можно обратить внимание, что благодаря move семантики, невозможно забрать одну и ту же периферию дважды (без unsafe механики).
// Глобальный пин - должен быть за мьютексомstatic ENBL_PIN: Mutex<CriticalSectionRawMutex, Option<Output<'static>>> = Mutex::new(None);impl PrintMngr { pub async fn init( a_spwnr: &Spawner, // Спавнер потоков исполнения a_p: Peripherals // Менеджер доступных переферий ) -> Self { // Берем пин, форматируем его в выходной пин и помещаем в глобальную переменную. let enbl = Output::new(a_p.PB9, Level::High, Speed::Medium); ENBL_PIN.lock().await.replace(enbl); // XYZ pins - D0-D7 let x_st = Output::new(a_p.PB5, Level::Low, Speed::VeryHigh); let x_di = Output::new(a_p.PB3, Level::Low, Speed::High); let y_st = Output::new(a_p.PA8, Level::Low, Speed::VeryHigh); let y_di = Output::new(a_p.PA9, Level::Low, Speed::High); let z_st = Output::new(a_p.PB12, Level::Low, Speed::VeryHigh); let z_di = Output::new(a_p.PB13, Level::Low, Speed::High); let e_st = Output::new(a_p.PB14, Level::Low, Speed::High); let e_di = Output::new(a_p.PB15, Level::Low, Speed::High); // концевые выключатели let x_br = Input::new(a_p.PB10, Pull::Down); let y_br = Input::new(a_p.PB2, Pull::Down); let z_br = Input::new(a_p.PB1, Pull::Down); // регуляторы напряжения let bed_pin = PwmPin::new(a_p.PB4, OutputType::PushPull); let thr_pin = PwmPin::new(a_p.PA7, OutputType::PushPull); let fan_pin = PwmPin::new(a_p.PB6, OutputType::PushPull); // считыватели температуры let bed_in = a_p.PA0.degrade_adc(); let thr_in = a_p.PA1.degrade_adc(); ... }}
ПО для моторов
Рассмотрим, как можно реализовать наше ПО для моторов. Для начала обозначим, какие константы нам нужны. В основном они состоят из выверенных расстояний, которые мы получаем за один тик в драйвер, и выведенных на их основе констант. Можете обратить внимание, что константы, отвечающие за окружность шива для ремня, как‑то не бьются. Точные цифры были мною откалиброваны «в бою» и их несоответствие теоретическим значениям (должно быть 6.0 вместо 6.1 мм, 9.80 вместо 9.84 и 8.0 мм вместо 8.2 мм) мне пока трудно объяснить.
// Ширина пульса - 1 микросекундаpub const PULSE_W_US: u32 = 1;// Расчет окружности колеса моторов XYZ и экструдера// то есть, какая дистанция проходится за полный оборот.// Измеряется в 1е-4 мм.pub const SHIV_PX_MMMM: u64 = 2 * (610 as u64 * 314);pub const SHIV_PE_MMMM: u64 = 2 * (984 as u64 * 314);// Расчет количество сигналов для полного поворота мотора.// У нашего мотора один сигнал = 1.8 градусам поворота или // 200 сигналов для полного оборота.// В режиме 1/16 (определяется драйвером) это умножается на 16pub const CYCLES_PER_TURN: u16 = 16 * 200; // 200 cycles to full turn in 1/16 mode// На основе выше описанного, рассчитываем расстояние на один поданный сигнал pub const CYCLE_X_MMMM: f64 = SHIV_PX_MMMM as f64 / CYCLES_PER_TURN as f64;pub const CYCLE_E_MMMM: f64 = SHIV_PE_MMMM as f64 / CYCLES_PER_TURN as f64;// Винтовой вал поднимает на 8.2 мм за полный поворотpub const CYCLE_Z_MMMM: f64 = 82000.0 / CYCLES_PER_TURN as f64;// размеры печатной площади - 16 на 16 см.pub const X_MMM: u32 = 160 * 10000;pub const Y_MMM: u32 = 160 * 10000;pub const Z_MMM: u32 = 160 * 10000;// Максимальная скорость и целевой рабочий циклpub const G0_SPEED: f32 = 2000.0 / 60.0;pub const CAL_WIDTH: u32 = (CYCLE_X_MMMM as f32 / (G0_SPEED * 1e4 / 1e6)) as u32;
Перейдем к основной функции — потокам, отвечающим за поведение моторов. Мы предполагаем, что некоторый процесс будет отправлять нам MoveCmd — декларация о командах к исполнению. Для передачи данных между потоками, Embassy предоставляет нам ряд очередей разного толка. В данной ситуации мы можем использовать Signal — минималистическая OPOC очередь максимальной длиной 1. Помимо этого, мы хотим отчитаться глобальному менеджеру о проделанной работе. В частности, о том, сколько шагов было недоделано — для этого возьмем еще один Signal. Получив команды, мы приступаем к исполнению. Если получили сигнал на экстренный стоп (обычно ввиду срабатывания концевого выключателя) — останавливаемся. Тут стоит отметить, что, так как соединение и пайка у меня получились не идеальными, стоп мог срабатывать просто так. В связи с этим реакция на стоп происходит только если из 100 тиков 15 наблюдали стоп сигнал. Можно обратить внимание, что Embassy предоставляет нативный корутинный интерфейс, такой как await, для паузы в исполнении.
// Манифест на команду к исполнениюpub struct MoveCmd { n: u16, // количество тиков для подачи t_us: u32, // ширина пульса (по сути, скорость) di: Level, // направление, может быть два значения ignr_stp: bool, // флаг на игнорирование стоп сигнала (например, // когда после калибровки надо вернутся в центр)}// Процесс для моторов. У нас будет 4 потока (3 оси + экструдер)#[embassy_executor::task(pool_size = 4)]pub async fn motor( mut a_st: Output<'static>, // Сигнальный пин mut a_di: Output<'static>, // Пин для направления a_stop: Input<'static>, // Пин для стоп сигнала // Канал для команды к исполнению a_ch: &'static Signal<CriticalSectionRawMutex, MoveCmd>, // Канал для отчета о не доделанных шагах a_si: &'static Signal<CriticalSectionRawMutex, u16>,) { 'outer: loop { // ожидаем команду let cmd = a_ch.wait().await; debug!("motor cmd: {:?}", cmd); // задаем направление a_di.set_level(cmd.di); let mut stops: u8 = 0; // Generate pulses based on the command for i in (0..cmd.n).rev() { if i % 100 == 0 { stops = 0; } if !cmd.ignr_stp && a_stop.is_high() { stops += 1; if stops >= 15 { debug!("Stop triggered"); a_si.signal(i); continue 'outer; } } a_st.set_high(); Timer::after_micros(PULSE_W_US as u64).await; a_st.set_low(); Timer::after_micros(cmd.t_us as u64).await; } a_si.signal(0); }}
Остальное в работе мотора затрагивать не буду, так как это больше мелкие технические моменты. Важно тут то, что теперь, когда мы генерируем MoveCmd через сигнал, мотор приводится в движение. Применяя константы выше, можно легко рассчитать количество шагов для требуемой дистанции.
Скрытый текст
Внимательный читатель может поинтересоваться: выше мы указывали про Pwm пин, который специализируется на генерации пульса. Почему бы его не использовать тут? Ответ прост. В функции мотора нам надо отдать конкретное количество шагов, и Pwm пин попросту не позволяет этого сделать (без максимально кривых костылей). Помимо этого, при остановке нам надо отчитаться о недоделанной работе, что также сходу не получается при текущем API.
ПО для регулирования температуры
Разберемся, как можем контролировать температуру термистера и горячей кровати. Для начала обозначим требуемые механизмы. Нам нужен будет обработчик входных данных и генератор выходного пульса. Для входных данных нам нужен конвертер Analog‑to‑Digital (ADC). Такой у нас один на нашем устройстве. Для генерации пульса нам нужен один из физических таймеров, который поддерживает до 4 каналов (пинов) генерации пульса.
// Резервируем глобальные ячейки доступаpub static T_PWM_GEN: Mutex<CriticalSectionRawMutex, Option<SimplePwm<TIM3>>> = Mutex::new(None);pub static ADC: Mutex<CriticalSectionRawMutex, Option<Adc<ADC1>>> = Mutex::new(None);// Возвращаемся в init нашего менеджераimpl PrintMngr { pub async fn init(a_spwnr: &Spawner, a_p: Peripherals) -> Self { ... // Обозначаем PWM генератор с 2 из 4 каналов *T_PWM_GEN.lock().await = Some(SimplePwm::new( a_p.TIM3, // прикрепляем физический таймер Some(bed_pin), // канал 1 - пин для подогрева кровати Some(thr_pin), // канал 2 - пин для термистера (горячая голова) None, // канал 3 - не используем None, // канал 4 - не используем Hertz(1), // нам достаточно 1 пульса в секунду embassy_stm32::timer::low_level::CountingMode::EdgeAlignedUp, )); // Обозначим конвертер аналог входа *ADC.lock().await = Some(Adc::new(a_p.ADC1)); ... }}
Имея эти переменные, можем приступить к главным потокам по контролю за температурой. Как было обозначено ранее, их у нас будет 2 — для термистора и для горячей кровати. Нам также будут нужны глобальная переменная для записи текущей температуры и входной канал для приема запроса на изменение целевой температуры. Процесс считывания температуры был во многом описан выше — надо при помощи ADC считать входящее напряжение и применить формулу для перевода его в градусы по Цельсию. Имея целевую и фактическую температуры, мы будем регулировать интенсивность — меньше разница — меньше интенсивность вплоть до нуля, если температура выше требуемой.
// Для термистора и кровати по одному потоку#[embassy_executor::task(pool_size = 2)]pub async fn temp_monitor( // Аналог пин канал для температуры mut a_temp_ch: AnyAdcChannel<'static, ADC1>, // номер канала нашего генератор - // или Channel::Ch1 (кровать) // или Channel::Ch2 (термистор) a_heater: Channel, // Сигнал для целевой температуры a_t_ch: &'static Signal<CriticalSectionRawMutex, u16>, // Глобальная переменная со статусом текущей температуры a_out: &'static Mutex<CriticalSectionRawMutex, f32>,) { let mut t_targ: u16 = 0; // Изначально задаем рабочий цикл 0 (отключаем нагреватель) let mut pwm = T_PWM_GEN.lock().await; let mut ch = pwm.as_mut().unwrap().channel(a_heater); ch.set_duty_cycle(0); ch.enable(); // Максимальный рабочий цикл для референса let max_c = ch.max_duty_cycle(); drop(pwm); loop { // считываем запрос на новую температуру или оставляем старую t_targ = a_t_ch.try_take().unwrap_or(t_targ); // считываем сырые данные из датчика температуры // (берем 480 циклов для меньшего разброса) let v_curr = ADC .lock() .await .as_mut() .unwrap() .blocking_read(&mut a_temp_ch, SampleTime::CYCLES480) as f64; // Применяем формуле для расчета температуры (все константы тривиальны) let rnst = (v_curr / (4095.0 - v_curr)) * R1; let t_curr = 1.0 / (B_INV * libm::log(rnst / R0) + T0_INV) - 253.15; // считаем разницу между целевой и фактической температурой let dt: f64 = t_targ as f64 - t_curr; let dt_abs = dt.abs(); // расчет целевого цикла в метрике [0, 1] let t_cycle: f32 = // температура выше целевой - почти нулевой выход if dt < 0.0 { if dt_abs < 2.0 { 0.05 } else if dt_abs < 5.0 { 0.03 } else { 0.0 } // температура ниже целевой - увеличиваем интенсивность в зависимости // от дистанции } else { if dt_abs < 1.0 { 0.1 } else if dt_abs < 3.0 { 0.2 } else if dt_abs < 5.0 { 0.4 } else if dt_abs < 15.0 { 0.8 } else { 1.0 } }; // обновляем пульс генератор с расчитанным рабочим циклом T_PWM_GEN .lock() .await .as_mut() .unwrap() .channel(a_heater) .set_duty_cycle((t_cycle * max_c as f32) as u32); *a_out.lock().await = t_curr as f32; Timer::after_millis(500).await; }}
Собственно, и всё. Также тут опущу остальные детали, так как они довольно тривиальны. Внимательный читатель заметить, что формула немного изменена. Я немного подкрутил bias, выверяя точную температуру лазерным термометром. Покрывать регулирование охлаждения не буду, так как принцип там схожий.
G‑code и его обработка
Рассмотрим кратко, что такое G‑code. Нашёл для нас краткий гайд. По сути, это последовательность, куда головке надо идти и какую температуру надо задать в корвате и термисторе. Для примера, вставлю вырезку из указанного гайда:
G21 G90 G17 (Units = mm, Absolute mode, XY plane)G00 X-15 Y-10 Z5 (Rapid to 15mm left, 10mm down, 5mm above origin)M03 S1200 (Spindle on clockwise, 1200 RPM)G01 Z-5 F150 (Feed down into material to Z = -5mm at 150 mm/min)G01 X15 Y-10 F200 (Cut along bottom edge to X = +15)G01 X15 Y10 (Cut right side up to Y = +10)G01 X-15 Y10 (Cut top edge back to X = -15)G01 X-15 Y-10 (Cut left side down to starting corner)G00 Z5 (Retract rapidly to Z = +5)M05 (Spindle stop)M30 (End program)
То есть мы будем получать команды в виде строки и нашей задачей будет это дело распарсить и пустить в дело. Определим, какие команды мы в целом будем парсить. По итогу парсим мы далеко не весь список возможных команд, но этого достаточно для наших целей.
pub struct MoveC { pub x: Option<f32>, pub y: Option<f32>, pub z: Option<f32>, pub e: Option<f32>, pub f: Option<f32>,}pub enum ComT { FanOff, // M107 FanSpeed(f32), // M106 SetTemp(u16), // M104 HomeAxes(MoveC), // G28 MoveBal(MoveC), // G1 MoveFst(MoveC), // G0 SetTempWait(u16), // M109, SetMM, // G21 SetAbsCoord, // G90 SetRelCoord, // G91 SetAbsExtr, // M82 SetRelExtr, // M83 SetExtrLvl(f32), // G92 MotorsDown, // M84 SetBedTemp(u16), // M140 SetBedTempWait(u16), // M190 None,}
Сам парсер и обработку команд также не буду затрагивать — имея у себя потоки, обозначенные выше, не составит большого труда написать их обработку. Можно кратко рассказать о команде позиционирования G28. Все, что тут необходимо, это до упора сдвинуть все оси пока не сработает соответствующий кольцевой выключатель (и не забыть отодвинуть назад, чтобы выключатель стал неактивным). Это и будет наша позиция 0,0,0 — нижний левый угол.
Как мы можем сгенерировать G‑code для определенной 3D модели? Я взял вот Cura. Загружаем туда модель, тюним параметры для принтера и готово. Не будем тут останавливаться.
Трансляция команд на принтер
Принимать команды для печати принтер будет через USB. Наш принтер как клиент должен получать команды и отчитываться, когда команда выполнена. Инициализация USB так же является относительно тривиальной процедурой и пример в том же репозитории. Таким образом, поддерживая usb соединение с хостом, по одной команде мы можем выполнять G‑code нашей модели. Для этих целей сделал небольшую утилиту на Python.
Ну и, собственно, готово — все компоненты на месте и мы готовы к пуску!
Какова цена пластика
Что в итоге нужно чтобы «с нуля» разобраться и смастерить такое чудо? Мне с моими кривыми руками и почти нулевым опытом во встроенной разработке понадобилось около 2 месяцев, чтобы шаг за шагом это сделать и валидировать. Какой‑то период времени надо было ждать доставок с AliExpress — иногда приходило не то, что‑то задерживали на целый месяц, что‑то было браком. Если попытаться перевести это в человекочасы, то наверно около 60 часов будет более менее корректной оценкой.
Что касается стоимости, у меня вышло почти 50 000 руб за все компоненты, включая брак и компоненты, которые в итоге не пригодились. Работа бы пошла заметно легче, если бы уже имелся 3D принтер в наличии, так как многие крепления было трудно найти, но запросто можно было бы начертить и напечатать. Так что если думаете, что собрать самостоятельно из деталей дешевле готового 3D принтера — это может быть правдой только на масштабе и с оптовыми закупками. Как говорят представители отечественного машиностроения — стоимость металла с доставкой уже превышает рыночную стоимость готового изделия. Из самых дорогих позиций могу перечислить валы (обычные и винтовые) — 4 200 руб; корпус без крепежей — 2 300 руб; моторы — 2 200 ₽ Помимо этого миллионы и миллионы мелочей, большинство из которых есть в любом дедовском сарае (нужные сверла для алюминия, шурупы, крепления и так далее). Наверное, начиная с моими текущими знаниями, стоимость можно было бы сократить раза в два.
И правда работает??
На 80% можно сказать, что ДА. Сильно не погружался в нюансы охлождения и виды филамента. Подозреваю, что из‑за этого иногда филамент не плавится полностью, головка задевает проделанную сетку и конструкция смещается на пару миллиметров. Из‑за этого можно наблюдать странное смещение выходящих моделей. Я взял достаточно дешевый филамент PLA. Думаю, если взять филамент покачественее и подойти к задаче терморегулирования более научно — проблема должна уйти.
Помимо этого, явно что‑то не так пошло с пайкой и в целом с некоторыми логическими проводами. Иногда сигнал направления двигателя или не проходит, или путается между осями. Замена проводов это обычно лечит.
Заключение
Ну чтошшш — это было интересное приключение. За примерно 50к мы получаем через раз работающий принтер. Часто казалось, что задача невыполнима, но шаг за шагом машинка собиралась. С мотивационно‑духовной точки зрения здесь важно настроить себя на большой ряд маленьких поражений и побед.
На удивление самым простым здесь мне показалось разработка кода — как‑то немного разобравшись, написался он быстро и почти сразу заработал в бою.
Если Вы хотите отважится проделать похожий подвиг, я бы рекомендовал следующее:
-
Найдите себе где‑нибудь 3D принтер — или у друзей или возьмите, если давно планировали. На разного рода крепежах это может сэкономить почти те самые 10к, в которые Вам обойдется самый дешевенький принтер
-
Не поленитесь и расчертите в CAD самые сложные элементы конструкции. Так как мне конструкция казалось несложной, я ПРИМЕРНО начертил ее, что стоило сантиметра промаха и полному редизайну на одной из стадий
-
Обойдитесь без масштабных паек как у меня. Это имеет малое отношение к самому вызову и большее к самому непостижимому искусству пайки. В наличии большое количество готовых бордов, как специально для принтеров, так и для, например, управления моторами
-
Убедитесь, что Вы понимаете азы электропитания. Если неправильно что‑то подключить — запросто сожжете ваши компоненты. По ошибке поставил не тот накопитель в 24V и наблюдал восхитительный белый дым (но было красиво)
Выполнив этот проект я преисполнился проектированием и сборкой. После этого в голове уже гораздо легче выстраиваются эскизы таких машин, как манипуляторы, дроны и так далее. Для полного набора можно добавить калибровку по камере с OpenCV, добавить Online Reinforcement Learning для корректировки самого себя, когда координаты сбились — и вот тебе полноценный робототехнический проект.
Понимаю, что для читателей с опытом в индустрии проект покажется наивным и с набором серьезнейших недочетов, с которыми был бы очень рад ознакомиться в комментариях. Проект принес много боли, но гораздо больше удовлетворения от содеянного. Если вы бравый пионер по сбору и утилизации собственного свободного времени — подобный проект вас точно не разочарует!
ссылка на оригинал статьи https://habr.com/ru/articles/1060172/