18.06.2009

Кухонный таймер на Arduino (2)

Часть II

(продолжение, см. начало в Части I)

Пришло время сосредоточиться на программной части.

Мне пришло в голову вполне тривиальное деление по функциям:
  1. поддержка отсчета времени
  2. индикация
  3. сигнализация
  4. опрос и чтение кнопок

1.Ура! С поддержкой времени нам повезло: в некотором смысле, поддерживать отсчет времени Arduino умеет самостоятельно.

В его ядре определена процедура прерывания по таймеру 0, которая прозрачно для нашего скетча инкрементирует счетчик "тиков". Пользовательская программа в любой момент может получить значение этого счетчика в виде количества миллисекунд с момента запуска через вызов millis(). Остается только зафиксировать в переменной начальное значение, а потом ожидать конечное, которое вычисляется путем прибавления задержки к зафиксированному начальному.

Правда, есть маленькая неприятность: как и любой другой счетчик, он подвержен переполнению. На наше счастье, это случится через 49 дней после старта скетча, поэтому попробуем пока этим пренебречь ;)

2. Алгоритм индикации можно с небольшими изменениями позаимствовать из моей статьи про Shield с семисегментным индикатором на шине I2C.

3. Включать и выключать пищалку надо записью HIGH или LOW в соответствующий порт. Так что вся премудрость сводится к вызову digitalWrite.

4. Чуть больше внимания надо уделить кнопкам. Во-первых, оставлять висеть в воздухе цифровые входы нежелательно, иначе мы получим непредсказуемые результаты. Обычно, в таких случаях вывод притягивают к Vcc, чтобы в разомкнутом состоянии поддерживать на входе МК логическую единицу. Во-вторых, надо бороться с дребезгом.

На схеме в первой части нет никаких подтягивающих резисторов. Не верьте своим глазам, на самом деле они есть! Просто внутри ATmega. Ну зачем ставить внешние, если можно использовать готовые внутренние? Вот так по datasheet-у выглядит логическая схема универсального Pin-а ATmega:



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

pinMode(PINNO,INPUT);
digitalWrite(PINNO,HIGH);


Функцию digitalWrite надо вызывать строго после перевода пина режим INPUT, в противном случае у нее будет другой смысл.

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

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

Вот как это будет выглядеть на практике, в нашем случае:



1. Исходное - после включения питания. Таймер бесконечно долго ждет, когда ему определят время срабатывания. В этом состоянии дисплей мигает целиком.

2. Выбор времени - нажимая кнопки, пользователь устанавливает желаемое время до срабатывания, в минутах. Чтобы войти в это состояние, достаточно нажать одну из кнопок. Одна кнопка увеличивает время, вторая - уменьшает. И поскольку клавиши Enter не предусмотрено, будем считать, что вы закончили ввод, если кнопки не трогали 5 секунд подряд и установлено время, отличное от нуля. Пока происходит установка времени, дисплей по-прежнему мигает целиком, отображая текущее установленное число минут.

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

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

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

Перейдем к рассмотрению скетча. Основной цикл будет выглядеть так:


void loop() {
indicate();
update_time();
update_alarm();
update_buttons();
}

Чтобы обеспечить стабильное отображение дисплея, каждая процедура должна работать фиксированное количество времени ( в таком простом устройстве даже не потребовалось изощряться - все именно так ).

indicate() - поочередно отображает все четыре цифры на дисплее (включая точки), показывая нам каждую по 1 мс (таким образом, в сумме выполняясь не менее 4 мс).

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

update_alarm() - управляет пьезоизлучателем, в том числе его прерывистой трелью.

update_buttons() - опрашивает кнопки и, в зависимости от состояния системы, предпринимает соответствующие действия.

Состояние системы хранится в глобальной переменной state, имеющий характерный для C тип enum:


typedef enum { E_WAIT=0, E_SETUP, E_STARTED, E_ALARMED} fsm_type;
fsm_type state = E_WAIT;


На самом деле, в state хранится целое число от 0 до 3, но применение enum позволяет безо всяких дополнительных #define придать этим значениям смысл, помогая в конечном итоге разбираться с текстом программы. Ведь куда проще смотрится if (state != E_STARTED), чем if (state!=2).

По мере необходимости, все функции в скетче сверяются со значением state: например, update_alarm() не будет пытаться включать пьезоизлучатель, если состояние state не равно E_ALARMED, а update_time() переведет систему в состояние E_ALARMED только из состояния E_STARTED и только по истечению установленного времени.

Кстати, я забыл упомянуть по борьбу с дребезгом. Так вот - при такой идеологии построения скетча, этой проблемы - нет. Ведь в чем суть дребезга? В момент нажатия кнопки случается переходной процесс, когда контакт еще не до конца нажат. В итоге происходит серия последовательных разрывов-замыканий с высокой скоростью (дребезг). Чтобы программа не реагировала на быстрые смены состояний кнопки, вводятся задержки (иногда их называют умным словом "гистерезис"). Однако, в моем случае на каждое однократное чтение кнопок в процедуре update_buttons() приходится один вызов indicate(), который и обеспечивает ту самую необходимую задержку.

Периодически по тексту можно встретить в операторах if такую конструкцию:

if (millis() % 1000UL > 500)

или

(millis() % 1000UL > 500) ? HIGH:LOW

Это - простой способ определить фазу в мигании или звуковом сигнале. Берем текущее время в миллисекундах, выделяем остаток от деления на 1000 и получаем таким образом число от 0 до 999 - это число миллисекунд в текущем времени. Далее сравниваем с границей ( 500 ) и решаем - включить или выключить наше устройство. Увеличивая или уменьшая остаток от деления, можно ускорить или замедлить мигание, а изменяя соотношение с границей - скважность, т.е. преобладание одного состояния над другим.

Вот полный текст скетча для скачивания: cook_timer_1.0.zip.

Кстати, в нем есть небольшая ошибка в процедуре установки времени, попробуйте ее отыскать в качестве тренировки (или развлечения ;)

В последней части я добавлю схему внешнего питания, спаяю и упакую таймер в корпус, а затем буду бороться со злыми духами аппаратной части (не путать с багами в скетче ;). Так что, окончание обязательно следует.

Комментариев нет:

Отправить комментарий