lyricalmagicalのアイコン画像

[FPGA]Spartan-3Eボードで音楽を演奏してみる[マルツ MFPGA-SPAR3E]

lyricalmagical 2021年09月23日に作成  (2021年09月23日に更新)

前回、CPLDを動かす記事を書いたので、少しパワーアップしてFPGAを使用してみました。

使用した部品

以下、部品詳細です

マルツ MFPGA-SPAR3E

キャプションを入力できます
タイトル通り今回のメインとなるFPGAボードです。
https://www.marutsu.co.jp/pc/i/65177/

XILINX社製Spartan-3E(XC3S250E-4VQG100C)というFPGAを搭載したボードです。安価でわりと遊べるボードです。
お試しで遊ぶには結構な規模で、単純なCPUくらいであれば十分に入りますし、IOピンも多めで、特殊なコネクターとかでもありません。
ただ、これ単体ではほぼ何もできません。周辺につける何かが必要です。
最低でもクロック源は事実上必須です。

クリスタルオシレーター KDK HAT7600A

キャプションを入力できます
というわけで、クロック源です。今回は手持ちに余ってた24MHzを使用しました。
今回使うFPGAはIO電圧が3.3Vのため、クロックも3.3V対応のものが必要ですが、ブレッドボードに刺して使えるようなクロック源で3.3V対応のものはあまりありません。
今回使用したパーツは本来5V用ですが、3.3Vでも動いてくれるようです。

ブザー

キャプションを入力できます
PCのマザーボードにつけるブザーです。自作PCパーツ売り場で売ってます。
FPGAボードとクロック源があれば、ボードにLEDとスイッチがあるので、Lチカくらいは可能ですが、それでは前回のCPLDと何も変わらないので、今回は音を出してみることにしました。

その他必要なもの

Xilinx ISE

キャプションを入力できます
Xilinx FPGA/CPLDの開発環境です。Xilinxからダウンロードできます。
今回の規模のFPGAであれば、無償版で対応できます。
かなり古いソフトで、最終版は14.7です。
ちなみに、Windows10には正式対応しておりません。
公式リリースされているVM版、もしくは、Win7ネイティブ版を使用する必要があります。
ネイティブ版をWindows10で使用する場合は、
Xilinx Answers AR#62380
ISE インストール - Windows 8.1 または Windows 10 マシンに ISE 10.1 または 14.7 をインストールして実行する方法

の手順が必要です。
※Xilinxのもっと新しいデバイスはISEではなくVivadoというツールを使用して開発するのですが、デバイスドライバが競合したりして厄介なことが起きるので、できればISEとVivadoは同じPCにインストールしないほうが良いです。

Xilinx Platform Cable USB II

キャプションを入力できます
FPGAに書き込むために必要です。
純正品が手に入ればそれでよいのですが、何せやたら高いので、互換品を使いました。

AliExpressでDLC9LPやDLC10という名前で売られています。3000円くらいで買えます。
https://ja.aliexpress.com/item/32672868436.htmlとか(保証しませんが・・・)。
AliExpressが不安であれば、amazonでも売っていますがもう少し高いです。
中華クオリティですので、接触不良など初期不良などはわりとあるようですが、正常品であれば問題なく使えます。

ちなみに、PCにパラレルポートが付いている機種であれば、USBではなくパラレルポート経由での書き込みも可能です。
こちらについてはケーブルの自作も簡単です。
作成方法については「xilinx download cable 自作」などで調べると見つかります。

配線する

図の通りです。作成例の写真とは配線の色が違うので気を付けてください。
キャプションを入力できます
FPGAは3.3Vで動作しますが、この図では5Vの電源を使うようになっています。
5Vから3.3Vへの変換はマルツボードに別途部品を乗せることで可能です。部品はマルツボードを購入すると付属されていますので、説明書を参考に取り付けてください。

もしくは、3.3V電源を使用することも可能ですが、単体の3.3VのACアダプターより、5VのACアダプターのほうが入手は容易でしょう。

