M5StickC(ESP32)でのリピートタイマー利用例

現時点の情報です。最新情報はM5StickC非公式日本語リファレンスを確認してみてください。

概要

マルチタスクを書いたので、タイマーで定期実行する場合のサンプルです。

単純な例

void setup() {
  Serial.begin(115200);
  delay(50);
}

void loop() {
  // 定期実行
  Serial.println(millis());
  Serial.flush();

  // 適度に時間のかかる処理
  delayMicroseconds(200);

  // 1秒Wait
  delay(1000);
}

起動経過時間をシリアルに出力して、200マイクロ秒Waitをしてから1秒間delayしています。

62155
63156
64157
65158
66159
67160
68161
69162
70163
71164
72165

実行した結果ですが、1秒のdelayの他に色々な処理をしているので1ミリ秒程度ずれていっています。

大抵の処理はこれぐらいのズレであれば許容できると思います。ただし、加速度などを計測する場合には時間精度が怪しいと、測定精度もおかしくなってしまいます。

補正Delay

void setup() {
  Serial.begin(115200);
  delay(50);
}

void loop() {
  // 開始時間保存
  unsigned long startTime = micros();

  // 定期実行
  Serial.println(millis());

  // シリアルのキャッシュをフラッシュ
  Serial.flush();

  // 適度に時間のかかる処理
  delayMicroseconds(200);

  // 経過時間取得
  unsigned long deltaTime = micros() - startTime;

  // 1ミリ秒以下のdelay
  delayMicroseconds(1000 - (deltaTime % 1000));

  // 1秒Wait
  delay(1000 - ((deltaTime / 1000) + 1));
}

millis()で取得すると1ミリ秒未満の誤差が発生しますのでmicros()でマイクロ秒単位の起動時間を取得しています。

処理時間を計算して、処理時間分だけdelay()の数値を減らしてあげています。1ミリ秒以下の時間あわせはdelayMicroseconds()を使っています。

高精度タイマー利用

// タイマー
hw_timer_t * timer = NULL;

// キュー
#define QUEUE_LENGTH 1
QueueHandle_t xQueue;

// タイマー処理用タスク
TaskHandle_t taskHandle;

// タイマー割り込み
void IRAM_ATTR onTimer() {
  int8_t data;

  // キューを送信
  xQueueSendFromISR(xQueue, &data, 0);
}

// 実際のタイマー処理用タスク
void task(void *pvParameters) {
  while (1) {
    int8_t data;

    // タイマー割り込みがあるまで待機する
    xQueueReceive(xQueue, &data, portMAX_DELAY);

    // 実際の処理
    Serial.println(millis());

    // シリアルのキャッシュをフラッシュ
    Serial.flush();

    // 適度に時間のかかる処理
    delayMicroseconds(200);
  }
}

void setup() {
  Serial.begin(115200);
  delay(50);

  // キュー作成
  xQueue = xQueueCreate(QUEUE_LENGTH, sizeof(int8_t));

  // Core1の優先度5でタスク起動
  xTaskCreateUniversal(
    task,           // タスク関数
    "task",         // タスク名(あまり意味はない)
    8192,           // スタックサイズ
    NULL,           // 引数
    5,              // 優先度(大きい方が高い)
    &taskHandle,    // タスクハンドル
    APP_CPU_NUM     // 実行するCPU(PRO_CPU_NUM or APP_CPU_NUM)
  );

  // 4つあるタイマーの1つめを利用
  // 1マイクロ秒ごとにカウント(どの周波数でも)
  // true:カウントアップ
  timer = timerBegin(0, getApbFrequency() / 1000000, true);

  // タイマー割り込み設定
  timerAttachInterrupt(timer, &onTimer, true);

  // 1000000カウントにセット(=1000000マイクロ秒=1000ミリ秒=1秒)
  timerAlarmWrite(timer, 1000000, true);

  // タイマー開始
  timerAlarmEnable(timer);
}

void loop() {
  delay(1);
}

高精度タイマーを利用して、タイマー割り込みからキューを使って、同期をとっています。この場合にはタイマー割り込みが1秒間隔で確実に発生します。タイマー割り込みの中ではI2Cなどにアクセスできないので、キューや通知を使う必要があります。

タイマー割り込みから呼び出されたタスクでデータを取得すれば、安定した時間精度での取得が可能になります。このデータを通信で送信したい場合には送信用のキューを準備して、別タスクで送信をすると通信に時間がかかっている場合でも、安定してデータ取得をすることができます。

その場合には、タイマー割り込みから呼ばれるタスクより、送信用タスクの方が優先順位が低い必要があるので注意してください。割り込みが最上位で、その次にタイマーから呼ばれるタスク、送信タスクなどのように、優先順位に順位をつけて管理していくことになります。

資料

高精度タイマーの資料です。この命令の他にTickerクラスを使ったタイマーがありますが、こちらは無線を利用すると、呼び出しが遅延する場合があるので注意してください。

まとめ

可能であれば補正Delayなどを使ったほうがいいとは思いますが、単純にdelay()だけで済ますことも多いです。

必要な時間軸の精度に従って使い分けてみてください。とはいえ、ESP32の内部タイマーなので絶対的な精度は温度などによって変わってしまうとは思います。

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。