Si5351A Linuxドライバの解析メモ

ドライバの実装仕様

  • Linux KernelのCommon Clk Frameworkに則り実装
  • Si5351シリーズに対応。最大8ch出力できる。
    • 今回はSi5351A 10-MSOP品を制御対象としているの3chでの使い方に絞ってます
  • 8ch出力時のclkout6/7のDivider制約にも対応していそう
  • dtsでクロックツリーを記述できる(各clkoutのクロックソースなど)
    • clocks にリファレンスクロックを指定。clocksは別途fixed-clockで定義しておき参照する。
    • silabs,pll-sourceはPLLのソースを指定
      • 0でXTAL
      • 1でClkin(Variant_Cのみ)
    • silabs,multisynth-sourceはMultiSynthのソースを指定
      • 0でPLLA
      • 1でPLLB or VCXO(Variant_Cのみ)
    • silabs,clock-sourceはclkoutにどのMultiSynthをソースにするか指定。
      • 0で自動的にclkoutのソースをMultiSynthに結び付ける。clockout_Nに対してMultiSynth_Nを割あてる。
      • 1でclkout1-3のソースをMultiSynth_0に結び付け、clkout5-7のソースをMultiSynth_4に結び付ける。clkout0,4には無効
      • 2でclkoutのソースをxtalに結び付ける
      • 3でclkoutのソースをclkinに直結。Variant_Cのみ有効
    • silabs,pll-master 指定したclkoutにPLLの再設定を許可する
      • この指定のあるclkoutに周波数設定を行ったとき、PLL設定周波数を算出・設定する。
    • silabs,drive-strength Drive Strength指定可(2,4,6,8mA)
    • silabs,disable-state Disable State指定可(LOW/HIGH/Hi-Z/Never)
    • clock-frequency clkoutの周波数初期値を設定

dts設定方法

  • dtsの例
/ {
        clocks {
                /* 25MHz reference crystal */
                ref25: oscillator {
                        compatible = "fixed-clock";
                        #clock-cells = <0>;
                        clock-frequency = <25000000>;
                };
        };
};

&i2c0 {
        status = "okay";
        si5351: clock-generator {
                compatible = "silabs,si5351a-msop";
                reg = <0x60>;
                #address-cells = <1>;
                #size-cells = <0>;
                #clock-cells = <1>;

                /* connect xtal input to 25MHz reference */
                clocks = <&ref25>;
                clock-names = "xtal";

                /* connect xtal input as source of pll0 and pll1 */
                silabs,pll-source = <0 0>, <1 0>;

                clkout0 {
                        reg = <0>;
                        silabs,drive-strength = <8>;
                        silabs,multisynth-source = <0>;
                        silabs,clock-source = <0>;
                        silabs,pll-master;
                        silabs,disable-state = <2>;
                        clock-frequency = <22579200>;
                };

                clkout1 {
                        reg = <1>;
                        silabs,drive-strength = <8>;
                        silabs,multisynth-source = <0>;
                        silabs,clock-source = <0>;
                        silabs,disable-state = <2>;
                        clock-frequency = <2822400>;
                };

                clkout2 {
                        reg = <2>;
                        silabs,drive-strength = <8>;
                        silabs,multisynth-source = <0>;
                        silabs,clock-source = <0>;
                        silabs,disable-state = <2>;
                        clock-frequency = <44100>;
                };
        };
};

使い方

  • pll-masterを付加したclkをclk_set_rateすると、PLLまで設定が走る
  • pll-masterが無いclkはclk_set_rateすると、そのときのPLL rateからMultiSynthとR Divで誤差の小さくなるクロックを作る
  • このdtsの場合clkout0 -> clkout1/2の順で設定する必要がある
  • clkout0にmclkを。clkout1にbclk,clkout2にlrclkを割り当てた。
  • clock-cells = <1>なので下記のように参照する。2番目の値がclkoutのreg値を指す
    clocks = <&si5351 0>, <&si5351 1>, <&si5351 2>;

備考

  • 最初のクロック設定で正しく設定しているのにクロックが出ない場合
    • 初回のPLL設定シーケンスに問題がありそう
    • device-treeに何でも良いので初期クロック(clock-frequency)を書いておくことで回避可能です
  • NEO2用Audio Kernelでは下記2点の修正を加えています
    • xtal_cl設定機能
      • device treeより水晶の容量負荷を変更できるようにしています
    • driver init時にクロック出力をPowerDownに設定
      • driver init時にクロック出力状態の場合、不安定になるケースが見られたので修正しています。

