Мультизадачность с Ардуино: несколько процессов одновременно

Этот материал описывает метод работы с Ардуино при котором создается иллюзия, что плата выполняет одновременно несколько задач.

Используя этот метод, Ардуино сможет одновременно делать ряд процессов:

  • переключение светодиода каждые 300 мс (миллисекунды);
  • прокрутка строки текста каждые 250 мс;
  • изменение настраиваемой гистограммы каждые 100 мс.

Вы сможете использовать данный материал как шаблон для своих собственных проектов.

Комплектующие

Для этого проекта требуется очень немного деталей:

  • 1x - микроконтроллер Arduino Uno R3
  • 1x - I2C ЖК-дисплей 16x2
  • 1x - USB-кабель arduino-to-PC
  • 4x - провода для перемычек с разъемами «папа-мама»

Все детали можно заказать в любом интернет-магазине.

Схема подключения

Изображение выше показывает как нам нужно всё соединить.

Код для Ардуино

Код вы найдете ниже. Вы можете скопировать его, либо скачать файл .ino и вставить его содержимое в новый эскиз arduino.

Вам также потребуется установить библиотеку I2C для вашего ЖК-дисплея. Можно посмотреть в наших библиотеках на сайте или использовать библиотеку скачанную с http://arduino-info.wikispaces.com/LCD-Blue-I2C#v1. Если у вас есть другой чип, а не тот, что отмечен на фото выше - вам нужно будет скачать другую библиотеку.

После того как вы найдете подходящую библиотеку, загрузите zip-файл и установите её, используя нашу инструкцию.

// ----- Библиотеки
#include <Wire.h>                 // Требуется для I2C
#include <LiquidCrystal_I2C.h>    // взято с http://arduino-info.wikispaces.com/LCD-Blue-I2C#v1

#define LED 13                    // светодиод платы

// ----- определяем выводы ЖК: addr,en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

// ----- custom bargraph patterns
byte column0[8] = {   // 1 column
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B11111
};
byte column1[8] = {   // 2 columns
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B11111,
  B11111
};
byte column2[8] = {   // 3 columns
  B00000,
  B00000,
  B00000,
  B00000,
  B00000,
  B11111,
  B11111,
  B11111
};
byte column3[8] = {   // 4 columns
  B00000,
  B00000,
  B00000,
  B00000,
  B11111,
  B11111,
  B11111,
  B11111
};
byte column4[8] = {   // 4 columns
  B00000,
  B00000,
  B00000,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};
byte column5[8] = {   // 4 columns
  B00000,
  B00000,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};
byte column6[8] = {   // 4 columns
  B00000,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};
byte column7[8] = {   // 4 columns
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111
};

// ----- message generator
char
MessageArray[] = "               Multi-tasking DEMO:     The arduino is controlling the LED, the text, and the custom bar graph.                ";

int
MessageLength = strlen(MessageArray);

String
Message = MessageArray,
Display;

// ----- task scheduler (Interrupt Service Routine)
int
TaskTimer1 = 0,
TaskTimer2 = 0,
TaskTimer3 = 0;

bool
TaskFlag1 = false,
TaskFlag2 = false,
TaskFlag3 = false;

/******************************
  setup()
*******************************/
void setup() {

  // ----- configure serial port (optional)
  Serial.begin(9600);               //serial port

  // ----- configure LCD display
  lcd.begin(16, 2);                 //16 char x 2 line LCD display

  // ----- install custom characters
  lcd.createChar(0, column0);
  lcd.createChar(1, column1);
  lcd.createChar(2, column2);
  lcd.createChar(3, column3);
  lcd.createChar(4, column4);
  lcd.createChar(5, column5);
  lcd.createChar(6, column6);
  lcd.createChar(7, column7);

  // ----- configure LED
  pinMode(LED, OUTPUT);
  digitalWrite(LED, LOW);

  // ----- configure Timer 2 to generate a compare-match interrupt every 1mS
  noInterrupts();            // disable interrupts
  TCCR2A = 0;                // clear control registers
  TCCR2B = 0;
  TCCR2B |= (1 << CS22) |    // 16MHz/128=8uS
            (1 << CS20) ;
  TCNT2 = 0;                 // clear counter
  OCR2A = 125 - 1;           // 8uS*125=1mS (allow for clock propagation)
  TIMSK2 |= (1 << OCIE2A);   // enable output compare interrupt
  interrupts();              // enable interrupts
}

