ZYBOでシンセサイザー作成(2)オーディオコーデックの使用

今回から徐々にシンセサイザーの作成を始めます。今回は、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の出力をミュートする機能であるため)、ミュート用のスイッチも次回のために付けておきます。

図1:音を折り返すシステムの構成
図1:音を折り返すシステムの構成

AXI IICとSWの追加(Vivado)

前回作成したLチカとHelloworldの回路に「AXI IIC」と「SW」の回路を追加します。

AXI IICの追加

「Open Block Design」で前回作成したダイアグラムを開きます。ダイアグラムが開いたら、「Add IP」で「AXI IIC」 を追加します(図2)。

図2:AXI IICの追加
図2:AXI IICの追加

追加したら、「Run Connection Automation」をクリックして自動接続をします。「IIC」と「S-AXI」をチェックONします。また「IIC」を選択して、「Select Board Part Interface」を「Custom」にして(図3)、「OK」をクリックします。
自動接続後は、ダイアグラムは図4のようになります。

図3:AXI IIC の自動接続の設定
図3:AXI IIC の自動接続の設定

図4:AXI IICの自動接続後のダイグラム
図4:AXI IICの自動接続後のダイグラム

つづいて、IICの出力ポート名を「iic_rtl」→「iic」に変更しておきます(図5)。
これで「AXI IIC」の接続は完了です。

図5:IICポート名の変更
図5:IICポート名の変更

注意:前回の記事のように「AXI IIC」の「+」部分を展開して、「Make External」をしてしまうと、双方向通信のためのIOBUFがラッパーファイルに追加されないため、自動接続の際にポートも作っています。

ミュート用のスイッチ(SW)追加

スイッチの追加といっても、スイッチの入力ポートとMUTE用の出力ポートをつなぐだけです。まず、ダイアグラムの何もないところで右クリックして、「Create Port...」を選択します(図6)。

図6:「Create Port...」コマンド
図6:「Create Port...」コマンド

スイッチの入力ポートは以下のように設定して(図7)、入力ポートを作成します。

  • Port Name:sw
  • Direction:Input
  • Type:Other
  • Create vector:チェックON   from 0 to 0

図7:「swポートの設定」
図7:「sw」ポートの設定

ミュートの出力ポートは以下のように設定して(図8)、出力ポートを作成します。

  • Port Name:ac_muten
  • Direction:Output
  • Type:Other
  • Create vector:チェックOFF

図8:「ac_muten」ポートの設定
図8:「ac_muten」ポートの設定

ポートが作成できたら、入力ポート「sw[0:0]」と出力ポート「ac_muten」を接続します(図9)。

図9:最終的なダイアグラム
図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)。

図10:「Update Hardware Specification」コマンド
図10:「Update Hardware Specification」コマンド

図11:プラットフォームファイルの選択
図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
};

メイン関数

メイン関数では、大まかに以下のようなことを行っています。

  1. 「IIC Start」をシリアル転送
  2. 「CodecInit」関数を用いて、オーディオコーデックで音を折り返すように設定
  3. 「IIC Finished」をシリアル転送

1と3のシリアル転送についてはプログラムが動いたことをわかりやすくするために、記述しています。

コーデックの設定箇所は以下です。

// Codec Initialization
status = CodecInit(&Iic);
if(status != XST_SUCCESS) {
    xil_printf("Error Codec Initialization");
    return XST_FAILURE;
}

オーディオコーデックを初期化して、失敗したら中断するプログラムとなっています。

コーデックの初期化(CodecInit関数)

「CodecInit」関数では、大まかに以下のようなことを行っています。

  1. XIicインスタンス変数の初期化
  2. コーデックの各レジスタの書き込み

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」の手順どおりに行っています。 その手順が以下です。

  1. R6レジスタを書き込んで、「Out」ビットを除いて、使用するものに電源を入れる
  2. 設定が必要なレジスタに書き込む
  3. カップリングコンデンサに電源が供給されるまで待つ
  4. 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 待機しています。

データシートによると

\displaystyle{
t = C \times 25000 / 3.5
}

だけ待機すればよいということでした。 Cは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のようにしてデータを送信します。

図12:書き込みシーケンス
図12:書き込みシーケンス(SSM2608のデータシートから引用)

  • 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 関数です。

各引数は以下のようになっています。

  1. UINTPTR BaseAddress:AXI IICのベースアドレス
  2. u8 Address:デバイスID
  3. u8 * BufferPtr:送信するデータのアドレス
  4. unsigned ByteCount:送信するデータの数
  5. u8 Option:送信後の動作

プログラムの実行

プログラムの実行前に以下のことを行います。

  • ZyboのMIC INにマイクまたはPCのライン出力を接続
  • ZyboのHPH OUTにスピーカーまたはイヤホンを接続
  • ZyboとPCをUSBで接続
  • シリアルポートを接続(面倒くさい場合はしなくてもいいです)

MIC INにPCのライン出力を接続する場合は、MICBOOSTで音量がかなり大きくなるのでPCの音量を下げておいてください。自分は爆音でビビりました。

前回は「Debug」で回路構築してから、「resume」でプログラムを実行しましたが、「Run」をクリックすることで回路構築とプログラムの実行が同時に行うことができます(図13)。

図13:「Run」でプログラムの実行
図13:「Run」でプログラムの実行

マイクをつないでいる場合は、声を出したりするとスピーカーから音声が返ってきます(図14)。また、PCのライン出力につないだ場合も、Youtubeなどを再生するとスピーカーから音が返ってきます。

図14:音声を折り返している様子
図14:音声を折り返している様子

おわりに

今回で、オーディオコーデックを使うことができました。次回はBRAMを使用して正弦波合成を行い、シミュレーションで確認しようと思います。

参考文献

お問い合わせフォーム プライバシーポリシー

© 2021 Setoti All rights reserved.