Arduinoのマルチタスクについて


 Arduinoのマルチタスクを学習してみました

Arduinoのマルチタスクについて



 Arduinoは、オペレーティングシステムを持たない単純なプロセッサーであり、一度に1つのプログラムだけしか 実行(逐次処理)できません。
つまり複数のタスク(並列処理)を管理することはできません。
 そこで、ビル・アールさんの説明 multi-tasking-the-arduinoを参考にさせていただき、Arduino のマルチタスクについて忘備録としてまとめてみました。



  delay()について


 タイミングを制御するためにdelay()をよく使いますが、機能を追加したいときには問題が発生します。
問題は、delay()がプロセッサーを独占する(ビジー状態)であることです。
 delay()をコールの間は、入力に応答することはできないし、データを処理することも、出力を変更することもできません。 コードの一部が delay() を使用している場合、その他の部分はすべて停止状態です。


(1)基本的なLEDの点滅プログラム
 LEDを1秒間オンにして、続けて1秒間オフにする。それを繰返すスケッチは次のようになります。

int led = 9;

void setup() {
  pinMode(led, OUTPUT);
}

void loop() {
  digitalWrite(led, HIGH);
  delay(1000);
  digitalWrite(led, LOW);
  delay(1000);
}
						

 このスケッチだと、delay () 関数がほぼすべての時間を費やすので、プロセッサーはLEDが点滅している間は 他の操作を行うことはできません。


(2)サーボモーターについても同様のことが言えます。
 サーボモーターのスイープ速度を制御するためにdelay()関数を使用します。 次のスケッチは、LEDの点滅とサーボモーターとを組み合わせたプログラムです。

#include <Servo.h>
int led = 13;
Servo myservo;
int pos = 0;

void setup() { 
  pinMode(led, OUTPUT);
  myservo.attach(9);
}

void loop() {
  digitalWrite(led, HIGH);
  delay(1000);
  digitalWrite(led, LOW);
  delay(1000);

  for(pos = 0; pos <= 180; pos += 1) {
    myservo.write(pos);
    delay(15);
  }
  for(pos = 180; pos>=0; pos-=1){
    myservo.write(pos);
    delay(15);
  }
}
						

 この場合、LEDの点滅とサーボのスイープが交互に行われます。 しかし、それは同時に両方を行うことはできません。 delay() 関数を使わずにタイミングを制御するにはどうすればよいでしょうか。




  delay () 関数を使わないで タイミングを制御するためにmillis()を使う


 タイミングを実装するための簡単なテクニックの 1 つは、スケジュールを作成し、クロックに目を光らせる方法です。 普段使用しているdelay()の代わりに、時計を定期的にチェックするだけで、いつ行動する時が来たのかがわかります。

(1)ここでの例は、IDE に付属する BlinkWithoutDelay のスケッチ例です。
  ボード上にあるLED(ピン 13 に接続されている)を点滅します。

						
 /* Blink without Delay*/

const int ledPin =  13;
int ledState = LOW;
long previousMillis = 0;
long interval = 1000;

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);
  }
}
						

(2) 一見すると、BlinkWithoutDelayは、delay()を使ってLEDを点滅させるより複雑な方法のように見えますが、
 特別なスケッチではないようです。 ただし、BinkWithoutDelay は、ステートマシンと呼ばれる非常に重要な概念を示しています。
 点滅をdelay()時間に頼る代わりに。 BlinkWithoutDelayは、LEDの現在の状態とそれが最後に変更された時間を記憶します。 ループを通過するたびに、millis()クロックを調べて、LED の状態を再度変更する時間があるかどうかを確認します。

(3)フラッシュ・ノーレイについて
 オンタイムとオフタイムが異なる、もう少し興味深い点滅方法を見てみましょう。
 LEDを点灯する時間を250ミリ秒、消灯する時間を750ミリ秒とします。

