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);

ALSA SoCドライバメモ No.001 ドライバ構成

ALSA SoCドライバの構成

ファイルの説明

  • DAI LINKドライバ 例: sound/soc/bcm/hifiberry-dac.c
    • 関連するドライバをまとめ(dai link)て登録(snd_soc_register_card)するドライバ
    • hifiberry-dac.cではほとんど制御は行わず、登録のみを行っている。
    • hifiberry-dacplus.cのようにクロック制御を行うものも存在する。
    • dai linkドライバは特に独立する必要はなく、CPU DAIドライバ自体がDAI LINKして登録するケースもある。(NEO2のI2Sドライバはこの方式)
    • dai linkのprobeは登録する配下モジュールがすべて揃うまで自動でRetryしてくれる。
  • CPU DAI ドライバ
    • 例: sound/soc/bcm/bcm2835-i2s.c
    • I2S制御ドライバ
    • サウンドカード構造体に一つのみ指定できる
    • I2Sブロックを制御する。DMAやFIFO、I2Sフォーマットのレジスタ設定などを行う
    • SoC内蔵PLLを使う場合はクロック制御も行う
  • CODEC DAI ドライバ
    • 例: sound/soc/codecs/pcm5102a.c pcm512x.cなど
    • CODEC制御ドライバ
    • サウンドカード構造体に複数指定できる
    • pcm5102a.cでは特にデバイス制御処理は実装していない。
    • CODECの許容するサンプリングレートやデータ形式を指定している。
    • I2Cによる制御が必要なDAC ICはCODECドライバでレジスタ設定処理を行う
      • pcm512x.cあたりでは、I2C制御でCodecのレジスタ制御を行っている。
  • AUXDEV ドライバ
    • 例: sound/soc/codecs/tpa6130a2.c
    • 非CODECドライバ
    • サウンドカード構造体に複数指定できる
    • AMPの電源制御や電子ボリュームなどCODEC以外の制御を行うドライバ
    • dai linkはできず、サウンドカード構造体(snd_soc_card)のメンバに登録を行う。

ドライバの組み合わせ

  • 最低でもCPU DAIと一つのCodecが必要そう。CPU DAIにCodecハンドラも書けば一つでも音は鳴りそう。
  • RPiではDAI LINK/CPU DAI/CODEC DAIの3ファイル構成を基本としている模様。
  • NEO2ではCPU DAI(with dai link)/CODEC DAIの2ファイル構成が基本。拙作のNEO2カーネルではRPi同様、DAI LINKドライバの利用も可能となるよう改造済み。

Raspberry Pi ZeroのI2S DACとポップノイズの関係 その3 MUTE制御の動き

概要

  • NosPiDAC Zeroなどで音楽を楽しんでいるときに気になるポップノイズについて調べたこと・やったことをメモ。
  • ちょっと時間が空いてしまいましたが、その3ではALSA SoCのCallbackを追っかけたメモを置いておきます。

調査

  • まずはsoc_pcm階層からのコールシーケンスを作成しました。
  • sound/soc/soc_pcm.cからdaiやcodecのcallbackを呼び出しています
  • 適当にdev_infoを埋めてMoodeAudioで再生・次曲送り・停止しdmesgから拾いあげてます。

  • digital_muteがcodec側にcallbackされるMUTE制御です。0で解除、1でミュートのActive High論理。

  • MUTE制御とクロック制御の関連を確認し、効果があるか検証します。

