この Web ページは、Smart Zynq ボードの機能を探求する小規模プロジェクトのグループに属しています。
このプロジェクトは、HelloFPGA にも掲載されています。中国語の読者にはそちらをおすすめします。
はじめに
このチュートリアルでは、通常のヘッドホンを Smart Zynq ボードに接続して音楽を聴く方法を説明します。このプロジェクトの目的は、FPGA に連続してデータを送るための Xillybus ストリームの使い方を示すことです。PWM 変調器を実装する Verilog コードもここで示します。
ここで示すコードは、オーディオ出力の実装例ではありません。アナログ出力の実装で通常使われるのは、シグマ-デルタ変調と呼ばれる、より複雑なテクニックです。このテクニックは FPGA でも実装できますが、理論的な背景ははるかに理解が困難です。
この実装には別の欠点もあります。サンプルレートが不正確なことです(48000 Hz ではなく 48828 Hz になります)。これは、ロジックが使うクロックの周波数を変えるだけで簡単に修正できます。この例は正確さよりも単純さに重点を置いているため、この目的でのクロックの操作方法はここでは示しません。
このデモに必要な機材は次のとおりです。
- Smart Zynq ボード (SP または SL)。
- 通常のアナログヘッドホン 1 組。
- ワニ口クリップとワイヤ、またはボードのピンに接続するための他の手段。
- 任意: 50Ω〜200Ω の抵抗。この抵抗の目的は、過剰な電気電流からヘッドホンと Smart Zynq ボードを保護することです。この抵抗がなくてもこの例は動作しますが、電子機器を損傷するリスクがあります。
Vivado プロジェクトの準備
demo バンドル (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 バンドル (demo bundle) 用にビットストリーム (bitstream) ファイルを作成したときと同じ方法で、ビットストリーム (bitstream) ファイルを作成します。また、同じ方法でビットストリーム (bitstream) ファイルを TF カードにコピーします(このプロジェクトで作成したファイルで、古い xillydemo.bit ファイルを上書きします)。
ヘッドホンの接続
音声信号が出力される I/O ピン、J6/1(左耳用)または J6/2(右耳用)に、50Ω〜200Ω の抵抗を接続します。J6 の位置を確認するには、Smart Zynq ボードの裏面に「Bank 33 VCCIO Vadj」と書かれている場所を探します。この表示の近くにあるピンの列が、使うピンヘッダです。つまり、J6/1 は HDMI コネクタに最も近いピンです。
抵抗のもう一方の端を、ヘッドホンプラグの先端 (tip) に接続します。ワニ口クリップを使うとよいでしょう。
ヘッドホンプラグのスリーブ部分を Smart Zynq のグランドに接続します。ピンヘッダのグランドは J6/35 または J6/36 にあります。ただし、これらのピンは電源ピンの近くにあるため、使用はお勧めしません。
代わりに、J6/3 から J6/34 までの範囲にある任意のピンをグランドとして使うこともできます。FPGA はこれらのピンを出力ピンとみなし、「0」の論理レベルに維持します。したがって、これらのピンをグランドとして使用できます。
また、ボードのコネクタの外側の金属部分にワニ口クリップを接続してグランドを取ることもできます。イーサネットコネクタ、HDMI コネクタ、または USB コネクタのいずれかです。
ボードの起動
Smart Zynq をいつもどおりに電源オンします(または再起動します)。次のステップは、正しいビットストリーム (bitstream) ファイルが FPGA(PL 部分)にロードされていることを確認することです。
シェルプロンプトで「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 のファイルシステムにコピーします。つまり、Linux システム内のコマンドからそのオーディオファイルを利用できる必要があります。
このファイルは WAV 形式である必要があります。非圧縮 PCM、2 チャンネル、s16le です(WAV ファイルはほとんどの場合この形式です)。サンプリング (sampling) レートは 48000 Hz にしてください。ただし、44100 Hz でも十分に動作します。
適切なオーディオファイルは、このリンクからダウンロードできます。
ファイルを Linux システムにコピーする方法はいくつかあります。たとえば、次のコマンドでイーサネットネットワークを使い、別のコンピュータから Xillinux のホームディレクトリへファイルをコピーできます。
$ scp sample.wav root@192.168.1.10:~/
このコマンドは、Microsoft Windows のコマンドプロンプトでも Linux シェルでも動作します。IP アドレス(この例では 192.168.1.10)は、ボードの IP アドレスに変更してください。
Xillinux にファイルをコピーする他の方法もあります。たとえば、NFS や CIFS を使う方法です。
ファイルを Xillinux のファイルシステムにコピーしたら、次のコマンドでオーディオを再生します。
# cat sample.wav > /dev/xillybus_write_32
「sample.wav」の部分は、再生したいファイルの名前に置き換えてください。ここに示したコマンドは、ファイルがカレントディレクトリにある場合に動作します。
このコマンドは、新しいシェルプロンプトが表示されるまで、ヘッドホンでファイルを再生し続けます。音楽が片方の耳(または、J6/1 と J6/2 の両方をヘッドホンプラグの別々の部分に接続した場合は両方の耳)で聞こえるはずです。
CTRL-C を押せば、このコマンドを途中で停止できます。
以上です。このページの残りでは、この仕組みについて説明します。
オーディオデータが FPGA に届く仕組み
「cat」コマンドは、オーディオファイル (sample.wav) の内容を、「xillybus_write_32」というデバイスファイルにコピーします。Linux システムでは、これはハードウェアドライバにデータを送る一般的な方法です。この例では、ドライバは Xillybus の IP コア (IP core) とインターフェースしています。その結果、データは FPGA のロジック内の 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 つあります。din、wr_en、full (満杯) です。これら 3 つのポートはすべて、Xillybus IP コア (IP core) に接続されています。つまり、3 つの信号 (user_w_write_32_data、user_w_write_32_wren、user_w_write_32_full) は、xillybus という名前のモジュールに接続されています。この構成により、Xillybus IP コアは FIFO にデータを書き込むことができます。
Xillybus は、この構成を使って、ソフトウェアが /dev/xillybus_write_32 に書き込んだデータを FIFO に満たします。Xillybus は FIFO にできるだけ多くのデータを書き込もうとし続けますが、オーバーフロー (overflow) を起こすことはありません(つまり、FIFO の full (満杯) 信号に従います)。
まとめると、次のようなことが起こります。
- 「cat」コマンドは、sample.wav のデータをデバイスファイル (/dev/xillybus_write_32) にコピーします。
- Xillybus のドライバは、このデータを DMA バッファにコピーします。
- FPGA 内の Xillybus のロジック(Xillybus IP コア)は、DMA バッファからデータを読み取り、FIFO にデータを書き込みます。
- FPGA 内のアプリケーションロジックは FIFO からデータを読み取り、このデータを消費します。
これらすべての操作は、同時にかつ継続的に行われます。
Xillybus の詳細については、一連のページ、特にこのページを参照してください。
オーディオ信号が生成される仕組み
ここまでの説明で、データが FPGA 内のアプリケーションロジックにどのように届くかがわかりました。次に、データがどのようにオーディオに変わるかを見ていきましょう。
まず、Verilog コードの中の次の行に注目してください。
assign J6 = { pwm_right, pwm_left };
この記述により、2 つのオーディオ出力は pwm_right と pwm_left から構成されます。これら 2 つのレジスタには、次のように値が代入されます。
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 は単純なカウンタであることに注意してください。このレジスタは 11 ビットで構成されているため、0 から 2047 まで数え、その後再び 0 から始まります。
pwm_left の値は、threshold_left が pwm_level よりも大きいとき「1」になります。つまり、threshold_left は、0 から 2047 までのすべての数値を繰り返し経由するカウンタと比較されます。threshold_left の値が高いほど、pwm_left が「1」である時間は長くなります。これが PWM(パルス幅変調)の原理です。パルスの長さは、生成したいアナログ信号の値に線形に比例します。
pwm_right も、threshold_right に対して同じように動作します。
threshold_left と threshold_right には、Xillybus IP コア (IP core) 経由で送られる WAV ファイルのデータが格納されます。ここでは、その仕組みを詳しく見ていきます。
まず、FIFO のインスタンシエーションの中で、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 が empty (空) でなく、fifo_out_valid が Low のときに 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 の出力が有効なときにこのレジスタが High になるということです。より正確には、fifo_out_valid は、FIFO の出力がまだ消費されていないときに High になります。これが、このレジスタが fifo_rd_en が High になった 1 クロックサイクル後に High に変わる理由です。このレジスタは、next_word が High のときに Low に変わります。後で説明するように、PWM を実装するロジックは、next_word が High のときに FIFO の出力を消費します。
next_word は次のように定義されます。
wire next_word = (pwm_level == 11'h7ff);
pwm_level は 0 から 2047 までのすべての値を巡回するカウンタであることを思い出してください。2047 の 16 進表記は 7ff です。したがって、next_word は、pwm_level がちょうど 0 に戻ろうとする直前に High になります。
next_word はどのくらいの頻度で High になるのでしょうか。bus_clk の周波数は 100 MHz です。next_word は、2048 クロックサイクルごとに 1 回 High になります。100 MHz ÷ 2048 ≈ 48828 Hz。したがって、next_word は毎秒約 48828 回 High になります。
先ほど、next_word が High のときに FIFO の出力が消費されると述べました。関連する 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 が Low の場合、これら 2 つのレジスタの値はゼロになります。これは FIFO にデータが送られていないため、FIFO が empty (空) になったときに発生します。
fifo_out_valid が High の場合、FIFO の dout ポートにオーディオサンプルの値が含まれていることを意味します。この値は、2 つのステレオチャンネルのアナログ信号を表します。各サンプルには、16 ビットの 2 の補数形式で表された 2 つの符号付き数値が含まれています。
左のステレオチャンネルに属するオーディオサンプルは、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 には値ゼロが代入されます。「threshold_left > pwm_level」という条件は決して満たされないため、pwm_left は常に Low のままです。一方、fifo_out[15:0] が 32767 に等しい場合、threshold_left の値は 2047 になります。その結果、pwm_left はほとんどの時間 High になります。このようにして、fifo_out[15:0] は、各パルスで pwm_left が High になる時間を制御します。fifo_out[31:16] は、pwm_right を同じように制御します。
メカニズム全体をまとめると、next_word は 2048 クロックサイクルごとに 1 回 High になります。next_word が High のとき、FIFO の出力が調整され、threshold_left と threshold_right にコピーされます。これにより FIFO の出力が消費されるため、fifo_out_valid は Low になります。その結果、FIFO が empty (空) でなければ、fifo_rd_en が High になり、FIFO から新しいオーディオサンプルが読み出されます。
Xillybus IP コアがこの FIFO を sample.wav の内容で満たすことを思い出してください。したがって、sample.wav の内容から threshold_left と threshold_right へのオーディオサンプルのデータフローがあります。前述のように、next_word は毎秒約 48828 回 High になります。これがこのメカニズムのサンプルレートです。
threshold_left は pwm_left が High である時間の割合を制御します。threshold_right と pwm_right についても同様です。さらに、pwm_right と pwm_left は J6 という名前の出力ポートに接続されているため、これらがピンヘッダに現れる信号です。
next_word が High のとき、2 つのことが起こることに注意してください。オーディオサンプルが消費され、pwm_level がゼロから数え始めます。したがって、オーディオサンプルごとに 1 つのパルスが生成されます。
「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 コアはこれらの信号を使って FIFO からデータを読み出し、このデータを /dev/xillybus_read_32 で提供されるストリームとして利用できるようにします。
xillydemo.v を変更する前、これらの信号は、Xillybus IP コアが書き込むのと同じ FIFO に接続されていました。その結果はループバックでした。つまり、ソフトウェアが /dev/xillybus_write_32 に書き込んだデータは、まず Xillybus IP コアによって FIFO に挿入されました。次に、Xillybus IP コアは FIFO からデータを読み出し、/dev/xillybus_read_32 で提供しました。このループバックの目的は、Xillybus の仕組みを学ぶための出発点となることです。
xillydemo.v を変更した後、これらの信号は FIFO から切り離されます。その代わり、user_r_read_32_data は常に 0x0a4d5750 に等しく、user_r_read_32_empty は常にゼロです。さらに、user_r_read_32_rden はロジックによって無視されます。これにより、決して empty (空) にならない架空の FIFO が作成されます。この架空の FIFO の出力は常に同じ値、0x0a4d5750 を持ちます。Xillybus IP コアは、常にこの一定値で満たされた FIFO があるかのように動作します。したがって、/dev/xillybus_read_32 から読み取ると、0x0a4d5750 というワードが繰り返し届きます。このワードが出力されると、4 つのバイト、0x50、0x57、0x4d、0x0a として解釈されます。つまり、文字 P、W、M と改行(Linux では行末を表す)です。
Verilog コードと実際のピンの関係
上記の Verilog コードは PWM 信号を J6 に接続しますが、それがどのようにピンヘッダに到達するのでしょうか。答えは 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 に接続すべきであることを示しています。U22 は FPGA の物理パッケージ上の位置です。Smart Zynq の回路図によると、この FPGA ピンはピンヘッダの最初のピンに接続されています。他のポートの位置も同じ方法で定義されています。
先ほど、J6/3 から J6/34 までの範囲にある任意のピンは、これらの出力ピンが「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 の構文によれば、他のすべてのビットにはゼロが代入されます。
DC バイアス
ピンヘッダは FPGA のロジック出力に接続されています。これらの各ピンは、ロジック状態が「1」のとき約 3.3V の電圧になります。ロジック状態が「0」のときは約 0V です。
元のオーディオサンプルの値がゼロの場合、threshold_left と threshold_right の値は 1024 になります。つまり、pwm_right と pwm_left は平均して半分の時間 High になります。したがって、平均電圧 (DC) は 3.3V ÷ 2 = 1.65V になります。つまり、WAV ファイルのオーディオサンプルが完全に DC バランスしていても、ヘッドホンには DC 成分として 1.65V がかかります。
したがって、100Ω 抵抗の目的は、音声レベルを下げることだけでなく、DC 電流を制限することでもあります。ただし、この抵抗がなくても、FPGA 自身の電流制限とヘッドホンの電気抵抗により、電流はおそらく無害です。抵抗は単なる予防策です。
まとめ
このプロジェクトでは、ソフトウェアから FPGA にデータを送る目的で Xillybus ストリームを使う方法を紹介しました。また、デジタル出力ピンを使って、ヘッドホンに直接接続できるアナログオーディオ信号を生成する方法も示しました。加えて、PWM の簡単な実装も紹介しました。
