ねっちゅう音響処理工房

音響処理やFPGAによる処理について主に書きます

ZYBOでシンセサイザー作成(3)波形テーブル(BRAM)による正弦波の合成

今回は、波形テーブル(BRAM)を使って正弦波を合成するIP (Intellectual Property) を製作します。前回まではハードウェア記述言語(HDL)を用いずにFPGAを使っていましたが、今回からHDLのSystemVerilog を用いていきたいと思います。

環境

  • パソコン:Windows10 64 bit
    • Vivado 2020.2 をインストール
    • Xilinx Vitis 2020.2 をインストール
  • ボード:Zybo Z7-20

波形テーブルによる合成について

SystemVerilogでは三角関数でさえ計算するのに苦労します。そこで、波形テーブル(BRAM)を使用して正弦波を合成します。一応、Xilinx社が三角関数を計算する IP を提供していますが、波形テーブルによる合成は様々な状況で応用が効くと思いますので、こちらの方法で合成します。

波形テーブルによる合成の例

波形テーブルによる合成では、図1のような1周期の波形をRAMの中に保存しておきます。図1の場合、2048個のデータを保存する必要があります。この波形テーブルから順次データを取り出すことによって任意周波数の波形を合成することができます。

図1:正弦波の波形テーブル

例えば、サンプリングレート  f_s が48kHzの場合、周波数 f が12kHzの波形を合成したいなら、0番目、512番目、1024番目、1536番目、0番目...と取り出せばよいです。

任意周波数の正弦波の合成

周波数  f の信号を合成したい場合、データを取り出す波形テーブルの番号 p は以下の式で計算されます [1]。

\displaystyle{
p = {\rm mod} \left(\frac{nfN}{f_s}, N\right)
}

ここで、 f_sはサンプリング周波数、 N は波形テーブルのデータ数、nはサンプル番号です。 {\rm mod}(x, N) というのは「 x Nで割ったときの余り」です。この式をSystemVerilogで計算する必要があります。

正弦波合成 IP の製作

VivadoでBRAMを使った正弦波合成 IP を製作していきます。

正弦波合成 IPのフォルダの用意

まず、以下のようにフォルダを作成して、ファイルを配置していきます。 sin_gen.sv と SinTbl.coe はこちらの GitHub3rdフォルダの中にあります。

ip_repo_synth
└── sin_gen
    ├── HDL
    │   └── sin_gen.sv
    └── ROMDATA
        └── SinTbl.coe

sin_gen.svの内容を以下に示します。sin_gen.svの内容については別の記事でしたいと思います。

