8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ESP32でPDMマイクから録音

Last updated at Posted at 2020-12-12

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となる,つまりクロック立ち上がり時のデータビット

PDM_interface_image.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スクリプトなどで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のプログラミングガイドがあるものの,情報はまだまだ不足しているように思います.
私も微力ながら細かいノウハウを蓄積できればと思います.

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?