Arduinoの周期処理を考える

Arduino

Arduinoで周期処理を行うやり方は、タイマー割り込みやFreeRTOSなど、いくつかあります。その中でも以下のようにmillis()を用いて経過時間を監視するのが一番簡単だと思います。

unsigned long previousMillis = 0;  // 前回の時間を記録する変数
const long interval = 10;          // 10msの間隔

void setup() {
  // 初期化処理
  Serial.begin(9600);
}

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

  if (currentMillis - previousMillis >= interval) {
    // 10msが経過した場合の処理
    previousMillis = currentMillis;

    // ここに周期的に行いたい処理を記述
    Serial.println("10ms経過");
  }
}

この方法を拡張してタイミングをずらして複数の処理を行うスケッチを考えてみました。

作るのは、以下のような周期処理のイメージです。

例えば、
・「処理1」でセンサーの値を取得
・「処理2」でモーターの制御値を算出
・「処理3」で算出した制御値を設定
といった処理を実現できます。

センサーの取得のタイミングと制御値の設定のタイミングを毎周期そろえることができるようになります。

考えたスケッチ

#define BIT_RATE 9600
#define PERIODIC_INTERVAL 10 // ms

static unsigned long current_time;
static unsigned long last_time;

// 構造体を使って関数ポインタとタイミングを管理
struct CycleFunc {
  void (*func)();
  unsigned long timeAfterStart;
  bool shouldCall;
};

CycleFunc cycleFuncList[10]; // 最大10個の関数をサポート(必要に応じて増減)
int numCycleFuncs = 0;

void SetCycleFunc(void (*func)(), unsigned long timeAfterStart);
void ResetCycle();
void CycleCall();

void Func1(void);
void Func2(void);
void Func3(void);

void setup()
{
	Serial.begin(BIT_RATE);
	Serial.println("Hello World");

	SetCycleFunc(Func1, 0);
	SetCycleFunc(Func2, 5);
	SetCycleFunc(Func3, 7);

	last_time = millis();
}

void loop()
{
	current_time = millis();


	if(current_time - last_time >= PERIODIC_INTERVAL)
	{
		last_time = current_time;
		ResetCycle();
	}

	CycleCall();
}

void Func1() {
	// Func1の処理
	// Serial.print("Func1 called:");
	// Serial.println(millis());
}

void Func2() {
	// Func2の処理
	// Serial.print("Func2 called:");
	// Serial.println(millis());
}

void Func3() {
	// Func3の処理
	// Serial.print("Func3 called:");
	// Serial.println(millis());
}
 
void SetCycleFunc(void (*func)(), unsigned long timeAfterStart) {
	if (numCycleFuncs < sizeof(cycleFuncList) / sizeof(cycleFuncList[0])) {
		cycleFuncList[numCycleFuncs].func = func;
		cycleFuncList[numCycleFuncs].timeAfterStart = timeAfterStart;
		cycleFuncList[numCycleFuncs].shouldCall = false; // 初期状態では呼び出さない
		numCycleFuncs++;
	}
}

void ResetCycle() {
	for (int i = 0; i < numCycleFuncs; i++)
	{
		if (cycleFuncList[i].shouldCall)
		{
			Serial.print("[error]This process has not been done:num");
			Serial.println(i);
			current_time = millis(); // シリアル出力時間を無視
		}
		
		cycleFuncList[i].shouldCall = true; // タイミングをリセットして次回呼び出せるようにする
	}
}

void CycleCall() {
	for (int i = 0; i < numCycleFuncs; i++) {
		if (cycleFuncList[i].shouldCall && (current_time - last_time >= cycleFuncList[i].timeAfterStart)) {
			cycleFuncList[i].func();
			cycleFuncList[i].shouldCall = false; // 一度呼び出したら次のリセットまで再呼び出ししないようにする
		}
	}
}

解説

このスケッチは、Arduinoで周期的に関数を呼び出すための仕組みを実装しています。以下に各部分の解説をします。

1. 定数と変数の定義

#define BIT_RATE 9600
#define PERIODIC_INTERVAL 10 // ms

