ESP8266とデジタルマイクADMP441を接続(Arduino SPIライブラリ版)

#1. はじめに
本記事ではデジタルマイクADMP441を使いESP-WROOM-02で音声を集音する方法を紹介します。
以下の内容について触れます。

 * ADMP441とESP-WROOM-02との接続(SPI)
 * 音声データをシリアルで取得する

今回は16bit 8kHzのフォーマットで2秒程度の音声データを収録することができました。収録秒数はESP-WROOM-02のハードウェア的仕様によるものです。(ユーザランドのRAMサイズ起因)
 また、 データの取得と外部への送信は同時にできません。（現在チャレンジ中）
ADMP441自体は[廃盤?](http://www.analog.com/jp/products/obsolete/admp441.html)になったようですが、秋月などの在庫はまだありそうです。

本記事は電子回路の知識が必要です。実施は各人の責任でお願いいたします。

# 2. 利用方法の検討

## 2.1 ADMP441の概要
　
ADMP441はMEMSマイク、アンプ、A/D変換、デジタルフィルタをワンパッケージに収めたデジタルマイクモジュールです。出力はI2Sと呼ばれる3線のシリアル通信を利用します。3線はそれぞれ以下のような機能割り当てになっています。

| 名称 | 機能 |
|------|------|  
| SCK  | 同期クロック(500kHz〜3MHz程度まで)<br>クロックに応じてサンプリング周波数が変わる|
| SD   | 音声のデータ信号(1クロックごとに1bit)|
| WS   | L側R側の出力セレクタ信号<br>SCKの64倍の周期(SDの64bit分)で動作させる。|


手に入れやすい製品としては以下の２つがあります。本記事では1番の秋月電子の製品を利用しました。

1. http://akizukidenshi.com/catalog/g/gK-06864/
2. https://strawberry-linux.com/catalog/items?code=12036

* 2.2 ADMP441の動作

ADMP441のピン配置は以下のようになっています。

| ピン番号 | 名称 | 機能 |
|---------|------|------|  
|  1      | SCK  | I2Sの動作クロック入力(500kHz〜3MHz)|
|  2      | SD   | データ出力 |
|  3      | WS   | LRセレクタ入力(LowでL側データ出力)|
|  4      | L/R  | マイクのL/R選択(GND接続でL側出力)|
|  5      | GND  | グランド|
|  6      | VDD  | 3.3V   |
|  7      | EN   | チップイネーブル(VDD接続) |
|  8      | GND  | グランド|

ADMP441は1番ピンのSCKクロックとWSクロック入力に従って動作を開始します。2^18乗分のSCKクロックを受けるとSDピンからデータを出力します。
　SDからはSCKクロックの立ち上がりのタイミング(Low->High)で1bit分のデータが出力されます。またL/Rチャンネル選択(４番ピン)の設定に従い、WSピンに入力されるクロックに同期してL/Rに分けて出力されます。
　SDから出力されるデータはモノラル1チャンネル分で32bitのデータになります。そのため、WSに入力するクロックはSCKの32クロックごとに切り替える必要があります。
　取得されるデータは片側32bitですが実際に有効なデータは24bitのデータが取得されるので、先頭1bitと後ろ7bitはソフトウェア的に除去する必要があります。
　前述のようにL/Rの設定とWSに従ってLチャンネルRチャンネルのデータを出力します。そのため、L/Rの設定が違うマイクを２つ接続するとステレオマイクとして利用できます。

タイミングチャートと詳細な仕様については以下のデータシートをご覧ください。

* http://akizukidenshi.com/download/ds/analog/admp441.pdf

# 3. ESP-WROOM-02との接続

## 3.1 WROOM-02との接続方法検討

前述の通り、ADMP441のインタフェースはI2Sです。I2SはSPIと共通点がります。そのため、SPIの機能を利用して取り込むことも可能です。ESP-WROOM-02にはI2S,SPIのモジュールが内蔵されています。I2Sで接続するかSPIで接続するか検討を行いました。

### 3.1.1 I2S接続

　ESP8266用のArduinoライブラリにI2S用APIが用意されていますがデータの出力はできるものの、I2S経由でデータを読み取る方法は提供されていません。
　さらに、I2Sのデータ受信において、マスタ(ESP8266側)はクロックを出力することができますが、WSはスレーブから送られてくる仕様になっています。これはADMP441の使用と合致しません。スレーブにした場合はクロックも外部からの入力になります。そのため、ADMP441と利用するためにはESP8266をスレーブに設定し、外部からクロックを入力する必要がありそうです。　
  ただ、I2S自体はデータの転送をDMAに任せることが出るため、読み書きしながら他の割り込み処理やシリアルへのデータ送信が可能です。特徴を以下にまとめます。

  1. メモリ転送にDMAが利用できるためCPUを有効活用できる
　2. I2Sデータ受信をマスタモードで動かすとWSクロックを出力できない
　3. スレーブモードで動かすと外部にクロック供給源を作る必要がある
　3. そもそも受信したデータの取り出し方が不明(情報が少ない)

オーディオデータを出力するだけであればハードルが低そうですが、取り込みに関してはハードルが高そうです。実際、チャレンジ中ですが難儀しています。

### 3.1.2 SPI接続

　SPIを利用した場合の特徴は以下のようになっています。
 
　1. SPIのAPIはread writeとも可能
　2. SPIの処理をCPUで処理するため、データ取り込み中は他の処理が割り込めない
　3. 他の処理を実行するとクロックが停止する
　4. WSクロックはIOを直接操作して出力する。

こちらもやや難がありそうですがI2Sよりはハードルが低そうなため今回はSPI接続で利用することとしました。


## 3.2 配線(SPI接続)
配線は以下のようになります。IOxxはESP-WROOM-02のピン番号になります。今回はスイッチサイエンスさんの[EsperDeveloper](https://www.switch-science.com/catalog/2500/)を利用しました。

| ADMP441ピン番号 | 接続 |
|----------------|------|  
|  1(SCK)        | IO14|
|  2(SD)         | IO12 (100kΩのプルアップ抵抗を入れる)|
|  3(WS)         | IO15|
|  4(L/R)        | グランド|
|  5(GND)        | グランド|
|  6(VDD)        | 3.3V   |
|  7(EN)         | 3.3V |
|  8(GND)        | グランド|

# 4. プログラム

## 4.1 ESP-WROOM-02(SPI)

　ESP-WROOM-02側のプログラムは以下のようになります。SPIのMODEは0に設定します。またADMP441が出力するデータはビックエンディアンになるため、BitOrderをMSBSHIFTに設定します。
　データの読み出し部についてはIO15をLowにし、4Byte(32bit)分SPIからデータを読み出します。これでLチャンネルのデータが取得できます。取得されたデータのうち24bitが必要なデータになりますが、扱いやすさを考慮してLSB側8bitを無視して16bitにしています。R側はデータが出ていないため読み飛ばします。
出力処理では貯めたバッファをシリアルに流しているだけです。
読み取り側で逐次シリアル側に出そうとするとクロック停止時間が長くなり、マイクが動作しません。また、EERROMも試してみましたが同じでした。読み取り側ではRAMへの書き込みが限界のようです。

```C++:ADMP441.ino
#include <SPI.h>

#define RING_BUFFER_SIZE 16000 //16000 sample / 8000Hz = 2sec

uint8_t inputs[4]={0};

//RING Buffer
int buffer_index = 0;
short ring_buffer[RING_BUFFER_SIZE]; //2 * 16000 = 32000 Byte

void setup() {
  Serial.begin(115200);
  SPI.begin();
  SPI.setDataMode(SPI_MODE0);// ClockPolarity active LOW, Clock Phase 立ち上がり
  SPI.setFrequency(500000);// 500kHz=~8000*64clock( wav-file sampling rate 8000Hz)
  SPI.setBitOrder(MSBFIRST);//Big Endian
  pinMode(15, OUTPUT);// Setting for WS
  delay(20);
}

void loop() {
  while(true){
    int LRSelect=0;
    //Read data from microfon
    for ( int i=0; i<RING_BUFFER_SIZE; i++) {
        ESP.wdtFeed();
       digitalWrite(15, LRSelect);//Left channel
       LRSelect = ~LRSelect;
       SPI.transferBytes(0, inputs, 4);                 
       //   |MSB    ||       ||    LSB|
       //  x1111111 11111111 11111111 1xxxxxxx
       //               to
       //  |MSB   | |      | |   LSB|
       //  11111111 11111111 11111111 xxxxxxxx
       uint8_t MSB = (inputs[0] << 1) | (0x01 & (inputs[1] >> 7));
       uint8_t MID = (inputs[1] << 1) | (0x01 & (inputs[2] >> 7));
       uint8_t LSB = (inputs[2] << 1) | (0x01 & (inputs[3] >> 7));
       buffer_index = i % RING_BUFFER_SIZE;
       ring_buffer[buffer_index] =   (0xFF00 & ((short)MSB<<8)) | (0x00FF &(short)MID); 
       //Don't write Serial.print or EEPROM.write. 
       //Because waiting I/O and SPI clock stopped long time.
       digitalWrite(15, LRSelect);//Right channels
       LRSelect = ~LRSelect;
       SPI.transferBytes(0, inputs, 4);
       
    }  

    //Output to Serial and stop SPI clock
    for(int i=0; i<RING_BUFFER_SIZE; i++){
      ESP.wdtFeed();
      Serial.println(ring_buffer[i]);
    }
   
  }
}
```


## シリアル受け側(Python)

シリアル側は以下のプログラムを準備しました。適当な時間で止めるとバイナリファイルが出力されます。ヘッダなどがないため、[Audacity](http://audacity.softonic.jp)などRAWのPCMデータが扱えるアプリケーションで開く必要があります。Linuxの場合は　aplayにオプション指定(aplay -f S16_LE -r 8000 wav.pcm)で再生できます。

```python:mic.py
# -*- coding: utf-8 -*-                                                                                                                                                                                                                       

import serial
import struct

com = "" #TODO シリアルのポートを環境に合わせて指定

def main():

    s = serial.Serial(com,115200)

    for i in range(0,100):
        trash = s.readline()
    with open("wave.pcm", "wb") as fout:
      while True:
          data = s.readline()
          bin = struct.pack('<h',int(data.strip())) #リトルエンディアンに変換
          fout.write(bin)

if __name__ == "__main__":

    main()

```

#結果

以下に、結果を報告します。

## SPIのクロック
8kHz*64bit(32*2チャンネル)≒500kHz で2us周期のクロックが出力されています。（１マスが2us）
![IMG_2834.JPG](https://qiita-image-store.s3.amazonaws.com/0/3264/f518de53-adf3-d10d-5cdb-9ff4dcfd7a1e.jpeg)

## WSと出力データ
上側WSがLの時のみ下側マイク側からの出力が出ています。WSは150us程度の周期になり、64bit分に相当します。(1マスが50us)
![IMG_2836.JPG](https://qiita-image-store.s3.amazonaws.com/0/3264/e3fa10e0-6e04-c43b-e5de-497517b657ef.jpeg)
ロジアナがあるともう少しわかりやすくなると思います。

##音声の波形
集音した音声の波形です。再生するとちゃんと聞こえます（当たり前ですが）
Audacityの[ファイル]->[取り込み]->[ローの取り込み]から、mic.pyで保存したファイルを開きます。(Signed 16bit リトルエンディアン 1チャンネル 8kHz)

![スクリーンショット 2016-09-25 11.50.53.png](https://qiita-image-store.s3.amazonaws.com/0/3264/24991481-49a7-3e8c-3ae6-467090663eeb.png)

波形がグニョっと下に歪むタイミングがあります。これはデータ転送後、再度集音を開始したタイミングです。おそらくマイクの出力が安定していないためだと思われます。8kHzの場合、0.5秒(2us*2^18)ほど読み飛ばせば安定した部分のみ収録が可能です。

#まとめ
マイク自体は接続できましたが、RAMの制約上取り込んだ音声が短いです。また集音と外部への転送が同時にできません。この問題の解決策としてI2Sを利用(DMAによるメモリ転送)が考えられます。こちらについては取り組み中ですが。。。

* *__2017/10/21追記__*

2017年の1月頃にI2Sからのデータ取得に成功しました。記事化していませんが、以下の資料に概要を記載しています。

[ESP8266とマイクをつないでみた話](https://speakerdeck.com/masatoka/esp-wroom-02tomaikuwotunaidemitahua)