Raspberry Piで、DACチップ無しでI2Sを出力する

# またニッチ
Raspberry Piは、GPIOに外付けDACを接続して高音質化という楽しみ方がある。この場合、DACのドライバーとセットで使用することになる。
もしDAC無しでI2S信号を得たい場合、もしくはDACの制御は別に行う場合、合うドライバーが無い。合わないドライバーを使うと、DACの制御に失敗してエラーになったりしないだろうか。
外付けDAC無しで、Raspberry PiをI2S信号発生器として使いつつ、BCLKなどのパラメーターを操作したい場合、どうすれば…
試したことを記載したが、専門外なので、間違っていたらすいません。
以下、Raspberry Pi Foundation-provided kernelの4.9.118で確認。

# ALSAドライバーの構造
[本家](https://www.kernel.org/doc/html/latest/sound/soc/codec-to-codec.html)で、CPU, dai, codecなどの構成要素を知る。
[こちら](https://elinux.org/images/b/b5/Belloni-alsa-asoc.pdf)は、本家より図が詳細で、構造体の説明もある。
Raspberry Piでの実装は、[こちら](http://tkz.hateblo.jp/entry/2019/05/01/164527)が詳しい。

# 作戦
上記の構造解説の理解に基づき…

- CPU(SoC) DAIは、Raspberry Pi用のもの([`sound/soc/bcm/bcm2835-i2s.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/bcm/bcm2835-i2s.c))をそのまま使う
- DAI linkは、Raspberry Piのカーネルの外付けDAC用の中で、単純なものを流用する
- CODEC DAIは、Raspberry Piのカーネルの外付けDAC用の中で、I2Cなどによるcodecの制御を伴わないものを流用する

## DAI link詳細
[`sound/soc/bcm/`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/bcm/)の中を漁ると、[`rpi-simple-soundcard.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/bcm/rpi-simple-soundcard.c)という気になるファイル名のファイルがある。シンプルですか、そして比較的新しい。
[こちら](https://www.raspberrypi.org/forums/viewtopic.php?t=219555)によると、同じcodecが複数のHATで使われて重複コードになったりしたので、整理したようだ。
これを改造しよう。

## CODEC DAI詳細
[`sound/soc/codecs/`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/codecs/)以下を眺めると、I2Cなどでcodecの制御を伴わないものは色々あるようだ。
この中に、[`bt-sco.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/codecs/bt-sco.c)というものがある。実は今回、受け側はBluetoothチップを想定しているので、都合が良さそうだ。[`Kconfig`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/codecs/Kconfig)の説明でも*Dummy BT SCO codec driver*とあり、codecが無くても動きそうな予感。
これをそのまま使ってみよう。

# Device Tree
Raspbrerry Pi用外付けDACは、使う時に`/boot/config.txt`に`dtoverlay=`でdtboファイルを追記するものが多い。dtboは、Device Tree Blob Overlayのこと…だと思う。
Device Tree Overlayは[こちら](https://source.android.com/devices/architecture/dto)に概要がある。
[`rpi-simple-soundcard.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/bcm/rpi-simple-soundcard.c)の中で、bt-scoを使うものは無いので、この組み合わせをDevice Tree Overlayに登録するのが流儀か。

## Device Tree詳細
現状Raspberry Piでは、Pi 4でも、[ビルド時のKERNELに設定する値はv7l](https://www.raspberrypi.org/documentation/linux/kernel/building.md)で、arm64ではなくarmで動かしている[模様](https://senyoltw.hatenablog.jp/entry/2016/04/24/043244)。
[`arch/arm/boot/dts/overlays/hifiberry-dac-overlay.dts`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/arm/boot/dts/overlays/hifiberry-dac-overlay.dts)などを見ると、CODEC DAIとDAI LINKの組み合わせが書いてあるように見える。
これを真似しよう。

# 実装
## Device Tree

```arch/arm/boot/dts/overlays/rpi-null-dac-overlay.dts
// Definitions for RPi Null DAC
/dts-v1/;
/plugin/;

/ {
        compatible = "brcm,bcm2835";

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

        fragment@1 {
                target-path = "/";
                __overlay__ {
                        bt-sco {
                                #sound-dai-cells = <0>;
                                compatible = "linux,bt-sco";
                                status = "okay";
                        };
                };
        };

        fragment@2 {
                target = <&sound>;
                __overlay__ {
                        compatible = "rpi,rpi-null-dac";
                        i2s-controller = <&i2s>;
                        status = "okay";
                };
        };
};
```

CODEC DAIとDAI LINKの名前を、`compatible`で指定。

```diff:arch/arm/boot/dts/overlays/Makefile
@@ -137,6 +137,7 @@ dtbo-$(CONFIG_ARCH_BCM2835) += \
        rpi-dac.dtbo \
        rpi-display.dtbo \
        rpi-ft5406.dtbo \
+       rpi-null-dac.dtbo \
        rpi-poe.dtbo \
        rpi-proto.dtbo \
        rpi-sense.dtbo \
```

## DAI link
```diff:sound/soc/bcm/rpi-simple-soundcard.c
@@ -227,6 +227,28 @@ static struct snd_rpi_simple_drvdata drvdata_merus_amp = {
        .fixed_bclk_ratio = 64,
 };

+static struct snd_soc_dai_link snd_rpi_null_dac_dai[] = {
+       {
+               .name           = "RPi Null DAC",
+               .stream_name    = "RPi Null DAC",
+               .codec_dai_name = "bt-sco-pcm-wb",
+               .codec_name     = "bt-sco",
+               .dai_fmt        = SND_SOC_DAIFMT_I2S |
+                                       SND_SOC_DAIFMT_NB_NF |
+                                       SND_SOC_DAIFMT_GATED |
+                                       SND_SOC_DAIFMT_CBS_CFS,
+               // .dpcm_playback = 1,
+               // .dpcm_capture = 1,
+       },
+};
+
+static struct snd_rpi_simple_drvdata drvdata_rpi_null_dac = {
+       .card_name = "snd_rpi_null_dac",
+       .dai       = snd_rpi_null_dac_dai,
+       .fixed_bclk_ratio = 128,
+};
+
+
 static const struct of_device_id snd_rpi_simple_of_match[] = {
        { .compatible = "adi,adau1977-adc",
                .data = (void *) &drvdata_adau1977 },
@@ -241,6 +263,8 @@ static const struct of_device_id snd_rpi_simple_of_match[] = {
        { .compatible = "rpi,rpi-dac", &drvdata_rpi_dac},
        { .compatible = "merus,merus-amp",
                .data = (void *) &drvdata_merus_amp },
+       { .compatible = "rpi,rpi-null-dac",
+               .data = (void *) &drvdata_rpi_null_dac },
        {},
 };
```

DTOの`compatible`で指定した名前を`snd_rpi_simple_of_match[]`に追記。
そこから、`drvdata_rpi_null_dac`、`snd_rpi_null_dac_dai`と情報をリンクさせる構造か。

### snd_rpi_simple_drvdata構造体
`.fixed_bclk_ratio`を設定すると、`snd_rpi_simple_init()`にて、`snd_soc_dai_set_bclk_ratio()`でBCLKを設定してくれるようだ。ratioなので、単位はFS?
試しに未設定状態でBCLKを測定したら500KHzで、これを2MHzにしたかったので、2048/16=128にしてみたら、2MHzになったっぽい。デフォルトは32FSということ? [参考](http://www.ezto.info/stpress/2017/01/822.html)

### snd_soc_dai_link構造体
CODEC DAIの指定と、どちらがクロックのマスターになるか、などの設定ができる。

CODEC DAIの指定は、`.codec_dai_name`に名前を書く。
他の例では、`snd_soc_dai_driver`構造体の`.name`と一致させているようなので、[`sound/soc/codecs/bt-sco.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/codecs/bt-sco.c)にある`bt-sco-pcm-wb`にしてみる。wbはWide Bandで、16KHzも行けそう。

クロックの設定は、[`include/sound/soc-dai.h`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/sound/soc-dai.h)によると、*This is wrt the codec, the inverse is true for the interface i.e. if the codec is clk and FRM master then the interface is clk and frame slave.* とのことで、**codecから見て**の設定値らしい。UARTのTX/RXもそうだが、どちらから見て、が重要。
`SND_SOC_DAIFMT_CBS_CFS`としておけば、codecから見てSlave、CPU(SoC)から見てMaster、になるはず。

以下蛇足。

`SND_SOC_DAIFMT_GATED`は、多分不要。最初DTOの記述が間違っていてBCLKが出ない時に試行錯誤で入れた名残り。*DAI bit clocks can be be gated (disabled) when the DAI is not sending or receiving PCM data in a frame. This can be used to save power.*とのこと。
Bluetoothの場合、I2SのBCLKは、SCO通信中のみ出ることが多い気がするので、このままでいいか。

`.dpcm_playback`と`.dpcm_capture`も試行錯誤の名残りで、同じbt-scoを使った[`sound/soc/mediatek/mt2701/mt2701-cs42448.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/mediatek/mt2701/mt2701-cs42448.c)に記述があったため、入れてみた。
結果的には、この設定が無くても、入出力共に動作。[`bt-sco.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/codecs/bt-sco.c)に`.playback`と`.capture`があるからか。

### CODEC DAI

```diff:arch/arm/configs/bcm2711_defconfig
@@ -1506,3 +1506,4 @@ CONFIG_FUNCTION_PROFILER=y
 CONFIG_KGDB=y
 CONFIG_KGDB_KDB=y
 CONFIG_KDB_KEYBOARD=y
+CONFIG_SND_SOC_BT_SCO=m
```

bt-scoをそのまま使いたいが、Raspberry Piでは無効になっているので、[`arch/arm/configs/bcm2711_defconfig`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/arm/configs/bcm2711_defconfig)(Raspberry Pi 4の場合)で有効化。

# ビルド
ビルドは、[本家手順](https://www.raspberrypi.org/documentation/linux/kernel/building.md)そのまま。今回はクロスでビルド。
この手順は、

- `make modules_install`による`.ko`のインストール
- `cp arch/arm/boot/dts/overlays/*.dtb* mnt/fat32/overlays/`によるDTOのインストール

を含んでいる。素晴らしい。

# コンテンツ
適当にフリー素材のwavを持ってきて、Audacityで、CODEC DAIであるbt-sco-pcm-wbに合わせて、モノラル/16kHz/16bitに変換。
VLCのメディア情報で、再生状態にして上記を確認。codecはPCM S16 LEとある。`aplay`の振る舞いまでは確認していないが、bt-sco-pcm-wbの`SNDRV_PCM_FMTBIT_S16_LE`と一致していれば、問題になることはないだろう。

# 実行
## config.txt
実行前に、`/boot/config.txt`の`[all]`の下に、`dtoverlay=rpi-null-dac`でDTOを追記。

## dmesg

```shell-session
[    3.041860] snd-rpi-simple soc:sound: bt-sco-pcm-wb <-> fe203000.i2s mapping ok
```

## aplay -L
```shell-session
default:CARD=sndrpinulldac
    snd_rpi_null_dac, RPi Null DAC bt-sco-pcm-wb-0
    Default Audio Device
```

## aplay
```shell-session
aplay -D default:sndrpinulldac mono-16KHz-16bit.wav
```
起動時にデバイスの優先順位?が変わることがあるようなので、`-D`でデバイスを指定。

…と、ここで問題発生。再生失敗。

```shell-session
aplay: main:828: audio open error: Invalid argument
```

`dmesg`を見ると、

```shell-session
[   24.194217] ASoC: bt-sco-pcm-wb <-> fe203000.i2s No matching channels
```

チャンネルがおかしいらしい。
コンテンツはAudacityモノラル化しており、[`bt-sco.c`](https://github.com/raspberrypi/linux/blob/rpi-4.19.y/sound/soc/codecs/bt-sco.c)では、`channels_min`も`channels_max`も`1`になっているので、問題なさそうだが…。
試しに`channels_max`を`2`にしてみる。

```diff:sound/soc/codecs/bt-sco.c
@@ -48,14 +48,14 @@ static struct snd_soc_dai_driver bt_sco_dai[] = {
                .playback = {
                        .stream_name = "Playback",
                        .channels_min = 1,
-                       .channels_max = 1,
+                       .channels_max = 2,
                        .rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000,
                        .formats = SNDRV_PCM_FMTBIT_S16_LE,
                },
                .capture = {
                         .stream_name = "Capture",
                        .channels_min = 1,
-                       .channels_max = 1,
+                       .channels_max = 2,
                        .rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000,
                        .formats = SNDRV_PCM_FMTBIT_S16_LE,
                },
```

# 結果
![i2s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104225/2551c89d-1f3f-9350-a9dd-20f3e29091e3.png)

わーい!

# おまけ

GPIO20(IN)とGPIO21(OUT)を直結して、再生中に

```shell-session
arecord -D default:sndrpinulldac rec.wav
```

で録音したら、録音もできました。

# 今後
- ふつーに48KHzステレオとかハイレゾとか
- モノラル片側1ch出力
- ユーザー空間からの動的設定変更
- てきとーなDACを指定すればふつーに動いたんじゃ疑惑