
PSoC DFB IIRフィルタ正弦波生成の理解
投稿日 2015/07/15
トランジスタ技術 2013年11月号で取り上げられたCQ出版社のPSoC 5ボードによる「IIRフィルタで正弦波を発生させる」の全体的な理解を試みました。
先日投稿した記事 CY8CKIT-059 IIRフィルタによる正弦波生成では、CY8CKIT-059ボード上で動かしてみました。
このIIRフィルタによる正弦波の生成は、デジタル・フィルタ・プログラムによる演算処理により正弦波を生成するものです。PSoC3または5にはDFB(Digital Filter Block)というコンポーネントが用意されており、比較的容易にこのようなアプリケーションが組めるようになっています。今回、最終的にこのDFBの使い方をマスタするために私なりに解釈をしてみました。
トランジスタ技術2013年11月号のダウンロード・サイトからプロジェクト一式をダウンロードし、フォルダに展開して、PSoC Creatorを起動してプロジェクトを開きます。
デザイン図を見ると以下のようなコンポーネントの組み合わせで、正弦波(100Hzから50KHz)を生成しているのが分かります。

デザイン図
PSoC Creatorでプロジェクトを読み込んで表示
どのような仕組みで正弦波を発生させているか
デザイン図より、使用されているコンポーネントは以下の通りです。
LED ボード上でLEDにつながれているピン
USBFS(USBUART_1) USB UART
CLKSPL クロック源
DMA(DMA_1,DMA_2) Direct Memory Access
DFB(DFB_1) Digital Filter Block
VDAC8(VDAC8_1) 8bit DA Convertor
LEDは点滅させるだけの動作確認用です。ここでは取り上げません。
USBFSコンポーネントはCOMポート経由でパソコンから発振周波数の指定用です。ここでは取り上げません。
CLKSPLは1MHz(周期1uS)で発振するクロック源で矩形波を出力します。
DMAはCPUを介在しないメモリ-メモリ間、メモリ-ペリフェラル間、ペリフェラル-ペリフェラル間などのデータ転送の仕組みです。(後述)
DFBはデジタルフィルタを構成できるコンポーネントで、積和演算器その他から構成される高速演算器です。アセンブラでプログラミングできるようになっています。
VDAC8は8ビットのDAコンバータです。正弦波の計算結果デジタル値をアナログ信号に変換します。信号はピンに出ているのでオシロ等で観測可能です。
作成する主なプログラムは、
main.c(後述)
DFB内のIIRfフィルタ・アセンブラ・プログラム(別記事)
です。そのほかはコンパイル時にPSOC Creatorが使用コンポーネントに基づいて自動生成します。
main.cはRTOSベースのマルチタスクで作られており、main()、Task_LED()、Task_USBUART()、Task_SINGEN()の4つで構成されています。main()は他のタスクを生成した後何もしません。Task_LED()はLEDを点滅させるだけです。Task_USBUART()はCOMポート経由でオペレータがキー入力した希望発振周波数をRTOSのキューに入れます。Task_SINGEN()はキューに周波数が入るのを待っており、入ったらそれをもとにIIRフィルタの演算に必要な係数等を計算し、DFBのRAMにロードし、DMA、DFB、DACをイネーブルにします。DFBはCLKSPLが1uS毎にDMA_1をトリガし、DMA_1はDFBのSTAGEレジスタを変化させるので、DFBが1us離散の正弦波値を指定された発振周波数の周期分の計算(たとえば10KHzならT=100uSなので100回)しながらDAコンバータにDMA経由で渡し、ピンP1[6]出力します。P1[6]にプローブを当てればオシロで観測できます。
DFBでどのようなフィルタを形成するかはDFB内のアセンブラ・プログラム(別記事)で決まります。このアセンブラを理解すればさまざまなデジタル・フィルタとして機能させることが可能となります。このDFBアセンブラを理解するのが最終目的です。
CLKSPL->DMA_1->DFB_1->DMA_2->VDAC8 という流れですが、各コンポーネントは初期設定後、CPUとは独立して動作します(そのためにDMAを使っている)。今回のIIRフィルタによる正弦波の生成は入力データが不要なため、DMA_1はDFBを一定周期(1uS)でトリガしているだけで、データを渡しているわけではありません。(STAGEレジスタのハイバイトを変化させているだけ)
DFB(Degital Filter Block)は、簡単に言えば高速積和演算器です。デジタル・フィルタのほとんどの計算は積和演算で構成されているからです。ただ積和演算器だけではなく、係数などのパラメータや、過去の演算結果などを保存するメモリ、+1やシフトなどの演算、論理判定なども必要になるため、ALU(Arithmetic Logic Unit)、さまざまな演算構成のためデータの流れ、組み合わせを決めるマルチプレクサなどで構成されています。
DFB内で動くアセンブラ(DFBアセンブラ)で書かれたプログラムは上記DFBの構成要素を、どのように使うか、動作させるかを指示するもので、これが心臓部です。DFBアセンブラについては別び記事で解釈を試みる予定です。
肝心なIIRフィルタの正弦波演算
なぜ正弦波を生成できる?と考え始めると数学の難しい話になりますが、IIRフィルタで正弦波を生成するときの演算式は一般的に以下のようになります。
シールバッテリ簡易充電回路