MUTE関連の動き シーケンス

  1. アクション 再生開始
  2. soc_pcm_open
    1. startup
      1. stop_clock
  3. soc_pcm_hw_params
    1. hw_params bclk算出・サンプリングレート設定
      1. start_clock
  4. soc_pcm_prepare
    1. prepare
    2. digital_mute 0 mute release
  5. soc_pcm_trigger
    1. trigger 1 再生開始 1曲目
      1. start_clock
  6. soc_pcm_trigger
    1. trigger 0 再生停止
      1. stop
        1. stop_clock dai->active
  7. アクション 次曲送り
  8. soc_pcm_prepare
    1. prepare
    2. digital_mute 0 mute release
  9. soc_pcm_trigger
    1. trigger 1 再生開始 2曲目
      1. start_clock
  10. soc_pcm_trigger
    1. trigger 0 再生停止
      1. stop
        1. stop_clock dai->active
  11. アクション 次曲送り サンプリングレート変更
  12. soc_pcm_close
    1. digital_mute 1 mute
    2. shutdown
      1. stop
        1. stop_clock continuos clock
      2. stop_clock
  13. soc_pcm_open
    1. startup
  14. soc_pcm_hw_params
    1. hw_params サンプリングレート設定
  15. soc_pcm_prepare
    1. prepare
    2. digital_mute 0 mute release
  16. soc_pcm_trigger
    1. trigger 1 start_clock 再生開始 3曲目
  17. soc_pcm_trigger
    1. trigger 0 再生停止
      1. stop
        1. stop_clock dai->active
  18. soc_pcm_close
    1. digital_mute 1 mute
    2. shutdown
      1. stop
        1. stop_clock continuos clock
      2. stop_clock
  19. 再生停止

確認結果

クロック制御周り

  • stop_clockの呼び出し場所を追っかけると、shutdown時以外はstop_clockしてなさそう。
  • digital_mute 1でMute解除前にstart_clockしてるので、外部Muteではポップを防げそうに見える。
  • 次曲送り時はクロック出っ放しにログ上では見える

  • サンプリングレートが変わるときはclose->openしているようで、shutdownまで走っている

  • SND_SOC_DAIFMT_CONT(continuous clock)フラグを立てても、shutdownではstop_clockする

    • trigger 0 のstop_clockはSND_SOC_DAIFMT_CONTフラグを立てなくてもdai->activeが有効なのでstop_clockしない。
    • shutdownでは、SND_SOC_DAIFMT_CONTフラグを立てるとstop関数ではstop_clockしないが、最終的にstop_clockを直接呼んでるので意味無い
    • 最初の曲やサンプリングレートが変わった後の一発目はポップノイズが出そうです。
  • でもポップノイズは出ている。TXONのオフ/オンのタイミングで出ている?

Volumio2のREST APIにgetqueueを追加する方法

 REST APIでは再生キューのリストが取れないため調査したところ、volumio2に数行のコード追加で取得できるように改造可能とわかったのでその方法をメモ。

 内部的には再生キューを生成するWeb Socket用のコードがすでに存在するため、REST APIのコードにgetqueueのハンドラを追加し、再生キューのリストをレスポンスとして返すようにしてあげるだけです。
 getstateの次あたりに追加すると良いんじゃないかと。v2.562で確認しました。

  • 修正するファイル
 /volumio/app/plugins/user_interface/rest_api/index.js
  • 追加するコード
    api.route('/getqueue')
        .get(function (req, res) {
            var response = self.commandRouter.volumioGetQueue();

            if (response != undefined)
                res.json(response);
            else
                res.json(notFound);
        });

 
 選曲はplayコマンドにリストの番号を追加するだけでOK。0オリジンなことに注意。
 3曲目を指定するならN=2を指定する。

/api/v1/commands/?cmd=play&N=2

 Web SocketとREST APIでは応答電文が共通のため頑張れば大部分をREST API化できるかもしれない。REST APIだとcurlコマンドとかで簡単に制御できるので、シェルスクリプトで色々やるのには便利です。

BCLK/LRCLK出力しかないRaspberry Piで旭化成のDAC IC(AK449xシリーズ)を鳴らす実験

 Raspberry PiでDAI/ASRC/CPLDなどのクロック生成周辺回路を用いず、旭化成DAC AK449Xシリーズを使用する方法についてメモ。
 Raspberry PiDACにAK449xシリーズの採用が少ない原因のひとつとして、AK449xの要求するMCLKをRaspberry Piから供給できない点が挙げられます。
 今回は、周辺回路の追加無しで、なんとかAK449xシリーズを鳴らせないかな?という実験となります。

 実験には@blue__sevenさん製作のChiKoiDac-AK4495Sを使用しました。ChiKoiDac-AK4495SはNanoPi NEO2向けの小型DACボードで、NEO2サイズの基板にAK4495S・OPAMP LPF・電源回路を搭載したお手軽高音質DACです。
 NanoPi NEO2では専用品だけあってそのまま乗せるだけですが、Raspberry Pi Zeroでは互換のあるピン部分をそのまま差し込み、不足するI2SのDATA/LRCLK/BCLKをジャンパケーブルで接続することで使用できます。

