SNSへはこちら

ESP8266でベアメタル(11) - 逐次タスク機能(その1)

お次はタスク機能です。だいぶレイヤが上がった気がしますね。

この記事は NonOS SDK におけるタスク機能の説明のみです。「動くコードがとにかく欲しい!」という方は次回記事をどうぞ。

NonOS SDKでの逐次タスク管理機能

NonOS SDK には、決まった処理を行うタスク機能があります。この機能は RTOS 等に見られる並列処理ではなく、優先順位の高い順に実行する逐次処理のようです。この辺も NonOS SDK API マニュアルに書いてないんだよなあ。

注意点

以下の記事はタスクや OS について知らない初心者が執筆したものです。情報が不正確かも知れないので、あくまでも参考や個人のお気持ちとして御覧ください。

タスク機能の使いどき

この機能の使いどきですが、自分は処理の終わる時間が不定である時だと思います。例えば、処理1、処理2、処理3の順番で何らかの関数を実行したいとしましょう。通常だと割り込み時に順番に呼ぶことになると思います。

大体の処理はタイマー割り込み時に逐次実行することで実現できると思います。が、仮に処理2がたまに時間がかかるときはどうしましょう。結論から言うと、タスク機能を使うほうが便利です。内部の実装がどうなっているか知らないけどね。

例えば以下の状況です。

処理番号 実行にかかる時間
1 1msec
2 10〜50msec
3 5msec

タイマー割り込みで処理を実現する方法(不便)

不便というか、場合によっては不可能です。愚直な実装は以下のとおりでしょう。

最大 50msec がかかるので、タスクごとのインターバル(割り込み周期)を 50msec とする

この一連のタスクが 100msec 周期とか十分長くて良いならこれでシンプルだし、十分実現可能だと思います。割り込み関数の実装はこんな感じでしょうか。

void timer_isr(void) {
  static uint8_t task_no = 0;
  switch(task_no) {
    case 0:
      func1();
      break;
    case 1:
      func2();
      break;
    case 2:
      func3();
      task_no = 0;
      break;
  }
  task_no++;
}

一方で、なるべく早めに処理を回さねばならない時にはこの方法は使えません。やるならせめて、

時間のかかる処理2の内容を分割して、タスク数を増やすことでタイマー周期を短くする

でしょうか。

処理番号 実行にかかる時間
1 1msec
2-1 5msec
2-2 0〜40msec
2-3 5msec
3 5msec

こうすれば一応割り込み周期を 40msec にすることで時短が可能です(ソースコードは上と似ているので省略)...が、却って全タスク終了までの時間が長くなってしまいました。これはいけない。しかも、処理1や処理3等々、だいぶ時間を損しています
この状態で処理1と処理2-1、あるいは処理2-3と処理3を合体させてしまうというのも手です(一応これで少し目標は達成できる)が、むしろ処理内容が分散してしまっていて、可読性は最悪です。

多重割り込み可能なマイコンもありますが、早々にフラグをクリアした所で限界があります。すぐにスタックがオーバーフローしそう。

タスク機能で処理を実現する方法

前述の通り、RTOS 等では疑似並列処理なのですが、今回は逐次処理を行う NonOS SDK のタスク機能のみについて述べさせていただきます。

この機能を使えば、割り込み関数内にて仕事を投げるだけなので、周期長すぎ問題、多重割り込み問題は解決します。割り込み関数内で関数をコールしているわけではなく、やってる処理は仕事を投げる(正確には「リクエストする」とでも言いましょうか)だけ。あとは SDK の実装部分が勝手に呼んでくれます。なので割り込み関数の処理は以下のコードのようで済みます。らくらく。

void timer_isr(void) {
  system_os_post(USER_TASK_PRIO_0); // 優先順位0のタスクをリクエスト
  system_os_post(USER_TASK_PRIO_1); // 優先順位1のタスクをリクエスト
  system_os_post(USER_TASK_PRIO_2); // 優先順位2のタスクをリクエスト
  // リクエストするだけなので、この時点で何も処理をせず関数から抜ける
}

このコードは型とか適当に書いてます。後に貼る実際の ESP8266 コードとは異なります。

そして、タスクには優先順位がつけられますので、これによって急ぐべき処理があったとしても実現可能なわけです。

しかし、それでも問題が残りますが、多少の工夫で回避可能です。

問題点:時間がかかるタスクの連続コール

上の例で言う処理2は、やたら時間がかかるときがあります。この時にリクエストを投げすぎた場合、メモリが足りなくなることが想定されます。

これはどういうことかというと、タスクの管理はタスクごとに用意する os_event_t 型の配列(およびポインタ)で行っています。こちらは内部でキュー構造を取っているらしくて、当該タスクがリクエストされるごとに enqueue され、タスクが終了するごとに dequeue されるようなのです。
とすると、処理2が短時間で何度も呼ばれて用意した領域以外を破壊し始めたら...という問題があります。

これについては簡単に回避策を考えてみたので提案します。実際にこれで動かしたわけではないんですが、許して。

// 策: ある程度呼び出しリクエストが溜まっていたら**無視**する

static volatile uint8_t nRequest; // 溜まっているリクエスト数。全て完了していれば0となる

void func2(void) {// 処理2
  // 何らかの処理...
  // ...
  nRequest--; // 自分の処理が終わったので減らしておく
}

void timer_isr(void) {
  system_os_post(USER_TASK_PRIO_0); // 処理1
  if( nRequest <= 5) { // 現状溜まっているリクエスト数が規定数を超えていたら無視
    nRequest++; // リクエストを呼び、カウンタを更新
    system_os_post(USER_TASK_PRIO_1); // 処理2
  }
  system_os_post(USER_TASK_PRIO_2); // 処理3
}

こんな感じでどうでしょう。もちろん無視なんかしちゃダメだなんていう処理も存在するでしょうが、取り敢えずこれは例として。
他にもニュートン算を頑張って行うことで、必要最大数の os_event_t 型配列を確保しておくという方法もありますよ。

いや、そのタスクを無視しちゃダメでしょ」という場合は(寧ろこっちですよね...)、フラグを作って全てのタスクが終わるまで次のタスクをリクエストしないという動作にしても良いかもです。

というわけで

ざっくりとタスクについてお気持ちを述べましたが、どうでしょうか(これで認識としては正しいでしょうか??)。次回は追加の解説と動かしてみたコードになります。

ただ、僕の実装力と想像力が乏しいため、単なるLチカ等を実装するだけとなります。このページで述べたタスク管理の重要性に関してを実演できるコードとは程遠いですのでどうかご了承を**。