int ledPin =  13;
int ledState = LOW;
unsigned long previousMillis = 0;
long OnTime = 250;
long OffTime = 750;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop(){
  unsigned long currentMillis = millis();

  if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
    ledState = LOW;
    previousMillis = currentMillis;
    digitalWrite(ledPin, ledState);
  }
  else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
    ledState = HIGH;
    previousMillis = currentMillis;
    digitalWrite(ledPin, ledState);
  }
}
						

 これを「フラッシュ・ノーレイ」と呼びます。
 LEDがONかOFFかを追跡する変数があることに注意してください。 最後の変更がいつ起こったかを追跡する変数は、 このステートマシンの状態部分です。
 また、状態を調べ、いつどのように変更する必要があるかを決定するコードもあります。 それがマシン部分です。 ループを通るたびに「マシンを実行」し、マシンは状態を更新します。




  実際にマルチタスクを行ってみる


 次に、まったく異なる速度で点滅する2つのLEDを作ってみます。
  ・LED1は、点灯が250ミリ秒、消灯が750ミリ秒で点滅。
  ・LED2は、点灯が330ミリ秒、消灯が400ミリ秒で点滅。
 2つ目の LED 用に別のステートマシンを作成します。 2つの別々のステートマシンを使用すると、2 つの LED を互いに完全に独立して点滅できます。
 delay() 関数を使用すると驚くほど複雑なものになります。

(1)配線図は次の図になります。



(2)スケッチプログラム は次のようになります。

int ledPin1 =  12;
int ledState1 = LOW;
unsigned long previousMillis1 = 0; long OnTime1 = 250; long OffTime1 = 750; int ledPin2 = 13; int ledState2 = LOW; unsigned long previousMillis2 = 0; long OnTime2 = 330; long OffTime2 = 400; void setup() { pinMode(ledPin1, OUTPUT); pinMode(ledPin2, OUTPUT); } void loop(){ unsigned long currentMillis = millis(); if((ledState1 == HIGH) && (currentMillis - previousMillis1 >= OnTime1)) { ledState1 = LOW; previousMillis1 = currentMillis; digitalWrite(ledPin1, ledState1); } else if ((ledState1 == LOW) && (currentMillis - previousMillis1 >= OffTime1)) { ledState1 = HIGH; previousMillis1 = currentMillis; digitalWrite(ledPin1, ledState1); } if((ledState2 == HIGH) && (currentMillis - previousMillis2 >= OnTime2)) { ledState2 = LOW; previousMillis2 = currentMillis; digitalWrite(ledPin2, ledState2); } else if ((ledState2 == LOW) && (currentMillis - previousMillis2 >= OffTime2)) { ledState2 = HIGH; previousMillis2 = currentMillis; digitalWrite(ledPin2, ledState2); } }

 GPIOピンが不足するまで、ステート マシンを追加できます。 各ステートマシンは、独自のフラッシュレートを 持つことができます。
 しかし、同じコードを何度も何度も書くのはむしろ無駄なようです。 これを行うには、 より効率的な方法が必要です!

 この複雑さを管理するより良い方法があります。 より簡単で効率的なプログラミング手法があります。
次に、Arduino プログラミング言語のより高度な機能の一部を紹介します。

 最後のスケッチをもう一度見てみましょう。 ご覧の通り、それは非常に反復的です。
同じコードは、点滅する LED ごとにほぼ逐語的に複製されます。 変更されるのは、可変名だけです。
このコードは、小さなオブジェクト指向プログラミング (OOP) の主要な候補です。




  オブジェクト指向 プログラミング (OOP)を使用する


 Arduino言語は、オブジェクト指向プログラミングをサポートする C++ のバリエーションです。
言語の OOP 機能を使用して、点滅する LED を C++ クラスに点滅させる状態変数と機能をすべてまとめることができます。 クラスとして再パッケージ化するだけです。

(1)クラスの定義
 まず、"Flasher" クラスを宣言します。
次に、FlashWithoutDelay からすべての変数を追加します。 これらはクラスの一部であるため、メンバ変数と呼ばれます。

						
class Flasher {      // クラス メンバ変数。これらは起動時に初期化されます。
  int ledPin;
  long OnTime;
  long OffTime;

  int ledState;      // LED を設定するために使用される ledState
  unsigned long previousMillis;      // LED が最後に更新された時刻を保存します
};
						

(2)コンストラクタを追加
 コンストラクタはクラスと同じ名前を持ち、そのジョブはすべての変数を初期化します。

class Flasher {
  int ledPin;
  long OnTime;
  long OffTime;

  int ledState;
  unsigned long previousMillis;
  // コンストラクター - フラッシャーを作成します。
  // メンバー変数と状態を初期化します。
  public:
  Flasher(int pin, long on, long off) {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);