f:id:tkztkztkz:20190205231002j:plain
Raspberry PI Zero with AK4495S DAC

方法1

  • @blue__sevenさんに教えて頂いた方法です。
  • AK449xは、MCLKにBCLKを入力するだけで音は出ます。仕様範囲外の動作となります。
    • 44.1kHz/32bit時にはBCLK = MCLK = 64fs = 2.8224MHz
  • MCLKが規定のクロックに達していないため、デジフィル(デジタルフィルタ)は正常に動作しません。
  • オシロで確認する限りNOS(Non-Oversampling)出力ではないようで、何らかの簡易フィルタは掛かっているように見えます。
  • デジフィルにショートディレイやNOSを選んでも、出力波形に変化はありません。
    f:id:tkztkztkz:20190205231256p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=64fs (AK4495S) 1kHz Sine Zoom
    f:id:tkztkztkz:20190205231354p:plain
    Raspberry Pi Zero Internal PLL MCLK=BCLK=64fs (AK4495S) 1kHz Square

方法2

  • MCLKとBCLKを同じクロックで入力するところまでは方法1と同様
  • BCLKを標準の32bit再生時64fsではなく、MCLK相当の512fsまでクロックを上げます。
    • 44.1kHz/32bit時にはBCLK = MCLK = 512fs = 22.5792MHz
  • I2Sドライバの改造が必要です。修正内容は下記の感じ
    • BCLKを大幅に上昇させる 64fs->512fs
    • fslenを適切に設定する
  • オシロで確認する限り、デジフィルはちゃんと動いているように見えます。

    f:id:tkztkztkz:20190205231555p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) 1kHz Sine Zoom DF SSD
    f:id:tkztkztkz:20190205232013p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) 1kHz Square DF SSD
    f:id:tkztkztkz:20190205231706p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) 1kHz Sine Zoom DF NOS

  • AK449XシリーズではBCLKをデータの取り込みタイミングだけに使っているから実現できているようです。LRCLKのエッジから少しだけデータを送出していることがわかります。通常64fsのところ8倍の512fsでBCLKを駆動しているため、エッジから1/8くらいデータが出ています。

    f:id:tkztkztkz:20190205232105p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) LRCLK & DATA

方法2 検証ソース修正例

  • 割とひどいパッチですが実験時のコードを例示します
    • bclk_rateは24.576MHzをfsで割り切れたら24.576MHz。22.5792MHzをfsで割り切れたら22.5792MHz固定出力にします
    • frame_lengthを変更したbclk ratioで再算出
    • tx_ch2_posもそのままでは正しく計算できないので、framesync_length(frame_length/2)から再算出
  • いろいろ改造しているのでdiffのoffsetは狂ってます。参考まで
*** sound/soc/bcm/bcm2835-i2s.c 2019-01-22 01:56:36.880202010 +0000
--- sound/soc/bcm/bcm2835-i2s_old.c     2019-01-22 01:25:58.276303397 +0000
***************
*** 31,37 ****
   * General Public License for more details.
   */

  #include <linux/bitops.h>
  #include <linux/clk.h>
--- 31,39 ----
   * General Public License for more details.
   */

+ #define AKFS

  #include <linux/bitops.h>
  #include <linux/clk.h>
***************
*** 393,398 ****
--- 394,424 ----
                if (bclk_rate < 0)
                        return bclk_rate;
        }
+ #ifdef AKFS
+       if ( !(24576000 % params_rate(params))) {
+               bclk_rate = 24576000;
+       }else
+       if ( !(22579200 % params_rate(params))) {
+               bclk_rate = 22579200;
+       }else{
+               dev_info(dev->dev, "unknownrate = %d", params_rate(params));
+       }
+       frame_length = bclk_rate / params_rate(params);
+       dev_info(dev->dev, "bclk = %d, flen = %d", bclk_rate, frame_length);
+ #endif

        /* Check if data fits into slots */
        if (data_length > slot_width)