ソースを作る

端子アサイン等を設定するucfファイルと、実際の動作を記述するverilog-HDLファイルの2つが必要です。
下記ファイル2つを事前に作成しておいてください。

top.ucf

NET "CLK" LOC ="P38" | IOSTANDARD = LVCMOS33; NET "CLK" PERIOD = 20.0ns HIGH 50%; NET "SOUND" LOC ="P2" | IOSTANDARD = LVCMOS33; NET "RESETn" LOC ="P89" | IOSTANDARD = LVCMOS33; NET "LED" LOC ="P98" | IOSTANDARD = LVCMOS33;

top.v

module top( RESETn, CLK, SOUND, LED ); input RESETn; input CLK; output SOUND; output LED; wire [3:0]rom1_a; wire [41:0]rom1_d; wire sound1; wire [3:0]rom2_a; wire [41:0]rom2_d; wire sound2; wire mixsound; reg mix; player player1( .RESETn(RESETn), .CLK(CLK), .A(rom1_a), .D(rom1_d), .SOUND(sound1) ); rom1 rom1( .A(rom1_a), .D(rom1_d) ); player player2( .RESETn(RESETn), .CLK(CLK), .A(rom2_a), .D(rom2_d), .SOUND(sound2) ); rom2 rom2( .A(rom2_a), .D(rom2_d) ); //ミキサー always @(negedge RESETn or posedge CLK) if (RESETn==1'b0) mix<=1'b0; else mix<=!mix; assign mixsound=(mix==1'b0)?sound1:sound2; assign SOUND=mixsound; assign LED=!mixsound; endmodule module player( RESETn, CLK, A, D, SOUND ); input RESETn; input CLK; output [3:0]A; input [41:0]D; output SOUND; reg [3:0]A;//再生中アドレス reg [24:0]cnt;//音長カウンタ reg [15:0]tmp_rate;//音程保持 reg [15:0]rate;//音程カウンタ reg SOUND; wire [41:0]D; wire [15:0]rom_rate; wire [24:0]rom_cnt; assign rom_rate=D[41:25]; assign rom_cnt=D[24:0]; always @(negedge RESETn or posedge CLK) if (RESETn==1'b0) cnt<=10;//チャタリング防止カウンタ else if (cnt != 0) cnt<=cnt-1; else cnt<=rom_cnt; always @(negedge RESETn or posedge CLK) if (RESETn==1'b0) A<=0; else if (cnt == 0 && A != 15) A<=A+1; always @(negedge RESETn or posedge CLK) if (RESETn==1'b0) tmp_rate<=0; else if (cnt == 0) tmp_rate<=rom_rate; always @(negedge RESETn or posedge CLK) if (RESETn==1'b0) rate<=0; else if (rate==0) rate<=tmp_rate; else rate<=rate-1; always @(negedge RESETn or posedge CLK) if (RESETn==1'b0) SOUND<=1'b0; else if (tmp_rate==0)SOUND<=1'b0; else if (A == 15)SOUND<=1'b0; else if (rate==0)SOUND<=!SOUND; endmodule module rom1( A, D ); input [3:0]A; output [41:0]D; wire [15:0]rate;//音程 wire [24:0]cnt;//長さ assign D={rate,cnt}; assign rate= (A== 0)?17181: //F5 (A== 1)?0: //無音 (A== 2)?17181: //F5 (A== 3)?0: //無音 (A== 4)?17181: //F5 (A== 5)?0: //無音 (A== 6)?17181: //F5 (A== 7)?0: //無音 (A== 8)?19285: //E-5 (A== 9)?0: //無音 (A==10)?15306: //G5 (A==11)?0: //無音 (A==12)?17181: //F5 (A==13)?0: //無音 0; //無音 assign cnt= (A== 0)?2817392: //8分90% (A== 1)?313044: //8分10% (A== 2)?2817392: //8分90% (A== 3)?313044: //8分10% (A== 4)?2817392: //8分90% (A== 5)?313044: //8分10% (A== 6)?2817392: //8分90% (A== 7)?313044+3130435: //8分10%+8分 (A== 8)?2817392: //8分90% (A== 9)?313044+3130435: //8分10%+8分 (A==10)?2817392: //8分90% (A==11)?313044+3130435: //8分10%+8分 (A==12)?12521740+6260870+3130435://2分+4分+8分 0; endmodule module rom2( A, D ); input [3:0]A; output [41:0]D; wire [15:0]rate;//音程 wire [24:0]cnt;//長さ assign D={rate,cnt}; assign rate= (A== 0)?22934: //C5 (A== 1)?0: //無音 (A== 2)?24297: //B4 (A== 3)?0: //無音 (A== 4)?25742: //B-4 (A== 5)?0: //無音 (A== 6)?27273: //A4 (A== 7)?0: //無音 (A== 8)?30613: //G4 (A== 9)?0: //無音 (A==10)?25742: //B-4 (A==11)?0: //無音 (A==12)?27273: //A4 (A==13)?0: //無音 0; //無音 assign cnt= (A== 0)?2817392: //8分90% (A== 1)?313044: //8分10% (A== 2)?2817392: //8分90% (A== 3)?313044: //8分10% (A== 4)?2817392: //8分90% (A== 5)?313044: //8分10% (A== 6)?2817392: //8分90% (A== 7)?313044+3130435: //8分10%+8分 (A== 8)?2817392: //8分90% (A== 9)?313044+3130435: //8分10%+8分 (A==10)?2817392: //8分90% (A==11)?313044+3130435: //8分10%+8分 (A==12)?12521740+6260870+3130435://2分+4分+8分 0; endmodule

