2011年4月12日火曜日

TOPPERSでスタック状況を確認するシステムコールを作る(quick-and-dirty)

概要

TOPPERS/ASPでアプリケーションを実装した後、やっぱり気になるのはスタックの状況です。
商用iTRONのツールの中には当たり前のようにあるスタック状況確認機能ですが、標準システムコールにはありません。

調べてみると非依存部にはスタックの先頭アドレスとサイズしかなく、依存部のタスクコンテキストブロックにスタックポインタがあります。
もしスタック状況を計算するようなシステムコールを標準として作りたいとすると、依存部に対する仕様(スタックポインタはXXXのように用意すること・・・のように。)が必要になるわけです。

スレッド型OSでスタック状況を確認できないのはなんとも気持ちが悪いものです。
そこで、今回はquick-and-dirtyでスタック状況を確認できるメカニズムを作ってみました。

動作

小規模組み込みシステムデバッグ用シェル - Natural Tiny Shell (NT-Shell)と組み合わせた場合の動作は以下のようになります。

>taskinfo
TSKID   STACK ADDR (HEAD:TAIL)  STACK USED (USED/TOTAL)
=========================================================
  1     0x10000b88:0x10000f87   132/1024
  2     0x10000f88:0x10001787   124/2048
  3     0x10001788:0x10001f87   108/2048
  4     0x10001f88:0x10002787   100/2048
  5     0x10002788:0x10002f87   204/2048
=========================================================

各タスクのスタックアドレスと使用量が一目瞭然で安心です。
上記の出力結果とプロセッサのメモリマップを見て「フムフム」と納得できます。


今回テストで動作させているシステムにはNXPセミコンダクターズのLPC1769が搭載されています。
このプロセッサの内蔵SRAMのアドレスにスタックポインタがあることがわかります。

inf_tskシステムコールを使う

inf_tskシステムコールを使うのは簡単です。
以下のようにタスクIDを指定してシステムコールを呼ぶだけです。

int i;
T_ITSK itsk;
syslog(LOG_NOTICE, "TSKID\tSTACK ADDR (HEAD:TAIL)\tSTACK USED (USED/TOTAL)");
syslog(LOG_NOTICE, "=========================================================");
for (i = 0; i < TNUM_TSKID; i++) {
    const int tskid = 1 + i;
    inf_tsk(tskid, &itsk);
    syslog(LOG_NOTICE, " %2d\t0x%x:0x%x\t%5d/%5d",
            tskid,
            itsk.stk_head, itsk.stk_tail,
            itsk.stk_used, itsk.stk_total);
}
syslog(LOG_NOTICE, "=========================================================");

inf_tskの実装

今回設計したシステムコールはinf_tskという名前です。(あー、そんな名前で作ちゃって!)
ref_tskを参考に実装を追加しました。

まず、include/kernel.hに定義を追加します。

extern ER inf_tsk(ID tskid, T_ITSK *pk_itsk) throw();

次に、システムコールで取得するデータを格納するための構造を用意します。

これもinclude/kernel.hです。

typedef struct t_itsk {
    PRI tsk_pri_curr; /* 現在のプライオリティ */
    PRI tsk_pri_base; /* ベースプライオリティ */
    SIZE stk_used;  /* タスクスタック使用量 */
    SIZE stk_total; /* タスクスタック総量 */
    void *stk_head; /* タスクスタック先頭アドレス */
    void *stk_tail; /* タスクスタック最終アドレス */
} T_ITSK;

ここにref_tskが持つタスク状態などを追加すると良いかもしれません。

次にシステムコール本体の実装を加えます。

今回は新たにtask_info.cというシステムコール実装用のソースを用意しました。
kernel/task_info.cがシステムコールの実装です。

/*
 * タスクの情報取得機能
 */

#include "kernel_impl.h"
#include "check.h"
#include "task.h"
#include "wait.h"
#include "semaphore.h"
#include "eventflag.h"
#include "dataqueue.h"
#include "pridataq.h"
#include "mailbox.h"
#include "mempfix.h"
#include "time_event.h"

/*
 *  タスクの情報取得機能
 */
#ifdef TOPPERS_inf_tsk