***************
*** 520,527 ****
                rx_mask, slot_width, data_delay, odd_slot_offset);
        bcm2835_i2s_calc_channel_pos(&tx_ch1_pos, &tx_ch2_pos,
                tx_mask, slot_width, data_delay, odd_slot_offset);

        /*
         * Transmitting data immediately after frame start, eg
         * in left-justified or DSP mode A, only works stable
--- 546,567 ----
                rx_mask, slot_width, data_delay, odd_slot_offset);
        bcm2835_i2s_calc_channel_pos(&tx_ch1_pos, &tx_ch2_pos,
                tx_mask, slot_width, data_delay, odd_slot_offset);

+
+ #if defined(AKFS)
+       tx_ch2_pos = framesync_length + 1;
+       dev_info(dev->dev, "tx_ch1_pos = %d, tx_ch2_pos = %d", tx_ch1_pos, tx_ch2_pos);
+ #endif
        /*
         * Transmitting data immediately after frame start, eg
         * in left-justified or DSP mode A, only works stable

考察とかなんとか

  • AK449Xの動作とか
    • AK449x系はMCLK/LRCLKの比を見てデジフィルの動作を決めてるっぽい。BCLKは関与していない?
    • 実際仕様書にもMCLKとLRCLKは同期する必要があると記載。BCLK/DATAについては触れていない
    • BCLKはDATAの取り込みタイミングだけに使われてると予想
  • MCLKが低いときのデジフィル動作について
    • 768kHz再生時のデジフィル動作に似ているとのこと
    • ハイレート再生時の簡易フィルタと同じ動きになるのかな?
    • 実際に768kHz/32bit=64fs再生時には、MCLK=BCLK=49.152MHzで再生している
  • 音質は?
    • NEO2のAUDIO PLLに比べると、なんかいまいちっぽい
    • ラズパイのAudio PLLはAccumlated Jitterが発生するのでそれの影響?

コードについて

 今回は検証用にI2Sドライバを無理やり改造したが、dai linkドライバ(hifiberry-dac.cに代わるもの)を新規に起こして、I2Sドライバはオリジナルのままで動作させる方向で開発中です。

おまけ:前提のお話

  • I2Sとは何か?
    • BCLK/LRCLK/DATAの3本の信号線を用いてIC間でオーディオ信号を転送する規格
    • Inter-IC Soundの略でI2S
  • LRCLK(LR Clock)とは何か?
    • I2S I/Fでデータを転送するとき左右どちらのチャネルを転送しているか示す信号
    • I2SではLRCLK信号がLOW区間でLチャネルのデータ、HIGH区間でRチャネルのデータ転送を行なっていることを示しています。
  • クロックのfs表記
    • DAC ICの仕様書でよく出てくる単位。BCLKやMCLKの周波数表記に良く使われる。MCLK=256,512fsなど
    • LRCLK一周期 = 1fsとする。LRCLKの別名がFrame Syncというだけの話
    • 1fsの周波数は、サンプリング周波数と同値になります。44.1kHzのとき1fs=44.1kHz。48kHzのとき1fs=48kHz
    • I2Sドライバ内ではBCLKとLRCLKの比(bclk_ratio)を表すため重要な単位となります。
  • BCLK(Bit Clock)とは何か?
    • I2S I/Fでデータを転送する基準クロック
    • BCLKは多くの場合32/64fsの二択(それぞれ量子化ビット数 16/32bit時の最小転送サイズになるため)
      • I2S 44.1kHz/16bit転送の場合、BCLK = 32fs = 16 * 2 * 44.1kHz = 1.4112MHz
      • I2S 44.1kHz/32bit転送の場合、BCLK = 64fs = 32 * 2 * 44.1kHz = 2.8224MHz
      • 24bit/48fsも時々使うが、クロックの生成が面倒なので古いDAC ICくらいでしか使わない。
  • MCLK(Master Clock)とは何か?
    • I2S I/Fでは必須ではない信号 (I2S自体はBCLK/LRCLK/DATAの3本あれば良い)
    • DAC ICが内部でオーバーサンプリングなどのデジタル信号処理を行う基準クロックとして要求している。別名SCLK(System Clock)
      • 多くの場合BCLKの2/4/8倍くらいを要求します。
      • LRCLK 44.1/48kHz 32bit時にBCLKの8倍だと64fs * 8 = 512fsとなり、MCLKはそれぞれ22.5792MHz/24.576MHz
      • この二つのクロックの最小公倍数は3.6GHzになるため一つの発振器から生成するのは困難
      • よって2系統のクロック発振器を使うか、PLLにて生成する必要がある
    • DAC IC次第だが、MCLKとBCLK and/or LRCLKの同期が必要なケースが多い
      • MCLKから分周してBCLK/LRCLKを生成するか、BCLK/LRCLKを逓倍してMCLKを生成する必要がある
      • 例外としてESSのDACは非同期でもOKなようです。
    • 要するに用意するのが面倒で、量産を考えるとコストが掛かる。
  • Raspberry PiはMCLK入出力機能を持っていない
    • Raspberry Piシリーズでは、内蔵PLLによるBCLK/LRCLKの生成・出力できるが、MCLKの出力機能はない。
    • Allwinner H5(NanoPi NEO2など)は内蔵AUDIO PLLにてMCLKを生成し出力可能。入力はなし。
    • TI AM335X(BBB/PocketBeagleなど)はMCLK入力が可能。出力はなし。入力したMCLKを基にBCLK/LRCLKを生成・出力が可能

Linux向けAK449x Codec Driverの紹介

 旭化成エレクトロニクス製 AK4490/AK4495/AK4497/AK4493向けLinux Driverの紹介です。NanoPi NEO2でAK449xシリーズを鳴らすために作成しました。

f:id:tkztkztkz:20190124231738j:plain
 ALSA SoC Codecドライバとして実装しています。NanoPi NEO2/Raspberry Pi Zeroで動作確認済み。そのほかのSBCでもDAI LINKドライバとdevice-treeを適切に用意すれば使用可能だと思います。Raspberry Pi Zeroで鳴らす手順はそのうちブログに書く予定です。

 ライセンスはLinux Kernel ModuleなのでGPLv2です。自由に組み込んで使用してください。何か面白いブツができたら教えて頂けるとうれしいです。

できること

  • 標準機能
    • PDN制御(DAC IC PDN信号のGPIO出力)
    • 外部MUTE制御(MUTE制御信号のGPIO出力)
    • ソフトMUTE制御(AKレジスタを叩いてのMUTE)
    • 複数Codec同時制御(I2Cバスあたり最大4個まで)
  • Alsa Mixer設定可能項目
    • ディエンファシス設定
    • 電子ボリューム設定
    • デジタルフィルタ設定
    • Sound Control設定
    • HLOAD ON/OFF設定(AK4497のみ)
    • Gain Control設定(AK4493/AK4497のみ)
    • ATT遷移時間設定(AK4493/AK4497のみ)
  • 出力制御機能(device-treeで設定)
    • STEREO出力の左右入れ替え
    • MONOモード設定
    • 出力位相の変更

ドライバのダウンロード

この辺にソースがあります。
https://raw.githubusercontent.com/tkztkztkz/linux/npi-audio-4.11.y/sound/soc/codecs/ak449x.c https://raw.githubusercontent.com/tkztkztkz/linux/npi-audio-4.11.y/sound/soc/codecs/ak449x.h

NanoPi NEO2用のイメージだとこの辺に導入済みです。
NanoPi Neo2用 Volumio2 - _tkz_ memo

dts設定方法

NanoPi NEO2

dtsの設定例 通常のステレオ仕様(I2C ADDR 0x10)
&i2c0 {
        status = "okay";

        ak449x: ak449x-codec@10 {
                #sound-dai-cells = <0>;
                compatible = "asahi-kasei,ak449x";
                reg = <0x10>; /* I2C addr */
                status = "okay";

                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                reset-gpios = <&pio 6 7 GPIO_ACTIVE_LOW>; /* PG7 */ /* pdn pin */
                mute-gpios = <&pio 6 8 GPIO_ACTIVE_LOW>; /* PG8 */ /* external mute */
        };