Si5351Aを使って簡易I2Sマスタを作ってみる

 外部クロックがらみでいろいろ調べていたところ、秋月電子通商でSilicon Labsの3CHクロックジェネレータSi5351Aモジュールを見つけたので、これでI2Sマスタを作ってみた。
 クロック出力をちょうど3系統持っているので、MCLK/BCLK/LRCLKの3種を生成してみる。

f:id:tkztkztkz:20190501210333j:plain

ポイント

  • Si5351Aモジュール搭載の25MHzクロックから計算上の誤差やAccumulated Jitterなしで44.1kHz/48kHz系のクロックを生成できる。
  • 通常SoCやDAI/DAC内部のDividerで生成するLRCLKまで低Jitterのクロック出力で賄えるのが特徴かな?
    • LRCLKで出力タイミングを作っているDACには効果があるかもしれない
    • SoC内蔵のDividerよりSi5351AのDividerのほうがJitterが少ないと仮定してですが
  • 内蔵PLLのAccumulated Jitterがそもそも多いRaspberry Piにも良さそう。SoCが出力できないMCLKも生成できる。
    • RPiの出力はAccumulated Jitterと呼ばれる計算上の辻褄あわせで、時々1周期の長さが可変するJitterが存在する。
    • RPiのAccumulated JitterはMASHフィルタを無効化することで抑制できるらしい(未テスト)
  • 8Pin DIPモジュール形状で500円とお手ごろ価格。電源とI2Cを繋ぐだけ。
    • 自作基板を作るならSi5351A 110円くらい+水晶 100円くらいかな?
  • 使用上の注意点としては、必ずVDDOを先かVDDと同時に立ち上げること。VDDOを供給せずVDDを入れると壊れることもあるようです。
    • チップが非常に弱いので逆接や電源シーケンスを守らないと簡単に壊れます

実装方法

ハードウェア

結線について

  • POWER
    • VCORE/VOUTに3.3Vを供給する
    • GNDを繋ぐ
  • I2C
    • SCL/SDAをSBCに接続する。NanoPi NEO2の場合にはPU抵抗を忘れずに。
  • CLKOUT
    • CLK0をMCLK/CLK1をBCLK/CLK2をLRCLKに接続する。

f:id:tkztkztkz:20190502124205j:plain - 実験の様子。上のほうに居る8pin DIP基板が秋月のSi5351Aモジュール。

対応ハードウェア

 blue-7さんが頒布しているハードウェアを使うと簡単に動作させることができます。

sites.google.com

 トップの写真はI2S AUDIO HATに秋月のSi5351Aモジュールを搭載したものです。

ソフトウェア

  • Si5351Aドライバはdrivers/clk/clk-si5351.cに居るのでこれを利用
  • DAI LINKドライバでSi5351Aのクロックを設定して音を出すようにした
    • DAI_FMTはCBM_CFMに設定する(BCLK/LRCLKを外部から供給する)
    • 適切にMCLK/BCLK/LRCLKを算出し、Si5351Aドライバに設定(clk_set_rate)
    • 適切にfsを算出し、CPU DAIドライバに設定(snd_soc_dai_set_bclk_ratio)

考察とかメモとか

  • 音質はIntPLLより向上。
    • 水晶発振器をOE切り替えて鳴らすのとどっこいかちょい劣るくらい?
  • Si5351Aの発振子の種類で結構音が変わるっぽい
    • 秋月のモジュールに載ってる25MHzは割と元気な音がする
  • BCLK_RATIO(MCLK/BCLK比)が正しく出力されるので、AK449X以外のMCLKが必要なDACでも問題なく動作可能
    • PCM1794(I2S/RJ24)やFN1242A(I2S/RJ24)あたりでも再生できることを確認済み

実験用のカーネルなど

  • ソースはgithubにコミット済み github.com
  • クロック制御の実体は下記ソース
sound/soc/codecs/i2s_mfdl.c
  • Si5351Aの指定は下記dtsを参照のこと
 arch/arm64/boot/dts/allwinner/sun50i-h5-nanopi-neo2-i2s_3clk.dts
  • 近いうちに対応済みVolumio2イメージを公開予定です
  • ラズパイ向けのドライバセットも出せるかも