実行

上記ソースを読み込ませて、ボードに転送して動かすまでの手順です。
まず、ISE(project navigatorを起動し、New Projectを選択します)
キャプションを入力できます
ちなみに、以前に別のProjectを開いていたことがある場合はそのプロジェクトが自動的に読み込まれていますので、FileメニューからCloseしてください。

New Projectを選択すると、Project名と作成するフォルダの場所の入力になりますので、適当に決めてください。
日本語は使えないと思います。スペースや記号なども使わない方がいいと思います。
入力したらNextで次に進みます。

次は、Projectの設定です。
特に気にするところは、Family、Device、Package、Speedです。マルツボードはSpartan3EのXC3S250EのVQ400の-4が乗っていますので、その通り設定します。
※ちなみに、実際のボードは-4ではなく5C/4Iを使っているようですが、上位互換ですので-4設定で問題ありません。
その他も基本的に下記と同じ設定で問題ありません。
キャプションを入力できます

最後に確認画面が出ますので、Finishしてください。

すると、ISEの左上に、Empty Viewと記載された箇所がでてきます。
ここで右クリックして、「Add Source」を選択してください。
キャプションを入力できます
ファイル名の選択ウィンドウが表示されますので、先ほど作ったtop.vとtop.ucfの両方を選択して開いてください。
すると、何かウィンドウが出ますが気にせずにOKしてください。

ここまで進むと、元の画面に戻りますが、Empty Viewだった個所にソースファイルが追加されています。
あとは、実際にコンパイルと書き込みです。まず書き込む手前まで進めましょうしょう。
そのためには、下にある「Configure Taget Device」を右クリックし、Runを選択します。
キャプションを入力できます
※厳密には、コンパイルと言っていますがコンパイルにも3工程あり、「Configure Taget Device」の上にある「Synthesize」→「Implement」→「Generate Programing File」となります。その後、「Configure Taget Device」で書き込みソフトが立ち上がります。ただ、最初から「Configure Taget Device」をRunすることで、未実施の前工程も自動で実行してくれます。