IIR フィルタ(無限インパルス応答フィルタ)による正弦波の生成
x[n] 今回のデータ
y[n - 1] 1回前の演算結果
y[n - 2] 2回前の演算結果
2cos(2PIfT) 係数a1
-1 係数a2
x[n - 1] 1回前のデータ
sin(2PIfT) 係数b
y[n] 今回の演算結果
2PIfのPIは3.141592... 、fはサンプリング周波数(今回は1MHz= 1000000Hz)
Tは周期で10KHzの場合はT = 100 (1000000 / 10000 = 100)
つまり係数a1, bは発振周波数が決まればあらかじめ計算で求めておくことができます。a2は-1です。
結局、DFB内の積和演算器で以下の計算を行えばよいことになります。
y[n] = a1y[n - 1] + a2y[n - 2] + bx[n - 1]
手順は
x[n]をx[n - 1]に入れる
a2と2回前の演算結果y[n -2]を掛ける
a1と1回前の演算結果y[n - 1]を掛ける
bと1回前のデータx[n - 1]を掛ける
上記の演算結果をすべて加算し、結果をy[n]とする
1回前の演算結果y[n - 1]を2回前の演算結果y[n - 2]に移す
今回の演算結果をy[n]を1回前の演算結果y[n - 1]に入れる
上記を1uS毎に周期の回数分繰り返す。
これでy[n]は1uSで離散した1周期分の正弦波の計算値となります。
10KHzの正弦波の場合は1uS間隔で100回を1周期として計算します。
この100は、係数と同様あらかじめ計算しておいてnとしてDFBに渡します。
y[n]は8ビットに変換してDAコンバータでアナログ信号に変換します。
DFBは同じ計算をn回ぐるぐる繰り返すながらDAコンバータに演算結果を渡すだけですので、DFBを1uSでトリガしてやればよいことになります。
DFB内には係数や過去の演算結果、過去のデータを一時的に保持するメモリが用意されています。
DMA
DMAは前述のように、CPUを介在しないメモリ-メモリ間、メモリ-ペリフェラル間、ペリフェラル-ペリフェラル間などのデータ転送の仕組みです。
今回のような波形生成のようなアプリケーションは、いちいちCPUを介在させていると速度的な制限を受けます。このため設定だけプログラム(CPU)で行って、後は独立して自動で動くようにしておくのが一般的です。
DMAはDFBの入力側と出力側に使っています。
今回の例では、DMA_1をDFBのトリガに使っています。DMA_2はDFBの出力をDAコンバータに転送するために使っています。
DMAはメモリ-メモリ間、ペリフェラル-メモリ間、ペリフェラル-ペリフェラル間などさまざまなデータ転送元と転送先の組み合わせで使われますが、今回はDMA_1/2ともにペリフェラル-ペリフェラル間の転送です。
DMA_1はクロック源CLKSPLを入力とし、CLKSPLの立ち上がりのエッジセンシティブに設定されています。これによりDMA_1は定周期で起動され、出力側のDFB STAGEレジスタのハイバイトを変化させます。これがDFBアセンブラ・プログラムの計算開始トリガとなります。
DMA_2はDFBのdma_req_aによりトリガされます。dma_req_aは1つの計算が終了すると発生します。これも立ち上がりのエッジセンシティブに設定されています。これによりDFBの演算出力をDAコンバータに転送します。
DMAの使用に際しては、まずDMAチャネルを定義します。DMAチャネルはデータの転送元と転送先。DMA開始のトリガ(CPUまたはペリフェラル)、DMA転送終了信号、およびTD(トランザクション・ディスクリプタ)チェインへのポインタを持っています。
DMAによるデータ転送はメモリやペリフェラル間をつないでいるPHUBというバスが使われます。PHUBには8, 16, 32ビット幅のスポークが最大8本あり、DMAチャネルは転送に先立って必要な幅のスポークを確保し、転送が終わったら解放します。このようにしてPHUBはいくつかのDMAチャネルによって共有されています。
TDにはデータの転送元と転送先、転送数、TDチェインの次のTDを示すポインタが含まれます。最後のTDは先頭のTDを指示してTDのループを形成しています。
DMA転送を開始する前に、DMAチャネルの設定(チャネル・コンフィグレーション)とTDの設定(TDコンフィグレーション)を行う必要があります。
DMAチャネルとTDはセットであり、DMAチャネルは何と何の間の転送であるか(今回はペリフェラル-ペリフェラル間)を定義し、そのチャネルにどのような転送要求(トランザクション)があるかを示すTDのチェインがつながっており、先頭から順に処理します。TDはトランザクションとして具体的にどのようなペリフェラル間の転送であるかや転送量を定義しています。
DMAで使用する関数と具体的な使用例
DMA_n_Chan = DMA_n_DmaInitialize()
バースト・カウント、リクエスト・パー・バースト、データ転送元のアドレス上位バイト、データ転送先のアドレス上位バイトを設定しDMAチャネルを生成します。
DMA_n_ChanにはDMAチャネルハンドルが戻ります。
今回はペリフェラル-ペリフェラル間の転送なので、データ転送元のアドレス上位バイトと、データ転送先のアドレス上位バイトにはCYDEV_PERIPH_BASEを設定しています。CYDEV_PERIPH_BASEはcydevice.hで定義されているペリフェラルのベースアドレスです。
DMA_n_TD[0] = CyDmaTdAllocate();
TD用のメモリをアロケートしDMA_n_TD[0]にアロケートしたTDのハンドルを戻します。
CyDmaTdSetConfiguration();
TDのハンドル、転送カウント、次のTDへのポインタ、TDプロパティを設定します。
CyDmaTdSetAddress();
TDにデータ転送元のアドレス下位バイト、データ転送先のアドレス下位バイトを設定します。
データ転送元のアドレス下位バイト、データ転送先のアドレス下位バイトには、DMA_1の場合DFB_1_STAGEAH_PTRが、DMA_2の場合DFB_1_HOLDAH_PTRとVDAC8_1_Data_PTRが設定されています。
CyDmaChSetInitialTd();
DMAチャネルとTDを関連づけます。(最初のTDをDMAチャネルにつなぐ)
CyDmaChEnable();
DMAチャネルをイネーブルにします。
これでDMAにトリガがかかれば転送が開始されます。
以下は実際のDMAチャネルとTDの設定例(main.c Task_SINGEN()の一部)です。
// Variable declarations for DMA_1 and DMA_2
uint8 DMA_1_Chan; //DMA_1のチャネル変数
uint8 DMA_1_TD[1]; //DMA_1のTD 1要素の配列
uint8 DMA_2_Chan; //DMA_2のチャネル変数
uint8 DMA_2_TD[1]; //DMA_2のTD 1要素の配列
//
// DMA Configuration for DMA_2 //DAコンバータ側のDMA
#define DMA_2_BYTES_PER_BURST 1
#define DMA_2_REQUEST_PER_BURST 1
#define DMA_2_SRC_BASE (CYDEV_PERIPH_BASE)
#define DMA_2_DST_BASE (CYDEV_PERIPH_BASE)
DMA_2_Chan = DMA_2_DmaInitialize(DMA_2_BYTES_PER_BURST,
DMA_2_REQUEST_PER_BURST, HI16(DMA_2_SRC_BASE),
HI16(DMA_2_DST_BASE));
DMA_2_TD[0] = CyDmaTdAllocate();
CyDmaTdSetConfiguration(DMA_2_TD[0], 2, DMA_INVALID_TD, 0);
CyDmaTdSetAddress(DMA_2_TD[0], LO16((uint32)DFB_1_HOLDAH_PTR), LO16((uint32)VDAC8_1_Data_PTR));
CyDmaChSetInitialTd(DMA_2_Chan, DMA_2_TD[0]);
CyDmaChEnable(DMA_2_Chan, 1);
//
// DMA Configuration for DMA_1 //DFB入力側のDMA
#define DMA_1_BYTES_PER_BURST 1
#define DMA_1_REQUEST_PER_BURST 1
#define DMA_1_SRC_BASE (CYDEV_PERIPH_BASE)
#define DMA_1_DST_BASE (CYDEV_PERIPH_BASE)
DMA_1_Chan = DMA_1_DmaInitialize(DMA_1_BYTES_PER_BURST,
DMA_1_REQUEST_PER_BURST, HI16(DMA_1_SRC_BASE),
HI16(DMA_1_DST_BASE));
DMA_1_TD[0] = CyDmaTdAllocate();
CyDmaTdSetConfiguration(DMA_1_TD[0], 1, DMA_INVALID_TD, 0);
CyDmaTdSetAddress(DMA_1_TD[0],
LO16((uint32)DFB_1_STAGEAH_PTR),
LO16((uint32)DFB_1_STAGEAH_PTR));
CyDmaChSetInitialTd(DMA_1_Chan, DMA_1_TD[0]);
CyDmaChEnable(DMA_1_Chan, 1);
(JF1VRR)