Кот поднял меня в шесть утра четвёртый день подряд, и я собрал кормушку за один вечер: банка из-под кофе, шнек из сантехнической трубы и моторчик 28BYJ-48, который лежал в ящике с запасными шлейфами. Механика заработала сразу, а вот логика — нет: первая версия выдавала корм каждые восемь часов честно, но стоило нажать кнопку ручной выдачи дважды — а это делал каждый гость, считая, что кот выглядит голодным, — и в миску вываливалась двойная порция. Так в скетче появились cooldown и счётчик порций, и это главный сюжет урока. Механика тем временем стоит пяти копеек: банка лежит набок, внутри шнек, выход — через горлышко в миску, и моторчик закреплён на крышке четырьмя винтами от игрушечного набора.
Раздел уже делал дозатор для печатника — подача филамента на том же моторе, — а соседний раздел собирал кормушку на серво с качающейся заслонкой над отверстием. Здесь беру шнек, и у него есть одно честное преимущество перед заслонкой: он дозирует, пока крутится, и не отсыпает лишнего от сотрясения двери или прыгнувшего на полку кота. Виток за четверть оборота, порция — четверть оборота шнека, 512 шагов.
Соберём полный цикл выдачи: расписание на millis, ручная кнопка с фронтом и антидребезгом, общая пауза от двойных выдач и счётчик в Serial. Хранение расписания и лимитов между выключениями питания — отдельная история про энергонезависимую память, до неё дойдём текстом, а в скетчах урока памяти сознательно нет: сначала логика, потом хранение.
Порция как четверть оборота
Порция в шнековой кормушке — чистая геометрия. За полный оборот шнек протаскивает объём одного витка: у трубы диаметром пятьдесят миллиметров с шагом витка в сорок это порядка 70-80 миллилитров крупки, четверть оборота — около двадцати. Сколько это в граммах, зависит от корма: насыпная плотность гранул около полукилограмма на литр, выходит граммов десять на порцию. Читайте упаковку, взвешивайте выданное на кухонных весах и подбирайте число шагов, начиная с 512 — калибровка двумя пробами занимает пять минут. Если корм мелкий и сыпучий, виток протаскивает меньше запланированного — порция уезжает вниз, и компенсируется это не механикой, а числом шагов: 640, 768, сколько весы скажут. Проверяйте пятью выдачами подряд: одна проба врёт из-за остатка в витке, пять подряд дают честную среднюю порцию.
Скорость выдачи — не гонка: 5-6 RPM, порция в 512 шагов занимает секунд пять. Быстрее нельзя по двум причинам. На скоростях выше 15 RPM моторчик начинает срывать шаги — потолок мы вымеряли в уроке про скорость и позиционирование — и порция остаётся недовыплаченной при честно насчитанном счётчике. На средних скоростях корм успевает пересыпаться через виток и порция размазывается от порции к порции неравномерно. Плюс медленный шнек не перемалывает гранулы в пыль, которой потом дышит банка. Есть и третья, неочевидная: на высокой скорости сухая крупка электризуется о пластик и слёживается комками раньше срока — медленное кормление продлевает и свежесть корма.
После каждой выдачи обесточиваю обмотки — приём из урока про отключение катушек: шнек заклинен кормом сам по себе, ток удержания здесь не нужен. Кормушка стоит в комнате, и греющийся в ней мотор первым делом замечает не кот, а шкаф над ним: пара градусов на корпусе — это постоянные ватты впустую и тёплый запах разогретой муки из вентилируемых щелей банки. Обесточили — и механизм беззвучен и холоден до следующей порции. Проверьте это в первый же день: тёплый корпус ULN2003 после выдачи — норма, горячие обмотки через час простоя — значит, что-то держит катушки под током, и искать надо в coilOff.
Расписание, кнопка и защита от жадности
Расписание на millis — это одна переменная последней выдачи и одно сравнение. Кормление каждые восемь часов — интервал 28 800 000 миллисекунд, число спокойно влезает в unsigned long с запасом в двадцать раз. Разность двух отметок millis() корректна даже через переполнение на 49-е сутки — как пережить переполнение мы разбирали отдельно, и кормушка из тех задач, которые реально живут месяцами без перезагрузки. В симуляторе тот же код живёт с интервалом в десять секунд: меняется одна константа, логика не трогается вообще. Если питомец должен есть дважды в день, не плодите таймеры: либо интервал в двенадцать часов, либо массив отметок и ближайшая из них — скелет выдачи один и тот же. Мой кот ел раз в сутки без вопросов, поэтому в скетче один INTERVAL.
Кнопка ручной выдачи — байпас для дня, когда расписание ещё не подошло, а кормить пора: приехали поздно, кот актёрствует, автомат молчит. Кнопка на пине 2 с INPUT_PULLUP, читаем фронт, а не уровень: уровень при удержании выдал бы порцию за порцией, каждый оборот loop() — ещё одна, и за секунду удержания в миску улетело бы пять порций. После сработавшего фронта держу блокировку на сто миллисекунд — звон контакта за это время успокаивается; остальные способы гасить дребезг разобраны в уроке про четыре способа антидребезга. Замыкайте кнопку на землю, а не на плюс: с INPUT_PULLUP второй провод кнопки можно вести чем угодно, хоть шлейфом от старого сканера, а обрыв провода при такой схеме выглядит как отпущенная кнопка, а не как вечная выдача.
Защита от двойной выдачи — общая пауза после любой порции, вышла она по расписанию или с кнопки: cooldown в шестьдесят секунд на настоящей кормушке, пять в симуляторе. Идея в том, что порция, пришедшая сразу за порцией, всегда чей-то сбой: жмущая кнопку пятилетняя дочь, зависший таймер, дребезг контакта, случайное замыкание шлейфа. Запрос внутри cooldown скетч честно печатает в Serial и молча съедает — корм в миске не дублируется. Светодиод на выдаче — страховка для человека: горит — механизм крутит, не горит при нажатой кнопке — запрос отсеян, и это видно, не открывая монитор. Ширину паузы подбирайте по жильцам: минута для взрослого с кнопкой в руке, пять-десять минут, если в доме ребёнок, который уже понял, что кнопка связана с котом.
| Событие | Условие | Что делает скетч |
|---|---|---|
| Выдача по расписанию | millis() - lastFeed >= 28 800 000 | 512 шагов при 6 RPM, LED горит, счётчик плюс единица, cooldown пошёл |
| Выдача с кнопки | фронт на D2 и cooldown истёк | та же порция, в Serial помечена как knopka |
| Запрос внутри cooldown | после последней порции прошло меньше паузы | мотор спит, LED молчит, в Serial уходит строка отказа |
| Конец выдачи | step() досчитал 512 шагов | обмотки обесточены, LED погас, напечатан счётчик порций |
Счётчик порций — одна переменная int, но в Serial она стоит дороже, чем выглядит. По ней видно, что расписание тикает, что кнопка доходит до мотора, а cooldown поедает лишние запросы; печать парой porciya=число после каждой выдачи превращает кормушку в устройство с чёрным ящиком. Отказы печатайте в тот же поток со своим тегом: через неделю таких логов видно, сколько запросов съел cooldown — у меня их набегало до десятка в день, все от гостей, считавших, что кот выглядит голодным. Добавьте к счётчику отметку millis в той же строке — и вы увидите дрейфует ли ваш интервал и не теряется ли где-то запрос между проверками.
Суточный лимит — следующий виток той же логики: после N порций за сутки ручная кнопка молчит до обнуления счётчика, сколько бы её ни просили. И вот тут без энергонезависимой памяти не обойтись: счётчик и метка начала суток должны переживать выдернутую на время уборки вилку, иначе лимит обнуляется вместе с питанием. Как хранить переменные без питания и сколько ячеек выжигается каждой перезаписью — начните с счётчика включений в разделе про EEPROM; для лимита хватает пары байт: один на счётчик порций, один на номер дня. В скетчах этого урока EEPROM сознательно нет — симулятор его не исполняет, а логика выдачи от хранения не зависит.
#include <Stepper.h> const int STEPS_PER_REV = 2048;const int PORTION = 512; // четверть оборота шнекаconst unsigned long INTERVAL = 28800000UL; // 8 часовconst unsigned long COOLDOWN = 60000UL; // минута после любой порции Stepper worm(STEPS_PER_REV, 8, 10, 9, 11); const int PIN_BTN = 2;const int PIN_LED = 12; int portions = 0;unsigned long lastFeed = 0;unsigned long lastBtn = 0;bool btnWas = false; void coilOff() { digitalWrite(8, LOW); digitalWrite(10, LOW); digitalWrite(9, LOW); digitalWrite(11, LOW);} void feed() { digitalWrite(PIN_LED, HIGH); worm.step(PORTION); digitalWrite(PIN_LED, LOW); coilOff(); portions++; lastFeed = millis(); Serial.print("porciya="); Serial.println(portions);} void setup() { pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_LED, OUTPUT); worm.setSpeed(6); Serial.begin(9600); Serial.println("kormushka gotova");} void loop() { unsigned long now = millis(); if (now - lastFeed >= INTERVAL) { Serial.println("vydacha: raspisanie"); feed(); } bool btn = digitalRead(PIN_BTN) == LOW; if (btn && !btnWas && now - lastBtn > 100) { lastBtn = now; if (now - lastFeed >= COOLDOWN) { Serial.println("vydacha: knopka"); feed(); } else { Serial.println("otkaz: cooldown"); } } btnWas = btn;}- Порция шнека — четверть оборота: 512 шагов при 6 RPM, порядка пяти секунд на выдачу
- Расписание — разность отметок millis(): 28 800 000 мс влезает в unsigned long и живёт через переполнение
- Кнопка читается фронтом с блокировкой 100 мс: уровень при удержании выдал бы порцию за порцией
- Cooldown после любой порции съедает двойные запросы — от кнопки, дребезга или зависшего таймера
- Счётчик порций в Serial — чёрный ящик кормушки; между выключениями его хранит EEPROM, в симуляторе памяти нет
- 1. Сколько шагов занимает порция, если шнек должен сделать четверть оборота?
- 2. Кнопка ручной выдачи опрашивается по уровню, а не по фронту. Что случится при удержании?
- 3. Зачем cooldown общий и после автоматической выдачи тоже?
- 4. Почему между выдачами обмотки обесточивают?
- 5. Где в кормушке место EEPROM?
Кормушка из этого урока переживает и гостя с двумя руками, и дочь с быстрыми пальцами, и выдернутый шнур питания — потому что в ней нет ни одного пути, по которому двойной запрос превращается в двойную порцию. Тот же скелет — цикл выдачи, байпас, пауза — переезжает в дозатор удобрений и в кормушку аквариума без единой правки логики: рыбам тоже вредно два корма за минуту, а курятник с тремя выдачами в сутки отличается только числом в INTERVAL. Следующий урок доводит жанр до предела ответственности: там вместо граммов корма будут литры воды, а цена промаха — лужа на подоконнике.
Частые вопросы по теме урока
- Сколько шагов занимает порция, если шнек должен сделать четверть оборота?
- Четверть от 2048 шагов на оборот — 512. Меняя это число, калибруют граммы порции по кухонным весам. 512
- Кнопка ручной выдачи опрашивается по уровню, а не по фронту. Что случится при удержании?
- Каждый оборот loop() видит нажатый уровень как новое событие. Фронт плюс блокировка на 100 мс оставляют одно срабатывание на нажатие. loop() выдаст порцию за порцией, пока кнопку держат
- Зачем cooldown общий и после автоматической выдачи тоже?
- Cooldown стоит после любой порции, иначе двойная выдача собирается из расписания плюс кнопки, нажатой через секунду. Чтобы кнопка не отдавала вторую порцию сразу вслед за автоматической
- Почему между выдачами обмотки обесточивают?
- Заклиненный кормом шнек не провернётся и без удержания. Обесточенные катушки не греются и не мотают ватты между порциями. Шнек держится сам трением корма, а ток покоя греет мотор в жилой комнате
- Где в кормушке место EEPROM?
- Лимиты и счётчики должны переживать выдернутую вилку — это работа энергонезависимой памяти; в симуляторе её нет, поэтому в скетчах урока её нет. Хранить счётчик порций и суточный лимит между выключениями питания