横のアイコンがくるくる回って、若干時間はかかりますが、最近のPCであれば1分程度で終わるでしょう。
その後、おそらくWarningのダイアログポップアップがでますが、OKしてください。
すると、ISE iMPACTという別のツールが自動で起動します。
この後、FPGAへの転送&実行になりますので、回路の作成、Platform Cableの接続、FPGAボードへの電源投入を済ませておいてください。
起動したら、Boundary Scanという項目をダブルクリックすると、右ペインに作業ウィンドウが表示されます(が、Right Click~と記載されているだけで何も表示されていません)。
キャプションを入力できます
ここで、右クリックし、「Initialize Chain」を選択します。すると、接続されているボードを認識にいき、
下図のような表示に変わります。
キャプションを入力できます
このような表示にならなかった場合、PCとFPGAボードの接続がおかしいか、FPGAボードに電源が供給されていないと思われますので確認してください。
ここで表示された「Device~」ウィンドウはいったんCancelしてください。
その後、緑色で表示されている「Xc3s250e」をダブルクリックすると、ファイル選択画面となります。別のところを触ってしまうと緑色ではなくなっているかもしれませんが、気にせずダブルクリックしてください。
先ほどのプロジェクトを作ったフォルダを選択すると「top.bit」というファイルができていますので、それを選択します。これがコンパイル結果ファイルのようなものです。
選択すると「Attach~」というポップアップが出ますが、「No」を選択してください。
これで、これで転送準備ができました。

それでは、転送してみましょう。
xc3s250eを右クリックし、Programを選択します。
ポップアップウィンドウが出ますので、特に気にせずにOKしてください。
一瞬転送中の表示がでますが、すぐに「Program Succeeded」と表示されるはずです。ここまで進んでいてこちらに失敗した場合、おそらくは違うデバイス用のファイルが出来上がっていますので、ISEのProjectのデバイスなどをチェックしてください。

成功した場合、先ほど作った回路からいきなり音が鳴ります。
ボード上のスイッチがリセットスイッチですので、スイッチを押すことで最初から再生しなおします。レベルアップしましたか?

ソースの解説

ソースは1つのファイルになっていますが、いくつかのmoduleに分かれていて、下図のような構成になっています。
キャプションを入力できます
リセットとクロックは省略しています。また、player1とplayer2は全く同じものです。
playerはリセットがかかると、rom定義に従って音を出します。ミキサーは2つのplayerの出力を合成して、SOUNDを出力します。
また、音声出力時はLEDも光るようにするため、LEDにも接続しました。無音時は消灯とするために、反転しています。

player(module)

まずはplayerモジュールです。まったく同じものを2個使用しています。
こちらはromモジュール(後述)から音程と音調データを取得し、実際に音の波形を作成します。
内部的にはカウンタの組み合わせで
(1)音長カウンタ
(2)ROMのアドレスカウンタ
(3)音程カウンタ
で構成されています。

(1)の音長カウンタは、ROMから読み出した音長値をカウントします。指定された値だけカウントすると、ROMから次の音長値を読み取り、再びカウントを始めます。
(2)のROMのアドレスカウンタは、(1)の音長カウンタが0となったとき、次のROMアドレスに進むようにカウントします。つまり、音長カウンタが一周するたびにROMアドレスはひとつ進み、次の音長と音程を読み出します。
(3)の音程カウンタは、指定した値だけカウントするごとに、音声出力信号を反転します。
具体的な値の計算方法は、romモジュールで記載します。
その他、発音停止中は音声出力を0固定する、ROMアドレスが最後まで行ったらそこであふれないようにとまるという機能もあります。

rom(module)

単純なromです。42bitのデータを16ワード格納できます。
ちなみにverilogでのromの表記方法としてはわりと悪い例ですが、ワード数が少ない場合はこんな感じでいいでしょう。
42bitの内訳は、上位16bitが音程、下位25bitが音調です。
・・・と、ここまで書いて1bit余ってることに気づきましたが、上位1bitが常に0になるだけなので問題はありません。
※下手にソース修正して掲載してコンパイル通らなかったら嫌だし、かといってもう一回組み立てて実験するのも面倒なので。