Si5351Aのスペックをメモ

  • Si5351Aは25MHzの水晶振動子を基に2.5kHzから200MHzまでのクロックを生成できる。
    • 150MHzを超えるクロックはMultisynth Dividerの設定値に制約が生じるがオーディオ用途では関係なさそう
    • 500kHzを下回るクロックはMultiSynth Dividerで生成できないが、Output StageのR Divと併用することで2.5kHzまで下げられる
  • PLLは2系統で3ch出力
    • 各chにMultisynth DividerとR Divが居て、クロックソースをPLLA or PLLBから選ぶ形式
    • オーディオ用途だと基本的にすべてが整数比で割り切れるのでPLLAだけ使えればよい
  • ジッタ関連
    • Phase Jitterは3.5psだそうな
      • Webにしか記述は無い。おそらくTypical
    • Period Jitter max 155ps / typ 70ps
    • 水晶発振器には及ばないがそこそこ良さそう。
      • 京セラのKCシリーズではPhase Jitter max 1.0ps/Period Jitter max 50ps

簡易外部クロック化について 補足

必要なスペック

DAI(I2Sコントローラ)側

  • 高いBCLK入力に対応可能であること
    • NEO2(H5)では98.304MHzでも動いてた。ただし、12.288MHzより高い周波数はスペック範囲外となる。
    • RPi Zero(BCM2835)では24.576MHzまで確認。それ以上は未確認。
  • CBM_CFS動作が可能であること
    • Codec Bitclk Master/Codec FrameSync Slaveを意味するALSA用語
    • SBCのI2Sコントローラが、入力したBCLKからLRCLKを生成・出力できること
    • NEO2(H5)/RPi Zero(BCM2835)ともに可能
  • 高いBCLK RATIO(BCLK/LRCLK比)を設定可能であること
    • 22.5792MHz/44.1kHz再生時には512fs必要
    • 45.1584MHz/44.1kHz再生時には1024fs必要
    • NEO2(H5)では2048fsまで指定可
    • RPi Zero(BCM2835)では1024fsまで指定可

DAC

  • 高いBCLK RATIO(BCLK/LRCLK比)を入力可能であること
    • 近代的なTDM SLOT対応DACだと結構大丈夫そう
    • 古いものや廉価なI2S DACはダメなことが多い。左詰(LJ+1シフト)になるからBCLKカウンタが必要なため?
    • 古くてもRJ入力だとOKなことがある。RJだからシリパラは垂れ流しで、LRCLKでラッチすれば良いから?
  • 高い再生周波数を再生できること
    • 384kHz/32bit再生可能だと大丈夫そう
    • これは44.1kHz再生時でも352.8kHz相当のデータ転送スピードとなるため
  • BCLK/MCLK比、LRCLK/BCLK比に制約が無いこと
    • AK4495のデータシートを読むと、MCLK/LRCLKについては位相の制約などがあるが、BCLKについての制約は書かれていない
    • BCLKをDAC内デジタル信号処理のクロックに使っているとダメな可能性が高い

ソフトウェア

  • CPU DAI(I2Sドライバ) (改造)

    • CBM_CFS動作が可能であること
      • NEO2(H5)は機能追加が必要
      • RPi Zero(BCM2835)は対応済み
    • set_bclk_ratioに対応していること
      • NEO2(H5)は機能追加が必要
      • RPi Zero(BCM2835)は対応済み
    • ハイレートなfslenの計算ができること
      • NEO2(H5)は機能追加が必要
      • RPi Zero(BCM2835)は対応済み
  • DAI LINKドライバ (新規作成)

    • 外部クロックの制御(OE制御やSi514などの可変クロック制御)ができること
    • BCLK RATIO(fs)を算出の上CPU DAI(I2Sドライバ)に設定すること

その他のDACでの動作

  • ES9023(SabreberryDAC ZERO)では512fsで音が出ることを確認
  • PCM5102Aでは64fs(384kHz)のみ音が出た。
    • 残念ながら512fs動作はできず。
    • 128fsから上は無音となる。
  • TDA1543/TDA1387T(I2S)では動作せず。TDA1545A(RJ)では512fs動作した。
    • 前者はI2Sのため、BCLKのカウンタが溢れたのではないかと推察
    • I2SはLRCLKのエッジから1clkディレイして取り込み開始とか、左詰なので量子化bit数分取り込んだらシリパラ止めるかデータラッチしておく必要があるなど制御が複雑
    • 後者はRJのためカウンタは不要で、シリパラにジャブジャブデータを突っ込んで、LRCLKのエッジでデータラッチすれば良いからだと推察

