Arduinoで疑似マルチタスキング - ezMTSの紹介 -

#更新情報
2021/10/17
  ・タスク周期の単位をミリ秒/マイクロ秒から選択できるようになりました。
  ・タイマーモードの設定バグを修正(割込み間隔が不正でした。)
#はじめに
Arduinoでプログラミングをしていてloop()が複数あってマルチタスク・マルチスレッドのように「同時に」動いていたらいいなと考えたことはありませんか？ここで紹介するezMTS(easy Mutli-tasking System)ライブラリはこのような要望に応えるために(ほぼ自分のために)作成しました。厳密にはコンテキストの保持等を行わないなど、マルチタスク・マルチスレッドとは異なるものですが、loop()が複数あって(別周期で)それぞれぐるぐると回っている状態は実現できます。例えば30ms周期でスイッチ状態を読みつつ16ms周期でLCDを描画するといったような処理が実現しやすくなるかと思います。本記事ではこの個別に動く関数を便宜的に「タスク(task)」と表現します。

#ezMTSの使い方
以下のサンプルコードで説明します。3つのタスクを生成してそれぞれ異なる周期でLEDを点滅させることとします。Arduino nano互換回路で実動作を確認済みです。

```cpp:example.ino
#include <Arduino.h>
// 必要に応じ本機能が占有するタイマーモジュールを指定してください(オプション)。
// 無指定時はTimer2が使用されます。
//#define EZMTS_USE_TIMER (0)
// 本機能を試すには以下のヘッダをインクルードしてください。
#include <ezMTS.hpp>
// 必要なタスク数nを「ezMTS xxx(n);」の形式で指定します。
// タスク実行周期の単位をマイクロ秒に指定するには第２引数に以下を指定します。
// ezMTS task(8, EZMTS_MICROSEC);
ezMTS task(8);
// タスクIDを保持する変数をタスクごとに用意します。変数名は任意です。
int taskIdA;
int taskIdB;
int taskIdC;

// 以下のようにタスクを定義します。タスク名(関数名)は任意ですが、戻り値・引数は必須です。
// タスクAの定義：
int led1(void *dummy) {
    // 以下にタスク内で行いたい処理を記述してください。
    digitalWrite(A3, !digitalRead(A3));
    // 処理の最後は以下のようにしてください。
    return 0;
}
// タスクBの定義：
int led2(void *dummy) {
    digitalWrite(A4, !digitalRead(A4));
    return 0;
}
// タスクCの定義：
int led3(void *dummy) {
    digitalWrite(A5, !digitalRead(A5));
    return 0;
}
void setup() {
  // 単にLEDを点灯させるためのピン設定です。
  pinMode(A3, OUTPUT);
  pinMode(A4, OUTPUT);
  pinMode(A5, OUTPUT);

  // タスクを生成します。
  taskIdA = task.create(led1);
  taskIdB = task.create(led2);
  taskIdC = task.create(led3);

  // タスクを始動します。
  // 引数1：始動するタスクのタスクID
  // 引数2：タスクを動かす周期(ミリ秒周期で指定)
  // 引数3：タスクをすぐに始動するか、指定ミリ秒後に初めて始動するかの指定
  //       すぐ始動：EZMTS_AT_ONCE
  //       指定ミリ秒後に初めて始動：EZMTS_TIMEDOUT(デフォルト)
  task.start(taskIdA, 3000, EZMTS_AT_ONCE);
  task.start(taskIdB, 2000, EZMTS_AT_ONCE);
  task.start(taskIdC, 1000, EZMTS_AT_ONCE);
}
void loop() {
  delay(1);
}
// ezMTSのその他の機能。
// - xxx.stop(task_id)
//   タスクを一時的に停止します。xxx.start(task_id, when_exec)でタスクの再始動が可能です。
// - xxx.del(task_id)
//   タスクを恒久的に削除します。
```