音程に関しては、電子工作の知識というか音楽的知識になりますので、あまり深入りせずさらっと触れて起きます。
いわゆる普通のピアノの真ん中少し右にある鍵盤にA4といわれる白い鍵盤があり、そこの周波数が440Hzと基本的に決まっています。
その右上の黒い鍵盤がA+4(もしくはB-4)といい、さらにその右の白い鍵盤がB4といいます。
B4の隣の白い鍵盤がC5です。ここから4ではなく5になります。同じく白い鍵盤だけを右側方向に見ていくと、
C5→D5→E5→F5→G5→A5→B5→C6
というふうに、A~Gが繰り返し出てきます。白い鍵盤を基準に左上の黒い鍵盤は-、右上の黒い鍵盤は+が付きます。
このとき、A4→A5で周波数がちょうど2倍で、A5が880Hzとなります。
周波数を計算する上では白い鍵盤と黒い鍵盤の区別は無く、A→A+→B→C→C+→D→D+→E→F→F+→G→G+と順に考え、これらの周波数は等比数列となっています。
12個で2倍になるため、鍵盤ひとつあたりの比は2の(1/12)乗です。
なので、ここからそれぞれの鍵盤の周波数が求められます。一覧は計算してもいいですが、ググると出てくるでしょう。
ここで、440Hzを出力したい場合、24MHz/440≒54545.4です。
しかし、playerの設計は、この周期で「出力が反転する」ので、実際に指定すべきあたいは、440Hzの1周期の値ではなく、パルス幅の値です。したがって、この計算値を2倍したものを設定します。
すると、27272.7となりますので四捨五入して27273とします。他の音階についても同様です。

長さに関しては、テンポ230としています。テンポ230の場合、、4分音符の長さがが1分の230分の1です。8分音符は4分音符の半分の長さですので、1分の460分の1です。
今回、24MHzですので、1秒は24MClkです。ですので1分は1440MClkです。
今、8分音符を鳴らしたい場合、1分の460分の1ですので、約3130436clk数えれば8分音符相当の長さになります。
しかし、冒頭の「テレレレッ」というところな同じ音が続くため、3130436clkすべて発音してしまうと全部繋がって一つの長い音に聞こえてしまいます。
ですので、実際に発音するのはその90%とし、残り10%は発音しないこととします。なので、発音期間は2817392clk、無音期間は313044clkとします。
これらが、長さ値に設定される値となります。

このように計算した音程値と長さ(音長)値を連続で格納しているromとなります。

ミキサー

playerモジュールが2つあるため、2つの音を合成する必要があります。
アナログ出力的に考えれば、0、0.5、1の3値の出力が必要となりますが、デジタル回路ではそのようなことはできないため、擬似的にアナログ値を出力する必要があります。
仕組みに関しては簡単で、player一つ目(ch1とします)とplayer2つ目(ch2)の出力を高速で切り替え出力するだけです。
「高速に」というのは、今出力したいものは「音」で、これはあくまで「人の耳に聞こえる音」であり、その周波数は耳がいい人でも大体20KHzが上限といわれています。なのでこの周波数より十分に高ければ問題ありません。
今回、クロック周波数が24MHzなので、あえて特に分周したりする理由もないので、24MHzで交互にch1とch2を切り替えて出力します。
なので、片方だけ発音中の場合、厳密には12MHzの信号が出力されることになりますが、人間の耳にはそんな高周波は聞こえませんし、そもそもブザー自体もそんな出力はできませんので、1と0が交互に出力されたものが平均化され、約0.5の出力となります。
原理的には格好良くいえばPWM出力となりますが、そもそも100%と50%と0%しか出ませんので、そんな難しいものではありません。

最後に

使用したFPGAに対して、作成した回路規模はかなり小さいので、まだまだいろいろな回路を組み込む要素はあります。
今回作成したものはかなり単純なもので、あえてFPGAを使うような内容ではありませんが、CPUだけではなくこういったデバイスも使用できるようになると、色々なものを作る際に役立つこともあると思います。

lyricalmagicalのアイコン画像
電子デバイスはとってもりりかるなの
ログインしてコメントを投稿する