実装周りのメモ

  • NEO2(H5)とRPi Zero(BCM2835)でFrameLengthの指定方法が違う
    • NEO2(H5)では片chのFrameLengthをPeriodとして指定するだけでOK
      • I2S(1bit shift)やRJのデータ出力開始位置は自動算出してくれる
      • 指定可能なのは8/12/16/20/24/28/32bit
      • 言い換えるとデータ出力開始位置を柔軟に指定できない
      • 古いDAC ICの18bitなどには対応できない(裏技で対応することはできそう?という情報有り。未確認)
    • RPi Zero(BCM2835)では、Lch/Rchのデータ開始位置をbit単位で指定する必要がある
      • 例 I2S 64fs の場合 Lch tx_ch1_pos = 1, Rch tx_ch2_pos = 33
      • 例 LJ24 64fsの場合 Lch tx_ch1_pos = 0, Rch tx_ch2_pos = 32
      • 例 RJ24 64fsの場合 Lch tx_ch1_pos = 8, Rch tx_ch2_pos = 40
      • I2S/LJ/RJでそれぞれ計算が必要で面倒だが、データ出力開始位置を柔軟に指定できる
      • RJで15bit出力するなど特殊なslotwidthを実現可能。古いマルチビットDAC ICを使用するときにはとても良さそう。

AK449Xの簡易外部クロック化について

 旭化成DAC AK449Xシリーズにて、複雑なクロック生成回路ではなく単純な発振器を用いて外部クロック化する手法の紹介。
 前回(BCLK/LRCLK出力しかないRaspberry Piで旭化成のDAC IC(AK449xシリーズ)を鳴らす実験 - _tkz_ memo])は外部回路追加無しでしたが今回はちょっとだけ外部回路を追加して、もっと高品質なクロックを供給できないかな?というお話です。

 NanoPi NEO2とRaspberry Pi Zeroで動作確認済み

 なお、データシート的にはダメとは書いてないように見えるが、イレギュラーなクロック入力になるので、動作保証はできません。

実現方法1 水晶発振器2個用意しOEピンで切り替え

  • MCLK=BCLKとし、BCLKをMCLK相当まで上げる
  • サンプリング周波数に合わせて22.5792MHz/24.576MHzのどちらかを切り替えMCLK=BCLKとする
    • 発振器のOEピンはGPIOで制御
  • LRCLKはSBC側I2SブロックでBCLK入力を基に生成する(CBM_CFS動作)
  • BCLK RATIO(BCLK/LRCLK比)は下記のような設定になる
    • 22.5792MHz/44.1kHz再生時 512fs
    • 22.5792MHz/88.2kHz再生時 256fs
    • 22.5792MHz/176.4kHz再生時 128fs
    • 24.576MHz/48kHz再生時 512fs
    • 24.576MHz/96kHz再生時 256fs
    • 24.576MHz/192kHz再生時 128fs
    • 24.576MHz/384kHz再生時 64fs

実装イメージ

  • 使用した水晶発振器はKyocera KC7050
  • GPIOでOEを制御
  • BCLKに22.5792MHz or 24.576MHzを入力 f:id:tkztkztkz:20190501202713j:plain

実現方法2 可変クロックジェネレータを制御

  • MCLK=BCLKとし、BCLKをMCLK相当まで上げる
  • 可変周波数出力のできるSi514を使用する
  • LRCLKはSBC側I2SブロックでBCLK入力を基に生成する(CBM_CFS動作)

実装イメージ

  • クロックジェネレータはSilicon Labs Si514
  • I2CでSi514を制御
  • BCLKには任意のクロックを入力可能だが、デフォルトでは22.5792MHz or 24.576MHzを入力
  • BCLK RATIO(BCLK/LRCLK比)は水晶発振器使用の場合と同じ f:id:tkztkztkz:20190501202746j:plain
  • 8pin DIPソケットは次回以降に説明する別のお話なので、今のところ無視してください。

メモと考察

  • IntPLLに比べると音質は向上
  • 比較的ハードウェア実装は簡単
  • ソフトウェアは結構面倒だが、DAI LINKドライバ化したので、OEピンはdevice-treeで指定できる

実験用のカーネルなど

  • ソースはgithubにコミット済み github.com
  • クロック制御の実体は下記ソース
sound/soc/codecs/i2s_mfdl.c
  • OE制御ピンの指定、Si514の指定は下記dtsを参照のこと
 arch/arm64/boot/dts/allwinner/sun50i-h5-nanopi-neo2-i2s_1clk.dts

ALSA SoCドライバメモ No.004 外部クロック関連

