ESP-IDF v5 で I2S 信号を出力 (ESP32)

ESP32 で ESP-IDF v5 を使用して I2S 信号を出力する方法です。

# 前提環境

* ESP32-DevKitC
* ESP-IDF master 版 (2022/09/19)

# ESP32 の I2S について

ここで使用する ESP-IDF は v5 系です。I2S を出力するためには ESP-IDF の I2S driver を使用しますが、v4 までの API は deprecated となっています。せっかくなので新しい方を使用する事にしました。

公式ドキュメントの I2S について: [Inter-IC Sound (I2S)](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html) 

* Master / Slave どちらも使用可。
* クロックは PLL 等から生成。
* 左詰め、I2S フォーマットどちらも対応。
* ビット幅は 8,16,24,32bit を設定可。
* MCLK 出力可能(倍率設定可)。
* 出力信号の反転機能。
* DMA 転送。
* ESP32 では I2S が 2 つ(I2S0 と I2S1)。

これだけ多機能だと使い方が難しそうですが、API は分かりやすくシンプルです。今回の様に、単純に出力する程度であれば簡単にできました。

ESP ではオーディオ用の Framework として ESP-ADF があります。現在のバージョンは ESP-IDF v4 をベースとしてるため、ESP-IDF v5 の API は使用できません。

# 今回の使い方

* Master で使用
* サンプリング周波数は 44.1KHz
* I2S フォーマット出力
* ビット幅 16bit
* MCLK x256 を出力する

# ソースコード

I2S の初期化を行ってから、信号出力(無限ループ)までのコードです。
「信号を出力する」のが目的のため、生成波形に意味はありません。

## 全体

```c:main.c
#include <stdio.h>
#include <driver/i2s_std.h>

void app_main(void)
{
    i2s_chan_config_t channel_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
    i2s_chan_handle_t tx_channel_handle;
    ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &tx_channel_handle, NULL /* rx_handle */));

    i2s_std_config_t std_config = {
        .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
        .slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
        .gpio_cfg = {
            .mclk = GPIO_NUM_0,
            .bclk = GPIO_NUM_25,
            .ws = GPIO_NUM_26,
            .dout = GPIO_NUM_27,
            .din = I2S_GPIO_UNUSED,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };
    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_channel_handle, &std_config));
    ESP_ERROR_CHECK(i2s_channel_enable(tx_channel_handle));

    const int SAMPLE_LENGTH = 4;
    int16_t* buffer = (int16_t*)calloc(sizeof(int16_t), SAMPLE_LENGTH);
    for (int i = 0; i < SAMPLE_LENGTH; ++i) {
        buffer[i] = (i & 1) ? 0x8000 + i : i;
    }
    for (;;) {
        size_t written_bytes = 0;
        const uint32_t TIMEOUT_MS = 1000; 
        ESP_ERROR_CHECK(i2s_channel_write(tx_channel_handle, buffer,
            SAMPLE_LENGTH * sizeof(int16_t), &written_bytes, TIMEOUT_MS));
    }
}
```

## 説明

### i2s channel 初期化

i2s_channel の初期化を行っています。
 