ER
inf_tsk(ID tskid, T_ITSK *pk_itsk)
{
    TCB *p_tcb;
    ER ercd = E_OK;
    uint_t tstat;

    p_tcb = get_tcb(tskid);
    t_lock_cpu();
    tstat = p_tcb->tstat;
    if (TSTAT_DORMANT(tstat)) {
        pk_itsk->stk_used = 0;
    } else {
        pk_itsk->stk_used = p_tcb->p_tinib->stksz
            - (p_tcb->tskctxb.sp - p_tcb->p_tinib->stk);
    }
    pk_itsk->stk_total = p_tcb->p_tinib->stksz;
    pk_itsk->tsk_pri_curr = EXT_TSKPRI(p_tcb->priority);
    pk_itsk->tsk_pri_base = EXT_TSKPRI(p_tcb->priority);
    pk_itsk->stk_head = p_tcb->p_tinib->stk;
    pk_itsk->stk_tail = p_tcb->p_tinib->stk + p_tcb->p_tinib->stksz - 1;
    t_unlock_cpu();

    return ercd;
}

#endif /* TOPPERS_inf_tsk */

後はMakefile.kernelにtask_info.cをコンパイルするような記述を追加してできあがりです。
実装をご覧になってわかる通り、p_tcb->tskctxbは依存部にあるタスクコンテキストブロックで、本来ここで参照してはいけません。

まとめ

今回の実装はTOPPERS/ASPから見ると「最悪」と言えます。
が、システムを実現する立場からすると「安心」が得られます。

スタックオーバーフローによってシステムが意味のわからない挙動になってしまっては、せっかくのRTOSを使った楽しい開発が台無しです。少しの機転でシステムの状態を視覚的に把握することができ、開発を少しでも楽にできれば嬉しいですよね?

謝辞

この実装はマイコン工作実験日記さんのアイデアに触発されています。
素晴らしいアイデアに感謝しております。

2011年4月9日土曜日

子供と一緒にお洒落な家具を作ろう!

概要

今回はちょっとブレイクネタです。

約15年前に作ったMDラックがありまして、引っ越すたびにどうしようかと考えていました。
最終仕上げなしの状態で作ったもので、これを何とかしようというのが今回の記事です。

エンジニアのそこのあなた!
家族を投げ出して開発の事ばかり家で考えていませんか?
それはいけません!(←それは自分だ。)

子供と一緒にお洒落な家具を作りましょう。

用意する物

  • パイン集成材を使った工作物。
  • 水性ウレタンニス。着色剤が入っているもの。
  • つや消し透明アクリルスプレー。
  • 紙ヤスリ。
  • ハケ。(水性ウレタンニスを塗るのに使います。)

手順

家具を作る

できました。(すいません。ここは適当に流します。皆さんで好きな形状にして下さい。)
パイン集成材は柔らかい材料で簡単に加工できます。
また、着色もしやすいのでお勧めの材料です。

水性ウレタンニスを使って木材に色を付ける

さきほど完成した家具はどこにでもありそうな?家具でお洒落かどうかは微妙なところです。
まずはハケを使って水性ウレタンニスを塗りたくります。


ジャンジャン雑に塗って構いません。
塗ると以下のようになります。


ちょっとだけ、アンティークな雰囲気になってきました。
が、ここからの仕上げ作業が肝心です。

紙ヤスリでウレタンニスでついた色を落とす

まずは、紙ヤスリで先ほど塗った色をゴシゴシ落としていきます。
コツは「端を意識的に多めに落とす」です。


端の色を意識的に落とすことで、家具の立体感が出てきます。
使い古された感じがアンティークな家具の雰囲気を醸し出します。

アクリルスプレーでコーティングする

最後にアクリルスプレーでコーティングして出来上がりです。

完成図

このようになりました。


ちょっと古めかしい感じがスタジオジブリか何かの作品で出てきそうな感じで、子供も喜びそうな仕上がりです。
自分たちで作ったワクワク感と相まって大切にしてくれること間違いありません。

2011年4月1日金曜日

ARM Cortex-M3 (NXP LPC1769)を用いたオーディオ処理の効率改善

概要

今回はプロセッサを用いた信号処理における効率改善に関するトピックです。


先日完成したARM Cortex-M3 (NXP LPC1769)を用いたオーディオ基板は @suikan_blackfin さんがお作りになったサンプルアプリケーションのおかげで素早く動作確認することができました。本当にありがとうございます。


氏はリアルタイムOSのポーティングから信号処理まで多岐に渡る分野で活躍されていて、言わば私の心の師匠ですが、恐れ多くもサンプルアプリケーションで気になったオーディオ処理の効率改善について記すことにしました。

巨人の肩に乗りまくりです。

オリジナルの設計

