たとえば10ms周期で処理を制御したいけど、割り込み処理が使えない時
##やること
ロボットの制御などでミリ秒単位で処理を進めたい時があります。
割り込み処理を使うのも手ですが、I2Cと割り込み処理の同居はそのままではできません。
またdelay()を使うと、そこで処理が止まってしまいこれもうまくいきません。
そこで、millis()で取得したArduinoの内部時計時間を利用し、フレーム単位で処理が進むようにしていきます。
##概念
たとえば1フレームを10ms(ミリ秒)と定め、フレーム単位で進んでいく時計(sframe)があると仮想します。
計算処理が1フレームの中に収まるようにしたいわけですが、処理が早く終了した場合は余った時間をループで消化し、時間内に終わらなければ次のフレームで帳尻を合わせるようにします。
millis()で絶対時刻を取得し、積算されていくフレーム単位時計に対し、処理が予定より進んでいるか遅れているかを監視します。
##スケッチ
※コメント欄によりよいコードをいただいております。(2021.3.12追記)
最初に投稿したスケッチ
// 16MHz動作のArduionoを想定 (Arduiono UNO, micro等)
//変数の準備
long frame_ms = 10;// 1フレームあたりの単位時間(ms)
long sframe = (long)millis(); // フレーム管理時計の時刻 schaduledなflame数
long curr= (long)millis(); // 現在時刻を取得
void setup() {
Serial.begin(115200); //115200bpsでシリアル通信を開始
}
void loop() {
sframe = sframe + frame_ms;//フレーム管理時計を1フレーム分進める
// ここから周期処理
// 内容は何でもよいが、ここでは1秒毎にミリ時刻をシリアル出力。
for (long i = 0; i <= 200; i++) {// ここ数値(200)で1フレームあたりの負荷を可変。230ぐらいで飽和。
curr = (long)millis(); // 現在時刻を更新
if ((curr % 1000) == 0) {//現在時刻が1000msで割り切れたらシリアルに表示する
Serial.print("millis:");
Serial.println(curr);
delayMicroseconds(800);//この数値を減らすと時刻を複数回表示するようになる
}
delayMicroseconds(5);//この数値はAdruinoの性質上3以下にしない方がよい
}
// 周期処理ここまで
// この時点で1フレーム内に処理が収まっていない時の処理
curr= (long)millis(); // 現在時刻を更新
if (curr > sframe) { // 現在時刻がフレーム管理時計を超えていたら何らかのアラートを出す
//この例ではシリアルに遅延msを表示
Serial.print("*** processing delay :");
Serial.println(curr - sframe);
}
// 余剰時間を消化する処理。時間がオーバーしていたらこの処理を自然と飛ばす。
while (curr < sframe) {
curr = (long)millis();
}
}
現在使っている処理のセット
// 周期処理用変数
const int err_led = 2; //処理遅延を識別するLEDのピン設定
const unsigned long frame_ms = 10;// 1フレームあたりの単位時間(ms)
unsigned long merc; // フレーム管理時計用
unsigned long curr; // 現在時刻取をミリ秒で取得する用
unsigned long curr_micro; // 現在時刻をマイクロ秒で取得する用
int framecount; // 現在フレーム何周期目かのカウント用
void setup() {
Serial.begin(115200);//シリアルモニター設定
// 周期処理のタイマー初期設定 ////////////////////////////////////////////
merc = millis(); // フレーム管理時計用の初期時刻
}
void loop() {
// ( 〜この中で周期処理を実施〜 )
// 周期処理フッタ開始 //////////////////////////////////////////////////
// この時点で1フレーム内に処理が収まっていない時の処理
curr = millis(); // 現在時刻を更新
if (curr > merc) { // 現在時刻がフレーム管理時計を超えていたらアラート
////Serial.print("*processing delay : ");
////Serial.println(curr - merc);//遅延フレーム数を表示
digitalWrite(err_led, HIGH);//処理落ちが発生していたらLEDを点灯
}
else {
digitalWrite(err_led, LOW);//処理が時間内に収まっていればLEDを消灯
}
framecount = framecount + 2;//フレームカウントを加算(制御などに使う)
if (framecount > 10000) {
framecount = 0; //フレームカウントのリセット
}
// 余剰時間を消化する処理。時間がオーバーしていたらこの処理を自然と飛ばす
curr = millis();
curr_micro = micros(); // 現在時刻を取得
//Serial.println(merc * 1000 - curr_micro); //余剰時間をμ秒単位で表示
while (curr < merc) {
curr = millis();
}
merc += frame_ms;//フレーム管理時計を1フレーム分進める
// 周期処理フッタ終了 //////////////////////////////////////////////////
}
気づいたこと
millis()はunsignedの変数なので減算ができません。
そこで、普通のlong型にキャスト変換して数値を取得しています。
なので時計は約25日でオーバーフローすることになります。
millis()のunsigned型の変数は結果が負にならないものは減算できます。(2021.3.12追記)
使い方
実際につかう場合は周期処理をスケッチし、その周期処理が処理速度的に大丈夫かどうかをアラートで確認したりして使います。
シリアル出力はそれだけで時間がかかるので、アラートをLEDの点灯などに置き換えても良いかもしれません。
フレーム時計を使って条件分岐すれば、10ms毎、20ms毎、500ms毎などと処理によって実行間隔を変えることができると思います。
その他
遅延時の処理や遅延繰り上げが不要な場合で割り込み処理が使える場合には割り込み処理が便利です。
また、処理を一定間隔で実行するMetroというライブラリも便利そうです。
https://www.pjrc.com/teensy/td_libs_Metro.html
今回のフレーム処理と組み合わせて使うとさらに便利そうです。
教えてください
変なところがあったらぜひ教えてください。