```c
i2s_chan_config_t channel_config = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
i2s_chan_handle_t tx_channel_handle;
ESP_ERROR_CHECK(i2s_new_channel(&channel_config, &tx_channel_handle, NULL /* rx_handle */));
```
* [i2s_new_channel](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html#_CPPv415i2s_new_channelPK17i2s_chan_config_tP17i2s_chan_handle_tP17i2s_chan_handle_t) 関数は [i2s_chan_config_t](https://github.com/espressif/espidf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_common.h#L56) の内容に従い、送信(tx_handle)と受信(rx_handle)ハンドルを生成します。
* [I2S_CHANNEL_DEFAULT_CONFIG](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_common.h#L22) は、I2S番号と Master / Slave のみの指定で i2s_chan_config_t 値を生成するマクロです。
* 受信は使用しないため i2s_new_channel の rx_handle は NULL を指定します。

### 標準モード設定

I2S を標準モードに設定しています。標準モードはデータとしてリニア PCM 値を扱います。他に PDM と TDM モードがあります。
```c
i2s_std_config_t std_config = {
    .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
    .slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
                                                   I2S_SLOT_MODE_STEREO),
    .gpio_cfg = {
        .mclk = GPIO_NUM_0,
        .bclk = GPIO_NUM_25,
        .ws = GPIO_NUM_26,
        .dout = GPIO_NUM_27,
        .din = I2S_GPIO_UNUSED,
        .invert_flags = {
             .mclk_inv = false,
             .bclk_inv = false,
             .ws_inv = false
         }
     }
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_channel_handle, &std_config));
ESP_ERROR_CHECK(i2s_channel_enable(tx_channel_handle));
```
1. [i2s_std_config_t](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_std.h#L262) の clk_cfg は [i2s_std_clk_config_t](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_std.h#L232) 型です。サンプリング周波数と MCLK 倍率等のメンバがあります。
   * [I2S_STD_CLK_DEFAULT_CONFIG](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_std.h#L197) はサンプリング周波数を指定して i2s_std_clk_config_t を生成します。MCLK の倍率はデフォルト値の 256fs (44100Hz * 256 = 11.2896MHz)になります。
1. slot_cfg は、[i2s_std_slot_config_t](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_std.h#L206) 型です。[I2S_STD_PHILP_SLOT_DEFAULT_CONFIG](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_std.h#L31) はデータ幅とモノ/ステレオの指定で i2s_std_slot_config_t を生成します。
   * I2S_STD_PHILP_SLOT_DEFAULT_CONFIG (PHILIP**S** ではない)は通常の I2S フォーマット用です。左詰めフォーマットの場合は代わりに [I2S_STD_MSB_SLOT_DEFAULT_CONFIG](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_std.h#L50) を使用します。
1. gpio_cfg は、[i2s_std_gpio_config_t](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/include/driver/i2s_std.h#L246) 型です。信号へ割り当てる GPIO ピン等を指定します。
   * 使用しないピンは I2S_GPIO_UNUSED を指定できます。今回は受信を使用しないため din へ I2S_GPIO_UNUSED を指定します。
   * ESP32 では MCLK を出力できる GPIO に制限があり、GPIO0/1/3 しか指定できない様です(参考: [i2s_check_set_mclk 関数の実装](https://github.com/espressif/esp-idf/blob/29830f609d96ed2cfb0639908e0a24ae40d2936c/components/driver/i2s/i2s_common.c#L738))。通常、GPIO1/3 は USB シリアル変換チップの TX/RX と接続されているため、実質指定できるのは GPIO0 だけです。未確認ですが、ESP32-S 等では任意の GPIO へ割り当て可能な様です。
   * invert_flags は信号を反転するかどうかの指定です。true の指定で反転します。
1. [i2s_channel_init_std_mode 関数](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html#_CPPv425i2s_channel_init_std_mode17i2s_chan_handle_tPK16i2s_std_config_t)で channel handle を標準モードへ設定します。
1. [i2s_channel_enable 関数](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html#_CPPv418i2s_channel_enable17i2s_chan_handle_t)で channel を有効化します。

### サンプル生成、出力
```c
const int SAMPLE_LENGTH = 4;
int16_t* buffer = (int16_t*)calloc(sizeof(int16_t), SAMPLE_LENGTH);
for (int i = 0; i < SAMPLE_LENGTH; ++i) {
    buffer[i] = (i & 1) ? 0x8000 + i : i;
}
for (;;) {
    size_t written_bytes = 0;
    const uint32_t TIMEOUT_MS = 1000; 
    ESP_ERROR_CHECK(i2s_channel_write(tx_channel_handle, buffer,
        SAMPLE_LENGTH * sizeof(int16_t), &written_bytes, TIMEOUT_MS));
}
```
1. 4サンプル分のバッファを準備しています。
   * 1サンプルが 16bit のため、バッファのサイズは sizeof(int16_t) * 4 です。
   * バッファを適当な値で初期化します。中身は 0x0000, 0x8001, 0x0002, 0x8003 になります。
2. i2s_channel_write 関数でバッファの内容を出力します。

# 出力信号の確認

ロジックアナライザで GPIO0 (MCLK)、GPIO25 (BCLK)、GPIO26 (WS)、GPIO27 (DOUT) を確認しました。上から順に D3=MCLK / D0=BCLK / D1=WS(LRCLK) / D2=DOUT です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553718/ab5ba258-4261-8556-e4b6-7b314d153534.png)
意図通りに出力されています。

以下は、周波数カウンタで周波数を測定した結果です。

|信号|規定値|測定結果|偏差|
|--|--|--|--|
|MCLK|11.2896MHz|11.28964MHz|30ppm|
|BCLK|1.4112MHz|1.4112085MHz|60ppm|
|WS|44100Hz|44100Hz|0ppm|

安価な周波数カウンタなので、値をどこまで信頼できるかの判断は難しいところですが…。
S/PDIF では許容偏差が 1000ppm ですので、通常の音声出力用には十分そうです。