&i2s0 {
        sound-dai = <&ak449x>; /* dai link target */
        status = "okay";
};
dtsの設定例 DualMonoバランス出力仕様(I2C ADDR 0x10/0x11)
&i2c0 {
        ak449x1: ak449x-codec@10 {
                #sound-dai-cells = <0>;
                compatible = "asahi-kasei,ak449x";
                reg = <0x10>; /* I2C addr */
                status = "okay";

                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                chmode = "MONO_LCH"; /* STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH */
                phase = "LIRN";  /* LNRN LNRI LIRN LIRI - N=non-invert I=invert*/
                reset-gpios = <&pio 6 7 GPIO_ACTIVE_LOW>; /* PG7 */ /* pdn pin */
                mute-gpios = <&pio 6 8 GPIO_ACTIVE_LOW>; /* PG8 */ /* external mute */
        };
        ak449x2: ak449x-codec@11 {
                #sound-dai-cells = <0>;
                compatible = "asahi-kasei,ak449x";
                reg = <0x11>; /* I2C addr */
                status = "okay";

                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                chmode = "MONO_RCH"; /* STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH */
                phase = "LIRN"; /* LNRN LNRI LIRN LIRI - N=non-invert I=invert*/
       };
};

&i2s0 {
        sound-dai = <&ak449x1 &ak449x2>; /* dai link target */
        status = "okay";
};

