1. はじめに
最近,国内の通販でI2S対応マイク部品が品薄になってきました.比較的残っているデジタルマイク部品はPDM対応です.
幸い,ESP32のI2SはPDMにも対応しているとテクニカルドキュメントに記載されています.
しかし,インターフェースとしてのPDMを理解していないと,どのように利用するのか見当が付かない場合もあるかと思います.
今回は,ESP32のI2SドライバでPDMでの読み取り方法について説明します.
2. 使い方
i2sドライバにPDMモードの設定をします.
DATはピン34に,CLKはピン26に接続しています.L/RはGNDに接続します.
読み込みはi2s_readを使います.実際はDMAで受信しており,この関数は設定したバイト数が受信が終わるまで待ってくれます.
#include "driver/i2s.h" //https://github.com/espressif/arduino-esp32/blob/master/tools/sdk/include/driver/driver/i2s.h
#define PIN_I2S_WS 26
#define PIN_I2S_DIN 34
#define HZ_SAMPLE_RATE 44100
#define N_LEN_READ_2BYTE 1000
#define N_BYTES_STRACT_PAR_SAMPLE 2
#define N_LEN_READ_BYTES (N_LEN_READ_2BYTE*N_BYTES_STRACT_PAR_SAMPLE)
void I2S_Init(void) {
esp_err_t erResult = ESP_OK;
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX| I2S_MODE_PDM),
.sample_rate = HZ_SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
.communication_format = I2S_COMM_FORMAT_I2S ,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 2,
.dma_buf_len = 128,
.use_apll = false
};
erResult = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_pin_config_t pin_config;
pin_config.bck_io_num = I2S_PIN_NO_CHANGE;
pin_config.ws_io_num = PIN_I2S_WS;
pin_config.data_out_num = I2S_PIN_NO_CHANGE;
pin_config.data_in_num = PIN_I2S_DIN;
erResult = i2s_set_pin(I2S_NUM_0, &pin_config);
erResult = i2s_set_clk(I2S_NUM_0, HZ_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
}
void setup() {
I2S_Init();
size_t uiGotLen=0;
uint8_t u8buf[N_LEN_READ_BYTES];
esp_err_t erReturns = i2s_read(I2S_NUM_0, (char *)u8buf, N_LEN_READ_BYTES, &uiGotLen, portMAX_DELAY);
uint16_t * u16buf = (uint16_t *)&u8buf[0];
Serial.begin(115200);
for (int i=0;i<N_LEN_READ_2BYTE;i++)
Serial.println(u16buf[i]);
}
void loop() {
}
3. PDMとは?
Pulse Density Modulationの略がPDMで,文字通り,パルスが沢山あると波高値としては高く,パルスが少ないと低い,という表現方法です.ArduinoシリーズはDACが無い変わりに,PWMつまりPulse Width Modulation出力でアナログ値を表現します.「ゼロ」と「イチ」の2値だけを,時間方向で組み合わせて,中間値を表現するという意味では,PWMとPDMとはほぼ同じ手法になると思います.異なるのは,PWMは単位時間におけるパルス幅で表現しており,PDMは細かい単位時間に一定パルス幅の個数で表現する,という点です.
なお,Pulse Code Modulationの略PCMは,A/D変換において,1サンプルに「ゼロ」から「255」などの数値で表現しています.PCMにおける数値をパルスの数にすれば,PDMになります.PDMで255まで表現する場合は,PCMのサンプルレートの256倍速くパルスの数を表現すれば良いことになります.255ならば,1,1,1,1,1...と255個並べて,128ならば,1,0,の組み合わせを128組並べれば良く,0ならば,0を255個並べます.
ちなみに,パルスを並べるのではなく,PCMの数値をシリアル化し送信するという手法もあります.これがI2Sです.255ならば,1,1,1と8個並べます.128ならば,1,0,0,0,0,0,0,0で,64ならば,0,1,0,0,0,0,0,0です.これで判ると思いますが,PDMを情報伝送の面で効率化したのが,I2Sと考えると判り易いと思います.
また,アナログ値からPDMに変換するための回路として挙げられるのが,ΔΣ変調(デルタ シグマ へんちょう)回路です.アナログをデジタルに変換しているとは言うけど,比較器と遅延素子だけでロジック要素がほとんど無く,アナログのままじゃないの?という感じの回路です.この発想をアナログ回路分野に展開すると「D級アンプ」というキーワードが出てきます.なお,ΔΣ変調は1ビット(2値)だけでなく,複数のビットで変調しても良いので,DAC出力で高音質化を狙った手法として通常の音声の数倍の出力レートで中間値を表現するというテクニックもあります.
以上,大半を端折って記載したため,意味が分からない場合もあるかと思いますが,この文章にあるキーワードを拾って検索すると,判り易い説明が出てくると思います.
4. インターフェースとしてのPDM
インターフェースとしてのPDMは,クロックのエッジでパルスの有無を示すことになっています.そして,パルスの立ち下がりでは右チャネル,立ち上がりでは左チャネルのパルスと定義されており,ステレオ音声を同時に送信できる仕様になっています.
このクロックはマイコンから供給することが多いです.マイクは,与えられたクロックに同期して,パルスをマイコンに送信します.
ADAU7002のデータシートが判り易いです.
SPM0405HD4Hの仕様
- LRをプルダウンで,Data1側出力となり,右chとなる,つまりクロック立ち上がり時のデータビット
- LRをプルアップで,Data2側出力となり,左chとなる,つまりクロック立ち上がり時のデータビット
5. ESP32のI2Sに関する参考資料
ESP32テクニカルドキュメント
https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf#page=308
API解説
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html
I2Sのヘッダファイル
https://github.com/espressif/arduino-esp32/blob/master/tools/sdk/include/driver/driver/i2s.h
I2Sドライバソースコード
https://github.com/espressif/esp-idf/blob/master/components/driver/i2s.c
6. サンプルコード
i2sはDMAを利用しているので,本来は読み込み中に別のが可能なのですが,i2s_readはブロッキングするため, xTaskCreateを用いて並列化します. 実行すると1秒間録音し,そのデータをシリアルモニタに数値で出力します.
数値だけをコピーしてテキストファイルで保存し, pythonスクリプトなどでwaveファイルに変換して聴いてください.
なお, M5stick-Cの場合,PIN_I2S_WSが0番に繋がってますので,その1行だけを変更します.
ESP32PdmMicSPM0405HD4H.ino
#include "driver/i2s.h"
// pin config
#define PIN_I2S_BCLK -1
#define PIN_I2S_WS 26 // M5stick-C: 0
#define PIN_I2S_DIN 34
// sampling info
#define HZ_SAMPLE_RATE 44100
#define N_SEC_REC 1 // second
#define N_SAMPLE_IN_MILLI_SEC 44
#define N_ITTER_READ_BUF (N_SEC_REC * N_SAMPLE_IN_MILLI_SEC )
#define N_LEN_BUF_2BYTE 1000
#define N_BYTES_STRACT_PAR_SAMPLE 2
#define N_LEN_BUF_ALL_BYTES (N_LEN_BUF_2BYTE*N_BYTES_STRACT_PAR_SAMPLE)
#define N_SKIP_SAMPLES (11*2)
#define N_BUF_SIDE 2
char gi8BufAll[N_BUF_SIDE][N_LEN_BUF_ALL_BYTES];
int16_t gi16strmData[N_ITTER_READ_BUF*N_LEN_BUF_2BYTE];
volatile uint8_t gui8Side = 0;
volatile uint8_t gui8flagReadDone = 0;
xTaskHandle gxHandle;
void I2S_Init(void) {
esp_err_t erResult = ESP_OK;
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX| I2S_MODE_PDM),
.sample_rate = HZ_SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
.communication_format = I2S_COMM_FORMAT_I2S ,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 2,
.dma_buf_len = 128,
.use_apll = false
};
erResult = i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_pin_config_t pin_config;
pin_config.bck_io_num = I2S_PIN_NO_CHANGE;
pin_config.ws_io_num = PIN_I2S_WS;
pin_config.data_out_num = I2S_PIN_NO_CHANGE;
pin_config.data_in_num = PIN_I2S_DIN;
erResult = i2s_set_pin(I2S_NUM_0, &pin_config);
erResult = i2s_set_clk(I2S_NUM_0, HZ_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
}
void skipNoisySound(){
size_t uiGotLen=0;
for (int j = 0; j < N_SKIP_SAMPLES ; j++)
{
i2s_read(I2S_NUM_0, (char *)gi8BufAll[0], N_LEN_BUF_ALL_BYTES, &uiGotLen,portMAX_DELAY);
}
}
void taskI2sReading(void *arg){
size_t uiGotLen=0;
while(1){
esp_err_t erReturns = i2s_read(I2S_NUM_0, (char *)gi8BufAll[gui8Side], N_LEN_BUF_ALL_BYTES, &uiGotLen, portMAX_DELAY);
gui8Side = (gui8Side+1)&1;
gui8flagReadDone =1;
}
}
void getData(){
int16_t tmp16=0;
size_t uiGotLen=0;
esp_err_t erReturns;
uint16_t * ptmp16;
skipNoisySound();
uint8_t ui8Side = 0;
gui8flagReadDone =0;
xTaskCreate(taskI2sReading, "taskI2sReading", 2048, NULL, 1, &gxHandle);
for (int j = 0; j < N_ITTER_READ_BUF ; j++)
{
while(gui8flagReadDone==0){}
gui8flagReadDone =0;
ui8Side = (gui8Side+1)&1;
ptmp16 = (uint16_t *)&gi8BufAll[ui8Side][0];
for (int i = 0; i < N_LEN_BUF_2BYTE ; i++) {
tmp16 = ((int16_t)(ptmp16[i]));
gi16strmData[i + N_LEN_BUF_2BYTE*j] = tmp16;
} // i
} // j
vTaskDelete(gxHandle);
}
void stdoutStrm(){
for (int j = 0; j < N_ITTER_READ_BUF*N_LEN_BUF_2BYTE ; j++)
{
Serial.println(gi16strmData[j]);
}
}
void setup() {
Serial.begin(115200);
Serial.println("---start---");
Serial.flush();
I2S_Init();
getData();
Serial.println("---rec done---");
stdoutStrm();
Serial.println("---done---");
}
void loop() {
}
7. おわりに
昨年,ESP32にデジタルマイクを接続して録音するセンサノードを構築しようと,I2Sマイクのコードをwebで漁りながら,ようやく実装しました.I2Sドライバについて勉強せずに,webにあるコードをトライアンドエラーで実装したのが仇となり,PDMマイクではどうしようも無い状態になり調査しました.ESP32で行う大半の事はespressifから提供されているドライバで解決します.公式のテクニカルドキュメントやESP-IDFのプログラミングガイドがあるものの,情報はまだまだ不足しているように思います.
私も微力ながら細かいノウハウを蓄積できればと思います.