まず初めにオリジナルの設計がどのようなオーディオ処理ステップを踏むのかを整理してみます。
  • DMA転送されたオーディオバッファサイズ分のデータがrxbufに入っている。
  • 並び替えながらrxbufからaudio_data.inputBufferにコピーする。
  • オーディオ処理を実行する。
    • ここでは処理に応じてaudio_data.inputBufferからaudio_data.outputBufferへのコピーが発生する。
  • 並び替えながらaudio_data.outputBufferからtxbufにコピーする。
図を用いて整理すると以下のようになります。

要するに主記憶上におけるメモリコピーが少なくとも3回発生していることになります。
このメモリコピーはforループで実装されており、3回のforループによる性能への影響も気になるところです。

通常、性能という観点で見た場合、主記憶上でのメモリコピーや重複したforループは処理性能低下の主要な要因のひとつとなります。

そこで、今回は上記処理の効率改善を考えてみます。

最小限の処理から考えてみる

まず初めに、最小限の処理について考えます。

入力をそのまま出力に伝達する場合、単なるメモリコピーで済みます。
入力に何らかの処理を加え、出力に伝える場合でもこの入出力間のメモリコピーの間に何らかの処理を追加するだけで済みますので、本質的に上記と変わりません。

オリジナルの実装ではコーデックのデータ形式を踏まえて処理を行なっています。

まず初めに入力されたデータを内部で扱いやすい形式にメモリコピーします。

これはrxbufからinputBufferへのコピーです。


次にオーディオの処理を実行します。
これはinputBufferからoutputBufferへのコピーです。

オーディオの処理を実装する過程で、ここに様々な演算が入ることになります。

最後に結果を出力バッファに書き込みます。
これはoutputBufferからtxbufへのコピーです。

この作業はオーディオバッファの内容を、前段で都合の良い形式に並び替えた結果発生する作業と言えます。


まとめると以下のようになります。
  • コーデックから得られたデータ形式は扱いにくいので並び替える。
  • 並び替えは主記憶上でMCUが実行する。
  • 並び替えたデータは、コーデックがそのまま扱えないので再変換する。
コードブロックは以下のようになっていました。(一部はオリジナルと少し異なります。)

   index = 0;
   for (sample = 0; sample < AUDIOBUFSIZE / 2; sample++) {
       for (ch = 0; ch < 2; ch++) {
           audio_data.inputBuffer[ch][sample] = rxbuf[index++];
       }
   }
   audio_effect_through(
           &effect_param,
           audio_data.inputBuffer,
           audio_data.outputBuffer,
           AUDIOBUFSIZE / 2);
   index = 0;
   for (sample = 0; sample < AUDIOBUFSIZE / 2; sample++) {
       for (ch = 0; ch < 2; ch++) {
           txbuf[index++] = audio_data.outputBuffer[ch][sample];
       }
   }

オーディオエフェクト処理の前後でデータ形式変換を行なっていることがわかります。
前段と後段で各((AUDIOBUFSIZE / 2) x 2)回分のメモリコピーを行なっています。

実際にオーディオ処理関数内部の実装も見てみます。(一部はオリジナルと少し異なります。)

   void audio_effect_through(
           effect_param_t *param,
           AUDIOSAMPLE input[2][AUDIOBUFSIZE / 2],
           AUDIOSAMPLE output[2][AUDIOBUFSIZE / 2],
           int count)
   {
       int i;
 
       const int var0 = param->var0;
       const int var1 = param->var1;
       for (i = 0; i < count; i++)
       {
           output[LCH][i] = (input[LCH][i] >> 10) * var0;
           output[RCH][i] = (input[RCH][i] >> 10) * var1;
       }
   }

ここで上位から渡されるcountは(AUDIOBUFSIZE / 2)です。
よって、ここでも((AUDIOBUFSIZE / 2) x 2)回分のメモリコピーを行なっていることになります。

改善の提案

ここまではオリジナルの設計について整理しました。
それでは実際にオーディオ処理の効率改善をしてみます。

基本的な思想は以下の通りです。
  • 主記憶上におけるメモリコピーは性能に対して著しい劣化を伴う。
  • より多くの処理を実現するためにはメモリコピーを排除すれば良い。
  • メモリコピーを行なっている主な理由はデータ形式変換である。
  • データ形式変換が不要となるような枠組みを用意すれば、データ形式変換が不要となるはずである。
  • データ形式変換が不要となれば、必要となるバッファも削減することができ、RAM容量という観点から見ても有利である。
