C++プログラムに組み込む

おまたせしました、お楽しみの時間です。DSPのサンプル・プログラムとしてよく見るのですが、「ぽん」とアルゴリズムだけアセンブリ言語で実装しておしまいというものがあります。いけませんね。学生実験じゃあるまいし。まして当サイトのような遊びを掲げる場としては、結果として楽しめる形にしたいものです。そういうわけでこれまで作ってきたFIRフィルタープログラムを高級言語から呼んでみましょう。例によってmcmを使います。

単純なフィルターで何かするといっても実用的なものにはなりません。そこで帯域制限をかけてみることにしました。世界のAM放送は9KHz間隔で放送局に電波が割り当てられています。したがって、放送される信号は4.5KHz以下の信号になります。ここでは48KHzサンプルの信号を1/10に帯域制限するLPFを作り、擬似AM音質を作り出してみます。

大まかな構造

入出力がステレオですのでFIRフィルターも2ch必要になります。したがってX可能で説明したような再利用不能なコードでは困ります。一つのFIRルーチンで複数のFIRフィルターを実現できるようにしなければなりません。そこで、ディレイラインとFIRルーチンをまとめてクラスとすることで、複数インスタンスに簡単に対応できるようにします。

ついでですのでタップ数も自由に指定できるようにします。このため、内部のディレイラインはヒープ領域から確保します。また、インパルス関数は一般に使いまわしが多いことが予想されますので、ここで外部で定義した配列をクラスの初期化時に与えることにします。こうすると左右両チャンネルでインパルス関数用の配列を共用できます。

FIR関数用のクラスCFIRはfir.cppにまとめて記述することでmain.cppの見通しが悪くなることを防ぎます。main.cppではfwEzKit2191クラスからCLoFiクラスを派生させます。CLoFiクラスは内部にCFIRクラスを二つ持ちます。これらは左右両チャンネル用で、コンストラクタで確保されデストラクタで開放されます。

CFIR

CFIR型の宣言はfir.hで行います。

class CFIR
{
private:
        int taps;
        int * delay;                    // delay line
        int * delayptr;                 // delay line pointer
        int * h;                        // impuls response
public: 
        CFIR( int * hArray, int tapOfFilter );
        ~CFIR( void );
        int filter( int x );
};

フィルター処理を行うメンバー関数はfilter()です。この関数は1サンプル受け取って結果を1サンプル返します。メンバー関数の実装は以下のとおりです。

#include "fir.h"

        // コンストラクタ
CFIR::CFIR( int * hArray, int tapOfFilter )
{
        int i;
        
        h = hArray;
        taps = tapOfFilter;
        delay = new int[ taps ];   // ディレイラインを確保
        delayptr = delay;
        
        for ( i=0; i<taps; i++ )   // ディレイラインの初期化
                delay[i] = 0;
}

CFIR::~CFIR( void )
{
        delete delay;              // ディレイラインの開放
}

int CFIR::filter( int x )
{
        // i0,l0,b0 : delay ptr
        // i6 : end address of h
        // cntr : taps-1
        // input : mx0
        // output: mr1
        asm( 
            "dis m_mode;\n"         // 固定小数点積和モード
            "i0=%2;\n"              // delay ptr
            "m1=1;\n"
            "reg(b0)=%3;\n"         // base address of buffer
            "i6=%4;\n"              // end address of h
            "mx0=%5;\n"             // input x
            "my0=%6;\n"             // taps 
            "l0=my0;\n"             // 
            "ar=my0-1;\n"           // taps -1
            "cntr=ar;\n"
        
                
            "mr=0;\n"               // SUM=0
            "dm(i0+=m1)=mx0;\n"     // 最新のサンプルをストア
            "mx0=dm(i0+=m1),my0=pm(i6+=m5);\n"              // プリロード

            "do fir_loop until ce;\n"
        "fir_loop:\n"
                "mr=mr+mx0*my0(ss), mx0=dm(i0+=m1),my0=pm(i6+=m5);\n" // 積和
            "mr=mr+mx0*my0(rnd);\n" // 最後の積和    rts;        
            "%0=mr1;\n"
            "%1=i0;\n"              // i0 : delay ptr
            "ena m_mode;\n"         // 整数積和モード
            "l0=0;\n"               // L0を戻す
        : "=e"( x ), "=e"( delayptr )
        : "e"( delayptr ), "e"( delay ), "e"( &h[taps-1] ), "e"( x ), "e"( taps )
        : "memory", "mr0","mr1","mr2","mx0","i0","m1","i6","m5","cntr","my0", "ar", "l0"
        );
        
        return( x );
}

インライン文のテンプレート部は、命令ごとに文字列を切って"\r"を挿入しています。こうすると中間ファイルを生成したときに命令ごとに改行します。うまくいっているときには必要ないことですが、デバッグ時には可読性があがって重宝します。

