- ZYBOでシンセサイザー作成(1)LチカとHelloWorld
- ZYBOでシンセサイザー作成(2)オーディオコーデックの使用
- ZYBOでシンセサイザー作成(3)波形テーブル(BRAM)による正弦波の合成
- ZYBOでシンセサイザー作成(4)パラシリ変換 IP の作成
- ZYBOでシンセサイザー作成(5)音を出力する
今回から徐々にシンセサイザーの作成を始めます。今回は、IICでオーディオコーデック(SSM2603)のレジスタ設定をして、コーデックで音を折り返します。
環境
- パソコン:Windows10 64 bit
- Vivado 2020.2 をインストール
- Xilinx Vitis 2020.2 をインストール
- ボード:Zybo Z7-20
- 入力デバイス:パソコンのライン出力 or マイク(ELECOM HS-MC06BK)
- 出力デバイス:イヤホン or スピーカー
システムの構成
今回作成するシステムの構成が図1です。「AXI GPIO」については省略しています。赤線が音データの経路です。「AXI IIC」を追加して、IIC通信で音を折り返すようにコーデック(SSM2603)を操作します。ADCとDACの電源は切っておきます。今回はDACを使いませんので、MUTEは使えませんが(DACの出力をミュートする機能であるため)、ミュート用のスイッチも次回のために付けておきます。
AXI IICとSWの追加(Vivado)
前回作成したLチカとHelloworldの回路に「AXI IIC」と「SW」の回路を追加します。
AXI IICの追加
「Open Block Design」で前回作成したダイアグラムを開きます。ダイアグラムが開いたら、「Add IP」で「AXI IIC」 を追加します(図2)。
追加したら、「Run Connection Automation」をクリックして自動接続をします。「IIC」と「S-AXI」をチェックONします。また「IIC」を選択して、「Select Board Part Interface」を「Custom」にして(図3)、「OK」をクリックします。
自動接続後は、ダイアグラムは図4のようになります。
つづいて、IICの出力ポート名を「iic_rtl」→「iic」に変更しておきます(図5)。
これで「AXI IIC」の接続は完了です。
注意:前回の記事のように「AXI IIC」の「+」部分を展開して、「Make External」をしてしまうと、双方向通信のためのIOBUFがラッパーファイルに追加されないため、自動接続の際にポートも作っています。
ミュート用のスイッチ(SW)追加
スイッチの追加といっても、スイッチの入力ポートとMUTE用の出力ポートをつなぐだけです。まず、ダイアグラムの何もないところで右クリックして、「Create Port...」を選択します(図6)。
スイッチの入力ポートは以下のように設定して(図7)、入力ポートを作成します。
- Port Name:sw
- Direction:Input
- Type:Other
- Create vector:チェックON from 0 to 0
ミュートの出力ポートは以下のように設定して(図8)、出力ポートを作成します。
- Port Name:ac_muten
- Direction:Output
- Type:Other
- Create vector:チェックOFF
ポートが作成できたら、入力ポート「sw[0:0]」と出力ポート「ac_muten」を接続します(図9)。
ダイアグラムが作り終わったら、「Validate Design」で回路のチェックを行います。つづいて、Sourcesペインで「design_1」を右クリックして、「Create HDL Wrapper...」を選択します。ラッパーを作成するときは、「Copy generated wrapper to allow user edits」を選択します。
制約ファイルの修正
制約ファイル(*.xdc)の修正をします。作成したラッパー(design_1_wrapper.v)をみると、以下のような記述があります。
output [0:0]ac_muten; inout iic_scl_io; inout iic_sda_io; output [3:0]led; input [0:0]sw;
「led」以外が追加されたポートなので、4つのポートの定義を行います。
swのポート定義は12行目付近にありますので、「sw[0]」の定義のコメントを外します。
##Switches set_property -dict { PACKAGE_PIN G15 IOSTANDARD LVCMOS33 } [get_ports { sw[0] }]; #IO_L19N_T3_VREF_35 Sch=sw[0] #set_property -dict { PACKAGE_PIN P15 IOSTANDARD LVCMOS33 } [get_ports { sw[1] }]; #IO_L24P_T3_34 Sch=sw[1] #set_property -dict { PACKAGE_PIN W13 IOSTANDARD LVCMOS33 } [get_ports { sw[2] }]; #IO_L4N_T0_34 Sch=sw[2] #set_property -dict { PACKAGE_PIN T16 IOSTANDARD LVCMOS33 } [get_ports { sw[3] }]; #IO_L9P_T1_DQS_34 Sch=sw[3]
オーディオコーデックに関するポートの定義は44行目付近にありますので、「ac_muten」と「ac_scl」と「ac_sda」のコメントを外します。ポート名は「ac_scl」→「iic_scl_io」、「ac_sda」→「iic_sda_io」と変更します。
##Audio Codec #set_property -dict { PACKAGE_PIN R19 IOSTANDARD LVCMOS33 } [get_ports { ac_bclk }]; #IO_0_34 Sch=ac_bclk #set_property -dict { PACKAGE_PIN R17 IOSTANDARD LVCMOS33 } [get_ports { ac_mclk }]; #IO_L19N_T3_VREF_34 Sch=ac_mclk set_property -dict { PACKAGE_PIN P18 IOSTANDARD LVCMOS33 } [get_ports { ac_muten }]; #IO_L23N_T3_34 Sch=ac_muten #set_property -dict { PACKAGE_PIN R18 IOSTANDARD LVCMOS33 } [get_ports { ac_pbdat }]; #IO_L20N_T3_34 Sch=ac_pbdat #set_property -dict { PACKAGE_PIN T19 IOSTANDARD LVCMOS33 } [get_ports { ac_pblrc }]; #IO_25_34 Sch=ac_pblrc #set_property -dict { PACKAGE_PIN R16 IOSTANDARD LVCMOS33 } [get_ports { ac_recdat }]; #IO_L19P_T3_34 Sch=ac_recdat #set_property -dict { PACKAGE_PIN Y18 IOSTANDARD LVCMOS33 } [get_ports { ac_reclrc }]; #IO_L17P_T2_34 Sch=ac_reclrc set_property -dict { PACKAGE_PIN N18 IOSTANDARD LVCMOS33 } [get_ports { iic_scl_io }]; #IO_L13P_T2_MRCC_34 Sch=ac_scl set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { iic_sda_io }]; #IO_L23P_T3_34 Sch=ac_sda
修正が終わったら、制約ファイルを保存してビットストリームを作成します。ビットストリームが作成できたら、「Export Hardware Platform」でVitisに渡す回路情報を作成します。
IICでコーデックのレジスタ設定(Vitis)
Vitisを起動して、IICでコーデックのレジスタ設定を行います。
プラットフォームプロジェクトの更新
ハードウェアが更新されたので、Vitisにそれを反映させます。Vitisでプラットフォームプロジェクトを選択して、マウス右ボタンから「Update Hardware Specification」を実行します(図10)。プラットフォームファイルを選択して、「OK」をクリックします(図11)。
あとは、アプリケーションプロジェクトを選択してビルドすれば、プラットフォームプロジェクトも再ビルドされます。
アプリケーションプロジェクトは前回「helloworld」という名前で作りましたが、今回からシンセサイザーをつくっていくので、「zybo_synthesizer」という名前で作りなおしました。テンプレートは前回と同じ「Empty Application」です。
プログラムの作成
プログラム名を「main.c」として、ソースコードを追加します。追加したら、以下のプログラムを記述します。「xiic.h」が定義されてないというエラーが出る場合は、プラットフォームプロジェクトが更新されていないので、更新しておいてください。記述したら、プロジェクトをビルドしてください。
#include "xparameters.h" #include "xiic.h" #include "xil_printf.h" #include <sleep.h> enum adauRegisterAddresses { R0_LEFT_ADC_VOL = 0x00, R1_RIGHT_ADC_VOL = 0x01, R2_LEFT_DAC_VOL = 0x02, R3_RIGHT_DAC_VOL = 0x03, R4_ANALOG_PATH = 0x04, R5_DIGITAL_PATH = 0x05, R6_POWER_MGMT = 0x06, R7_DIGITAL_IF = 0x07, R8_SAMPLE_RATE = 0x08, R9_ACTIVE = 0x09, R15_SOFTWARE_RESET = 0x0F, R16_ALC_CONTROL_1 = 0x10, R17_ALC_CONTROL_2 = 0x11, R18_ALC_CONTROL_2 = 0x12 }; int CodecWrite(XIic*, u8 Address, u16 data); XStatus CodecInit(XIic *Iic); int main(void){ XIic Iic; int status; xil_printf("IIC Start\n"); // Codec Initialization status = CodecInit(&Iic); if(status != XST_SUCCESS) { xil_printf("Error Codec Initialization"); return XST_FAILURE; } xil_printf("IIC Finished\n"); return XST_SUCCESS; } // Write Audio Codec Register int CodecWrite(XIic* Iic, u8 Address, u16 data){ u8 Device_Address = 0x1A; // Device ID UINTPTR BaseAddress = Iic->BaseAddress; // AXI IIC BaseAddress int num; // Number of Data sent // set write data u8 WR_data[2]; Address = ((Address<<1) & 0xFE); WR_data[0] = Address + ((data>>8)&0x01); WR_data[1] = (data&0xFF); // send data num = XIic_Send(BaseAddress, Device_Address, WR_data, 2, XIIC_STOP); if(num!=2){ xil_printf("Writing data Failed\r\n"); return XST_FAILURE; } return XST_SUCCESS; } // Audio Codec Initialization XStatus CodecInit(XIic* Iic){ int status; // Initializes XIic instance. status = XIic_Initialize(Iic, XPAR_IIC_0_DEVICE_ID); if (status != XST_SUCCESS) { return XST_FAILURE; } // Codec register settings CodecWrite(Iic, R6_POWER_MGMT, 0x3D); // power on CodecWrite(Iic, R0_LEFT_ADC_VOL, 0x97); CodecWrite(Iic, R1_RIGHT_ADC_VOL, 0x97); CodecWrite(Iic, R2_LEFT_DAC_VOL, 0x79); CodecWrite(Iic, R3_RIGHT_DAC_VOL, 0x79); CodecWrite(Iic, R4_ANALOG_PATH, 0x2B); CodecWrite(Iic, R5_DIGITAL_PATH, 0x08); CodecWrite(Iic, R7_DIGITAL_IF, 0x0A); CodecWrite(Iic, R8_SAMPLE_RATE, 0x00); usleep(80000); // wait for charging capacity on the VMID pin CodecWrite(Iic, R9_ACTIVE, 0x01); // digital core active CodecWrite(Iic, R6_POWER_MGMT, 0x2D); return XST_SUCCESS; }
プログラムの説明
わかりにくいと思われるプログラム箇所について説明していきたいと思います。
レジスタアドレスとレジスタの役割の関連づけ
各レジスタの役割がわかりにくいので、列挙型(enum)を使ってレジスタアドレスとレジスタの役割を関連付けています。
enum adauRegisterAddresses { R0_LEFT_ADC_VOL = 0x00, R1_RIGHT_ADC_VOL = 0x01, ︙ R18_ALC_CONTROL_2 = 0x12 };
メイン関数
メイン関数では、大まかに以下のようなことを行っています。
- 「IIC Start」をシリアル転送
- 「CodecInit」関数を用いて、オーディオコーデックで音を折り返すように設定
- 「IIC Finished」をシリアル転送
1と3のシリアル転送についてはプログラムが動いたことをわかりやすくするために、記述しています。
コーデックの設定箇所は以下です。
// Codec Initialization status = CodecInit(&Iic); if(status != XST_SUCCESS) { xil_printf("Error Codec Initialization"); return XST_FAILURE; }
オーディオコーデックを初期化して、失敗したら中断するプログラムとなっています。
コーデックの初期化(CodecInit関数)
「CodecInit」関数では、大まかに以下のようなことを行っています。
XIicインスタンス変数を初期化している箇所は以下です。
// Initializes XIic instance. status = XIic_Initialize(Iic, XPAR_IIC_0_DEVICE_ID); if (status != XST_SUCCESS) { return XST_FAILURE; }
この記述でデバイスIDに基づいて、XIicインスタンス変数の「Iic」に「AXI IIC」の設定やアドレスなどが代入されます。XIic_Initialize 関数は Xilinx によって用意されている API 関数です。
コーデックのレジスタを書き込んでいる箇所は以下です。
// Codec register settings CodecWrite(Iic, R6_POWER_MGMT, 0x3D); // power on ︙ CodecWrite(Iic, R4_ANALOG_PATH, 0x2B); ︙ usleep(80000); // wait for charging capacity on the VMID pin CodecWrite(Iic, R9_ACTIVE, 0x01); // digital core active CodecWrite(Iic, R6_POWER_MGMT, 0x2D); return XST_SUCCESS; }
レジスタの設定はSSM2603のデータシートにおける「CONTROL REGISTER SEQUENCING」の手順どおりに行っています。 その手順が以下です。
- R6レジスタを書き込んで、「Out」ビットを除いて、使用するものに電源を入れる
- 設定が必要なレジスタに書き込む
- デカップリングコンデンサに電源が供給されるまで待つ
- R9レジスタの「active」ビットとR6レジスタの「Out」ビットをセットする
1に関する記述箇所は以下です。
CodecWrite(Iic, R6_POWER_MGMT, 0x3D); // power on
モードを「Sidetone 」として使うので、R6レジスタの「MIC」、「CLKOUT」、「PWROFF」ビットを「0」にして電源ONします。
2に関する記述箇所は以下です。
CodecWrite(Iic, R4_ANALOG_PATH, 0x2B);
R4レジスタで「SIDETONE_EN」と「MICBOOST」をデフォルト値から変更しています。 それぞれのビットの説明は以下です。
- SIDETONE_EN:「1」でサイドトーンを可能にさせる
- MICBOOST:「1」でマイクのゲインを20dB上げる
「MICBOOST」を使わないと、音の折り返しがあるかわかりづらいので、「MICBOOST」をセットしました。 また、他のレジスタも書き込んでいますが、デフォルト値から変更していません。次回、音を再生するために書いておきました。
3に関する記述箇所は以下です。
usleep(80000); // wait for charging capacity on the VMID pin
usleep関数を使って、80000 us 待機しています。
データシートによると
だけ待機すればよいということでした。はVMINピンとGNDの間にあるコンデンサの容量です。ZYBO Z7-20の回路図を見ると、100nFと10uFのコンデンサがありますので、72142us だけ待てばよいということになります。念のために80000 us (80 ms) 待っています。
4に関する記述箇所は以下です。
CodecWrite(Iic, R9_ACTIVE, 0x01); // digital core active CodecWrite(Iic, R6_POWER_MGMT, 0x2D);
R9レジスタに0x01を書き込んで、R6レジスタを0x3D→0x2Dに変更することでオーディオコーデックを動作させています。
コーデックのレジスタの書き込み(CodecWrite関数)
レジスタにデータを書き込むには図12のようにしてデータを送信します。
- S/P = START/STOP BIT.
- A0 = I2C R/W BIT.
- A(S) = ACKNOWLEDGE BY SLAVE.
DEVICE ADDRESSの設定は以下で記述しています。
u8 Device_Address = 0x1A; // Device ID
SSM2603のDEVICE ADDRESSは「CSB」ピンが「L」ならば0x1A (0b0011010)、「H」ならば0x1B (0b0011011)です。ZYBO z7の回路図を見ると、CSBピンはGNDに接続されていますので、0x1Aとしています。
[B0-B15]のRESISTER ADDRESSとRESISTER DATAの設定は以下で記述しています。
// set write date u8 WR_data[2]; Address = ((Address<<1) & 0xFE); WR_data[0] = Address + ((data>>8)&0x01); WR_data[1] = (data&0xFF);
シフトとマスクを使って、アドレスとデータを形式にあうようにセットしています。
IICを使用した送信は以下の記述で行っています。
// send data num = XIic_Send(BaseAddress, Device_Address, WR_data, 2, XIIC_STOP);
XIic_Send 関数は Xilinx によって用意されている API 関数です。
各引数は以下のようになっています。
- UINTPTR BaseAddress:AXI IICのベースアドレス
- u8 Address:デバイスID
- u8 * BufferPtr:送信するデータのアドレス
- unsigned ByteCount:送信するデータの数
- u8 Option:送信後の動作
プログラムの実行
プログラムの実行前に以下のことを行います。
- ZyboのMIC INにマイクまたはPCのライン出力を接続
- ZyboのHPH OUTにスピーカーまたはイヤホンを接続
- ZyboとPCをUSBで接続
- シリアルポートを接続(面倒くさい場合はしなくてもいいです)
MIC INにPCのライン出力を接続する場合は、MICBOOSTで音量がかなり大きくなるのでPCの音量を下げておいてください。自分は爆音でビビりました。
前回は「Debug」で回路構築してから、「resume」でプログラムを実行しましたが、「Run」をクリックすることで回路構築とプログラムの実行が同時に行うことができます(図13)。
マイクをつないでいる場合は、声を出したりするとスピーカーから音声が返ってきます(図14)。また、PCのライン出力につないだ場合も、Youtubeなどを再生するとスピーカーから音が返ってきます。
おわりに
今回で、オーディオコーデックを使うことができました。次回はBRAMを使用して正弦波合成を行い、シミュレーションで確認しようと思います。