「データ形式変換」を実現しながらも、「メモリコピー」を発生させないという一見矛盾した課題を解決すれば良い事になります。この中でforループについても削減可能と判断しました。

オリジナルの実装ではオーディオ処理関数に渡るデータ形式が重要でした。
この点は改善案でも特に変わるものではありません。

オリジナルと異なるのはその実現手法です。
ここで実際のコードを示します。

   for (index = 0; index < AUDIOBUFSIZE; index+=2) {
       audio_effect_through(
               &effect_param,
               rxbuf + (index + 0), rxbuf + (index + 1),
               txbuf + (index + 0), txbuf + (index + 1));
   }

オーディオ処理関数へL-Rのステレオデータを揃えて渡す部分はコールバック関数とし、オーディオ処理関数内部で直接出力データを格納させる形式としました。

オリジナルに存在した前段と後段における形式変換用メモリコピーを排除することができます。
これにより内部作業用バッファinputBufferとoutputBufferも不要となりました。

audio_effect_throught関数はオーディオデータをスルーコピーする関数です。

   void audio_effect_through(
           const effect_param_t *param,
           const AUDIOSAMPLE *in_left,
           const AUDIOSAMPLE *in_right,
           AUDIOSAMPLE *out_left,
           AUDIOSAMPLE *out_right)
   {
       const int var0 = param->var0;
       const int var1 = param->var1;
       *out_left = ((*in_left) >> 10) * var0;
       *out_right = ((*in_right) >> 10) * var1;
   }

オーディオエフェクト関数には1サンプル毎に処理を依頼します。
処理結果は関数に渡されたバッファへのポインタを用いて直接格納します。


要するに冒頭にあった最小限の処理に近くなる仕組みです。



上記により中間バッファを排除しながらも、渡されるデータ形式はL-Rのステレオで揃っているという状況を作ることができます。
また、バッファサイズ分のforループも1回で済むようになり、効率改善が期待できます。

改善の効果

ここで実際の処理時間に与える効果を調査しました。
処理時間を測定するために、オーディオ処理ブロックに差し掛かったところでGPIOをハイレベルにし、オシロスコープにより観測します。

ここでの処理対象は1オーディオサンプルブロックでAUDIOBUFSIZEバイト分のデータです。
初めにオリジナル実装でオーディオスルーにかかっている処理時間を示します。
1オーディオサンプルブロックの処理に約70[us]の時間を要しています。


次に改良した実装でオーディオスルーを行なった場合の処理時間を調べます。
同じ1オーディオサンプルブロックの処理を約25[us]の時間で処理していることがわかります。


オリジナルの実装に対して約35%の時間で同等の処理が実現できる事が確認できました。
削減できた約45[us]は別の演算に割り当てることができます。
従来より高度な演算も可能な他、他のタスクにプロセッサを素早く譲ることができるようになります。

改善のまとめ

オーディオ処理関数の呼び出し方を変更し、主記憶上におけるメモリコピーを大幅に削減しました。
結果的に処理時間を削減でき、従来よりもシステムプロセッサを効率的に運用することが可能となりました。

今回の改善でオーディオエフェクト部分の設計者はデータ長を気にすることなく、1サンプルの処理に集中して記述できるようにもなりました。
オーディオエフェクト関数を同じパラメータで作成すれば、関数ポインタの切り替えのみでオーディオエフェクト処理を切り替えることが可能となります。


当然のことですが、オーディオ処理では特定サンプルに対して、時間軸方向前後のデータも用いてフィルタリング処理を行ないます。
提案手法ではオーディオ処理関数に渡ってくるデータは1サンプル分のみですが、オーディオエフェクト関数内部で静的メモリを保持し、バッファリングしながら処理をすることで、時間軸の前後方向のデータも用いて処理することが可能です。

RTOSを用いたシステムを構築する場合、局所的に見た性能改善とシステム全体を見た性能改善の双方の視点が欠かせません。
今回行なった性能改善は局所的なものですが、タスクの性質上高いプライオリティで動作します。
このタスクの処理時間を削減するだけで、他のタスクで行うことの出来るサービスが飛躍的に増えます。
例えば、ディスプレイに表示したい内容を別のタスクに伝達しようとか、そういった付加的なサービスにプロセッサ時間を使う事ができます。こういった要素が「他と違う何か(=付加価値)」に繋がっていきます。

なお、実験結果はコンパイラの最適化オプションを外した状態で行ないました。
最適化を施すことで、場合によってはより大きな改善が得られる可能性もあります。