#ezMTSのインストール
GitHubの[yeisapporo/ezMTS](https://github.com/yeisapporo/ezMTS)リポジトリからezMTS.zipをダウンロードし、C:\Users\user\Documents\Arduino\libraries\(Windowsの場合)などご自身のArduino IDEライブラリフォルダにフォルダ名ごと解凍して格納してください。
PlatformIOの場合は、適当にインクルードしてください。

#ezMTSのソースコード
上記GitHubリポジトリのソースが最新になりますがここにも掲載します。

```cpp:ezMTS.hpp
/*
ezMTS(Easy Multitasking System) is a library for Arduino with ATMEGA 
microcontrollers. This provides simple multitasking-like system to your 
Arduino. You can use multiple loop()-like functions on your Arduino 
sketch. This is not an operating system but an extension of an interrupt 
handler for Timer2. You should not use tone() in your sketch when using 
this library.

Copyright (c) 2021 Kazuteru Yamada(yeisapporo).  All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef _HPP_EZMTS_HPP_
#define _HPP_EZMTS_HPP_

#include <Arduino.h>

// specify the timer module that ezMTS occupies. (N) : TimerN(0, 1 or 2)
// when using timer0 all Arduino time-related functions(such as delay())
// may not work correctly. when using timer2 tone() must not be used. 
#ifndef EZMTS_USE_TIMER
//#define EZMTS_USE_TIMER (0)
//#define EZMTS_USE_TIMER (1)
#define EZMTS_USE_TIMER (2) // default
#endif

// specify the unit of timer ivterval value.
#define EZMTS_MILLISEC  (0) // default
#define EZMTS_MICROSEC  (1)

#define EZMTS_TASK_UNUSED   (0)
#define EZMTS_TASK_STOPPED  (1)
#define EZMTS_TASK_RUNNING  (2)

// specify when FIRST executes the callback function registered by .start().
#define EZMTS_TIMEDOUT  (0) // default
#define EZMTS_AT_ONCE   (1)

// task handling information
class taskInfo {
    public:
    int _task_id;
    int _task_state;
    long _timeout_val;
    long _time_rest;
    int (*_cb_func)(void *);
};

int g_task_num = 0;
taskInfo *g_taskInfo = NULL;

class ezMTS {
    private:
    unsigned char _ocrna = 249; // OCRnA for millisec.
    float _k = 1.0;             // coefficient for millisec.

    public:
    ezMTS(int task_num, int unit = EZMTS_MILLISEC) {
        noInterrupts();
        // keep the number of tasks available.
        g_task_num = task_num;
        // prepare OCRnA and the coefficient according to the unit specified.
        if(unit != EZMTS_MILLISEC) {
            // for microsec.
            _ocrna = 9;
            _k = 0.02538071066;
        }
        // get task management area.
        g_taskInfo = new taskInfo[task_num];
        // initialize task management variables.
        for(int i = 0; i < g_task_num; i++) {
            g_taskInfo[i]._task_id = -1;
            g_taskInfo[i]._task_state = EZMTS_TASK_UNUSED;
            g_taskInfo[i]._timeout_val = 0;
            g_taskInfo[i]._time_rest = 0;
            g_taskInfo[i]._cb_func = NULL;
        }
        // settings for ATMEGA microcontroller timer interruption.
// specify the timer module that ezMTS uses.
#if (EZMTS_USE_TIMER == 0)
        // ezMTS occupies Timer0.
        TCCR0A = TCCR0B = 0;
        // set mode to CTC.
        TCCR0A |= (1 << WGM01);
        // set top to OCRA0 and prescaler to clk/64.
        TCCR0B |= (1 << WGM02) | (1 << CS01) | (1 << CS00);
        // set top value.
        OCR0A = _ocrna;
        TIMSK0 |= (1 << OCIE0A);
#elif (EZMTS_USE_TIMER == 1)
        // ezMTS occupies Timer1.
        TCCR1A = TCCR1B = 0;
        // set mode to CTC.
        //TCCR1A |= (0 << WGM10) | (0 << WGM11);
        // set top to OCR1A and prescaler to clk/64
        TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);
        // set top value.
        OCR1A = _ocrna;
        TIMSK1 |= (1 << OCIE1A);

#elif (EZMTS_USE_TIMER == 2)
        // ezMTS occupies Timer2.
        TCCR2A = TCCR2B = 0;
        // set mode to CTC.
        TCCR2A |= (1 << WGM21);
        // set prescaler to clk/64.
        TCCR2B |= (1 << CS22);
        // set to value.
        OCR2A = _ocrna;
        TIMSK2 |= (1 << OCIE2A);
#endif
        interrupts();
    }
    int create(int (*cb_func)(void *)) {
        int ret = -1;
        noInterrupts();
        // search unused task info area
        for(int i = 0; i < g_task_num; i++) {
            if(g_taskInfo[i]._task_state == EZMTS_TASK_UNUSED) {
                g_taskInfo[i]._task_id = i;
                g_taskInfo[i]._task_state = EZMTS_TASK_STOPPED;
                g_taskInfo[i]._timeout_val = 0;
                g_taskInfo[i]._time_rest = 0;
                g_taskInfo[i]._cb_func = cb_func;
                ret = i;
                break;
            }
        }
        interrupts();
        return ret;
    }
    int start(int task_id, long timeout_val, int when_exec = EZMTS_TIMEDOUT) {
        int ret = -1;
        noInterrupts();
        if((task_id < 0) || (task_id > g_task_num - 1) || timeout_val < 0) {
            interrupts();
            return ret;
        }
        g_taskInfo[task_id]._task_id = task_id;
        g_taskInfo[task_id]._task_state = EZMTS_TASK_RUNNING;
        g_taskInfo[task_id]._timeout_val = timeout_val * _k;
        g_taskInfo[task_id]._time_rest = g_taskInfo[task_id]._timeout_val;
        ret = 0;
        interrupts();
        if(when_exec == EZMTS_AT_ONCE) {
            g_taskInfo[task_id]._cb_func(NULL);
        }

        return ret;
    }
    int stop(int task_id) {
        noInterrupts();
        g_taskInfo[task_id]._task_state = EZMTS_TASK_STOPPED;
        g_taskInfo[task_id]._time_rest = 0;
        g_taskInfo[task_id]._timeout_val = 0;
        interrupts();
        return 0;
    }
    int del(int task_id) {
        noInterrupts();
        g_taskInfo[task_id]._task_id = -1;
        g_taskInfo[task_id]._task_state = EZMTS_TASK_UNUSED;
        g_taskInfo[task_id]._time_rest = 0;
        g_taskInfo[task_id]._timeout_val = 0;
        g_taskInfo[task_id]._cb_func = NULL;
        interrupts();
        return 0;
    }
    int handle() {
        return 0;
    }
};

// timer interruption handler.
#if (EZMTS_USE_TIMER == 0)
ISR (TIMER0_COMPA_vect) {
    TCNT0 = 0;
#elif (EZMTS_USE_TIMER == 1)
ISR (TIMER1_COMPA_vect) {
    TCNT1 = 0;
#elif (EZMTS_USE_TIMER == 2)
ISR (TIMER2_COMPA_vect) {
    TCNT2 = 0;
#endif
    noInterrupts();
    //fastDigitalWrite(10, !digitalRead(10));
    for(int i = 0; i < g_task_num; i++) {
        if(g_taskInfo[i]._task_state == EZMTS_TASK_RUNNING) {
            if(--g_taskInfo[i]._time_rest == 0) {
                //_taskInfo[i]._task_state = EZMTS_TASK_STOPPED;
                g_taskInfo[i]._time_rest = g_taskInfo[i]._timeout_val;
                interrupts();
                g_taskInfo[i]._cb_func(NULL);
                noInterrupts();
            }
        }
    }
    interrupts();
}

#endif  /* _HPP_EZMTS_HPP_ */

```
![DSC_1381.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/a8e25c54-3fd4-6975-2d39-700fe6f952f8.jpeg)
![DSC_1380.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/84de1566-9059-d022-7568-98f27ba1c5f6.jpeg)
![DSC_1379.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/21e8389a-5477-57d5-4c8e-08035bdc696f.jpeg)

#この記事を投稿直後、Arduino nanoでFreeRTOSが走ることを知りました。撃沈↓↓
[Using FreeRTOS with Arduino Nano](https://forum.arduino.cc/t/using-freertos-with-arduino-nano/512744)