воскресенье, 8 сентября 2013 г.

Делаем несколько дел одновременно-периодически

продолжение статьи МИГАЕМ СВЕТОДИОДОМ БЕЗ DELAY() или всегда ли официальный примеру учат "хорошему".

Сразу оговорюсь, в рамках 8-ми битных контроллеров настоящей одновременности добится невозможно (только если какое-то действие будет выполняется независимым аппаратным блоком, например - генерация PWM). Но для подавляющего большинства задач - хватает "псевдо-одновременности". Когда контроллер по очереди "хватается" то за одно дело, то за другое. Если на каждом "деле" он не задерживается слишком на долго, то с точки зрения человека это выглядит как "одновременно".
Типичный пример чем может одновременно заниматься контроллер:

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

В принципе, как обходится без delay() - мы уже умеем благодаря примеру [src="blink..."]. Так же в прошлой статье [src....] мы детально разобрали стилистические ошибки этого примера и постарались его улучшить. Так что сделать, по образу и подобию этого примера два одновременных периодических действия - не должно составить проблемы. Просто заводим для каждого действия заводим свою переменную previousTime  (естественно имя чуть-чуть меняем) и оборачиваем действий в if(millis()-previousTime)

Два дела сразу

Скажем мы хотим мигать диодом раз в пол-секунды и, при этом выводить, раз в секунду, в Serial текущие значение millis()  (что-бы даже не видя ардуины физически иметь уверенность что скетч не завис).

Полностью по аналогии со скетчем из предыдущей статьи


/* Blink And Print Without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/09/BlinkAndPrintWithoutDelay.html
 */
 
#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  BLINK_INTERVAL  5000UL  // интервал между включение/выключением светодиода (5 секунд)
#define PRINT_INTERVAL 1000UL  // периодичность вывода времени в Serial (1 cсекунда)
#define SERIAL_SPEED 9600 // скорость работы Serial
 
void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
  
  // задаем скорость работы ком-порта
  Serial.begin(SERIAL_SPEED);
  
  
}
 
void loop()
{
  // мигаем диодом   (периодически переключаем его состояние)
  static unsigned long prevBlinkTime = 0; // время когда последний раз переключали диод
  if(millis() - prevBlinkTime > BLINK_INTERVAL) {
    prevBlinkTime = millis();  // 
    digitalWrite(LED_PIN,!digitalRead(LED_PIN)); 
  }
  
  // периодически выводим millis() в Serial
  static unsigned long prevPrintTime=0;
  if(millis()-prevPrintTime>PRINT_INTERVAL){
    prevPrintTime=millis();
    
    Serial.print("Current time:");
    Serial.println(millis());

  }
  
}

Причесываем код

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

/* Blink And Print Without Delay
 2013
 by alxarduino@gmail.com
 http://alxarduino.blogspot.com/2013/09/BlinkAndPrintWithoutDelay.html
 */
 
#define LED_PIN  13      // номер выхода,подключенного к светодиоду
#define  BLINK_INTERVAL  5000UL  // интервал между включение/выключением светодиода (5 секунд)
#define PRINT_INTERVAL 1000UL  // периодичность вывода времени в Serial (1 cекунда)
#define SERIAL_SPEED 9600 // скорость работы Serial
 
void setup() {
  // задаем режим выхода для порта, подключенного к светодиоду
  pinMode(LED_PIN, OUTPUT);      
  
  // задаем скорость работы ком-порта
  Serial.begin(SERIAL_SPEED);
  
  
}
 
void loop()
{
  blinkLed(BLINK_INTERVAL);  // мигаем
  printTime(PRINT_INTERVAL); // выводим время
}

// мигает диодом с периодичностью interval
void blinkLed(unsigned long interval ){
  static unsigned long prevTime = 0; // время когда последний раз переключали диод
  if(millis() - prevTime > interval) {
    prevTime = millis();  // 
    digitalWrite(LED_PIN,!digitalRead(LED_PIN)); 
  }
}

// выводит в Serial время с периодичностью interval
void printTime(unsigned long interval){
  static unsigned long prevTime=0;
  if(millis()-prevTime>interval){
    prevTime=millis();
    
    Serial.print("Current time:");
    Serial.println(millis());

  }
}

Шаблон/Заготовка

Как видите, благодаря тому что мы, с помощью ключевого слово static, "спрятали" объявление prevTime внутрь функции (но при этом сохранили ее способность сохранять значение при выходе из функции подобно глобальной переменной) мы смогли внутри обеих функций (blinkLed и printTime) использовать одно и тоже имя переменной prevTime. Нам теперь не нужно хмурить мозг что-бы каждый раз придумывать новое уникальное имя переменное. Более того, у нас получилась как-бы "универсальная заготовка периодической функции".

// "заготовка/шаблон функции" которая периодически выполняет КАКОЕ-ТО-ДЕЙСТВИЕ
void somePeriodical(unsigned long interval){
  static unsigned long prevTime=0;
  if(millis()-prevTime>interval){
    prevTime=millis();
    
    КАКОЕ-ТО-ДЕЙСТВИЕ;

  }
}

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

А зачем нужен параметр функции?

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

void loop(){
  
  if(digitalRead(PIN_BUTTON)) blinkLed(BLINK_INTERVAL)
                        else blinkLed(BLINK_INTERVAL*2); // в два раза медленее
  .....
}

Тут мы мигаем либо с нормальной, либо с половинной частотой в зависимости от того HIGH или LOW уровень сейчас на пине PIN_BUTTON (нажата или нет кнопка)

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


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

UPD: Оформил этот прием в виде библиотеки. Про это следующая статья:  Мигаем без delay() с комфортом

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

  1. Этот комментарий был удален автором.

    ОтветитьУдалить
  2. а каким образом можно организовать два ШИМ сигнала?

    ОтветитьУдалить
  3. Скетч отлично работает на Nano Версия Ide 1.0.5

    ОтветитьУдалить
  4. Если попытаемся параллельно измерять температуру датчиком DS18B20? Ему на измерение требуется 750 мс. Стало быть быстрее чем раз в 750 мс остальные операции делать не получится?

    ОтветитьУдалить