この Web ページは、 Smart Zynq boardの機能を探求する小規模プロジェクトのグループに属しています。
このプロジェクトは HelloFPGAでも公開されており、中国語の読者におすすめです。
序章
このチュートリアルでは、通常のヘッドフォンを Smart Zynq ボードに接続して音楽を聴く方法を説明します。このプロジェクトの目的は、 FPGAに連続データを送信する目的での Xillybus stream の使用を実証することです。 PWM modulator を実装する Verilog コードもここに示されています。
ここに示されているコードは、オーディオ出力を実装する方法の例ではありません。アナログ出力を実装する通常の方法は、 Sigma-Deltaと呼ばれるより複雑な手法です。この手法は FPGAに実装できますが、理論的背景を理解するのは非常に困難です。
この実装のもう 1 つの欠点は、 sample rate が不正確であることです ( 48000 Hzではなく48828 Hz )。これは、 logicが使用する clock の周波数を変更することで簡単に修正できます。この例では正確さよりも単純さに重点を置いているため、この目的での clocks の操作に関するトピックはここでは示しません。
このデモンストレーションに使用する機器は次のとおりです。
- Smart Zynq ボード (SP または SL)。
- 通常のアナログヘッドフォン。
- ワニ口クリップとワイヤー、またはボードの pinsに接続するためのその他の手段。
- オプション: 50Ω-200Ω resistorです。この resistor の目的は、ヘッドフォンと Smart Zynq ボードを過剰な電流から保護することです。この例はこの resistorがなくても動作しますが、電子機器が損傷する危険があります。
Vivado プロジェクトの準備
demo bundleの zip ファイル ( boot partition kit) から新しい Vivado プロジェクトを作成します。テキスト エディタで verilog/src/xillydemo.v を開きます。コードの「PART 2」というラベルの付いた部分を削除します。その代わりに、次のコード スニペットを挿入します。
/*
* PART 2
* ======
*
* This code demonstrates a PWM-based audio output
*/
reg [10:0] pwm_level, threshold_left, threshold_right;
reg pwm_left, pwm_right;
reg fifo_out_valid;
wire [31:0] fifo_out;
wire fifo_empty;
wire fifo_rd_en = !fifo_out_valid && !fifo_empty;
wire next_word = (pwm_level == 11'h7ff);
assign J6 = { pwm_right, pwm_left };
always @(posedge bus_clk)
begin
pwm_level <= pwm_level + 1;
if (next_word && fifo_out_valid)
begin
// The audio samples are signed integers. Change them to
// unsigned by adding 1024.
threshold_left <= fifo_out[15:5] + 1024;
threshold_right <= fifo_out[31:21] + 1024;
end
else if (next_word) // FIFO's output not valid, keep silent
begin
threshold_left <= 0;
threshold_right <= 0;
end
pwm_left <= (threshold_left > pwm_level);
pwm_right <= (threshold_right > pwm_level);
if (fifo_rd_en)
fifo_out_valid <= 1;
else if (next_word)
fifo_out_valid <= 0;
end
// 32-bit FIFO for audio samples
fifo_32x512 fifo_32
(
.clk(bus_clk),
// Interface with Xillybus IP core
.srst(!user_w_write_32_open),
.din(user_w_write_32_data),
.wr_en(user_w_write_32_wren),
.full(user_w_write_32_full),
// Interface with application logic
.rd_en(fifo_rd_en),
.dout(fifo_out),
.empty(fifo_empty)
);
// Send the text "PWM" to reassure that the correct bitstream is used.
assign user_r_read_32_eof = 0;
assign user_r_read_32_empty = 0;
assign user_r_read_32_data = 32'h0a_4d_57_50; // "PWM" + LF
あるいは、 ここから xillydemo.v ファイルをダウンロードします。
demo bundleの bitstream ファイルを作成したのと同じ方法で、更新されたプロジェクトから bitstream ファイルを作成します。 bitstream ファイルも同様に TF card にコピーします (古い xillydemo.bit ファイルをこのプロジェクトで作成したファイルで上書きします)。
ヘッドフォンを接続する
50Ω-200Ω resistor をオーディオ信号を伝送する I/O pin に接続します。 J6/1 (左耳) または J6/2 (右耳)。 J6を見つけるには、 Smart Zynq ボードの裏側にある「Bank 33 VCCIO Vadj」と書かれている場所を探します。このマークに近い pins の行が、これから扱う pin header です。したがって、 J6/1 は HDMI コネクタに最も近い pin になります。
resistor の反対側をヘッドホンプラグの先端に接続します。この目的にはワニ口クリップを使用できます。
ヘッドホンプラグのスリーブ部分を Smart Zynqの groundに接続します。 pin headerの ground は J6/35 または J6/36に位置します。ただし、これらの pinsは power supply pinsに近いため、使用はお勧めしません。
代わりに、 J6/3 から J6/34 までの範囲内の任意の pin を使用することもできます。 FPGA はこれらを output pinsとして認識し、 '0' logic levelに維持します。したがって、これらの pins を groundとして使用することが可能です。
ボードのコネクタの 1 つの外側の金属部分にワニ口クリップを接続して、 ground に接続することもできます。 Ethernet コネクタ、 HDMI コネクタ、または USB コネクタのいずれか。
ボードを開始します
通常どおり Smart Zynq の電源をオンにします (または rebootを実行します)。次のステップは、正しい bitstream ファイルが FPGA (PL 部分) にロードされていることを確認することです。
shell promptで「head /dev/xillybus_read_32」というコマンドを入力します。このコマンドは、 /dev/xillybus_read_32 から最初の行を読み取り、結果を出力します。
# head /dev/xillybus_read_32 PWM PWM PWM PWM PWM PWM PWM PWM PWM PWM
このコマンドからの出力がない場合、または出力が上記のものと異なる場合は、間違った bitstream が使用されています。
オーディオファイルの再生
音声ファイルを Xillinuxの file systemにコピーします。言い換えれば、オーディオ ファイルは Linux システム内のコマンドで使用できる必要があります。
このファイルは WAV 形式である必要があります。 Uncompressed PCM, 2 channels, s16le (これはほとんどの場合、 WAV ファイルの形式です)。 sampling rate は 48000 Hzのはずですが、 44100 Hz でも十分に動作します。
適切な音声ファイルは、 このリンクからダウンロードできます。
ファイルを Linux システムにコピーするには、いくつかの方法があります。たとえば、次のコマンドを使用して、 Ethernet network を使用して別のコンピュータから Xillinuxの home directory にファイルをコピーすることができます。
$ scp sample.wav root@192.168.1.10:~/
これは、 Microsoft Windowsの command prompt だけでなく、 Linux shellでも機能します。 IP address (この例では192.168.1.10 ) をボードの IP addressに変更します。
ファイルを Xillinux にコピーする方法は他にもあります。たとえば、 NFS または CIFSを使用します。
ファイルが Xillinuxの file systemにコピーされたら、次のコマンドを使用してオーディオを再生します。
# cat sample.wav > /dev/xillybus_write_32
「sample.wav」を再生したいファイル名に置き換えます。ここで示すコマンドは、ファイルが current directoryにある場合に機能します。
このコマンドは、新しい shell prompt が表示されるまで、ヘッドフォンでファイルを再生します。片耳 ( J6/1 と J6/2 の両方をヘッドフォンのプラグの別々の部分に接続している場合は両耳) で音楽を聞くことができるはずです。
CTRL-Cではこのコマンドを途中で止めることが可能です。
それでおしまい。このページの残りの部分では、これがどのように機能するかについて説明します。
音声データが FPGAに到達する仕組み
「cat」コマンドは、オーディオ ファイル (sample.wav) の内容を「xillybus_write_32」という名前の device file にコピーします。 Linux システムでは、これが hardware driverにデータを送信する通常の方法です。この例では、 driver は Xillybusの IP coreと接続します。その結果、データは FPGAの logic内の FIFO に送信されます。
上で示した Verilog コードの関連部分を見てみましょう。
fifo_32x512 fifo_32
(
.clk(bus_clk),
// Interface with Xillybus IP core
.srst(!user_w_write_32_open),
.din(user_w_write_32_data),
.wr_en(user_w_write_32_wren),
.full(user_w_write_32_full),
// Interface with application logic
.rd_en(fifo_rd_en),
.dout(fifo_out),
.empty(fifo_empty)
);
これは標準の FIFOの instantiation です。 FIFO の仕組みに関する一般的な説明については、このページを参照してください。
この FIFO には、 FIFOへのデータの挿入に関連する 3 つの ports があります。 din、 wr_en 、 full。これら 3 つの ports はすべて Xillybus IP coreに接続されています。つまり、3 つの信号 (user_w_write_32_data、 user_w_write_32_wren 、および user_w_write_32_full) は、 xillybusという名前の module に接続されます。この配置により、 Xillybus IP core から FIFOにデータを書き込むことができます。
Xillybus は、この配置を使用して、ソフトウェアが /dev/xillybus_write_32に書き込むデータを FIFO に書き込みます。 Xillybus は継続的に可能な限り多くのデータを FIFOに書き込もうとしますが、 overflow を引き起こすことはありません (つまり、 FIFOの full 信号に従います)。
要約すると、次のことが起こります。
- 「cat」コマンドは、 sample.wav から device fileにデータをコピーします。 (/dev/xillybus_write_32)。
- Xillybusの driver は、このデータを DMA bufferにコピーします。
- FPGA ( Xillybus IP core) 内のXillybusの logic は、 DMA buffer からデータを読み取り、 FIFOにデータを書き込みます。
- FPGA 内の application logic は、 FIFO からデータを読み取り、このデータを消費します。
これらすべての操作は同時に継続的に行われます。
Xillybusの詳細については、この一連のページ、特にこのページを参照してください。
オーディオ信号がどのように作成されるか
ここまでの説明では、 FPGA内部でデータがどのように application logic に到達するかを説明しました。次に、データがどのように音声に変換されるかを見てみましょう。
まず、 Verilog コードの次の行に注目してください。
assign J6 = { pwm_right, pwm_left };
これによると、2つのオーディオ出力は pwm_right と pwm_leftで構成されます。これら 2 つの registers には次のように値が割り当てられます。
always @(posedge bus_clk)
begin
pwm_level <= pwm_level + 1;
[ ... ]
pwm_left <= (threshold_left > pwm_level);
pwm_right <= (threshold_right > pwm_level);
[ ... ]
end
pwm_level は単純な counterであることに注意してください。この register は11ビットで構成されているので、0から2047までカウントし、また0からやり直します。
threshold_left が pwm_levelより大きい場合、 pwm_left の値は '1' になります。言い換えれば、 threshold_left は、0 から 2047 までのすべての数値を繰り返し処理する counter と比較されます。 threshold_left の値が大きいほど、 pwm_left が値 '1'を保持する時間が長くなります。これが PWMの原理です。 pulseの長さは、生成するアナログ信号の値に直線的に比例します。
pwm_right は threshold_rightと同様に動作します。
threshold_left および threshold_right には、 Xillybus IP core経由で送信される WAV ファイルのデータが含まれています。これがどのようにして起こるのかを詳しく見ていきます。
まず、 FIFOの instantiation のうち、 FIFOからの読み取りに関連する部分を見てみましょう。
// Interface with application logic
.rd_en(fifo_rd_en),
.dout(fifo_out),
.empty(fifo_empty)
fifo_rd_en は次のように定義されます。
wire fifo_rd_en = !fifo_out_valid && !fifo_empty;
したがって、 FIFO が空ではない場合、および fifo_out_valid が Low の場合、 FIFOの read enable は High になります。それでは、 fifo_out_validの定義を見てみましょう。
always @(posedge bus_clk)
begin
[ ... ]
if (fifo_rd_en)
fifo_out_valid <= 1;
else if (next_word)
fifo_out_valid <= 0;
end
fifo_out_valid の意味は、 FIFO の出力が有効なときにこの register が High になることです。より正確には、 FIFOの出力がまだ消費されていないとき、 fifo_out_valid は High になります。これが、 fifo_rd_en がハイになった後、この register がハイの clock cycle に変わる理由です。この register は、 next_word が High のときに Low に変化します。以下で説明するように、 PWM を実装する logic は、 next_word が High のときに FIFOの出力を消費します。
next_word は次のように定義されます。
wire next_word = (pwm_level == 11'h7ff);
pwm_level は、0 から 2047 までのすべての値を通過する counter であることを思い出してください。2047 の 16 進コードは 7ffです。したがって、 pwm_level がゼロに戻ろうとする直前に、 next_word はハイになります。
next_word はどのくらいの頻度でハイになりますか? bus_clk の周波数は 100 MHzです。 next_word は、2048 clock cyclesのラウンドごとに 1 回ハイになります。 100 MHz ÷ 2048 ≈ 48828 Hz。つまり、 next_word は 1 秒あたり約 48828 回ということになります。
FIFOの出力が消費されると next_word がハイになると前述しました。これは、 Verilog コードの関連部分です。
always @(posedge bus_clk)
begin
[ ... ]
if (next_word && fifo_out_valid)
begin
// The audio samples are signed integers. Change them to
// unsigned by adding 1024.
threshold_left <= fifo_out[15:5] + 1024;
threshold_right <= fifo_out[31:21] + 1024;
end
else if (next_word) // FIFO's output not valid, keep silent
begin
threshold_left <= 0;
threshold_right <= 0;
end
[ ... ]
end
まず、 next_word が High の場合、 threshold_left と threshold_rightの両方に新しい値が割り当てられることを観察します。 fifo_out_valid が低い場合、この 2 つの registers の値はゼロになります。これは、 FIFOにデータが送信されず、空になったときに発生します。
fifo_out_valid が高い場合、 FIFOの dout port に audio sampleの値が含まれていることを意味します。この値は 2 つのステレオ チャンネルのアナログ信号を表します。このような各 sample には、 16-bit 2's complement 形式で与えられる 2 つの符号付き数値が含まれています。
左ステレオチャンネルに属する audio sample は fifo_out[15:0]で与えられます。これは、-32768 ~ 32767 の符号付き数値です。下位 5 ビットが削除されるため、 fifo_out[15:5] の範囲は -1024 ~ 1023 になります。したがって、式「fifo_out[15:5] + 1024」は 0 ~ 2047 の符号なし数値になります。この数値範囲は次のとおりです。 pwm_levelとの比較に適しています。
したがって、 fifo_out[15:0] が -32768 に等しい場合、 threshold_left には値 0 が割り当てられます。 「threshold_left > pwm_level」という条件が満たされることはないため、 pwm_left は常に低いままになります。一方、 fifo_out[15:0] が 32767 の場合、 threshold_leftの値は 2047 になります。その結果、 pwm_left はほぼ常に高い値になります。これは、 fifo_out[15:0] が各 pulse上で pwm_left がハイになる時間を制御する方法です。 fifo_out[31:16] は同様に pwm_right を制御します。
メカニズム全体を要約すると、次のようになります。 next_word は、2047 clock cyclesごとに 1 回ハイになります。 next_word が High の場合、 FIFO の出力が調整されて threshold_left および threshold_rightにコピーされます。これにより FIFOの出力が消費されるため、 fifo_out_valid が Low になります。したがって、 FIFO が空でない場合、 FIFOから新しい audio sample を読み取るために fifo_rd_en が High になります。
Xillybus IP core がこの FIFO に sample.wavのコンテンツを埋め込むことを思い出してください。したがって、 sample.wav のコンテンツから threshold_left および threshold_rightへの audio samples のデータ フローが存在します。前述したように、 next_word は1秒あたり48828回程度と高い。こちらがこの機構の sample rate です。
threshold_left は、 pwm_left がハイになる時間の割合を制御します。 threshold_right と pwm_rightについても同様です。最後に、 pwm_right と pwm_left は J6という名前の output port に接続されているため、これらは pin headerで表示される信号です。
next_word が High の場合、次の 2 つのことが起こることに注意してください。 audio sample が消費され、 pwm_level はゼロからカウントを開始します。したがって、 audio sampleごとに 1 つの pulse が生成されます。
「PWM」をプリントアウトする
前に、 FPGA に正しい bitstreamが含まれていることを確認するために、「head /dev/xillybus_read_32」コマンドを使用することをお勧めしました。予想された結果は、「PWM」が何度も出力されるということでした。これは、 Verilog コードのこの部分によって実装されます。
// Send the text "PWM" to reassure that the correct bitstream is used.
assign user_r_read_32_eof = 0;
assign user_r_read_32_empty = 0;
assign user_r_read_32_data = 32'h0a_4d_57_50; // "PWM" + LF
変更前と同じように xillydemo.v を見ると、 user_r_read_32_rden、 user_r_read_32_data 、および user_r_read_32_empty が FIFOに接続されていることがわかります。 Xillybus IP core は、これらの信号を使用して FIFO からデータを読み取り、このデータを /dev/xillybus_read_32で提供されるデータ ストリームとして利用できるようにします。
xillydemo.vが変更される前は、これらの信号は Xillybus IP core が書き込むのと同じ FIFO に接続されていました。結果は loopbackでした。 ソフトウェアによって /dev/xillybus_write_32 に書き込まれたデータは、最初に Xillybus IP coreによって FIFO に挿入されました。次に、 Xillybus IP core は FIFO からデータを読み取り、それを /dev/xillybus_read_32に提示します。この loopback の目的は、 Xillybus がどのように機能するかを学ぶための出発点となることです。
xillydemo.vでの変更後、これらの信号は FIFOから切断されます。代わりに、 user_r_read_32_data は常に 0x0a4d5750 と等しく、 user_r_read_32_empty は常に 0 です。また、 user_r_read_32_rden は logicによって無視されます。これにより、決して空ではない仮想の FIFO が作成されます。この架空の FIFO の出力は常に同じ値になります。 0x0a4d5750。 Xillybus IP core は、常にこの定数値が入力された FIFO があるかのように動作します。したがって、 /dev/xillybus_read_32から読み取ると、 0x0a4d5750 というワードが繰り返し到着します。この単語が出力されると、4 バイトとして解釈されます。 0x50、 0x57、 0x4d 、 0x0a。つまり、文字 P、 W、 M 、および line feed ( Linuxで行の終わりをマークするために使用されます)。
Verilog コードと実際の pinsの関係
上記の Verilog コードは PWM 信号を J6に接続しますが、これはどのようにして pin headerに届くのでしょうか?答えは xillydemo.xdcにあります。このファイルは、 bitstream を作成する Vivado プロジェクトの一部です (「vivado-essentials」ディレクトリ内)。
xillydemo.xdc には、 FPGA が電子部品として正常に動作するために必要なさまざまな情報が含まれています。特に、このファイルには次の行が含まれています。
[ ... ]
## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22 [get_ports {J6[0]}]; #J6/1 = IO_B33_LN2
set_property PACKAGE_PIN T22 [get_ports {J6[1]}]; #J6/2 = IO_B33_LP2
set_property PACKAGE_PIN W22 [get_ports {J6[2]}]; #J6/3 = IO_B33_LN3
set_property PACKAGE_PIN V22 [get_ports {J6[3]}]; #J6/4 = IO_B33_LP3
set_property PACKAGE_PIN Y21 [get_ports {J6[4]}]; #J6/5 = IO_B33_LN9
set_property PACKAGE_PIN Y20 [get_ports {J6[5]}]; #J6/6 = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}]; #J6/7 = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}]; #J6/8 = IO_B33_LP7
[ ... ]
最初の行は、信号 J6[0] を U22に接続する必要があることを示しています。これは、 FPGAの物理パッケージ上の位置です。 Smart Zynqの schematicsによると、この FPGA pin は pin headerの最初の pin に接続されています。もう一方の ports の位置も同様に定義されます。
J6/3 から J6/34 までの範囲の pin は groundとして使用できると上で述べました。これらの output pins には値 '0'があるからです。 xillydemo.vの先頭の次の行によれば、 J6 は 34 ビットで構成されているため、これは当てはまります。
inout [33:0] J6, //BANK33 VADJ
J6 の値の割り当てが次のとおりであることを思い出してください。
assign J6 = { pwm_right, pwm_left };
これは、 J6[0] が pwm_left に等しく、 J6[1] が pwm_rightに等しいことを意味します。残りはどうですか? Verilogの構文に従って、他のすべてのビットには値 0 が割り当てられます。
DC bias
pin header は FPGAの logic outputsに接続されています。これらの pins は、 logic state が '1'の場合、 3.3V 付近の電圧を持ちます。 logic state が '0'だと 0Vくらいの電圧になります。
元の audio sample の値が 0 の場合、 threshold_left と threshold_right の値は 1024 になります。つまり、 pwm_right と pwm_left は平均して半分の時間で High になります。したがって、平均電圧 (DC) は 3.3V ÷ 2 = 1.65Vになります。したがって、 WAV ファイル内の audio samples に完全な DC balanceが含まれている場合でも、ヘッドフォンは DC コンポーネントとして 1.65V に公開されます。
したがって、 100Ω resistor の目的は、サウンド レベルを下げるだけではなく、 DC currentを制限することです。ただし、この抵抗がなくても、 FPGA自体の制限とヘッドフォンの電気抵抗により、電流はおそらく無害です。 resistor は単なる予防措置です。
概要
このプロジェクトでは、ヘッドフォンに直接接続できるアナログ オーディオ信号を生成する目的でデジタル output pin を使用する方法を示しました。このプロジェクトで重点を置いたのは、ソフトウェアから FPGAにデータを送信する目的での Xillybus stream の使用方法を示すことでした。 PWM の簡単な実装も示されました。