// ------------------------------
// loop()
// ------------------------------
void loop() {
  /*
     Tasks are only performed if the TaskFlag
     has been set by the task-scheduler.
  */

  // ----- toggle LED
  if (TaskFlag1) {
    TaskFlag1 = false;
    digitalWrite(LED, !digitalRead(LED));
  }

  // ----- scroll message
  if (TaskFlag2) {
    TaskFlag2 = false;
    scrollMessage();
  }

  // ----- bargraph
  if (TaskFlag3) {
    TaskFlag3 = false;
    bargraph();
  }

//  // ----- title for Instructable
//  lcd.setCursor(0,0);
//  lcd.print("How to multitask");
//  lcd.setCursor(0,1);
//  lcd.print("  your ARDUINO  ");
}


// -------------------------------
// task scheduler (1mS interrupt)
// -------------------------------
ISR(TIMER2_COMPA_vect)
{
  // ----- timers
  /*
     Each timer counts the number of milliseconds since its associated task was
     last performed. Each additional task requires another timer and flag.
  */
  TaskTimer1++;
  TaskTimer2++;
  TaskTimer3++;

  // ----- task1
  if (TaskTimer1 > 300 - 1) { // toggle LED (300mS)
    TaskTimer1 = 0;
    TaskFlag1 = true;
  }

  // ----- task2
  if (TaskTimer2 > 250 - 1) {  // scroll message (250mS)
    TaskTimer2 = 0;
    TaskFlag2 = true;
  }

  // ----- task3
  if (TaskTimer3 > 100 - 1) {  // bargraph (100mS)
    TaskTimer3 = 0;
    TaskFlag3 = true;
  }
}

// ----------------------------
// displayMessage()
// ----------------------------
void scrollMessage() {
  // ----- locals
  /*
     static variables remember the last value
     when the subroutine is re-entered.
  */
  static int index = 0;

  // ----- grab the next 16 characters from the message
  lcd.setCursor(0, 0);
  Display = Message.substring(index, index + 16);
  lcd.print(Display);
  index++;
  if (index > MessageLength - 15) index = 0;
}

// -------------------------------
// bargraph()
// -------------------------------
void bargraph() {
  // ----- locals
  static int column = 0;

  // ----- reset display
  if (column == 0) {
    lcd.setCursor(0, 1);
    lcd.print("Volume:         ");
  }

  // ----- display next symbol
  lcd.setCursor(8 + column, 1);
  lcd.write(byte(0 + column));
  column++;
  if (column > 7) column = 0;
}

Теория

Пустой цикл loop(){} в коде arduino заставит ардуино «вращаться» (прим.ред. - цикличность) несколько тысяч раз в секунду. Давайте посмотрим, что произойдет, когда мы добавим эту задачу

Одна задача.

Следующий код будет отображать «Hello World!» на вашем последовательном мониторе один раз в секунду:

Serial.println(“Hello World!”);
delay(1000);

Каждому циклу loop(){} теперь нужно более 1 секунды для завершения из-за команды задержки delay(1000).

Предполагая, что arduino имеет 16-мегагерцовый кристалл, команда delay(1000) тратит 16 000 000 тактов. Другие задачи возможны, если мы устраним эту задержку.

Многозадачность.

Давайте перепишем вышеприведенный фрагмент кода:

if (Flag1)
{
  Serial.println(“Hello World!”);
  Flag1=false;
}

