TangNanoで遊んでみた例3つ目です
目標
GOWINのS/PDIF(TX/RX)のIP coreの動きを調べて、2経路のS/PDIF信号をミキシングする
最低限読むべきデータシート
S/PDIFの仕様
IEC60958-3という規格で定められています。ただし、原本は有料です。
ただ、ネット上ではこの規格について言及しているサイトは多数ありますので、今回必要となる大雑把な部分についての情報は簡単に入手可能と思います。
基本的にはこの規格を知っている体でこの記事は記載していますが、必要な部分についてはある程度説明します。
TangNano 9K 回路図
https://dl.sipeed.com/fileList/TANG/Nano 9K/2_Schematic/Tang_Nano_9k_3672_Schematic.pdf
回路図です。TangNanoのpinと実際のFPGAのpinの接続を調べる必要があります。
まあ、基板の裏に書いてますが・・・。
Gowin SPDIF Receiver IP User Guide
https://www.gowinsemi.com/upload/database_doc/2108/document/626a25b772b2b.pdf
今回のメイン、S/PDIF信号の受信IPです。主に見るべきところは下記です。
・必要クロック
The input working clock rate of the controller shall not be less than 512Fs (FS is the audio sampling rate)
ターゲットのサンプリングレートの512倍以上が必要です。今回のターゲットはサンプリングレート48KHzとしますので、24.576MHz以上が必要です。
・IPのpinとタイミングチャート
O_sub_frame0_flag がアクティブの時、O_audio_dにch0(左)のPCMデータが渡されます。
同様に、O_sub_frame1_flagがアクティブの時、O_audio_dにch1(右)のPCMデータが渡されます。
その他の信号は今回めんどくさいので見ません。
Gowin SPDIF Transmitter IP User Guide
https://www.gowinsemi.com/upload/database_doc/2107/document/626a2572d75fd.pdf
今回のメイン2、S/PDIF信号の送信IPです。
・必要クロック
The input working clock rate of the controller is SPDIF data transmission rate (i.e., audio sampling rate *64);
サンプリングレートの64倍のクロックが必要です。つまり3.072MHzです。
・IPのpinとタイミングチャート
O_sub_frame0_flagがアクティブになった次のクロックで、I_audio_dにch0(左)のPCMデータを渡します。
同様に、O_sub_frame1_flagがアクティブになった次のクロックで、I_audio_dにch1(右)のPCMデータを渡します。
その他、少なくともChannel Status Bit dataは本来渡す必要がありますが、わりとめんどくさいので固定値にします。
※実験に使ったDACでは再生できました。ただし、使用するDACによっては一切再生できないかもしれません。
制作例
必要パーツ
すべて秋月電子通商で入手可能です
・TangNano 9K
https://akizukidenshi.com/catalog/g/g117448/
・S/PDIF 光レシーバー
https://akizukidenshi.com/catalog/g/g109597/
・S/PDIF 光トランスミッター
https://akizukidenshi.com/catalog/g/g109598/
結線
データシートによると、コンデンサやインダクタをつけることになっていますが、まあ実験なので省略でいいでしょう。ノイズ対策用なので、高サンプリングレートだと動かない可能性はありますね。
現物
ソース
SPDIFミキサー
module top(
input wire RESETn,
input wire CLOCK,
input wire SPDIFI1,
input wire SPDIFI2,
output wire O_Spdif_tx_data
);
wire gresetn;
wire pll_locked;
wire pllclk;
rPLL #(
.FCLKIN (27),
.IDIV_SEL (9-1),
.FBDIV_SEL (32-1),
.ODIV_SEL (8),
.DEVICE ("GW1NR-9C")
) pll (
.CLKIN (CLOCK),
.CLKFB (1'b0),
.RESET (~RESETn),
.RESET_P (1'b0),
.FBDSEL (6'b0),
.IDSEL (6'b0),
.ODSEL (6'b0),
.DUTYDA (4'b0),
.PSDA (4'b0),
.FDLY (4'b0),
.CLKOUT (pllclk),
.LOCK (pll_locked),
.CLKOUTP (),
.CLKOUTD (),
.CLKOUTD3 ()
);
assign gresetn = RESETn & pll_locked;
//64fs clock作成
clockdiv div(
.RESETn(gresetn),
.ICLK(pllclk),
.OCLK(divclk)
);
//SPDIF1 受信
wire [16:0] rx1_ch0;
wire [16:0] rx1_ch1;
recv rx1(
.RESETn(gresetn),
.CLOCK(pllclk),
.SPDIFI(SPDIFI1),
.ch0_data(rx1_ch0),
.ch1_data(rx1_ch1)
);
//SPDIF2 受信
wire [16:0] rx2_ch0;
wire [16:0] rx2_ch1;
recv rx2(
.RESETn(gresetn),
.CLOCK(pllclk),
.SPDIFI(SPDIFI2),
.ch0_data(rx2_ch0),
.ch1_data(rx2_ch1)
);
//ミキシング
wire [16:0] mix_ch0;
wire [16:0] mix_ch1;
assign mix_ch0=rx1_ch0+rx2_ch0;
assign mix_ch1=rx1_ch1+rx2_ch1;
reg [15:0] r_mix_ch0;
always @(negedge pllclk)
if (mix_ch0[16:15]==2'b10) r_mix_ch0<=16'h8000; //負方向丸め込み
else if (mix_ch0[16:15]==2'b01) r_mix_ch0<=16'h7fff; //正方向丸め込み
else r_mix_ch0<=mix_ch0[15:0];
reg [15:0] r_mix_ch1;
always @(negedge pllclk)
if (mix_ch1[16:15]==2'b10) r_mix_ch1<=16'h8000;
else if (mix_ch1[16:15]==2'b01) r_mix_ch1<=16'h7fff;
else r_mix_ch1<=mix_ch1[15:0];
//送信
wire TX_O_sub_frame0_flag;
reg ch;
always @(posedge divclk)
ch<=TX_O_sub_frame0_flag;
SPDIF_TX_Top tx(
.I_clk(divclk), //input I_clk
.I_rst_n(gresetn), //input I_rst_n
.I_audio_d((ch==1'b1)?r_mix_ch0:r_mix_ch1), //input [15:0] I_audio_d
.I_validity_bit(1'b0), //input I_validity_bit
.I_user_bit(1'b0), //input I_user_bit
.I_chan_status_bit(1'b0), //input I_chan_status_bit
.O_audio_d_req(), //output O_audio_d_req
.O_validity_bit_req(), //output O_validity_bit_req
.O_user_bit_req(), //output O_user_bit_req
.O_chan_status_bit_req(), //output O_chan_status_bit_req
.O_block_start_flag(), //output O_block_start_flag
.O_sub_frame0_flag(TX_O_sub_frame0_flag), //output O_sub_frame0_flag
.O_sub_frame1_flag(), //output O_sub_frame1_flag
.O_Spdif_tx_data(O_Spdif_tx_data) //output O_Spdif_tx_data
);
endmodule
module clockdiv(
input wire RESETn,
input wire ICLK,
output reg OCLK
);
//clk1 :マスタークロック周波数
//clk2 :求めるクロック周波数
//width:clk1+clk2*2に必要なbit数
//clk1とclk2は最大公約数で割っておく
parameter clk1 = 125;// 96000/768
parameter clk2 = 4; // 3072/768
parameter width = 8;
reg [width-1:0] cnt;
wire [width-1:0] cnt_tmp;
assign cnt_tmp=cnt+clk2+clk2;
always @(posedge ICLK or negedge RESETn)
if (RESETn==1'b0) begin
cnt<=0;
OCLK<=1'b0;
end
else if (cnt_tmp > (clk1-1)) begin
cnt <= cnt_tmp -clk1;
OCLK<=!OCLK;
end
else cnt<=cnt_tmp;
endmodule
//SPDIF受信&PCMデータ同期化
module recv(
input wire RESETn,
input wire CLOCK,
input wire SPDIFI,
output wire [16:0] ch0_data,
output wire [16:0] ch1_data
);
wire O_sub_frame0_flag;
wire O_sub_frame1_flag;
wire [15:0] O_audio_d;
reg [31:0] pcmdata;
SPDIF_RX_Top rx(
.I_clk(CLOCK), //input I_clk
.I_rst_n(RESETn), //input I_rst_n
.I_spdif_rx_data(SPDIFI),//input I_spdif_rx_data
.O_audio_d(O_audio_d), //output [15:0] O_audio_d
.O_validity_bit(), //output O_validity_bit
.O_user_bit(), //output O_user_bit
.O_chan_status_bit(), //output O_chan_status_bit
.O_spdif_data_en(), //output O_spdif_data_en
.O_block_start_flag(), //output O_block_start_flag
.O_sub_frame0_flag(O_sub_frame0_flag), //output O_sub_frame0_flag
.O_sub_frame1_flag(O_sub_frame1_flag), //output O_sub_frame1_flag
.O_parity_check_error(), //output O_parity_check_error
.O_lock_flag(), //output O_lock_flag
.O_spdif_recovery_clk() //output O_spdif_recovery_clk
);
reg [15:0] ch0_tmp;
always @(negedge CLOCK)
if (O_sub_frame0_flag==1'b1) ch0_tmp<=O_audio_d;
always @(negedge CLOCK)
if (O_sub_frame1_flag==1'b1) pcmdata<={ch0_tmp,O_audio_d};
assign ch0_data={pcmdata[31],pcmdata[31:16]};
assign ch1_data={pcmdata[15],pcmdata[15:0]};
endmodule
cst
IO_LOC "O_Spdif_tx_data" 49; IO_PORT "O_Spdif_tx_data" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3; IO_LOC "SPDIFI2" 31; IO_PORT "SPDIFI2" IO_TYPE=LVCMOS33 PULL_MODE=UP PCI_CLAMP=OFF BANK_VCCIO=3.3; IO_LOC "SPDIFI1" 32; IO_PORT "SPDIFI1" IO_TYPE=LVCMOS33 PULL_MODE=UP PCI_CLAMP=OFF BANK_VCCIO=3.3; IO_LOC "CLOCK" 52; IO_PORT "CLOCK" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3; IO_LOC "RESETn" 4; IO_PORT "RESETn" IO_TYPE=LVCMOS18 PULL_MODE=UP BANK_VCCIO=1.8;
動作解説
クロック系
前述のとおり、RX系には24.576MHz以上、TX系には3.072MHzのクロックが必要です。
RX系は正確にこの値である必要はないため適当でよいのですが、TX系は正確に3.072MHzのクロックが必要です。
ただし、この周波数は原発振(27MHz)とrPLLの組み合わせでは実現できません。
そのため、なんとかして3.072MHzを作成する必要があります。その原理について説明します。
まず、作りたいクロックより十分に高いクロック(今回は96MHz)を用意します。
96MHzから3.072MHzを作りたい場合、計算上は31.25分周すればよいことになりますが、整数値以外の正確な分周というのはロジック回路ではできません。
ざっくり計算だと、96MHzを16clkごとに出力クロックを反転(つまり32分周)すると3MHzが作成できますが、この場合だと所望値より若干遅く、
96MHzを15clkごとに反転すると3.2MHzとなり、所望値より早くなります。
この場合、どうやって3.072MHzにできるだけ近いクロックを作るかということになりますが、基本的に16clkごとに出力を反転、ただし、数回に1度だけ15clkで出力を反転させます。
具体的には、8回に3回だけ15clkで反転するパターンを作ると、(16x5+15x3)÷8=15.625となり、所望の31.25分周となります。15clkで反転するパターンはなるべく連続して起きないようにしたいため、
16,16,15,16,16,15,16,15
というサイクルで出力クロックを反転すると、平均3.072MHzのクロックを生成することができます。
ただし、パルス幅は96MHzの1clk分変動することになります(クロックジッターとなる)。そのため、生成元クロックはできるだけ高速なのが望ましいです。
この動作をしているのが、clockdivモジュールになります。
一見なぜこのソースでその動作になるのかがわかりにくいですが、以下のような動作です。
まず中途半端な分周比だとわかりにくいので、簡単に6分周して16MHzを作るパターンです。
16MHz半周期の時間を96000カウント(生成元クロック周波数の値。無単位)として置くと、96MHzの半周期は16000カウント(生成したい周波数の値)となります。つまり96MHzが1周期で16000x2=32000カウントです。
このカウント値を計算するカウンターを持ち、96MHzのクロックごとにカウント値に32000を加算していき、96000となったところで繰り上がり&出力クロックの反転とします。
すると、この図のように6分周を作ることができます。
つぎに、今回のように中途半端な分周比となるパターンです。
同様に96MHzの1クロックごとに、カウンタを3072x2=6144増加していきます。
まず15クロック目にカウンタ値は92160となり、その次のクロックで98034となりますが、カウンタは96000で繰り上がる(0リセットではない)ため、実際の値は98034-96000=2304となります。このタイミングで出力クロックを反転すると、H幅は16clk@96MHzとなります。
その15クロック後に、カウンタ値は2304+6144x15=94464となり、さらにその次のクロックで100608となりますが、繰り上がり、100608-96000=4608となります。このタイミングで出力クロックを反転すると、L幅は16clk@96MHzとなります。
この14クロック後、カウンタ値は4608+6144x14=90624となり、さらにその次のクロックで96768となりますが、繰り上がり、96768-96000=768となります。このタイミングで出力クロックを反転すると、今度はH幅は15clk@96MHzとなります。
このように、繰り上がりで余った分を次の周期に持ち越すことで、いい感じにパルス幅が15~16clkと分散して変動することになり、結果として平均すると所望の周波数の出力が得られます。
ただ、この際96000と6144でそのまま扱うとbit数が多くフリップフロップが無駄になるため、両方の最大公約数(768)で割って計算に使う値を小さくしておきます。
というのが、clockdivモジュールの動作となります。
ちなみに、この構成だと生成元クロックは116MHz程度が限界でしたので、96MHzとしています。
なんかもっと賢い方法がありそうなので、良い方法があれば教えてください。
すなわち、この方法で生成した3.072MHzクロックは最大96MHz 1clk分のパルス幅のブレが発生します。
具体的に言うと、3.072MHzクロックのパルス幅はduty50%として約163nsですが、96MHzの16clkでは約167ns、15clkでは約156nsですので、
-4%~+2.4%(-40000~24000ppm)の範囲でクロックパルスが変動することになります。
S/PDIFの仕様では、標準仕様で1000ppm、高精度仕様で50ppmとなっていますので、仕様を満たせていません。
こちらも実験に使ったDACでは再生できましたが、使用するDACによっては一切再生できないかもしれません。
受信系
recvモジュールです。
基本的には受信用IP coreを呼び出すラッパー階層となります。
S/PDIFはシリアル通信ですので、順次データが送られてきます。その際、ステレオデータですので左ch→右ch→左ch→右chと交互に送られてきますが、最初に送られるのは左chです。
受信用IP coreの出力は、各chのデータを受け切った瞬間だけO_sub_frameX_flagがアクティブとなり、PCMデータが1clk幅のみ出力されます。つまり、PCMデータを後でミキシングに使うためには、このデータを保持しておく必要があります。
まずch0(左)のデータを受け取った段階でch0_tmpにデータを一時的に保持しておきます。
その後、ch1(右)のデータを受け取った段階で、この階層の出力信号(pcmdata)にch0_tmpの内容と今IP coreから受信したch1の情報を保持します。
こうすることで、かならずpcmdata、すなわちこの階層の出力データは、正しい左右ペアの信号を常に出力することになります。ch0_tmpを経由しないでch0もch1も直接pcmdataに保持した場合、タイミングによっては左右で1サンプルずれた状態になるため、このような方法が必要です。
また、IP coreから受信するデータは16bitの符号付PCMデータを左右チャンネル分の2つとなりますが、その後のミキシングを考え、1bit拡張して17bit×2として出力します。
符号付データのbit拡張は、単純に最上位ビットをそのままコピーするのみで可能です。
そこで、pcmdata(これは16bitデータが左右2つ分連結して32bitになっている)を2つの17bit信号(chX_data)にassignしています。
ミキシング
2つのS/PDIF入力があるため、recvモジュールは2つ使用しますが、この2つのrecvモジュールから得られるPCMデータを加算します。
17bitデータ(左右あるので2つあるが、左右別々に全く同じ処理をするため、片chのみ説明)をrecvモジュールから受け取りますが、bit幅が1bit増えているだけで、元データは16bin符号付きデータなので、取り得る値は10進数で-32768~+32767です。
2系統のミキシング(単純加算)をするため、ミキシング後の取り得る値は-32768x2=-65536~+32767×2=+65534となります。
これを最終的に16bitデータとして出力する必要があるので、オーバーフローしたぶんは丸め込みとします。
-32768より小さいものを-32768、32767より大きいものを32767として丸めますが、こちらは2進数で考えると上位2bitのみ確認すれば丸め込みの必要性がわかります。
-65536~-32769は2進数で表すと1_0000_0000_0000_0000~1_0111_1111_1111_1111となり、上位2bitが10の時だけマイナス方向への丸め込みが必要で、
+32768~+65534は2進数で表すと0_1000_0000_0000_0000~0_1111_1111_1111_1110となり、上位2bitが01の時だけプラス方向への丸め込みが必要となります。
丸め込みが不要な場合、上位2bitは同じ値になっていますので、上位1bitを捨てるだけで16bit信号として使用できます。
このあたりは左右で同じ処理になりますが、記載が単純なのでモジュールに分けると記載量が増えるのでtopにべたに書きました。ただ、保守性を考えれば本来はモジュールに分けるべきでしょう。
送信系
ミキシングしたデータはpllclk(=受信用クロック)で毎クロック作成されます。それを送信用タイミングに合わせて送信用IP Coreに渡す必要があります。
送信用IP coreがO_sub_frame0_flagをアクティブにした際、次のクロックで左ch用のPCMデータ(r_mix_ch0)をI_audio_dに渡す必要があります。
また、O_sub_frame1_flagをアクティブにした際、次のクロックで右ch用のPCMデータ(r_mix_ch1)をI_audio_dに渡す必要があります。
それ以外の場合は不問なので、とりあえず基本的には右chデータを常に渡しておいて、O_sub_frame0_flagに合わせてその時だけ左chデータを渡すようにします。
あえて渡す必要がないときは0を渡したりする必要もありませんし、そのようなロジックを組むだけで回路規模が無駄になりますので、この実装で問題ないでしょう。
ミキシングの計算もこれに合わせれば毎クロック実施する必要もないため、そのような実装にすれば多少省電力化できるかもしれません。微々たるものですが。
冒頭にも記載しましたが、本来PCMデータ以外(チャンネルステータス等)も送る必要がありますが、ここではすべて0としています。
チャンネルステータスには著作権情報も含まれます。S/PDIFの著作権仕様(SCMS)は著作権情報が乗っていない(all 0)の場合はコピー禁止となりますので、おそらくPC等での録音やスルー再生はできないでしょう。
問題点
非同期クロック
基本的に複雑な論理の分周クロック(今回でいえば3.072MHz)をクロックとして使用してフリップフロップを動かすのは、タイミング検証が正しく行われない可能性があるため、避けるのが賢明です。
が、デバイスのスペックに対して十分に遅いクロックなので、問題なく動いているのであまり気にしないことにします。
IP Coreの仕様に伴う音質劣化
仮にミキシングで片方を無音としてスルーさせた場合でも、音質劣化が起きます。どちらかというと劣化というよりはプチノイズとして聞こえるかもしれません。
基本的に、今回使用したIP coreはこのような(RXで受けたものをそのままTXで出力する)目的には適しません。その理由は以下のようになります。
まず、今回出力の48KHzの基準となるクロックは、オンボードの27MHz発振器になります。対して、入力される信号の基準はそれぞれの音源になります。
どちらも仕様上は48KHzですが、そこには水晶の製造誤差等がありますので、正確に一致は絶対にしません。
そのため、長時間再生を続けていると、かならず誤差が発生し、データが余るor足りなくなるのどちらかの現象が発生します。
受信データが足りなくなった場合は同じデータを2度送出、余った場合は1サンプル捨てることになりますので、その部分は定期的にノイズとなって聞こえる場合があります。
この現象を回避するためには、受信したS/PDIF信号から正確に元クロックを再作成できる仕組みが必要となります。
S/PDIF信号から正確に送信元の48KHz(サンプリングレート)を取り出すことは(結構頑張れば)FPGAでも可能ですが、48KHzからビット送信用クロック(64倍クロック)を作り出す方法がありません。
仕組みとしては48KHzをPLLに入力し、単純に64倍することで可能ですが、rPLLはそこまで低い周波数を入力として使うことはできません。
FPGA外部にPLL回路を持つことで対処可能ですが、そのような回路を持つのであればクロック再生機能を持ったS/PDIF受信IC(DIX9211等)を乗せるほうが簡単です。
また、入力する2系統の信号にも必ず若干の誤差がありますので、ミキシング時も同様な現象が発生します。
ちなみに、超高級オーディオ機だと外部にルビジウムクロック発生装置などを用意して、全部の機材をそのクロックに同期するなどができるらしいです。
サンプリングレート変換に伴う音質劣化
今回、送信を48KHzで設計していますが、48KHz以外のサンプリングレートを入力した場合も、RX側クロック要求を満たす(サンプリングレート×512が96MHz以下、すなわち一般的な値なら96KHz以下)なら、それなりに音はなります。
ただ、正しいサンプリングレート変換が行われず、同じデータを2回出力するなどになりますので、ノイズの原因になります。
チャンネルステータス
S/PDIFの構造は、大雑把に言うと一番大きい単位はブロックとなります。
ブロックの中にはフレームが192個あり、フレームはサブフレーム2個で構成されます。
図示すると以下のようになります。
サブフレームは、ステレオであれば先に来るほうが左chです。
この中の、「C」で記載されているbitがチャンネルステータスとなります。
1ブロック=384bitでチャンネルステータスは1セットとなりますので、チャンネルステータスを正しく設定するためには、カウンターを作ってブロックの開始からカウントし、今フレーム目かを保持して情報送信タイミングを作成する必要があります。
※チャンネルステータスは左右チャンネルで1bitを除いて同一のものとなるので、現実的には192bit+1bitです。差異があるのは左右チャンネルの識別に使うbitのみです。
再生に重要な情報としては、サンプリングレートの情報はデータフォーマット(無圧縮PCM等)があります。
そのほか、クロック精度情報もあります。今回はall0なので高精度モードとなっていますが、前述したとおり標準精度すら満たせておりません。
また、実装として面倒なものとして著作権情報があります。
著作権情報を正しく実装するのは場合分けがかなりあり複雑です。ざっくりいうと2系統の入力のうち、著作権保護が厳しいほうを採用して出力側に転送する必要があります。
もともとはSCMS(CD→DAT/MDは1世代だけコピー可等)だけでしたが(それでも複雑)、デジタル放送のCGMSも追加仕様であり、かなり複雑になっています。
ぶっちゃけall0で常にコピー禁止にしておくのが一番簡単です。まあアンプにつないで聞くだけであればこれで問題ないでしょう。
著作権保護されている場合でもデータ自体は暗号化されていませんので、ミキシングなどの加工は容易にできます。
※V/Uはあまり気にしなくて問題ありません。PはパリティなのでIP coreが自動で設定してくれます。
最後に
次回は問題点(特に音質面)を解決したものを作りたいです。
また、回路規模もかなり小さいので、もっと多チャンネル化もできると思います。
各チャンネルごとにボリューム設定などもできるといいですね。
投稿者の人気記事
-
lyricalmagical
さんが
2025/01/09
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する