static unsigned long current_time;
static unsigned long last_time;
  • BIT_RATEはシリアル通信のボーレートを設定します。
  • PERIODIC_INTERVALは周期的に処理を行う間隔(10ms)を設定します。
  • current_timelast_timeは時間管理のための変数です。

2. 構造体と関数ポインタの定義

struct CycleFunc {
  void (*func)();
  unsigned long timeAfterStart;
  bool shouldCall;
};

CycleFunc cycleFuncList[10]; // 最大10個の関数をサポート(必要に応じて増減)
int numCycleFuncs = 0;
  • CycleFunc構造体は、関数ポインタ、開始後の時間、呼び出しフラグを持ちます。
  • cycleFuncListは最大10個の関数を管理する配列です。
  • numCycleFuncsは登録された関数の数を追跡します。

3. 関数のプロトタイプ宣言

void SetCycleFunc(void (*func)(), unsigned long timeAfterStart);
void ResetCycle();
void CycleCall();

void Func1(void);
void Func2(void);
void Func3(void);
  • これらは後で定義される関数のプロトタイプ宣言です。

4. setup関数

void setup()
{
	Serial.begin(BIT_RATE);
	Serial.println("Hello World");

	SetCycleFunc(Func1, 0);
	SetCycleFunc(Func2, 5);
	SetCycleFunc(Func3, 7);

	last_time = millis();
}
  • シリアル通信を初期化し、”Hello World”を出力します。
  • SetCycleFunc関数を使って、Func1Func2Func3をそれぞれ異なるタイミングで登録します。
  • last_timeを現在の時間に設定します。

5. loop関数

void loop()
{
	current_time = millis();

	if(current_time - last_time >= PERIODIC_INTERVAL)
	{
		last_time = current_time;
		ResetCycle();
	}

	CycleCall();
}
  • current_timeを更新し、10ms経過したかをチェックします。
  • 10ms経過した場合、last_timeを更新し、ResetCycleを呼び出します。
  • CycleCallを呼び出して登録された関数を実行します。

6. 関数の定義

void Func1() {
	// Func1の処理
}

void Func2() {
	// Func2の処理
}

void Func3() {
	// Func3の処理
}
  • これらは実際に呼び出される関数です。現在はコメントアウトされていますが、必要な処理を記述できます。

7. SetCycleFunc関数

void SetCycleFunc(void (*func)(), unsigned long timeAfterStart) {
	if (numCycleFuncs < sizeof(cycleFuncList) / sizeof(cycleFuncList[0])) {
		cycleFuncList[numCycleFuncs].func = func;
		cycleFuncList[numCycleFuncs].timeAfterStart = timeAfterStart;
		cycleFuncList[numCycleFuncs].shouldCall = false; // 初期状態では呼び出さない
		numCycleFuncs++;
	}
}
  • 関数ポインタとタイミングをcycleFuncListに登録します。

8. ResetCycle関数

void ResetCycle() {
	for (int i = 0; i < numCycleFuncs; i++)
	{
		if (cycleFuncList[i].shouldCall)
		{
			Serial.print("[error]This process has not been done:num");
			Serial.println(i);
			current_time = millis(); // シリアル出力時間を無視
		}
		
		cycleFuncList[i].shouldCall = true; // タイミングをリセットして次回呼び出せるようにする
	}
}
  • 各関数の呼び出しフラグをリセットします。未実行の関数がある場合はエラーメッセージを出力します。

9. CycleCall関数

void CycleCall() {
	for (int i = 0; i < numCycleFuncs; i++) {
		if (cycleFuncList[i].shouldCall && (current_time - last_time >= cycleFuncList[i].timeAfterStart)) {
			cycleFuncList[i].func();
			cycleFuncList[i].shouldCall = false; // 一度呼び出したら次のリセットまで再呼び出ししないようにする
		}
	}
}
  • 各関数を適切なタイミングで呼び出し、呼び出しフラグをリセットします。

まとめ

まだ実際に何かを作ったわけではないので、今後、問題は出てくるかもしれませんが、
我ながら、周期的に複数の関数を呼び出すための柔軟なフレームワークができたと思います。

コメント

タイトルとURLをコピーしました