ESP32でPDMマイクから録音

# 1. はじめに

最近，国内の通販でI2S対応マイク部品が品薄になってきました．比較的残っているデジタルマイク部品はPDM対応です．
幸い，ESP32のI2SはPDMにも対応しているとテクニカルドキュメントに記載されています．
しかし，インターフェースとしてのPDMを理解していないと，どのように利用するのか見当が付かない場合もあるかと思います．
今回は，ESP32のI2SドライバでPDMでの読み取り方法について説明します．

# 2. 使い方

i2sドライバにPDMモードの設定をします．
DATはピン34に，CLKはピン26に接続しています．L/RはGNDに接続します．
読み込みはi2s_readを使います．実際はDMAで受信しており，この関数は設定したバイト数が受信が終わるまで待ってくれます．


```c++
#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出力で高音質化を狙った手法として通常の音声の数倍の出力レートで中間値を表現するという[テクニック](https://qiita.com/geachlab/items/0cd0cd58f666710e0607)もあります．

以上，大半を端折って記載したため，意味が分からない場合もあるかと思いますが，この文章にあるキーワードを拾って検索すると，判り易い説明が出てくると思います．
 

# 4. インターフェースとしてのPDM
インターフェースとしてのPDMは，クロックのエッジでパルスの有無を示すことになっています．そして，パルスの立ち下がりでは右チャネル，立ち上がりでは左チャネルのパルスと定義されており，ステレオ音声を同時に送信できる仕様になっています．
このクロックはマイコンから供給することが多いです．マイクは，与えられたクロックに同期して，パルスをマイコンに送信します．

[ADAU7002のデータシート](https://www.analog.com/media/jp/technical-documentation/data-sheets/ADAU7002_jp.pdf#page=9)が判り易いです．

SPM0405HD4Hの仕様
- LRをプルダウンで，Data1側出力となり，右chとなる，つまりクロック立ち上がり時のデータビット
- LRをプルアップで，Data2側出力となり，左chとなる，つまりクロック立ち上がり時のデータビット

![PDM_interface_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698531/ed789926-bddd-0374-a63f-4990e94c6e30.png)



# 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スクリプト](https://qiita.com/dzonesasaki/items/d1439294657a99559b73)などでwaveファイルに変換して聴いてください．
なお， M5stick-Cの場合，PIN_I2S_WSが0番に繋がってますので，その１行だけを変更します．

<details><summary>ESP32PdmMicSPM0405HD4H.ino</summary><div>

```c++

#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() {
}


```
</div> </details>

# 7. おわりに

昨年，ESP32にデジタルマイクを接続して録音するセンサノードを構築しようと，I2Sマイクのコードをwebで漁りながら，ようやく実装しました．I2Sドライバについて勉強せずに，webにあるコードをトライアンドエラーで実装したのが仇となり，PDMマイクではどうしようも無い状態になり調査しました．ESP32で行う大半の事はespressifから提供されているドライバで解決します．公式のテクニカルドキュメントやESP-IDFのプログラミングガイドがあるものの，情報はまだまだ不足しているように思います．
私も微力ながら細かいノウハウを蓄積できればと思います．