    OnTime = on;
    OffTime = off;

    ledState = LOW;
    previousMillis = 0;
    }
};
						

(3)Update()" というメンバー関数に変換
 最後に、ループを取り、"Update()" というメンバー関数に変換します。 これは、元の void loop() と 同じことに注意してください。名前だけが変更されました。

class Flasher {
  int ledPin;
  long OnTime;
  long OffTime;

  int ledState;
  unsigned long previousMillis;

  public:
  Flasher(int pin, long on, long off) {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);

    OnTime = on;
    OffTime = off;

    ledState = LOW;
    previousMillis = 0;
  }

  void Update() {
    // LEDの状態を変更する時間があるかどうかを確認する
    unsigned long currentMillis = millis();

    if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
      ledState = LOW;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
      ledState = HIGH;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
  }
};
						

 既存のコードを Flasher クラスに再配置するだけで、すべての変数 (ステート) と LED を点滅させる機能 (マシン) を すべてカプセル化できました。

(4)Flasher クラスのインスタンスを作成
 ここで、フラッシュするすべての LED について、コンストラクタを呼び出して
Flasher クラスのインスタンスを作成します。
 そして、ループを通過するたびに、FlasherのインスタンスごとにUpdate()を呼び出す必要があります。

 ステートマシン コード全体を再現する必要はもうありません。 Flasher クラスの別のインスタンスを求めるだけです。

class Flasher {
  int ledPin;
  long OnTime;
  long OffTime;

  int ledState;
  unsigned long previousMillis;

  public:
  Flasher(int pin, long on, long off) {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);

    OnTime = on;
    OffTime = off;

    ledState = LOW;
    previousMillis = 0;
  }

  void Update() {
    unsigned long currentMillis = millis();

    if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
      ledState = LOW;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
      ledState = HIGH;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
  }
};

Flasher led1(12, 250, 750);
Flasher led2(13, 330, 400);

void setup() {
}

void loop() {
  led1.Update();
  led2.Update();
}
						

 各追加のLEDは、コードのちょうど2行を必要とします!
また、重複したコードがないため、コンパイルも小さくなります!

(5)次のビデは、上記スケッチを実行した場合です。




  他の場合に、オブジェクト指向 プログラミング (OOP)を使用してみます


次に示すような、2つのサーボと3つのLEDを動かしてみましょう。
 ・LED1は、点灯が123ミリ秒、消灯が400ミリ秒で点滅。
 ・LED2は、点灯が350ミリ秒、消灯が350ミリ秒で点滅。
 ・LED3は、点灯が200ミリ秒、消灯が222ミリ秒で点滅。
 ・2つのサーボを独自のレートでスイープさせます。



(1)サーボのためのSweeper クラスを作成
 Sweeper クラスはスイープアクションをカプセル化しますが、Flasher クラスが LED に対して行うのと同様に、
タイミングに millis() 関数を使用します。
 また、サーボを特定のピンに関連付けるために Attach() 関数と Detach() 関数を追加する必要があります。

class Sweeper {
  Servo servo;
  int pos;
  int increment;
  int  updateInterval;
  unsigned long lastUpdate;

  public:
  Sweeper(int interval) {
    updateInterval = interval;
    increment = 1;
  }

  void Attach(int pin) {
    servo.attach(pin);
  }

  void Detach() {
    servo.detach();
  }

  void Update() {
    if((millis() - lastUpdate) > updateInterval) {
      lastUpdate = millis();
      pos += increment;
      servo.write(pos);
      Serial.println(pos);

      if ((pos >= 180) || (pos <= 0)) {
        increment = -increment;
      }
    }
  }
};

						

(2)実際のコード
 必要な数のフラッシャー(LED)とスイーパー(サーボ)をインスタンス化することができます。
 Flasher の各インスタンスには2行のコードが必要です。また、スイーパーの各インスタンスには 3行のコードが必要です。

#include <Servo.h>

class Flasher {
  int ledPin;
  long OnTime;
  long OffTime;

  int ledState;
  unsigned long previousMillis;

  public:
  Flasher(int pin, long on, long off) {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);

    OnTime = on;
    OffTime = off;

    ledState = LOW;
    previousMillis = 0;
  }

  void Update() {
    unsigned long currentMillis = millis();

    if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
      ledState = LOW;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
      ledState = HIGH;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
  }
};