Raspberry Pi Zero

dt-overlayの設定例 通常のステレオ仕様(I2C ADDR 0x10)
// Definitions for HiFiBerry DAC
/dts-v1/;
/plugin/;

/ {
        compatible = "brcm,bcm2708";

        fragment@0 {
                target = <&i2s>;
                __overlay__ {
                        status = "okay";
                };
        };
        fragment@1 {
                target = <&i2c1>;
                __overlay__ {
                        #address-cells = <1>;
                        #size-cells = <0>;
                        status = "okay";

                        ak449x: ak449x-codec@10 {
                                #sound-dai-cells = <0>;
                                compatible = "asahi-kasei,ak449x";
                                reg = <0x10>; /* I2C addr */
                                status = "okay";

                                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                                chmode = "STEREO"; /* STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH */
                                reset-gpios = <&gpio 15 1>; /* GPIO15 */ /* pdn pin */
                                mute-gpios = <&gpio 23 1>;  /* GPIO23 */ /* external mute */
                        };
                };
        };

        fragment@2 {
                target = <&sound>;
                __overlay__ {
                        compatible = "hifiberry,hifiberry-dac";
                        i2s-controller = <&i2s>;
                        i2s-codec = <&ak449x>;
                        status = "okay";
                };
        };
};

パラメータについて

  • chip (AK4490/AK4495/AK4497/AK4493)
    • 制御対象チップを設定します
    • デフォルトは4495となります
    • 下記項目の動作が変わります
      • HLOAD(4497のみ)
      • Gain Control(4493/4497のみ)
      • ソフトMUTEの遷移時間
  • chmode (STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH)
    • 動作モードを設定します
    • デフォルトはSTEREOとなります
    • Dual MonoやSTEREOの左右を入れ替える場合に指定します
  • phase (LNRN LNRI LIRN LIRI - N=non-invert I=invert)
    • 出力位相を設定します
    • デフォルトはLNRNとなります。L/R CHとも正相出力。
    • LIRIにするとL/R CHとも逆相出力となります。
    • 出力位相を変えたり、Dual Mono設定時、バランス出力する場合などに指定します
  • reset-gpios
    • PDN制御出力を行うピンを指定します
    • デフォルトは無効です
    • 出力論理はGPIOドライバの指定に従います(出力論理変更ができない場合はActive Highになるはず)
  • mute-gpios
    • 外部MUTE制御出力を行うピンを指定します
    • デフォルトは無効です
    • 出力論理はGPIOドライバの指定に従います(出力論理変更ができない場合はActive Highになるはず)
    • MUTEの制御順は下記となります
      • Power On -> 外部MUTE assert
      • 再生開始 MCLK/LRCLK設定 -> 外部MUTE negate -> ソフトMUTE negate -> wait
      • 再生停止 ソフトMUTE assert -> wait -> 外部MUTE assert -> MCLK/LRCLK設定