"Привет мир!" выведется после чего цикл loop(){} возвращается к цикличности с полной скоростью до тех пор, пока флажок 1 = истина.

Поскольку loop(){} нечего делать, а процесс работает на полной скорости, добавим еще одну задачу.

if (Flag2)
{
  digitalWrite(LED, !digitalRead(LED));
  Flag2=false;
}

Эта часть кода заставляет светодиод «переключаться», после чего loop(){} снова возвращается в процесс, пока флаг Flag2 = true (флажок 2 = ложь).

Не обращая внимания на то, как устанавливаются флажки, мы теперь можем выполнять несколько задач в одно и тоже время.

Длительные задачи требуют специальных методов кода.

Многозадачность, скажем так, "коротких" задач, таких как мигание светодиода, довольно простое дело. Но что делать с многозадачностью при "длинных" задачах, такие как прокрутка текстового сообщения например? Это требуют другого подхода.

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

Это достигается за счет использования «статических» переменных* для отслеживания того, как далеко продвинулась задача. В данном случае это показывают функции «scrollMessage ()» и «bargraph()».


* Когда подпрограмма вызывается, все «переменные» забывают свои предыдущие значения, если они не были объявлены «статическими», и в этом случае предыдущее значение доступно при вызове следующей подпрограммы.


Планировщик заданий

Секрет установки флагов - создать цикл в 1 миллисекунду (мс) с использованием одного из таймеров Arduino Uno R3. Можно использовать Timer/Counter 2 (8 бит), который оставляет Timer/Counter 1 (16-бит) свободным для других задач.

1 мс достигается путем деления частоты 16 000 000 Гц на 128 для получения интервала времени 8 микросекунд. Если теперь считать эти импульсы 8 мкс и генерировать прерывание, когда счетчик достигнет 125, то 125 x 8 или 1 мс истечет**.


** Фактически мы загружаем 125-1 = 124 в «сравнительный регистр соответствия», потому что прерывание не происходит до следующего (125-го) тактового импульса.


Флаги устанавливаются путем помещения следующего кода в ISR (процедуру обслуживания прерываний):

// ----- count the number of milliseconds that have elapsed
Counter1++;
Counter2++;

// ----- print “Hello World!”
if (Counter1 > 1000 - 1)    //1 second
{
  Counter1 = 0;
  Flag1 = true;             //the loop(){} sees this flag and prints the message
}

// ----- toggle LED
if (Counter2 > 300 - 1)     //300 milliseconds
{
  Counter2 = 0;
  Flag2 = true;             //the loop(){} sees this flag and toggles the LED
}

Как только счетчик достигнет своего целевого значения, счетчик очищается и устанавливается флаг. Вышеприведенный код занимает очень мало времени для выполнения, так как команд очень мало.

Основной цикл loop(){} видит каждый флаг, который задает планировщик задач и выполняет задачу. Добавление дополнительных задач достаточно просто. Создайте дополнительный счетчик и флаг, а затем имитируйте приведенный выше код. На графике выше показана взаимосвязь между тремя задачами, которые выполняет ардуино в данном примере.

Вы можете заметить, что:

  • иногда loop(){} не имеет задач для выполнения;
  • иногда loop(){} выполняет только одну или две задачи;
  • каждая из задач вызывается разное количество раз;
  • гистограмма продвигается каждые 100 мс;
  • текст прокручивается каждые 250 мс;
  • светодиод переключается каждые 300 мс.

Ключевые моменты

Для успешной многозадачности:

  • Избегайте функций delay() или delayMicroseconds(). Вместо этого
  • создавайте циклы и
  • используйте планировщик задач,
  • а длительные задачи следует рассматривать как серию небольших «кусков»
  • со «статическими» переменными, отслеживающими прогресс.

На этом пока всё про мультизадачность в Ардуино. Хороших вам проектов.

Ардуино+