module sin_gen(
    input logic clk96M,
    input logic reset,
    input logic [14:0] freq,
    output logic [15:0] dout
    );
    
    logic [20:0] cnt;      // Int 21bit
    logic [10:0] cnt2000;
    logic cnt2000_flag;
    logic [14:0] freq_ff;  // Int 15bit
    logic chg_freq_flag;
    logic [35:0] p_int;    // Int 36bit
    logic [39:0] p_int_ff; // Int 40bit 
    logic [50:0] p;        // Int 32bit  Decimal 19bit
    logic [10:0] p_ff;     // Int 11bit
    
    localparam tmp = 11'd1398; // Int 1bit, Decimal 10bit
    
    // cnt2000
    always_ff @(posedge clk96M) begin
        if(reset)
            cnt2000 <= 11'd0;
        else if(cnt2000_flag)
            cnt2000 <= 11'd0;
        else
            cnt2000 <= cnt2000 + 11'd1;
    end
    
    // cnt2000_flga
    assign cnt2000_flag = (cnt2000 == 11'd1999);
    
    // freq_ff
    always_ff @(posedge clk96M) begin
        if(reset)
            freq_ff <= 15'd0;
        else
            freq_ff <= freq;
    end
            
    // chg_freq_flag
    assign chg_freq_flag = (freq != freq_ff);
    
    // cnt
    always_ff @(posedge clk96M) begin
        if(reset)
            cnt <= 21'd0;
        else if(chg_freq_flag)
            cnt <= 21'd0;
        else if(cnt2000_flag)
            cnt <= cnt + 21'd1; 
        else
            cnt <= cnt;
    end 
    
    // p_int
    assign p_int = cnt*freq;
    
    // p_int_ff
    always_ff @(posedge clk96M) begin
        if(reset)
            p_int_ff <= 40'd0;
        else
            p_int_ff <= {p_int, 4'd0};
    end
            
    // p
    assign p = p_int_ff * tmp;
    
    // p_ff
    always_ff @(posedge clk96M) begin
        if(reset)
            p_ff <= 11'd0;
        else
            p_ff <= p[30:19];
    end
    
    // BRAM
    WAVE_TBL WAVE_TBL(
        .addra (p_ff),
        .douta (dout),
        .clka (clk96M)
    );
endmodule

SinTbl.coeの内容を以下に示します。振幅16384、波形テーブルのデータ数  N=2048 の正弦波の波形テーブルとなっています。Pythonを用いて符号付き整数で出力しました。

memory_initialization_radix=16;
memory_initialization_vector=
0000,
0032,
0065,
0097,
00c9,
…
ffce;

VivadoでIPの製作

プロジェクトを以下のようにして作成します。FPGAに書き込むためのプロジェクトではないので、制約ファイルは作らなくていいです。

  • 作業フォルダ:sin_wave_gen
  • プロジェクト名:sin_wave_gen

このプロジェクトでIPの製作を行い、IPのシミュレーションも行います。

プロジェクトができたら、「Tools」→「Create and Package New IP...」を実行します。

図2:「Create and Package New IP...」を実行

最初の画面(図3)では、そのまま「Next」をクリックします。

図3:そのまま「Next」をクリックする

「Packaging Options」(図4)では、「Package a specified directrory」を選択して、「Next」をクリックします。これでフォルダを指定することでIPが作成されます。

図4:「Package a specified directory」を選択する

IPのフォルダはさきほど作成した「sin_gen」を選択して、「Next」をクリックします(図5)。自分は「ip_repo_synth」というシンセサイザー用のIPリポジトリに「sin_gen」を置いてありますので、それを選択しました。

図5:IPのフォルダを選択

次の画面(図6)では、IPを編集するプロジェクト名の入力をします。ここはデフォルトのまま「Next」をクリックします。

図6:IPを編集するプロジェクト名の入力

最後の画面(図7)では、そのまま「Finish」をクリックします。HDLのSystemVerilogファイルにエラーがある場合、エラーが生じて、IPが作成できないので気をつけてください。

図7:「Finish」をクリックする

IP編集用のVivado画面が図8のように起動され、「sin_gen/HDL」フォルダ内の回路記述が自動で読み込まれます。起動した画面の「Sources」ペインでは「WAVE_TBL」に?マークがあり、プロジェクトにBRAMが不足しています。そのため、BRAMを追加していきます。

図8:IP編集用のVivado画面

BRAMを追加するには「PROJECT MANAGER」→「IP Catalog」を実行します。IPカタログ内の「Memories & Storage Elements」→「RAMs & ROMs & BRAM」→「Block Memory Generator」をダブルクリックします。

図9:「Block Memory Generator」を実行

BRAM生成用のウィザードではタブごとに設定を変更していきます。まず、「Basic」タブ(図10)では以下のように設定を変更します。

  • Component Name: WAVE_TBL
  • Memory Type: Single Port ROM

「Single Port ROM」とすることで、1ポートのROMとなります。RAMでもよいのですが、信号線が多くなるのでROMにしています。

図10:「Basic」タブでの設定

「Port A Options」タブでは、以下のように設定を変更します。

  • Port A Depth: 2048
  • Enable Port Type: Always Enabled
  • Primitives Output Register: チェックOFF

「Port A Depth」で波形テーブルのデータ数を設定しています。「Enable Port Type」では「Always Enabled」にすることでイネーブル信号を不要としています。また「Primitives Output Register」をチェックOFFすることで出力にレジスタ(FF)を付けないようにしています。

図11:「Port A Options」タブでの設定

「Other Options」タブでは、以下のように設定を変更します。

  • Load Init File: チェックON
  • Coe File: sin_gen/ROMDATA/SinTbl.coe を選択

これで、SinTbl.coeに記述してあるデータがBRAMにロードされます。

図12:「Other Options」タブでの設定

最後に、「Summary」タブで設定を確認します。以下のようになっていることを確認してください。

  • Memory Type: Single Port ROM
  • Block RAM resource(s)(18K BRAMs): 0
  • Block RAM resource(s)(36K BRAMs): 1
  • Total Port A Read Latency: 1 Cock Cycle(s)
  • Address Width A: 11

波形テーブルのデータ数を2048にすることで、36K BRAMsをちょうど1個使うようにしています。

図13:「Summary」タブで確認

保存場所を確認して(図14)、「OK」をクリックします。

図14:保存場所を確認

また、「Create Output Products」の画面では、「Out of context per IP」を選択して、「Generate」をクリックします。「Out of context per IP」の場合、個々のIPごとに論理合成を行うので、全体の論理合成時間が短いというメリットがあります。一方、「Global」の場合、全体の論理合成時にIPを再合成するので、論理合成時間は長いが、生成ファイルが少ないという特徴があります。

図15:「Out of context per IP」を選択

これで、「Package IP」→「Files Groups」の「Merge changes from File Groups Wizard」をクリックして(図16)、「Synthesis(3)」となれば、問題ないです。

図16:「Package IP」の「File Groups」

ただ、基本的にはVivadoの不具合のため「Synthesis(2)」となり、WAVE_TBLが読み込まれません(図17)。

図17:「Merge changes from File Groups Wizard」した後

対策方法としては、一度削除し追加することです。

  1. WAVE_TBLを「Remove File from Project...」する
  2. 「PROJECT MANAGER」→「Add Sources」からWAVE_TBLを追加
  3. 「Merge changes from File Groups Wizard」をクリックすると、「synthesis(3)」となる(図19)

「Add Sources」するときは「Add or create design sources」を選択して、「sin_gen/src/WAVE_TBL/WAVE_TBL.xci」を追加してください。また、Copy sources into IP DirectoryはチェックOFFしてください(図18)。

図18:「Copy sources into IP Directory」をチェックOFF

図19:「Synthesis(3)」となる

最後に「Package IP」→「Review and Package」の「Package IP」をクリックすることでIPが作成されます(図20)。

図20:「Package IP」をクリック

プロジェクトを閉じていいかのダイアログが出ますので(図21)、「OK」をクリックしてください。

図21:プロジェクトを閉じるかのダイアログ

以上でIPが作成され、元のVivadoプロジェクト画面に戻ります。

IP のシミュレーション

「sin_wave_gen」プロジェクトで IP「sin_gen」のシミュレーションをしていきます。

IP インテグレータで回路作成

IPインテグレータで回路を作っていきます。

  1. 「IP INTEGRATOR」→「Create Block Design」で「Diagram」を作成
  2. 「Add IP」で「sin_gen_v1_0」を追加
  3. 「sin_gen_v1_0」の全てのポートに対して「Make External」を実行
  4. ポート名を「_0」がないように変更

最終的な回路は図22のようになります。

図22:最終的な IP インテグレータ

回路ができたら、「Validate Design」をして、回路のチェックを行います。また、「Create HDL Wrapper」でラッパーを作成します。いつも通り「Copy generated wrapper to allow user edits」にしてください。

テストベンチの追加

回路のシミュレーションを行うためのファイルであるテストベンチをプロジェクトに追加します。追加するテストベンチ「sim_sin_gen.sv」はGitHubにあります。

このテストベンチをプロジェクトディレクトリの中に置いて、読み込みます。読み込む手順は以下です。

  1. 「PROJECT MANAGER」→「Add Sources」を実行
  2. 「Add or create simulation sources」を選択
  3. 「Add Files」で「sim_sin_gen.sv」を選択
  4. 「Scan and add RTL include files into project」と「Include all design sources for simulation」をチェックON(図23)
  5. 「Finish」をクリックする

図23:ファイル追加時のチェック

シミュレーションの実行

シミュレーションを実行をします。シミュレーションは「SIMULATION」→「Run Simulation」→「Run Behavioral Simulation」で実行できます(図24)。シミュレーションファイルにエラーがある場合は、実行できないので気をつけてください。

図24:シミュレーションの実行

シミュレーションの実行が上手くいくと図25のようになります。1nsまではシミュレーションが実行されています。「Run All」ボタンをクリックすることで、全てのシミュレーションが実行されます。

図25:シミュレーション時の画面

今回の場合は80msまで実行されます。「Zoom Fit」をクリックすることで、図26のように全体の結果をみることができます。

図26:全体のシミュレーション結果

このままでは、doutが正弦波になっているかわかりづらいので、表示の仕方を以下のように変更します。

  • 「dout」を右クリックして、「Waveform Style」→「Analog」を選択
  • 「Waveform Style」→「Analog Setting」で「Y Range」を「Fixed」にして、Minを-18000、Maxを18000に変更(図27)
  • 「dout」を右クリックして、「Radix」→「Signed Decimal」を選択

図27:「Analog Setting」の画面

表示の仕方を変更すると図28のようになります。正弦波になっていることがわかると思います。

図28:設定変更後の結果
図28:設定変更後の結果

おわりに

今回で正弦波を作成することができました。次回はパラシリ変換器を作成してスピーカーから音を出したいと思います。また、SystemVerilogの説明を全くしていないので、補足記事を書きたいと思います。

参考文献

[1] 波形テーブルの参考文献

[2] IP作成の参考文献
FPGAプログラミング大全 Xilinx編 第2版

FPGAプログラミング大全 Xilinx編 第2版

  • 作者:小林 優
  • 発売日: 2021/01/30
  • メディア: 単行本

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:音を折り返すシステムの構成

AXI IICとSWの追加(Vivado)

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

AXI IICの追加

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

図2:AXI IICの追加

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

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

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

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

図5:IICポート名の変更

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

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

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

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

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

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

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

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

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

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

ポートが作成できたら、入力ポート「sw[0:0]」と出力ポート「ac_muten」を接続します(図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」コマンド

図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 date
    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」の設定やアドレスなどが代入されます。

コーデックのレジスタを書き込んでいる箇所は以下です。

 // 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:書き込みシーケンス(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);

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

  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」でプログラムの実行

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

図14:画像を折り返している様子

おわりに

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

参考文献

ZYBOでシンセサイザー作成(1)LチカとHelloWorld

ZYBOを使ってシンセサイザーを作成したいと思います。久しぶりにVivadoを使うので、まずはLチカとHello Worldからやってみます。主に今回の記事は以下の参考文献をもとに書きました。

FPGAプログラミング大全 Xilinx編 第2版

FPGAプログラミング大全 Xilinx編 第2版

  • 作者:小林 優
  • 発売日: 2021/01/30
  • メディア: 単行本

環境

  • パソコン:Windows10 64 bit
    • Vivado 2020.2 をインストール
    • Xilinx Vitis 2020.2 をインストール
  • ボード:ZYBO Z7-20

Board ファイルのインストール

初期状態ではDigilentのボードであるZYBO-z7-20のボードファイルがないため、ボードファイルをインストールします。DigilentのボードファイルのダウンロードはInstalling Digilent Board Filesの「archive」をクリックすることでダウンロードすることができます。そのzipファイルを解凍して、そのフォルダの中の

「vivado-boards-master\new\board_files\zybo-z7-20」

のフォルダを

「C:\Xilinx\Vivado\<version>\data\boards\board_files」

の下に貼り付けすることでインストールできます。

プロジェクトの作成

まず、「Create New Project」をクリックして、プロジェクトを新規に作成します。

「Create a New Vivado Project」はプロジェクトを作成するためにやることが書いてあるだけなので、そのまま「Next」をクリックします。

「Project Name」では、プロジェクトの名前とプロジェクトの格納フォルダを指定します。私の場合は、

  • プロジェクト名:zybo_synthesizer
  • プロジェクトの格納フォルダ:D:/v20

として、「Create project subdirectory」にチェックしました。「Create project subdirectiory」にチェックONすることで、プロジェクトの格納フォルダの下にプロジェクト名のフォルダが作成され、その下にプロジェクトファイルが作成されます。

「Project type」では、作成するプロジェクトのタイプを指定します。ここでは「RTL Project」を選択します。「Do not specify sources at this time」にもチェックONしておきます。チェックONしておくと、verilogファイルなどのソースファイルを追加する画面を省略します。あとで追加することもできるので基本的にチェックONしておきます。

「Default Part」では、使用するFPGAバイスを選択します。ボードファイルのインストールをしていれば、「Boards」を選択することで、「Display Name」の中に「Zybo Z7-20」があると思います。「Zybo Z7-20」を選択して、「Next」をクリックします。

「New Project Summary」では、プロジェクトの設定内容を確認して、問題なければ「Finish」をクリックします。

回路の作成

Verilogファイルは書かないで、VivadoのIPインテグレータだけを使って、回路の作成をします。

IP インテグレータによる回路作成

◎IP インテグレータの起動
Flow Navigator から 「IP INTEGRATOR」の「Create Block Design」 をクリックします。 小さいウィンドウが出て、「Design Name」 や 「Directory」を変えることができますが、デフォルトのままでいいです。これで「Diagram」のタブができます。「Diagram」で GUI 的に回路作成ができます。

◎IP の配置
Diagramで「ADD IP」をクリックすると、IP(機能ごとにまとまった回路)のリストが表示されますので、「ZYNQ7_Processing_System」をダブルクリックして配置します。また、同じようにして「AXI GPIO」も配置します。

◎IP の接続
IPを配置したら、「Run Block Automation」をクリックします。Zynq の設定が表示されますので、「Apply Board Preset」にチェックがあることを確認して、「OK」をクリックします。「Apply Board Preset」にチェックONすることで、ボード上にあるDDRメモリなどにしたがって Zynq の設定をしてくれます。

「AXI GPIO」をダブルクリックすることで、「AXI GPIO」の設定ができます。「IP Configuration」タブで「All Outputs」にチェックをして、「GPIO Width」を 4 と設定します。「OK」をクリックして設定を終了します。

「Run Connection Automation」をクリックして、Zynq と「AXI GPIO」を自動接続をします。接続の設定では、「GPIO」はチェックOFFし、「S-AXI」にはチェックONして、「OK」をクリックします。

「Run Connection Automation」を実行すると、「Processor System Reset」(リセット信号を生成する回路)と「AXI Interconnect」(AXI のハブ的な回路)が自動生成されて、IP間が接続されます。

つづいて、「AXI GPIO」の「GPIO」端子の「+」部分をクリックすると、端子が展開されて「gpio_io_o[3:0]」が表示されます。これを右クリックして、メニューから「Make External」を実行することで、「AXI GPIO」の出力とFPGAの出力ポートを接続します。

これで回路ができました。

制約ファイル(*.xdc)の読み込みと修正

◎制約ファイル(*.xdc)のダウンロード
FPGA端子の設定をする制約ファイルをダウンロードします。ZYBO Z7用の xdc ファイルは、Digilentページの「Documentaion」→「Master XDC Files」をクリックすると、Digilentの各ボードの制約ファイルがあるGItHubにとぶので、そこからZybo-Z7-Master.xdcをダウンロードします(一つのファイルだけダウンロードすることはできなかったので、他のボードの制約ファイルと一緒にダウンロードしました)。

◎制約ファイル(*.xdc)の読み込み
制約ファイルをプロジェクトに読み込むには、「Project MANAGER」の「Add Sources」をクリックし、「Add or create constraints」を選択します。続いて、さきほどダウンロードしたZybo-Z7-Master.xdcをプロジェクトファイル(*.xpr)と同じ階層にコピーして、それを読み込みます。「Copy constraints files into project」はチェックOFFにしておきます。チェックONにすることで、プロジェクトフォルダのどこかに制約ファイルがコピーされて置かれますが、深い階層に置かれてしまって、制約ファイルの修正や確認がしづらくなるので、私はこのようにしています。「Finish」で制約ファイルが読み込まれます。

◎制約ファイルの修正
次に、制約ファイルの修正をします。今回はLEDを使いたいので、LED設定箇所の27行目から30行目のコメントを外します。

##LEDs
set_property -dict { PACKAGE_PIN M14   IOSTANDARD LVCMOS33 } [get_ports { led[0] }]; #IO_L23P_T3_35 Sch=led[0]
set_property -dict { PACKAGE_PIN M15   IOSTANDARD LVCMOS33 } [get_ports { led[1] }]; #IO_L23N_T3_35 Sch=led[1]
set_property -dict { PACKAGE_PIN G14   IOSTANDARD LVCMOS33 } [get_ports { led[2] }]; #IO_0_35 Sch=led[2]
set_property -dict { PACKAGE_PIN D18   IOSTANDARD LVCMOS33 } [get_ports { led[3] }]; #IO_L3N_T0_DQS_AD1N_35 Sch=led[3]

◎ポート名の変更
制約ファイル上でポート名が「led」になっているので、IPインテグレータでもポート名を「led」にします。「gpio_io_o_0」を選択して、「External Port Properties」ペインで「Name」を「led」に変更します。

上位階層とビットストリームの作成

◎ダイアグラムのチェック
ダイアグラムが完成したら、右クリックのメニューから「Validate Design」を実行して、整合性のチェックをします。回路に問題がなければ、「Validation successful.」を表示します。

もしかしたら、以下のようなクリティカルワーニングが発生するかもしれませんが、Digilentのボードファイルによる問題なので無視しても大丈夫です。

CRITICAL WARNING: [PSU-1]  Parameter : PCW_UIPARAM_DDR_DQS_TO_CLK_DELAY_0 has negative value -0.050 . PS DDR interfaces might fail when entering negative DQS skew values. 
CRITICAL WARNING: [PSU-2]  Parameter : PCW_UIPARAM_DDR_DQS_TO_CLK_DELAY_1 has negative value -0.044 . PS DDR interfaces might fail when entering negative DQS skew values. 
CRITICAL WARNING: [PSU-3]  Parameter : PCW_UIPARAM_DDR_DQS_TO_CLK_DELAY_2 has negative value -0.035 . PS DDR interfaces might fail when entering negative DQS skew values. 
CRITICAL WARNING: [PSU-4]  Parameter : PCW_UIPARAM_DDR_DQS_TO_CLK_DELAY_3 has negative value -0.100 . PS DDR interfaces might fail when entering negative DQS skew values. 

◎上位階層の作成
Sourcesペインにおいて、ダイアグラムで作成した階層「design_1」を選択し、右クリックから「Create HDL Wrapper...」を実行します。「Create HDL Wrapper...」によって、ダイアグラムで作った回路を使用するVerilogファイルが作成されます。「Options」では、「Copy generated wrapper to allow user edits 」を選択します。「Let Vivado manage wrapper and auto-update」を選択すると、自動的にラッパーが更新されるそうですが、経験的にポートが追加されないことがあったので、「Copy generated wrapper to allow user edits 」を選択します。

◎ビットストリームの作成
「PROGRAM AND DEBUG」の「Generate Bitstream」をクリックして、ビットストリームを作成します。論理合成と配置・配線をしていないので、「No Implementation Results Available」のダイアログが出ますが、「Yes」をクリックします。「Launch Runs」では、そのまま「OK」をクリックします。Number of jobsで使用するスレッドの数を指定できますが、デフォルトのままでいいと思います。

正常終了すると、右上のステータスが「write bitstream Complete」となり、次の処理を確認するダイアログが表示されます。「Open Implemented Design」を選択すると、回路規模などを確認することができます。普段、私は「Open Implemented Design」で回路規模を確認することが多いです。回路規模に興味がない人は「Cancel」をクリックで大丈夫です。

Zynq のプログラムを実行

Vitisを使って、Zynqのプログラムを実行していきます。

回路情報のコピーとVitis の起動

◎回路情報のコピー
メニューバーから「File」→「Export」→「Export Hardware...」を実行します。最初の画面では、「Next」をクリックして、「Output」で「Include bitstream」を選択します。これで、Vitisに回路情報を渡すことができます。私は「Files」は以下のように設定しました。

  • 名称:design_1_wrapper(デフォルト設定)
  • 保存先:D:/v20/zybo_synthesizer/zybo_synthesizer.vitis

最後に「Finish」をクリックしてエクスポートします。

◎Vitisの起動
メニューバーから「Tools」→「Launch Vitis IDE」でVitisを起動します。

ワークスペースの場所は先ほど作成したzybo_synthesizer.vitisフォルダを指定し、「Launch」をクリックします。

各種プロジェクトの作成

◎プラットフォームプロジェクトの作成
起動画面から「Create Platform Project」をクリックします。

最初の画面では、プロジェクト名を入力します。プロジェクト名は「zybo_synthesizer_platform」としました。

つぎは「Create a new platform from hardware (XSA)」タブで、Vivadoで作成したプラットフォームファイル(*.xsa)を指定します。「Finish」でプロジェクトが作成されます。

◎アプリケーションプロジェクトの作成
メニューバーから「File」→「New」→「Application Project...」を実行します。

最初の画面では、そのまま「Next」をクリックします。

次の画面では、プラットフォームの選択をします。デフォルトでさきほど作成したプラットフォームが設定されていますので、確認してそのまま「Next」をクリックします。

つづいて、アプリケーションプロジェクト名を入力する画面になります。アプリケーションプロジェクト名は「helloworld」としました。

ドメインの選択では、そのまま「Next」をクリックします。

テンプレートを選択する画面では、「Empty Application」を選択して、「Finish」をクリックします。

プログラムの作成と実行

◎Cファイルの作成
アプリケーションプロジェクト内の「src」フォルダを右クリックして、「New」→「File」をクリックします。ファイル名を入力する画面では、ファイル名を「helloworld.c」として、「Finish」をクリックします。

◎プログラムの記述
プログラムはAPIを用いて記述します。「helloworld.c」に以下のプログラムを記述します。

#include "xparameters.h" // 各IPのパラメータがあるファイル
#include "xgpio.h"       // AXI GPIO用のAPIがあるファイル
#include "xil_printf.h"  // xil_printfを使うためのファイル

int main(void){
    int status;
    XGpio Gpio;

    // Hello World をシリアル転送
    xil_printf("Hello World. \n");

    /* GPIO の初期化 */
    // Gpio変数の初期設定
    status = XGpio_Initialize(&Gpio, XPAR_GPIO_0_DEVICE_ID);
    if(status != XST_SUCCESS) return XST_FAILURE;

    // 入出力の設定
    // 2番目の引数:設定するインターフェース  1:GPIO 2:GPIO2
    // 3番目の引数:各ポートの入出力の設定  0:出力  1:入力
    XGpio_SetDataDirection(&Gpio, 1, 0x0); // 全て出力に設定

    // 信号レベルの設定
    // 2番目の引数:設定するインターフェース  1:GPIO 2:GPIO2
    // 3番目の引数:各ポートの出力レベルの設定   0:Low  1:High
    XGpio_DiscreteWrite(&Gpio, 1, 0xF); // 全てHighに設定
}


◎ビルド
アプリケーションプロジェクトの「helloworld」を選択して、メニューバーから「Project」→「Build Project」を実行して、プロジェクトのビルドを行います。

◎PCとボードの接続
ZYBOのボード上のジャンパーピンを以下のように設定します。

こうすることで以下のように設定されます。

  • 電源供給方法:USBから供給
  • 回路情報やプログラムの実行方法:USB経由でPCから実施

この状態でPCとZYBOをMicro-USBで接続すると、赤色のLEDが点灯します。

デバッグの設定
デバッグ設定は、アプリケーションプロジェクト「helloworld」を選択して、虫マークのプルダウンメニューから「Debug As」→「Lunch on Hardware (Single Application Debug)」で実行する。

実行すると、FPGAに回路情報を渡して、デバッグ時の操作画面に変更されます。

◎シリアルターミナルの設定
シリアルターミナルの設定は、「Vitis Serial Terminal」タブの「+」をクリックし、COMポートと通信速度を設定して「OK」をクリックします。基本的には通信速度はデフォルトで、ポートは選択可能なものを選べばいいと思います。

◎プログラムの実行
プログラムの実行は「Resume」ボタンをクリックすることで行われます。

実行すると、シリアルターミナルには「Hello World」と表示されます。

また、4つのLEDが以下のように点灯します。

おわりに

ZYBOでLチカとHello Worldをしました。まだ、Vitisに慣れないので、慣れていきたいと思います。

Pythonで打楽器音を合成してみた

前回の記事:Pythonで撥弦音を合成してみた

こないだは撥弦音の合成をしたので、次は打楽器音の合成をしました。 前回と同様にKarplus-Strong方式で合成しました。

参考文献

K.Karplus and A. Strong, “Digital Synthesis of Plucked-String and Drum Timbres”, Computer Music Journal, vol. 7, No. 2, pp. 43–55, MIT Press, 1983.

合成方法

(1). 遅延数  p と確率  b を適当に決定。

(2).  x_0 \cdots x_{p} に乱数(白色雑音)を入れる。
       x_n (n\geq p+1) には0を入れる。

(3). 以下の式によってデジタルフィルタをかける。

{\displaystyle 
\begin{equation}
  y(n) =
  \left\{
    \begin{array}{ll}
      \cfrac{1}{2} \{ y(n-p)+y(n-p-1) \} + x_n & 確率\ b\\
      -\cfrac{1}{2} \{ y(n-p)+y(n-p-1) \} + x_n & 確率\ 1-b
    \end{array}
  \right.
\end{equation}
}

プログラム

Karplus Strong方式で打楽器音を合成するプログラムを作りました。

import sys
import scipy.signal as sg
import numpy as np
import soundfile as sf

# コマンドラインの引数を取得
args = sys.argv
if len(args)!=5:
    print("\nusage: python Karplus.py amp filename p b")
    print("   amp        : amplitude")
    print("   filename   : file name of sound")
    print("   p          : Number of delays")
    print("   b          : Probability")
    raise Exception("Argument error ")

amp = float(args[1]) # 初期値の振幅範囲
filename = args[2]   # ファイル名
p = int(args[3])     # 遅延数
b = float(args[4])   # 確率

fs = 48000               # サンプリング周波数
interval = int(fs*0.5)   # 音符が変化する間隔
n_data = interval*7      # データの数
wave = np.zeros(n_data)  # 波形データを入れる場所 

for i in range(7):
    
    # 初期値の乱数生成
    n = i*interval
    wave[n:n+p+1] = (np.random.rand(p+1)-0.5)*amp
    rd = np.random.rand(interval)
    
    # デジタルフィルタに通す
    for m in range(interval):
        if m > p:
            wave[n+m] = 0.5*(wave[n+m-p]+wave[n+m-p-1])
            if rd[m] > b:
                wave[n+m] *= -1.0

# wavで書きだす
sf.write(filename, wave, fs, subtype='PCM_16')

合成した打楽器音

サンプリング周波数  f_s=48 {\rm kHz} p=400 b=0.2, 0.5, 0.8で合成した打楽器音が以下です。

 b=0.2

 b=0.5

 b=0.8

 b が0や1に近いと、撥弦音と打楽器音が混ざったような音のようになりますね。

波形とスぺクトログラムの観察

サンプリング周波数  f_s=48 {\rm kHz} p=400 b=0.5で合成した打楽器音の波形とスぺクトログラムは以下です。

打楽器音の波形とスペクトログラム

撥弦音の場合は、ある特定の周波数成分だけがゆっくり減衰していたのに対して、打楽器音は全ての周波数成分がゆっくり減衰しています。 p を長くすることによって、減衰時間が長くなるそうです。

おわりに

今回は、打楽器音の合成を試してみました。打楽器だけで演奏しても何の曲を演奏しているかわからないと思いますので、他の楽器音と一緒に演奏しようと思います。

 \leftarrow 前回の記事:Pythonで撥弦音を合成してみた

マイクロマウス(旧ハーフサイズ)始めました

マイクロマウス(旧:ハーフサイズ)を始めました。学部生のころ、マイクロマウスを製作するサークルに入っていたのですが、全くわからなくて挫折してしまいました。ただ、いまならできるかなと思ったのでマイクロマウスを始めました。とりあえずの目標は4 x 4迷路の走破です。現在、設計までは終えていて、機械部品と基板を発注しているところです。ちなみに私は機械系ではなく情報系の人間なので、優しい目で見ていただければ幸いです。

コンセプト

コンセプトは「作りやすいマウス」です。このコンセプトのため、全日本マイクロマウス大会のテクニカルデータを見て、IC やモータなどのキーパーツについては一般に使われているものを選びました。また、オシロスコープがないため、電気的トラブルがないようにはんだ付けしやすい IC を選びました。あとは自分で加工する必要がないように機体を設計しました。

機体情報

マイクロマウスの外観
機体名 wakaba
サイズ(長さ x 幅 x 高さ) 60.0mm x 45.0mm x 14.8mm
マイコン RX631 (48pin)
モーター Didel MK06-4.5
モータードライバー DRV8836
壁センサ OSI5FU3A11C & LTR-4206E
ジャイロセンサ MPU6000
エンコーダ AS5047P
ギア比 4.44 (40 : 9)
タイヤ径 14.8mm

機械設計にはFusion 360を使いました。機体は2輪となっています。 センサーホルダーを作って、センサーを固定するつもりです。 また、磁気式エンコーダを使えるようにモーターマウントや磁石マウントを設計しました。 加えて、ホイールはギア付きホイールとなっています。調べてもモジュール0.3で穴径が大きいギアは見つからなかったので、ブログでよく見られたギア付きホイールとしました。センサーホルダー、ホイール、モーターマウント、磁石マウントはDMM.makeに発注しました。素材はアクリル(Ultra Mode)です。

回路図

回路図PDF版

回路についてはサークルの先輩の回路やブログで見られた回路、マウス本の回路を参考にして設計しました。基本的にはコンセプト通りに半田付けしやすい足があるIC を選びました。ただ、モータードライバーのDRV8836とジャイロセンサーのMPU6000は足がないため頑張るしかないです。半田付けを久しぶりにやるので、不安で仕方がありません。

配線図

配線図PDF版(縮尺:2.5倍)

ベタGNDをする前の配線となっています。基本的には0.1524mm幅で配線しました。赤外線LEDやスピーカー、モータードライバーにおける配線幅については0.3048mmや0.4064mmを使用しました。羽のように繋がっている基板は磁気式エンコーダ用の基板です。あとでニッパーでカットして、真ん中の溝に嵌めるつもりです。また、線幅を変え忘れたので見にくいと思いますが、センサーホルダーを差し込むための溝もあります。基板についてはelecrowに発注しました。

おわりに

4月から就職なので、時間がなくなりそうですが少しずつ進めていきたいと思います。また、elecrowの注文でクレジットカード情報を入力していなかったため、面倒くさいことになっているので解決したいと思います。

Cambridge Music Technologyでマルチトラックの音源を提供しているというハナシ

はじめに

機械学習などをやっていると困るのはデータを集めることだと思います。私も音源分離の機械学習をやったことがあるのですが、データを集めるのに苦労しました。音楽データの音源分離をする場合は、データセットMUSDB18が一番有名ではないかと思います。MUSDB18は150曲(約10時間)の音楽トラックデータセットで、曲ごとにボーカル、ベース、ドラム、その他の楽器に分けられたステムデータがあります。試しにモデルを動かしてみたいときはMUSDB18だけで十分だと思いますが、もっとモデルを学習させたいときがあるかと思います。

Cambridge Music Technology

Cambridge Music Techonologyではミキシングの練習をサポートするために無料でダウンロードできるマルチトラックを提供しています。

www.cambridge-mt.com

今見たところ471曲あるそうです。ボーカルが入っていない曲もありますので、全てのデータを学習に使うことはできませんが、学習データをかなり多く増やすことができます。学習の際の注意点としては、ステムデータではなくパラデータなので、学習前に自分でステムデータにする必要がある点です。そこは、自分で頑張りましょう。

ブログに掲載することに関して

今後、このブログでは音楽データに対する処理結果を載せたいと思うのですが、なかなか掲載できる音楽データは見つかりません。そこで、Cambridge Music Techhologyのサイト運営者の方に「信号処理技術を紹介するためにCambridge Music Techologyのデータで処理した結果をブログにアップロードできないか」と質問してみました。返信としては、「教育目的に該当するので掲載しても大丈夫です」とのことでした。また、「アーティストとCambridge Music Technologyのクレジットを付けてください」とのことでした。返信がきたときは、かなり嬉しかったです。本当にありがとうございます。

音源分離や自動ミキシングなどの処理結果を掲載する際は、音楽データを使わせていただきたいと思います。

Pythonで撥弦音を合成してみた

はじめに

音の合成というものをいままでしたことがなかったので、撥弦音を合成してみました。 今回は、Karplus-Strong方式で合成しました。 Karplus-Strong方式は物理モデルではなく信号モデルで合成するようです。 つまり、弦の発音機構に着目しているのではなく、 波形やスぺクトログラムをモデル化しようとして合成したということです。

参考文献

K.Karplus and A. Strong, “Digital Synthesis of Plucked-String and Drum Timbres”, Computer Music Journal, vol. 7, No. 2, pp. 43–55, MIT Press, 1983.

合成方法

(1). 周波数  f によって、遅延数  p を以下のように決定。

\displaystyle{
p = \frac{f_s}{f}-0.5
}

 f_s はサンプリング周波数です。また、 p は整数でないといけないので整数に変換します。

(2).  x_0 \cdots x_{p-1} に乱数(白色雑音)を入れる。 x_n (n\geq p) には0を入れる。

(3). 以下のデジタルフィルタに通す。

撥弦音合成のためのデジタルフィルタ

数式で書くと以下の式のようになります。

\displaystyle{
y_{n}=\frac{1}{2}(y_{n-p}+y_{n-p-1})+x_n
}

プログラム

Karplus Strong方式でドレミファソラシドを弾くプログラムを作りました。

import sys
import scipy.signal as sg
import numpy as np
import soundfile as sf

# コマンドラインの引数を取得
args = sys.argv
if len(args)!=3:
    print("\nusage: python Karplus.py amp filename");
    print("   amp        : amplitude");
    print("   filename   : file name of sound");
    raise Exception("Argument error ")

amp = float(args[1]) # 初期値の振幅範囲
filename = args[2]   # ファイル名

# ドレミファソラシド
freq = [261.626, 293.665, 329.628, 349.228, 391.995, 440.0, 493.883, 523.251]
fs = 48000               # サンプリング周波数
interval = int(fs*0.5)   # 音符が変化する間隔
n_data = interval*8      # データの数
wave = np.zeros(n_data)  # 波形データを入れる場所 


for i, f in enumerate(freq):
    
    # 乱数生成
    p = int(fs/f-0.5)
    n = i*interval
    wave[n:n+p] = (np.random.rand(p)-0.5)*amp
    
    # デジタルフィルタに通す
    b = np.ones(1)
    a = np.zeros(p+2)
    a[0] = 1.0
    a[p:p+2] = 0.5
    wave[n:n+interval] = sg.lfilter(b, a, wave[n:n+interval])

sf.write(filename, wave, fs, subtype='PCM_16')

合成した撥弦音

合成した撥弦音が以下です。

たしかに弦の音だわ!

波形とスぺクトログラムの観察

波形とスぺクトログラムは以下のような感じです。

合成した撥弦音のスペクトログラム

弦楽器の特徴である調波構造があります。調波成分の減衰のしかたとかも撥弦音のような感じがします。ただ、初期値の白色雑音が少し違和感ありますね。

MIDIを演奏するプログラム作成

midiデータを解析するmidoを用いて、合成した撥弦音で演奏させるプログラムを作成してみました。 プログラムは以下です。処理が複雑になるため、MIDIメッセージはノートオンとノートオフ以外は無視したものとなっています。

import sys
import scipy.signal as sg
import numpy as np
import soundfile as sf
from mido import MidiFile


# 弦の音を生成する関数
def gen_string(freq, amp, time_n, fs):

    # 乱数生成
    p = int(fs/freq-0.5)
    wave = np.zeros(time_n)
    if time_n < p:
        return wave
    wave[:p] = (np.random.rand(p)-0.5)*amp
    
    # デジタルフィルタに通す
    b = np.ones(1)
    a = np.zeros(p+2)
    a[0] = 1.0
    a[p:p+2] = -0.5
    wave = sg.lfilter(b, a, wave)

    return wave

# コマンドラインの引数を取得
args = sys.argv
if len(args)!=2:
    print("\nusage: python Karplus.py amp filename")
    print("   filename   : file name of mid")
    raise Exception("Argument error ")

# ファイル名
filename = args[1]   
mid = MidiFile(filename)

ch_list   = []     # チェンネル番号のリスト
pos_list  = []     # 開始時間のリスト
note_list = []     # 音符のリスト
velocity_list = [] # ベロシティのリスト
time_list = []     # 音符の長さのリスト

fs = 48000      # サンプリング周波数
abs_time = 0.0  # 演奏開始からの経過時間

# MIDIのメッセージを受け取ってリストの作成
for msg in mid:

    # 経過時間の更新
    abs_time += msg.time

    # 打鍵を押したとき
    if msg.type == 'note_on':
        
        # ベロシティが0のとき
        if msg.velocity==0:
            l = len(note_list)
            for i in reversed(range(0,l)):
                # ノート番号とチャンネル番号が一致したとき
                if note_list[i] == msg.note and ch_list[i] == msg.channel:
                    time_list[i] = abs_time-pos_list[i]
                    break
        # ベロシティが0以外
        else:
            ch_list.append(msg.channel)  # チャンネル番号記録
            pos_list.append(abs_time)    # 開始位置記録
            note_list.append(msg.note)   # 音符記録
            velocity_list.append(msg.velocity) # ベロシティ記録
            time_list.append(0.0)        # 音符の長さの記録

    # 打鍵を離したとき
    elif msg.type == 'note_off':
        l = len(note_list)
        for i in reversed(range(0,l)):
            if note_list[i] == msg.note and ch_list[i] == msg.channel:
                time_list[i] = abs_time-pos_list[i]
                break

# 空の波形データ作成
n_data = int(abs_time*fs)
wave = np.zeros(n_data)

# numpy配列に変換
pos_list  = np.array(pos_list)
note_list = np.array(note_list)
velocity_list = np.array(velocity_list)
time_list = np.array(time_list)

# 変換
pos_np  = pos_list*fs                # 時間からサンプル番号に変換
pos_np  = pos_np.astype(np.int32)    # 整数に変換
freq_np = 440*2**((note_list-69)/12) # ノート番号から周波数に変換
amp_np  = velocity_list/127          # ベロシティから振幅値に変換
time_np = time_list*fs               # 時間からサンプル番号に変換
time_np = time_np.astype(np.int32)   # 整数に変換

# リストから波形生成
for i in range(len(pos_np)):
    pos  = pos_np[i]
    freq = freq_np[i]
    amp  = amp_np[i]
    time_n = time_np[i]

    if time_n != 0:
        gen_wave = gen_string(freq, amp, time_n, fs)
        wave[pos:pos+time_n] = wave[pos:pos+time_n] + gen_wave

# wavファイルに書き込む
sf.write(filename[:-4]+".wav", wave, fs, subtype='PCM_16')

演奏してみた

ヨハン・パッヘルベルのカノンを演奏してみました。MIDIデータについてはこちらのサイトが提供しているMIDIデータを使わせていただきました。

自作したため、少し感動しました。

おわりに

今回は、撥弦音の合成を試してみました。原理についてはまたの機会に調べようと思います。また、他の合成音についても作成してみたいと思います。