備考

  • ポップノイズ回避について
    • AK449xシリーズはMCLK/LRCLKの開始・停止でポップノイズが発生します
    • ソフトMUTE制御だけでは回避できません
    • 外部MUTE制御を使用して、リレーかFETでMUTE回路を組むことをオススメします
  • 複数のAK449Xを制御する場合には、最初に指定するcodecにのみGPIO設定を記述してください
  • PDN制御について
    • 旭化成DAC ICはPDN制御が鬼門だったりします。規定のタイミングを守らないと動かないケースがあります。
    • POWER ONから一定時間LowにしHighを入れる必要があります。
    • PDN信号をCRで鈍らせてプルアップを入れても動かなくはありませんが、場合によっては不安定になるため外部からきっちりリセットパルスを入れたほうが良いです。

Raspberry Pi ZeroのI2Sオーディオドライバに非Codecドライバを追加登録する方法

概要

  • ALSA SoCドライバで電子ボリュームを利用する方法を調べてみた。
  • 今回は電子ボリューム内蔵ヘッドフォンアンプIC TPA6130A2ドライバをhifiberry-dacドライバに登録してみる。

仕組み

  • TPA6130A2のドライバはsound/soc/codecs/tpa6130a2.cを使用する
  • TPA6130A2のようなCodecではないボリューム制御のみのICはaux devとして登録する
  • aux devはcodecではないため、codecとしてdai link登録はできない
  • dai linkではなく、soc_card構造体のaux devsに登録してregisterする

ソース修正例

  • サウンドカード(soc_card)を登録するDAI LINKドライバにdevice treeのi2s-auxdevエントリ参照処理を追加する
    • device treeにi2s-auxdevエントリがあればそのデバイスノードをaux devに登録する
    • i2s_auxdevに加えてi2s-codecをdevice treeから指定する機能も組み込んでいます。
  • 修正コード sound/soc/bcm/hifiberry_dac.c
@@ -14,6 +14,8 @@
  * General Public License for more details.
  */

+#define DEBUG
+
 #include <linux/module.h>
 #include <linux/platform_device.h>

@@ -22,6 +24,8 @@
 #include <sound/pcm_params.h>
 #include <sound/soc.h>
 #include <sound/jack.h>
