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

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

今回の記事で参考にした文献は以下の2つです。

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

[2] IP作成の参考文献

環境

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

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

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

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

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

図1:正弦波の波形テーブル
図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 はこちらの GitHub の「3rd」フォルダの中にあります。

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_flag
    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[29: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...」を実行
図2:「Create and Package New IP...」を実行

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

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

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

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

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

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

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

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

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

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

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

図8:IP編集用のVivado画面
図8:IP編集用のVivado画面

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

図9:「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」タブでの設定
図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」タブでの設定
図11:「Port A Options」タブでの設定

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

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

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

図12:「Other Options」タブでの設定
図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」タブで確認
図13:「Summary」タブで確認

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

図14:保存場所を確認
図14:保存場所を確認

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

図15:「Out of context per 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」
図16:「Package IP」の「File Groups」

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

図17:「Merge changes from File Groups Wizard」した後
図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
図18:「Copy sources into IP Directory」をチェックOFF

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

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

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

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

図21:プロジェクトを閉じるかのダイアログ
図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 インテグレータ
図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:ファイル追加時のチェック
図23:ファイル追加時のチェック

シミュレーションの実行

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

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

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

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

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

図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」の画面
図27:「Analog Setting」の画面

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

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

おわりに

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

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

© 2021 Setoti All rights reserved.