class Sweeper {
  Servo servo;
  int pos;
  int increment;
  int  updateInterval;
  unsigned long lastUpdate;

  public:
  Sweeper(int interval) {
    updateInterval = interval;
    increment = 1;
  }

  void Attach(int pin) {
    servo.attach(pin);
  }

  void Detach() {
    servo.detach();
  }

  void Update() {
    if((millis() - lastUpdate) > updateInterval) {
      lastUpdate = millis();
      pos += increment;
      servo.write(pos);
      Serial.println(pos);

      if ((pos >= 180) || (pos <= 0)) {
        increment = -increment;
      }
    }
  }
};

Flasher led1(11, 123, 400);
Flasher led2(12, 350, 350);
Flasher led3(13, 200, 222);

Sweeper sweeper1(15);
Sweeper sweeper2(25);

void setup() {
  Serial.begin(9600);
  sweeper1.Attach(9);
  sweeper2.Attach(10);
}

void loop() {
  sweeper1.Update();
  sweeper2.Update();

  led1.Update()
  led2.Update();
  led3.Update();
}

						

 これで、干渉なくノンストップで実行される 5つの独立したタスクがあります。 そして、void loop()は、コードでわずか5行です!

(3)次のビデは、上記スケッチを実行した場合です。




  ボタンスイッチを追加してみよう!


 delay()ベースのタイミングのもう 1 つの問題は、ボタンスイッチを押すなどのユーザー入力が無視される傾向がある点です。 それはプロセッサが遅延状態にあるときにボタンスイッチの状態を確認できないためです。
 millis()ベースのタイミングでは、プロセッサは定期的にボタンスイッチの状態や他の入力をチェックして自由です。 これにより、一度に多くのことを行うが、応答性を維持する複雑なプログラムを構築することができます。

(1)次に示すように、回路にボタンを追加します。
 3つのLEDは、独自の間隔で点滅し、 2つのサーボは各自のレートでスイープします。
そしてボタンを押すと、sweeper2とled1はボタンを離すまでトラックで停止するようにします。



(2)以下のコードは、ループの各パスのボタンの状態を確認します。
 Led1 と sweeper2 は、ボタンを押すと更新されませんので停止状態です。


#include <Servo.h>
	 
class Flasher {
  int ledPin;
  long OnTime;
  long OffTime;
  int ledState;
  unsigned long previousMillis;

  public:
  Flasher(int pin, long on, long off) {
    ledPin = pin;
    pinMode(ledPin, OUTPUT);

    OnTime = on;
    OffTime = off;
    ledState = LOW;
    previousMillis = 0;
  }

  void Update() {
    unsigned long currentMillis = millis();

    if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) {
      ledState = LOW;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
    else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) {
      ledState = HIGH;
      previousMillis = currentMillis;
      digitalWrite(ledPin, ledState);
    }
  }
};

class Sweeper {
  Servo servo;
  int pos;
  int increment;
  int  updateInterval;
  unsigned long lastUpdate;

  public:
  Sweeper(int interval) {
    updateInterval = interval;
    increment = 1;
  }

  void Attach(int pin) {
    servo.attach(pin);
  }

  void Detach() {
    servo.detach();
  }

  void Update() {
    if((millis() - lastUpdate) > updateInterval) {
      lastUpdate = millis();
      pos += increment;
      servo.write(pos);
      Serial.println(pos);
      if ((pos >= 180) || (pos <= 0)) {
        increment = -increment;
      }
    }
  }
};

Flasher led1(11, 123, 400);
Flasher led2(12, 350, 350);
Flasher led3(13, 200, 222);

Sweeper sweeper1(15);
Sweeper sweeper2(25);

void setup() {
  Serial.begin(9600);
  sweeper1.Attach(9);
  sweeper2.Attach(10);
}

void loop() {
  sweeper1.Update();
  if(digitalRead(2) == HIGH) {
    sweeper2.Update();
    led1.Update();
  }

  led2.Update();
  led3.Update();
}
						

 これにより、一度に多くのことを行いますが、応答性を維持する複雑なプログラムを構築することができます。
ループに遅延delay()がないため、ボタン入力はほぼ瞬時に応答します。 これで、5つのタスクを独立して実行できます。

(3)次のビデは、上記スケッチを実行した場合です。