発生した問題

 ソフトウエア技術者のHさんは,オープンソースのリアルタイムOSを,新たなプロセサに移植していました。そのために,タスク・ディスパッチャや割り込みの出入り口処理を,そのプロセサ用にアセンブリ言語で記述しました。一通りの機能を実装した後,タスクや割り込みハンドラ(割り込み処理のためにメモリ上に待機している関数)を実行できることを確認しました。次にUART( universal asynchronous receiver-transmitter)のドライバをテストするため,外部機器とUARTを接続し,外部機器からデータを受信して,そのデータのエコーバックをするタスク(エコーバック・タスク)を実行しました。ところが,ごくまれに外部機器がデータを送信してから,エコーバックが返ってくるまでの時間が,通常よりもケタ違いに遅くなることがありました。

原因と対策

 エコーバックの仕組みは次のようになっています。まず,エコーバック・タスクがUARTによるデータ通信を制御するシリアル・ドライバの受信ルーチンを呼び出します。データがシリアル・ドライバの受信バッファにあれば,それを受け取ります。データがなければ,エコーバック・タスクはデータ受信待ちとなります。

 データ受信待ちの状態のときにUARTからデータが送られてくると,プロセサに割り込み要求が入り,UARTのシリアル・ドライバの割り込みハンドラが実行され,受信バッファのデータを読み込みます。そして,データ受信待ちだったエコーバック・タスクにデータを渡して起床させます。データを受信したエコーバック・タスクは,送信ルーチンによりそのデータを送信します。

 Hさんは,まず受信割り込みが正常に動いているかどうかを疑いましたが,時間がかかるもののエコーバックは返ってくるので,割り込みは入っていると判断しました。さらに,状況を分析すると次のことが分かりました。

  • (a)他の優先度の低いタスクが常に動作しているときは,エコーバックが遅くなることはない。
  • (b)エコーバックは最長でも1ms以上遅くなることはない。

 (a)より,システム上に実行状態のタスクが存在しない場合に,実行されるリアルタイムOSのコードに原因がありそうだなと考えました。該当するコードを見直したところ,割り込みに関連しそうな個所として,省電力のためにプロセサをスリープ・モードにして,割り込みを待っている部分がありました(図3-1)。

 このコードはアセンブリ言語で書かれており,プロセサのステータス・レジスタの割り込み要求マスク・ビットを“0”として割り込みを許可した後,sleep命令でスリープ・モードに移行します。sleep命令を実行すると,割り込みが来るまでスリープ・モードを維持します。割り込みが来てそれを受け付けると,割り込みハンドラを実行します。割り込みハンドラの実行によってタスクが起床されたり,新たなタスクが実行状態となったりすると,割り込みハンドラはタスク切り替えの要求(ディスパッチ要求)の変数をセットします。割り込みハンドラの実行が終了するとsleep命令から復帰します。復帰すると割り込みを禁止し,ディスパッチ要求の変数をチェックし,ディスパッチ要求があればディスパッチを行い,タスクを実行します。ディスパッチ要求がなければ最初の命令に戻り,再び割り込みを許可してスリープ・モードで割り込みを待ちます。

 このコードに問題を照らし合わせて考えてみました。データ受信割り込みがプロセサに入力され,プロセサがスリープ・モードから復帰するまで時間がかかるのではと考えましたが,データシートを見ると復帰は1msもかからないことが分かりました。

 次に(b)の状況を考えました。1msという値はリアルタイムOSで使用しているシステム・タイマの周期であることに気付きました。システム・タイマは1msごとに割り込みを発生させます。そのため,システム・タイマの割り込みが入るとデータが送信されるのではないかと考えました。

 これらの状況から,次のような現象が発生していることが分かりました(図3-2)。UARTによる割り込みが発生している状態で,割り込みを許可する命令を実行すると,sleep命令を実行する前に割り込みを受け付けます。このとき割り込みハンドラはデータを受信して,エコーバック・タスクを起床させるためにディスパッチ要求をセットして終了します。ところが割り込みハンドラが終了すると,プロセサは次の命令であるsleep命令を実行してしまい,ディスパッチが行われず,即座にエコーバックができないという問題が発生します。システム・タイマ割り込みが入り,システム・タイマの割り込みハンドラが実行された後にsleep命令から復帰してから,ようやくエコーバッグ・タスクが実行されるので,エコーバックが遅くなったのでした。

 この問題は,割り込みの許可とスリープ・モードへの移行を不可分に実行していないために発生していました。これらを不可分にする方法は,プロセサによって異なります。

 例えば「SH3」では,ステータス・レジスタにBLビットがあります。このBLビットをセットすると,割り込み要求マスク・ビットがどのような状態であっても割り込みを禁止します。ただし,スリープ・モードになるとBLビットは無視されます。そのため,sleep命令を実行する前に割り込み要求マスク・ビットを“0”にするとともにBLビットを“1”に設定することで,割り込みの許可とスリープ・モードへの移行を不可分に実行できます(図3-3)。

 一方,「H8」の場合,ステータス・レジスタを操作して割り込みを許可する命令の終了時点では,割り込みは許可とならないため,図3-1の記述方法で割り込みの許可とスリープ・モードへの移行が不可分に行えます。

 ちなみに,図3-1のsleep命令をnop命令に置き換えたときに,割り込みが“常に”受け付けられなくなるという問題が発生する場合に注意しましょう。パイプラインが深いプロセサでは,割り込みが発生しているときに割り込みを許可する命令を実行しても,その後の数命令の間は割り込みが許可されないことがあります。そのため,割り込みを許可した後,実際に許可される前に,割り込みを禁止する命令が実行されてしまいます。

技術者必修の基本

 データの受信など,割り込みにより通知される事象(イベント)を待つ場合には,割り込みの許可と事象(今回の場合はUARTでのデータの受信とエコーバック)を待つ処理(今回の場合はsleep命令)を不可分に行う必要があります。不可分に行わないと,割り込みを許可した直後,事象待ち(今回の場合はスリープ・モードによる割り込み待ち)になる前に割り込みが入ってしまう可能性があります。そうすると,割り込みにより事象が通知された後に事象待ちになるため,事象を取り逃がしてしまいます。なお,リアルタイムOSの機能を使用して同様のことを実現する場合にも注意が必要です。

 アセンブラ命令では,1命令実行するたびにプロセサの状態が変化するように考えがちですが,命令は複数のクロック・サイクルで実行されるため,見た目より複雑なタイミングで動作をします。特にsleep命令のような特殊な命令は,動作や扱いが複雑ですので,マニュアルをしっかり読んで使用しましょう。

タスク・ディスパッチャ=タスクの切り替え機構。タスクが中断されたときに,それまで実行していたタスクのコンテキスト(レジスタやスタックの内容)などを所定の場所に退避・保存し,次に実行するタスクがあれば,そのタスクのコンテキストを復帰させ実行する。