この部分はこれまで作ったアセンブリ関数を単にインライン・アセンブリに落としただけですので、大きな変更はありません。ただし、テンプレートの最後の一行だけは注意が必要です。ここでは使用したL0を元の0に戻しています。実際には破壊されるレジスタとしてL0を宣言していますのでこれは必要ないはずです。が、なぜか VisualDSP++3.0ではこれが復帰されません。したがって、誤動作を防ぐために0に戻しています。

CLoFi

fwEzKit2191から派生させるCLoFiの宣言を以下に示します。

class CLoFi : public mcm::fwEzKit2191
{
private:
        CFIR * lFilter, * rFilter;
public: 
        CLoFi();
        ~CLoFi();
        virtual void handleBuffer( struct mcm::sample * bufTx, 
                                   struct mcm::sample * bufRx );
};

このクラスはこれまでどおり、規定の数のサンプル(48)ごとに割り込みを受け取ってまとめて処理を行います。これまでと違うのは2点です。

内部変数を持つ
左右両チャンネルのフィルターオブジェクトを内蔵しています。
コンストラクタとデストラクタを持つ
これはフィルターの確保と開放のためです。

このクラスの実装を以下に示します。

    // コンストラクタ
CLoFi::CLoFi() : mcm::fwEzKit2191()
{
        lFilter = new CFIR( hcoeff, TAPS );  // Lch フィルタ
        rFilter = new CFIR( hcoeff, TAPS );  // Rch フィルタ
}

   // デストラクタ
CLoFi::~CLoFi()
{
        delete lFilter;  // Lch 開放
        delete rFilter;  // Rch 開放
}

   // 受信信号にフィルターをかけて送信する
void CLoFi::handleBuffer(   struct mcm::sample * bufTx, 
                            struct mcm::sample * bufRx )
{

        for ( int i=0; i< this->bufSize; i++ ){      // bufSizeは送受信バッファの長さ
                bufTx[i].l = lFilter->filter( bufRx[i].l ); // Lch フィルター
                bufTx[i].r = rFilter->filter( bufRx[i].r ); // Rch フィルター
        }
}

コンストラクタとデストラクタについては特記するようなことはありません。

handleBufferメンバー関数も至ってシンプルです。やっているのは1サンプルごとにフィルターにかけて出力するという処理だけです。それなりの作業をしているのですがオブジェクト指向の長所が遺憾なく発揮されて可読性の高いプログラムになっています。

インパルス応答

FIRフィルターの特性を決めるインパルス応答はフィルター設計プログラムを使います。最近はいろいろなフィルター設計ソフトがあるようです。私が愛用しているのは石川高専山田研究室が開発し公開しているWWW版DF-Designです。このプログラムは簡単な操作でFIRフィルタとIIRフィルタの係数を生成することができます。係数はHTMLページに表示され、同時にインパルス応答や周波数応答、位相特性がグラフで表示される大変使いやすいサービスです。

このようにツールを使えば設計は簡単にできますが、実際に生成された結果を利用する場合には固定小数点DSPならではの問題があります。C/C++言語側で与えられた係数を使う場合、定数は整数か浮動小数表記になります。また、配列は整数としてしか宣言できません。そこで与えられた係数列を整数型に変更する必要があります。同時に、スケーリングして整数型定数のフルスケールにあわせる必要があります。具体的には、フィルターのゲインを変更する必要があります。

この辺の話で混乱してしまう人は多くいます。問題は次の点をどう把握するかです。

ここで深みにはまる人はこのような考え方をします。「えーと、C++の値の範囲は±32768だから固定小数点数に変更するために32768で割って…」。

そんな必要はありません。上のような話は16ビットのパターンをどう解釈するかという話でしかありません。確かにDSPは数値を固定小数点数として捕らえますが、同じビットパターンをC/C++側で固定小数点数として捕らえなければならないという法はありません。整数として扱えばいいのです。乗除算を行わない限り、C/C++側ではデータを普通に整数として扱えばいいのです。ただし、本来の値が32768倍されて見えるだけのことです。

さて、話が長くなりました。結局フィルター設計ソフトが出した係数をいったいどのように扱えばいいのでしょう。答えは簡単で32768倍して係数配列に格納してください。私は普段、DF-DESIGNの出力を表計算ソフトで一括スケーリングし、ついでに","を追加して配列初期化データに変換しています。

デュアル・ロード

我々が作ったプログラムのFIR部はデュアル・ロードを行います。そこでインパルス応答配列とディレイ・ラインを別のメモリー・ブロックに配置するように調整しなければ、100%の性能を出すことはできません。

幸いなことに、別段特殊なことをせずともこれは解決されます。と、いうのは一般のグローバル変数とヒープ領域は別のメモリー・ブロックに配置されるからです。もちろんこれは標準LDFの場合であり、自分でLDFに手を加えた場合には話がかわってきます。詳しくはオブジェクトの配置を参考にしてください。

プログラム

最後に全プログラムをダウンロードできるよう用意しておきました。解凍してEZ-KIT Liteから利用してください。

次は⇒オーバーヘッドの考察

2191空挺団 | プログラム | EZ-KIT | こぼれ話 | アーキテクチャー | 命令 | レジスタ | DSP掲示板 | FAQ |