+#include <linux/of_device.h>
+#include <linux/of_graph.h>

 static int snd_rpi_hifiberry_dac_init(struct snd_soc_pcm_runtime *rtd)
 {
@@ -72,12 +76,14 @@ static struct snd_soc_card snd_rpi_hifiberry_dac = {
 static int snd_rpi_hifiberry_dac_probe(struct platform_device *pdev)
 {
        int ret = 0;
+       int cnt;

        snd_rpi_hifiberry_dac.dev = &pdev->dev;

        if (pdev->dev.of_node) {
                struct device_node *i2s_node;
                struct snd_soc_dai_link *dai = &snd_rpi_hifiberry_dac_dai[0];
+
                i2s_node = of_parse_phandle(pdev->dev.of_node,
                                        "i2s-controller", 0);

@@ -87,6 +93,67 @@ static int snd_rpi_hifiberry_dac_probe(struct platform_device *pdev)
                        dai->platform_name = NULL;
                        dai->platform_of_node = i2s_node;
                }
+
+               cnt = of_count_phandle_with_args(pdev->dev.of_node, "i2s-codec", "#sound-dai-cells");
+               dev_info(&pdev->dev, "%s: i2s-codec sound-dai = %d", __func__, cnt);
+
+               if ( cnt > 0 ) {
+                       struct snd_soc_dai_link_component *codecs;
+                       struct of_phandle_args args;
+                       int i;
+
+                       dai->codec_name = NULL;
+                       dai->codec_dai_name = NULL;
+                       dai->codec_of_node = NULL;
+
+                       codecs = devm_kzalloc(&pdev->dev, sizeof(struct snd_soc_dai_link_component)*cnt, GFP_KERNEL);
+                       if (!codecs)
+                               return -ENOMEM;
+
+                       for ( i = 0; i < cnt ; i++ ) {
+                               ret = of_parse_phandle_with_args(pdev->dev.of_node, "i2s-codec",
+                                       "#sound-dai-cells", i, &args);
+                               if (ret) {
+                                       dev_err(&pdev->dev, "%s: Can't find codec by \"i2s-codec\"\n", __func__);
+                                       return 0;
+                               }
+                               codecs[i].of_node = args.np;
+                               if (snd_soc_get_dai_name(&args, &codecs[i].dai_name) < 0 ) {
+                                       dev_err(&pdev->dev, "%s: failed to find dai name, use codec's name as dai name.\n", __func__);
+                                       codecs[i].dai_name = codecs[i].of_node->name;
+                               }
+                               dev_dbg(&pdev->dev, "add codec %s @ %s", args.np->name, codecs[i].dai_name);
+                       }
+                       dai->codecs = codecs;
+                       dai->num_codecs = cnt;
+               }
+
+               cnt = of_count_phandle_with_args(pdev->dev.of_node, "i2s-auxdev", "#sound-dai-cells");
+               dev_info(&pdev->dev, "%s: i2c-auxdev sound-dai = %d", __func__, cnt);
+
+               if ( cnt > 0 ){
+                       struct snd_soc_aux_dev *auxdevs;
+                       struct of_phandle_args args;
+                       int i;
+
+                       auxdevs = devm_kzalloc(&pdev->dev, sizeof(struct snd_soc_dai_link_component)*cnt, GFP_KERNEL);
+                       if (!auxdevs)
+                               return -ENOMEM;
+
+                       for ( i = 0; i < cnt ; i++ ) {
+                               ret = of_parse_phandle_with_args(pdev->dev.of_node, "i2s-auxdev",
+                                                               "#sound-dai-cells", i, &args);
+                               if (ret) {
+                                       dev_err(&pdev->dev, "%s: Can't find codec by \"i2s-auxdev\"\n", __func__);
+                                       return 0;
+                               }
+                               auxdevs[i].codec_of_node = args.np;
+                               auxdevs[i].name    = args.np->name;
+                               dev_dbg(&pdev->dev, "add aux dev %s", auxdevs[i].name);
+                       }
+                       snd_rpi_hifiberry_dac.aux_dev = auxdevs;
+                       snd_rpi_hifiberry_dac.num_aux_devs = cnt;
+               }
        }

        ret = snd_soc_register_card(&snd_rpi_hifiberry_dac);

device tree overlay設定例

  • fragment@1でTPA6130A2を定義
  • fragment@3のi2s-auxdevでTPA6130A2を参照するように定義
// Definitions for HiFiBerry DAC
/dts-v1/;
/plugin/;

/ {
        compatible = "brcm,bcm2708";

        fragment@0 {
                target = <&i2s>;
                __overlay__ {
                        status = "okay";
                };
        };

        fragment@1 {
                target = <&i2c1>;
                __overlay__ {
                        #address-cells = <1>;
                        #size-cells = <0>;
                        status = "okay";

                        tpa6130a2: tpa6130a2@60 {
                                #sound-dai-cells = <0>;
                                compatible = "ti,tpa6130a2";
                                reg = <0x60>;
                                status = "okay";
                        };

                };
        };

        fragment@2 {
                target-path = "/";
                __overlay__ {
                        pcm5102a: pcm5102a-codec {
                                #sound-dai-cells = <0>;
                                compatible = "ti,pcm5102a";
                                status = "okay";
                        };
                };
        };


        fragment@3 {
                target = <&sound>;
                __overlay__ {
                        compatible = "hifiberry,hifiberry-dac";
                        i2s-controller = <&i2s>;
                        i2s-codec = <&pcm5102a>;
                        i2s-auxdev = <&tpa6130a2>;
                        status = "okay";
                };
        };
};