外部クロックを使う場合

  • 外部クロックを使うときのクロック制御はDAI LINKに組み込むか、CODECに組み込むか。どちらでも良さそう。
  • PCM5122のGPIOとDividerを利用しているHiFiBerry DAC+PROではdai link (sound/soc/bcm/hifiberry-dacpro.c)でやっている
    • PCM5122内蔵GPIOの制御はdai linkから子のcodecハンドラを辿ってi2cハンドラを取得し、i2c writeするという離れ業をやっている。あまり真似したくない方法。
  • クロックの向きの話
    • CBS_CFS
      Codec_BCLK_Slave /Codec_FrameCLK_Slave
      クロック全部をCPU DAI(I2S)側が用意する。デフォルト。
    • CBM_CFM
      Codec_BCLK_Master/Codec_FrameCLK_Master
      クロック全部をCODEC側が用意する
    • CBM_CFS
      Codec_BCLK_Master/Codec_FrameCLK_Slave
      BCLKをCODEC側が用意し、LRCKはCPU DAI(I2S)が生成する
  • CBM_CFM/CBM_CFS時のfs指定
    • DAI LINK or CODEC to CPU DAI
      • snd_soc_dai_set_tdm_slotかsnd_soc_dai_set_bclk_ratio経由で外部から指定できる。
  • Dai link/codecからCPU DAIに設定可能なCallbackはこの辺。実装はされていないものもある。
    • うまく使えば、dai linkからMCLKを指定して、内蔵PLLに設定させるとかもできそう
        int (*set_sysclk)(struct snd_soc_dai *dai,
                int clk_id, unsigned int freq, int dir);
        int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,
                unsigned int freq_in, unsigned int freq_out);
        int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);
        int (*set_bclk_ratio)(struct snd_soc_dai *dai, unsigned int ratio);

ALSA SoCドライバメモ No.003 callbackシーケンス

 ALSA SoCドライバ開発でいちいちgrepするのが面倒な情報をメモしておく

Callbackシーケンスのメモ 制御でよく使いそうなhw_paramsとshutdownのみ

  • ポイントは開始時はdai_link->codecs->cpudai。終了時はcpudai->codecs->dai_linkとなること
  • codecsが複数ある場合は、hw_params/shutdown両方とも昇順のみ。shutdown時に降順にはしてくれないので注意が必要!
  • cpu daiが最後になるので、外部クロック制御はdai_link/codecどちらでやっても大丈夫そう。
    • dai_link/codecでfsやclkを決定し、cpu daiに設定
    • cpu daiは設定されたfs/clkを元にPLLやDIVを決定し設定
    • と、いうことができる。

hw_params call sequence in soc_pcm_hw_params

  1. dai_link rtd->dai_link->ops->hw_params
  2. codec for (i = 0; i < rtd->num_codecs; i++) { soc_dai_hw_params }
  3. cpu dai ret = soc_dai_hw_params(substream, params, cpu_dai);
  4. platform platform->driver->ops->hw_params

shutdown call sequence in soc_pcm_close

  1. set rate = 0
  2. snd_soc_dai_digital_mute
  3. cpu_dai cpu_dai->driver->ops->shutdown
  4. codec codec_dai->driver->ops->shutdown
  5. dai link rtd->dai_link->ops->shutdown
  6. platform platform->driver->ops->close

ALSA SoCドライバメモ No.002 private data

 ALSA SoCドライバ開発でいちいちgrepするのが面倒な情報をメモしておく

それぞれのドライバでprivate dataをどこに置くのか?

dai link

  • snd_soc_cardが自分の持ち物
  • 設定方法 (probe時にsnd_soc_register_card前にやる) snd_soc_card_set_drvdata(&hoge_card, priv);
  • 参照方法
    • いろいろ面倒だが、stream->runtime->cardまで辿れば読める
struct snd_soc_pcm_runtime *rtd = substream->private_data; // streamから
struct snd_soc_card *card = rtd->card; // runtimeから
struct snd_soc_card *card = snd_kcontrol_chip(kcontrol); // mixerから
struct hoge_priv *priv = snd_soc_card_get_drvdata(card);

cpu dai

  • platform_device配下のdev pdev->dev
  • 設定方法 dev_set_drvdata(&pdev->dev, priv); snd_soc_add_dai_controls(dai, ... // mixerに設定
  • 参照方法
    • struct snd_soc_dai daiから取れる struct hoge_priv priv = snd_soc_dai_get_drvdata(dai);

    struct snd_soc_dai *dai = snd_kcontrol_chip(kcontrol); // mixerから

codec

  • platform_device配下のdevかstruct i2c_client配下のdev pdev->dev i2c_client->dev
  • 設定方法 i2c_set_clientdata(i2c, priv); dev_set_drvdata(dev, priv);

  • 参照方法

    • mixerから struct snd_soc_codec codec = snd_soc_kcontrol_codec(kcontrol); struct hoge_priv priv = snd_soc_codec_get_drvdata(codec);

    • struct snd_soc_dai daiから struct snd_soc_codec codec = dai->codec; struct hoge_priv *priv = snd_soc_codec_get_drvdata(codec);