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)
- 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 の初期化を行ってから、信号出力(無限ループ)までのコードです。
「信号を出力する」のが目的のため、生成波形に意味はありません。
全体
#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 の初期化を行っています。
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 関数は i2s_chan_config_t の内容に従い、送信(tx_handle)と受信(rx_handle)ハンドルを生成します。
- I2S_CHANNEL_DEFAULT_CONFIG は、I2S番号と Master / Slave のみの指定で i2s_chan_config_t 値を生成するマクロです。
- 受信は使用しないため i2s_new_channel の rx_handle は NULL を指定します。
標準モード設定
I2S を標準モードに設定しています。標準モードはデータとしてリニア PCM 値を扱います。他に PDM と TDM モードがあります。
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));
-
i2s_std_config_t の clk_cfg は i2s_std_clk_config_t 型です。サンプリング周波数と MCLK 倍率等のメンバがあります。
- I2S_STD_CLK_DEFAULT_CONFIG はサンプリング周波数を指定して i2s_std_clk_config_t を生成します。MCLK の倍率はデフォルト値の 256fs (44100Hz * 256 = 11.2896MHz)になります。
- slot_cfg は、i2s_std_slot_config_t 型です。I2S_STD_PHILP_SLOT_DEFAULT_CONFIG はデータ幅とモノ/ステレオの指定で i2s_std_slot_config_t を生成します。
- I2S_STD_PHILP_SLOT_DEFAULT_CONFIG (PHILIPS ではない)は通常の I2S フォーマット用です。左詰めフォーマットの場合は代わりに I2S_STD_MSB_SLOT_DEFAULT_CONFIG を使用します。
- gpio_cfg は、i2s_std_gpio_config_t 型です。信号へ割り当てる GPIO ピン等を指定します。
- 使用しないピンは I2S_GPIO_UNUSED を指定できます。今回は受信を使用しないため din へ I2S_GPIO_UNUSED を指定します。
- ESP32 では MCLK を出力できる GPIO に制限があり、GPIO0/1/3 しか指定できない様です(参考: i2s_check_set_mclk 関数の実装)。通常、GPIO1/3 は USB シリアル変換チップの TX/RX と接続されているため、実質指定できるのは GPIO0 だけです。未確認ですが、ESP32-S 等では任意の GPIO へ割り当て可能な様です。
- invert_flags は信号を反転するかどうかの指定です。true の指定で反転します。
- i2s_channel_init_std_mode 関数で channel handle を標準モードへ設定します。
- i2s_channel_enable 関数で channel を有効化します。
サンプル生成、出力
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));
}
- 4サンプル分のバッファを準備しています。
- 1サンプルが 16bit のため、バッファのサイズは sizeof(int16_t) * 4 です。
- バッファを適当な値で初期化します。中身は 0x0000, 0x8001, 0x0002, 0x8003 になります。
- i2s_channel_write 関数でバッファの内容を出力します。
出力信号の確認
ロジックアナライザで GPIO0 (MCLK)、GPIO25 (BCLK)、GPIO26 (WS)、GPIO27 (DOUT) を確認しました。上から順に D3=MCLK / D0=BCLK / D1=WS(LRCLK) / D2=DOUT です。
意図通りに出力されています。
以下は、周波数カウンタで周波数を測定した結果です。
信号 | 規定値 | 測定結果 | 偏差 |
---|---|---|---|
MCLK | 11.2896MHz | 11.28964MHz | 30ppm |
BCLK | 1.4112MHz | 1.4112085MHz | 60ppm |
WS | 44100Hz | 44100Hz | 0ppm |
安価な周波数カウンタなので、値をどこまで信頼できるかの判断は難しいところですが…。
S/PDIF では許容偏差が 1000ppm ですので、通常の音声出力用には十分そうです。