пятница, 30 августа 2013 г.

Мигаем светодиодом без delay()

или всегда ли официальный примеру учат "хорошему".

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

Приведу его код:
/* Blink without Delay
 2005
 by David A. Mellis
 modified 8 Feb 2010
 by Paul Stoffregen
 */

const int ledPin =  13;      // номер выхода, подключенного к светодиоду
// Variables will change:
int ledState = LOW;             // этой переменной устанавливаем состояние светодиода 
long previousMillis = 0;        // храним время последнего переключения светодиода

long interval = 1000;           // интервал между включение/выключением светодиода (1 секунда)

void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(ledPin, OUTPUT);      
}

void loop()
{
  // здесь будет код, который будет работать постоянно
  // и который не должен останавливаться на время между переключениями свето
  unsigned long currentMillis = millis();
 
  //проверяем не прошел ли нужный интервал, если прошел то
  if(currentMillis - previousMillis > interval) {
    // сохраняем время последнего переключения
    previousMillis = currentMillis;  

    // если светодиод не горит, то зажигаем, и наоборот
    if (ledState == LOW)
      ledState = HIGH;
    else
      ledState = LOW;

    // устанавливаем состояния выхода, чтобы включить или выключить светодиод
    digitalWrite(ledPin, ledState);
  }
}
  



В принципе отправка к этому примеру - вполне правильна. В нем действительно виден стандартный паттерн как нужно выполнять какое-то переодическое действие (или отложенное):
1. Сохраняем время в какую-то переменную
2. В loop() все время смотрим на разницу "текущие-время - сохраненное"
3. Когда эта разница превысила какое-то значение (нужный нам "интервал переодичности")
4. Выполняем какое-то действие (меняем состояние диода, заново запоминаем "стартовое время и т.п.")

С задачей "объяснить идею" - пример справляется. Но, с моей точки зрения, в нем есть несколько методологических ошибок. Написание скетчек в подобно стиле - рано или поздно приведет к проблемам.

Итак, что же тут не так?

1. Не экономно выбираем тип переменных

Переменная ledPin у нас объявлена как тип int. Зачем?  Разве номер пина может быть очень большим числом? Или может быть отрицательным числом? Зачем под его хранение выделять два байта, когда вполне хватит одного. Тип byte может хранить числа от 0 до 255. Для номера пина - этого вполне хватит.

  const byte ledPIN = 13; //  номер выхода, подключенного к светодиоду

Этого будет вполне достаточно.

2. А зачем нам переменная для малых чисел?

А зачем нам тут вообще переменая? (пусть и объявленная как const). Зачем тратить такты процессора на чтение переменной? И расходовать еще один байт?  Воспользуемся директивой препроцессора #define

#define LED_PIN  13 //  номер выхода, подключенного к светодиоду

Тогда еще на этапе компиляции компилятор просто подставить 13-ть везде где в коде используется LED_PIN и не будет выделять отдельных переменных.

3. Тип int опять выбрали как "первое что в голову пришло"?

И опять спотыкаемся на объявлении следующей же переменной. Почему ledState опять int? Кроме того что снова "два байта там где можно один использовать", так еще и "смысловая нагрузка" теряется. Что у нас хранится в переменной? Состояние светодиода. Включен/выключен. Горит/Не горит. Явно же напрашивается тип boolean. По крайней мере до тех пор, пока светодиод у нас может принимать два состояния.

4. И вновь не верный тип. Теперь уже критично

Наверное уже надоел :)  Но объявление следующей переменной мне опять не нравится. Почему previousMillis как long? В нее  мы сохраняем время  возвращаемое функцией millis(). В документации на нее говорится что она возвращает unsigned long  (кстати currentMillis объявили правильно). Значит и все переменные где хранится время должны быть unsigned long.
Эта ошибка с типом - гораздо более опасная. В отличает от предыдущих где тип был выбран "избыточно", в этом случае он выбран "недостаточно". Функция millis() возвращает unsigned long которая может принимать максимальное значение 4,294,967,295 . А в long у нас может поместится 2,147,483,647 . В два раза меньше.  Примерно на 24-тый день в переменной previousMillis из-за этого начнут образовыватся отрицательный значения. И все логика  (currentMillis - previousMillis) - может поломатся.

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

Кстати недавно на форуме, как раз и ловили подобную ошибку. Там вместо "unsigned long" подобная переменная была объявлена вообще как int. Повезло в том, что работали не с миллисекундами, на более мелких интервалах. Вместо millis() использовали micros() . И скетч "зависал" через 32-секунды (это число и натолкнуло на необходимость поиска ошибочного int). А если бы был millis() - все бы висло раз в несколько дней. И догадатся "что не так" - было бы гораздо сложней. Грешили бы на "питание/помехи" и т.п.
Вообщем я думаю вы уже поняли, что небрежное обращения с типами, использование "первого попавшегося с которым заработало" - в итоге выливается в часы жизни проведенные за поиском бага.


5. Мелочный придирки

Ну и парочка придирок которые "не ошибки", но можно было-бы компактней написать. Хотя возможно тут автор примера просто старался написать "попроще для новичков". Но раз мы уже "смотрим скетч под лупой.." :)
Переменную previousMillis можно, с помощью ключевого слово static объявить внутри loop(). Не засорять простраство имен глобальных переменных. Полезная привычка.

От переменное ledState - вообще можно отказаться. Текущие состояние пина можно узнать с помощью digitalRead().  Вообщем-то этот "совет" довольно спорен. С переменой ledState - код более читабелен... просто покажу что можно и без нее.

Да и без переменной currentMillis можно обойтись. Просто два раза вызвать функцию millis() . 

6. Наш ответ Чемберлену

Ну вот, а теперь попытаемся переписать этот пример, учтя все "замечания".

 /* Blink without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/08/delay.html
 */

#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  INTERVAL  1000UL           // интервал между включение/выключением светодиода (1 секунда)

void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
}

void loop()
{
  // здесь будет код, который будет работать постоянно
  // и который не должен останавливаться на время между переключениями свето
  
  static unsigned long previousMillis = 0;        // храним время последнего переключения светодиода
 
  //проверяем не прошел ли нужный интервал, если прошел то
  if(millis() - previousMillis > INTERVAL) {
    // сохраняем время последнего переключения
    previousMillis = millis();  
    
    // меняем состояние выхода светодиода на противоположное текущему. 
    // если горит - тушим, не горит - зажигаем.
    digitalWrite(LED_PIN,!digitalRead(LED_PIN));
  }
}

UPD: Больше примеров вы найдете в следующей статье Делаем несколько дел одновременно-переодически

P.S. Задать вопросы и почитать обсуждение вы можете либо в комментариях ниже, либо на сайте arduino.ru в ветке Еще раз мигаем светодиодом без Delay | Аппаратная платформа Arduino

6 комментариев: