2016年8月7日日曜日

mbed rtosの注意点

rtosの副作用

mbed rtos (mbed os じゃないよ)は非常に強力なライブラリなのですが、mbedの根本的なところに作用するらしく、副作用が結構あるのでその注意点を書いておきます。(前の記事と一部かぶってますが)
  • rtosライブラリはライブラリをimportするだけで作用が始まる。
    includeしなくてもライブラリをimportするだけで下に書く副作用が起こります。これは結構はまるので注意
  • Serialとの併用は不可
    シリアル通信を行うライブラリであるSerialは内部でFIFO(stream)を使っているのですがそれがrtosと相性が悪いみたいです。変わりにRawSerialを使いましょう。いくつかの注意点がありますが、大体Serialと同じように使えます。
  • main関数が始まる前に threadをはじめてはいけない
    公式のどこにも書いていなくて僕がだいぶ悩まされた現象です。たとえば自作のC++ライブラリのクラスのコンストラクタでthreadを開始するようなコードを書き、main.cのグローバル領域でそのクラスを宣言したとします。その場合main関数が始まる(rtosを使った場合mainもthread扱いになります)前に自作クラスのthreadが始まることになります。
    この動作を行うとプログラムがフリーズします。どういう原理かは知りませんが。

SerialとRawSerialの違い

rtosとSerialの相性が悪いからRawSerialを使えと公式リファレンスに書いてありますが、RawSerialがどのようなものかのリファレンスが皆無なのでここで説明します。
  • 基本的にはSerialと同じ
    初期化はSerial name(tx, rx)でbaud()やformat()もSerialと同じように動きます。また割り込み関連も同じように動きます。
  • write()とread()は実装されていない
    上記の2つが実装されていないので、複数バイトの操作は1バイトずつの書き込み、読み込み関数であるputc()、getc()を複数回呼び出すという方法で実装します。ただし送信に関してはprintfは実装されているのでそれで代用できます。
  • バッファーは16バイト(LPC1768の場合)
    Serialではソフト的にバッファリングされているのでそこそこの大きさのバッファーが効いていますが(具体的に何バイトか知らない)RawSerialではソフトウェアバッファーは無く、ハードウェアバッファーのみです。LPC1768は送信受信共に16バイトあります。
    送信ではprintf()やputc()で16バイトよりも多く送信した場合、各API中で16バイトの送信が終了するまで待ってそれから残りのデータを送信します。つまりprintf()は16バイト以下の送信の場合は一瞬で実行が終わりますが17バイト以上の送信になる場合は16バイトの送信を待つウェイトが入るので一気に時間がかかります。
    受信は単純でバッファーに16バイトたまった後に新しくデータが来た場合は破棄されます。

Class内で自分のクラスメソッドを割り込み、スレッドに登録する方法

こんな面倒なことするかと思いますが、これができるとクラスを宣言するだけで裏でIO周りの処理を自動でやってくれるというクラスが作れるので地味に便利です。説明するよりもコードを乗せるほうが簡単なので相します。コメントで説明を書きます。
Class MyClass{
public:
    Myclass(PinName tx_pin, PinName rx_pin):com(tx_pin, rx_pin){
    }
    init(){
        com.attach(this, &MyClass::mythread, Serial::TxIrq);
        threadp = new Thread(&MyClass::mythread,this);
   
    }
    static void mythread(void const *argument){
        SRSLink*instance = (SRSLink*)argument;
        instance->com.write(0x00);
        /*処理*/
    }
    myisr(){
        /*処理*/
    }
    RawSerial com;
    Thread *threadp;
   
}

ビジーウェイトの扱い

以下のようなプログラムを実行した場合を考えます。

void thread1(void const *args) {
    LED1!=LED1;
    Thread::wait(100);
}
main(){
    Thread thread(thread1);


    while(true){
        LED0=!LED0:
        wait(100);
    }
}
thread1は0.1秒(=100ms)ごとにLED1をトグルさせるコードに見えます。一方main関数のほうも0.1秒ごとにLED0をトグルさせるみたいですが、このコードは良いのでしょうか。mainのwait(100)がThread::wait(100)ならRTOSの入門にありそうなコードですね。
Thread::waitはそのスレッドをInActiveにして他のスレッドに制御を渡し、時間が来たらRTOSから通知を受けて処理を再開する関数です。一方waitは処理を他に渡さずにwaitをするビジーウェイトです。
上記の説明を鵜呑みにするとLED0のトグルだけが起こって、LED1のトグルは起こらないように思えますが、実際はLED0とLED1ともにトグルが起こります。これはビジーウェイトしているmain関数を一定時間おきにRTOS本体が割り込みをかけて中断させ他の処理を行っているからです。
mbed rtosで動かしたところ5ms程度おきに強制的にこの処理が行われていました。
今回はthread1は100msと長いウェイトだったので影響は無いですが、これが1msのwaitだったり、シグナル起動だったりすると実行タイミングが思ったようにいきません。rtosを使うプログラムでは、ビジーウェイトを入れないようにしましょう。

1 件のコメント:

  1. わたしもはまりました。threadを立てて、serialを開いて、waitすると止まってしまいます。いろいろ試しましたが、mbed rtosの問題なのですね。RawSerialでもだめでした。タイマー(Ticker)を使うことにしてthreadは使わないようにします。ありがとうございました。

    返信削除