M5StickC Plusに搭載されたマイクから、音声データを取り込んでみようと思います。
本HPで今まで使って来たのは、コンデンサマイクと呼ばれるアナログマイクでした(下図)。
上図マイクの出力はアナログ信号なので、マイコンのAD変換器でデジタル値に変換して取り込んでいました。このようにして取り込んだデータを、PCM(Pulse Code Modulation)と言います。
過去、コンデンサマイクを使った電子工作は、こちらのリンクより参照して見てください。
M5StickC Plusに搭載されたマイク🎤は、I/Fがデジタル信号です。型名はSPM1423、開発元はSiSonic社でした。
SPM1423データシートを検索する(別ウインドウが開きます)
見つかったデータシートを見ても、何の事やらさっぱり解りません!ただ、タイミング図が1枚あるだけ、です(下図)。
※SPM1423データシートから引用
少なくとも、通信に必要な信号線は、クロックとデータの2本だけ、みたいです。SPM1423を2個搭載して、ステレオマイクとして使えるよう、クロックのHIGH期間にデータを出力するか、LOW期間にデータを出力するか、設定する選択端子が1本出ているようです。
しかし、出力されるデータがどういう性格のものなのか、全く記載がありません。
このままでは、どうしようもないので、更に突っ込んで検索してみたところ、SiSonic社のデザインガイドが見つかりました。
SiSonic社デザインガイドを検索する(別ウインドウが開きます)
このガイドを読んだところ、SPM1423の使い方が判りました。
SPM1423は、PDM(Pulse Density Modulation)方式のデータを出力する、デジタルマイクだと判りました。
SPM1423が送ってくるのは、PDMと呼ばれるデジタル信号です。デジタルなので、レベルは0と1の二値となります。英語を直訳すると、「パルス密度変調」です。即ち、一定時間当たりの"1"の密度がアナログ値を表します。
これって、どこかで説明した覚えがあります。 → 本HPでΔΣ型AD変換器を作った時(「#9-2 ΔΣ型AD変換器を作る」)に登場していました。
上図は、ディスクリートで自作したΔΣ型AD変換器のシミュレーション波形です。上段深緑色のDoが、下段緑色のアナログ信号(正弦波)AinをΔΣ型AD変換器で変換した結果のPDM出力です。Doの"1"の密度(粗/密)が正弦波Ainの電圧(低/高)と対応していることが読み取れます。
その他、下段青線がΔ値Vdlt、下段赤線がΣ値Vsgmです。詳しくは「#9-2 ΔΣ型AD変換器を作る」をご確認ください。
SPM1423には、ΔΣ型AD変換器が搭載されていて、その出力PDM信号が、そのままSPM1423のデータ出力になっているのです。従って、ΔΣ型AD変換器の出力データPDMから、アナログ量を示すデジタル値PCMを取得するには、デジタルフィルタ(LPF)を通す必要があります。
音声データをデジタル信号でやり取りする通信規格の1つにI2S(Inter-IC Sound)があり、ESP32にはI2S通信可能なI/Oユニットが搭載されています。
※I2S規格は、3線式シリアル通信(SCK(Serial Clock),WS(Word Select),SD(Serial Data))です。
SPM1423とのI/Fは、クロックとデータの2線ですので、これはI2S規格ではありません。ですが、デファクトなのか、ESP32搭載I2Sユニットのオプション(PDMモード)で送受信することが可能です。
ESP-IDFライブラリ関数を使ってI2SユニットをPDMモードで動作させると、PDMを受信してデジタルフィルタ(LPF)通過後のPCMデータを読み出すことができます。
ここ、M5StickC Plusでは少し奇妙な設計になっています。SPM1423のクロック端子はESP32のGPIO0端子が接続されているのですが、GPIO0(G0)はM5StickC Plusの拡張ポートに引き出されている端子なのです(下図)。
貴重な、拡張ポートに引き出されたGPIO0を、SPM1423のクロックとして使ってしまうと、拡張ポートGPIO0は、実質使用できなくなってしまいます。
何のためにこのような回路構成になっているのか?理解不可能でした。
また、SPM1423のデータ出力は、2個使ってステレオマイクに拡張できる様、設定端子があります。M5StickC Plus回路図で確認すると、搭載されているSPM1423は、クロックのLOW期間出力に固定されていました。
※後ほど、ESP-IDFを使用する場合は、モノラル、右音声に設定して使う事になります。
PDMをデジタルフィルタ(LPF)に通すとPCMに変換できます。ESP32のI2Sユニット内に回路が入っているため、I2Sユニットを使うと、変換後のPCMデータを読み出すことができます(下図)。
※ESP32データシートから引用
さてここで、出力されるPCMの周波数をfPCM[Hz]、SPM1423から読み出すPDMの周波数をfPDM[Hz]とします。
fPCMは、音声データのサンプリングレートに相当します。例えば、CDであればfPCM=44.1kHz、高音質の音楽データだとfPCM=48kHzなどです。
この辺りの数値の根拠となっているのは、人間の耳の可聴域が20〜20k[Hz]であることです。標本化定理によると、記録したい最高周波数の×2倍以上でサンプリングしないと、元の音声データを復元できません。このことから、fPCM>20kHz×2=40kHzが必要だと解ります。ここに少しマージンを取ったのがfPCM=44.1kHz(CD並みの音質)なのでしょう。
ESP32のI2Sユニットでは、fPCMを決めると自動的にfPDMが決定されます。fPCMとfPDMには以下の関係式が成立しています。
fPDM=fPCM×64
または、
fPDM=fPCM×128
×64/×128は、オプションで選択可能です。後ほどESP-IDFを使った解説で再登場します。
さらに、SPM1423のデータシートを参照すると、SPM1423のクロック周波数(fPDM)には以下の様な制限のあることが判りました。
SPM1423データシート: fPDM=1MHz(MIN)〜3.25MHz(MAX)
MIN〜MAXで抑えられているので、「この範囲で使いなさい」、と言う規定です。周波数を3.25MHzより上げて使うことも、逆に1MHzより下げて使う事も、許されていません。
ここ、大事です、テストに出ますよ(1^^)d。
自ずと、fPCMにも上限と下限が存在します。
fPCMの上限: fPDM=fPCM×64の場合で、fPCM≦3.25MHz÷64≒50.7kHz
fPCMの下限: fPDM=fPCM×128の場合で、fPCM≧1MHz÷128≒7.9kHz
つまり、サンプリングレートfPCMは、8〜50k[Hz]くらいの範囲で使わなくてはなりません。
上限は、高音質の音楽データでも48kHz程なので、50kHzまでカバーできれば十分です。
下限は、音質を犠牲にしてでも、音声データのサイズを小さくしたい時に関わります。実際、fPCM<8kHzに下げると、音質の劣化が激しすぎて使い物にならないので、下限が8kHzなら、こちらも十分です。
よく使うI2CやSPI等の通信規格でもクロックを使って通信します。これらのクロックは通信用のクロックなので、通信したい時だけ発振して、そうでない時は停止しています。
通信用のクロックは、大抵の場合、上限周波数(MAX)が規定されていて下限(MIN)の規定がありません。普通、周波数を上げると高速な回路動作が必要となるので、上限が制限されます。最近のスタティックロジック設計では、クロックが遅い分には不都合が無いので、下限は規定する必要がありません。
ですが、SPM1423に供給するクロックは、単なる通信用のクロックではありません。
SPM1423に搭載されたΔΣ型AD変換器を動作させるためにはクロックが必要なので、ESP32が供給するクロックでΔΣ型AD変換器が動作していると考えられます。
ΔΣ型AD変換器は、仮にもアナログ回路です。ΔΣ型AD変換器の内部にはアナログ積分器(Σ回路)があるので、積分器の時定数とクロック周波数の関係次第では、積分値がオーバフロウ(またはアンダフロウ)してしまって、正しくAD変換できなくなってしまいます。
即ち、SPM1423に搭載されたΔΣ型AD変換器が正常に動作可能なクロック周波数が、fPDM=1MHz(MIN)〜3.25MHz(MAX)の範囲だという事になります。これを無視すると、AD変換器の動作(もっと言うと、変換値の正しさ)に影響します。
正しいAD変換結果を取得するためには、確実にfPDMがSPM1423の規定を満たすような範囲で使用しなくてはなりません。
クロックが停止した状態からクロックの供給を始めると、安定したPCMデータを読み出せるようになるまでに、数100msオーダの時間が掛かるようです。
SPM1423データシートでは、「Wake-up time」が10ms(MAX)規定なので、クロック供給から10msで安定するみたいなのですが、実際実験してみると、もっと長い時間経過しないと安定しません。SPM1423の安定時間だけではなく、その後通過するI2Sユニット(ESP32)内デジタルフィルタ(LPF)等の安定時間まで全て積み重なって、数100msオーダに見えているのだと思われます。
「スイッチを押したら、1〜2[秒]後に録音を開始する」、みたいな、の〜んびりしたアプリケーションの場合は、
と言う制御で問題ありません。
しかし、「今!今すぐに録音開始したい」と言うような、リアルタイムの対応が必要なアプリケーションの場合には、
と言う対応が必要となります。この場合、録音していない間も、バシバシとクロックが動き続けているので、消費電力が増大してバッテリの動作可能時間に影響します。
Arduino IDEから参照可能な、M5StickC Plusサンプルコードの中に、I2Sユニットを使ってPDMマイクと通信するコードがありました。
「ファイルメニュー ⇒ スケッチ例 ⇒ M5StickCPlus ⇒ Basics ⇒ Microphone」
このサンプルコードを確認したところ、どうやら、ESP-IDFライブラリを使って制御可能だと解りました。ESP-IDFには「API Reference」ページがあるので、このページを読むと、I2S制御に関連した関数の仕様(使い方)が解ります。
以下、ESP-IDFを使ったI2S制御方法について、順を追って解説して行きます。
ESP-IDFライブラリの関数は、基本的には、そのまま何もしなくても使用可能です。が、今回のI2Sユニットを制御する関数の場合、以下のインクルードファイルを指定する必要があります。
#include <driver/i2s.h>
プログラム先頭付近に追加しておいてください。
まずは、以下の関数でドライバをインストールしてください。
i2s_driver_install (I2S_NUM_0,&i2s_config,0,NULL);
「インストール」と聞くと、「OS(Windows)にソフトウェアをインストール」することを思い浮かべてしまいますが、ここのインストールは、「初期化」という意味に近いです。I2S制御関数を使えるように初期化します。
※実際のところ、ArduinoのESP32開発環境には、FreeRTOSというリアルタイムOSが載っているので、このOSにドライバソフトウェアをインストールする関数なのでしょう。
肝となるのは、引数に渡しているi2s_config構造体の中身ですので、以下、構造体の設定値を示して、その下にコメントを追記します。
// I2S setting
i2s_config.mode=i2s_mode_t (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM);
ESP32がクロックを出力するマスタモード、受信モード、PDMモードを設定。
i2s_config.sample_rate=f_smpl;
受け取るPCMデータの周波数(サンプリングレート、fPCM)を指定。f_smpl=44.1kHz等。
i2s_config.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT;
PCMデータは16ビット長。後ほど実施する実験結果を確認のこと。
i2s_config.channel_format=I2S_CHANNEL_FMT_ONLY_RIGHT;
右チャンネルのデータのみ受信(モノラル音声)。
i2s_config.communication_format=I2S_COMM_FORMAT_STAND_I2S;
i2s_config.intr_alloc_flags=ESP_INTR_FLAG_LEVEL1;
Arduino IDEサンプルコードのまま。PDMモードでは関係のないI2Sフォーマットの指定と、割り込みレベルの設定。このままでOK。
i2s_config.dma_buf_count=2;
i2s_config.dma_buf_len=siz_buf;
DMACのバッファ設定(後ほど解説がある)。ここでバッファ数とサイズを指定するだけで、ESP-IDFライブラリ関数が自動的にDMACを設定してくれる。別途DMACを自分で設定する必要は無い。
i2s_config.use_apll=false;
i2s_config.tx_desc_auto_clear=false;
i2s_config.fixed_mclk=0;
i2s_config.mclk_multiple=I2S_MCLK_MULTIPLE_DEFAULT;
i2s_config.bits_per_chan=I2S_BITS_PER_CHAN_DEFAULT;
この辺は使用しないので、デフォルトの設定値としておく。
続いて、以下の関数を使って、端子の設定を実行します。
i2s_set_pin (I2S_NUM_0,&pin_config);
こちらも、pin_config構造体の設定値を示して、その下にコメントを追記します。
// I2S pin setting
pin_config.ws_io_num=pin_CLK;
SPM1423へ出力するクロック端子。M5StickC Plusでは、pin_CLK=GPIO0。
pin_config.data_in_num=pin_DAT;
SPM1423の出力データ端子。M5StickC Plusでは、pin_DAT=GPIO34。
pin_config.mck_io_num=I2S_PIN_NO_CHANGE;
pin_config.bck_io_num=I2S_PIN_NO_CHANGE;
pin_config.data_out_num=I2S_PIN_NO_CHANGE;
本来のI2S通信規格の端子。PDMモード(受信)では使用しない。
最後にI2Sの動作クロックを設定します。以下、設定コードを示して、その下にコメントして行きます。
i2s_set_clk (I2S_NUM_0,f_smpl,I2S_BITS_PER_SAMPLE_16BIT,I2S_CHANNEL_MONO);
サンプリングレート(fPCM=f_smpl=44.1kHz等)、PCMビット長(16ビット)、チャンネル数(モノラル音声)等を指定して、I2Sユニットの動作クロックを設定する。
i2s_set_pdm_rx_down_sample (I2S_NUM_0,
(f_smpl>3250000/128)? I2S_PDM_DSR_8S: I2S_PDM_DSR_16S);
fPDM=1MHz(MIN)〜3.25MHz(MAX)の範囲に収まる様に、サンプリングレート(fPCM=f_smpl)に従って適切な設定を切り替える。
I2S_PDM_DSR_16Sの方が、取得できるPCMデータの解像度が高くなるので、可能な限り、こちらを使う。サンプリングレートfPCM(=f_smpl)が高くなってくると、fPDM=fPCM×128がSPM1423のクロック周波数上限値3.25MHzを超えてしまうので、I2S_PDM_DSR_8Sに切り替える。
ここで少しDMACに寄り道します。ドライバインストールの所で、DMACの設定項目が2つありました。
i2s_config.dma_buf_count=2;
i2s_config.dma_buf_len=siz_buf;
dma_buf_countは、2以上の値を設定してください。特段の理由がない限り、2を設定すれば問題ありません。
dma_buf_lenには、データを受信するバッファのサイズ(8〜1,024)を指定します。ある程度バッファにデータを貯めてから一気にCPUで受け取って処理した方が効率が高いので、適切なサイズを指定してください。
DMACとは、Direct Memory Access Controllerの略語です。
メモリ上のデータを転送する場合、普通は、CPUが特定のメモリA(アドレス)からデータを読んで、そのデータを別なメモリB(アドレス)へ書く様な処理となります。
この場合、読み出しと書き込みで2回のバスサイクルを要します。
一方、元々のDMACのアイディアは、
メモリAからメモリBへ、直接(Direct)データを転送するので、DMACという名前が付きました。DMACを使うと、データ転送の為にCPUをディスターブすることなく、1バスサイクルで高速にデータ転送を実行できます。
最近のDMACは、もう少し汎用的に拡張されていて、「データを、あるアドレスから別なアドレスへ転送する」、ただのデータ転送ユニットとなっています。CPUと同様、読み出しと書き込みに2バスサイクル掛かるので、D=Direct感が失われているのですが、昔の名残でDMACと呼ばれ続けています。
今回、DMACの役割は以下図の通りです。
I2Sからは、音声データのサンプリングレート(fPCM)に従って、次々とデータが出力されるので、2組あるバッファの一方に書き込み、他方から読み出しする事によって、連続してデータを受信し続けることができるのです。
しかも、中途半端に短い周期(1÷fPCM)で発生するI2Sからのデータ転送要求に、一々CPUが煩わされることがありません。CPUは、バッファにデータが溜まった後で、一気に読み出して一気に処理することができます。
これが、DMACを使う利点であり、DMACに与えられた役割です。
バッファからデータを読み出すには、ESP-IDFライブラリのi2s_read()関数を使います。
// read I2S
i2s_read (I2S_NUM_0,(uint8 *)spl_buf,siz_buf*2,&bcnt,
(msec_tout<0)? portMAX_DELAY: msec_tout/portTICK_RATE_MS);
DMACが転送したバッファ内のデータを一気に読み出すのが一番効率的なので、DMACのバッファサイズと、i2s_read()関数の受信バッファサイズを等しくして使うのが基本です。
DMACのバッファサイズが「サンプル数」で指定するのに対して、i2s_read()関数のバッファサイズは「バイト数」で指定するので、そこだけ注意しましょう。
i2s_read()関数の最後の引数は、タイムアウト時間です。
バッファに読み出し可能なデータが無かった場合に、タイムアウトするまでポーリングします。タイムアウトまでに新しいデータが届いたら、そのデータを読み出すことができます。新しいデータが届かなかった場合は、受信データサイズbcnt=0で戻ってきます。
DMACがバッファをフルにするまで転送するのに必要な時間をタイムアウト時間に設定すると、仮にバッファが空の状態でi2s_read()関数を実行したとしても、DMACが裏のバッファをフルにするまでポーリングするので、確実にデータを受信できます。
その時間は、msec_tout=dma_buf_len÷fPCM[ms]です。
なお、i2s_read()関数へ渡すタイムアウト時間は、単位がFreeRTOSのティック数なので、ミリ秒時間を、定数portTICK_RATE_MSで割り算する必要があります。
定数portMAX_DELAYを指定すると、タイムアウトせず、受信できるまでブロックすることもできます。
そうそう、ArduinoのESP32には、FreeRTOSが載っているみたいです。
FreeRTOSは、プリエンプティブマルチタスクなので、スレッドを起こして時分割並列処理させることができます。その際、CPUのコア番号を選択できるので、デュアルコアのESP32を最大限に活用することも可能です(マルチコア並列処理)。
そのうち使って見たいけど、今回はスルーします。
予備実験と言っても、色々と煩雑だったので、実験用のコードなどは提供せず、やった事と結果だけ報告します。
予備実験でやったこと:
普通、音声データを記録したPCMデータは、グラフ化すると以下図の様になります(画像データは使い回し)。
赤いプロットがPCM波形です。横軸に時間、縦軸にPCMの振幅を取っています。ギザギザが無く、平らになっている部分は、無音部分です。
音声データは、無音部分のレベルを中心に、上下に振動することで表現されます。この、振動の中心線をバイアスと呼びます。バイアスから上下に振れた部分が、何らかの音声データなのですが、その振幅が音の大きさを表しています。振幅が大きいほど大きな音、と言う事になります。
これを踏まえて、先に進みましょう。
M5StickC Plusで録音した16ビットPCMデータを受信してグラフ化すると、以下の様になりました。
縦軸はPCM値[LSB]、横軸に時間[ms]を取りました。
多少の個体差が有ったとしても、読み出したPCMは、とても16ビットなんて言う解像度は無くて、精々12ビットと考えて良さそうです。
バイアスにはSPM1423の個体差があるかもしれないので、1,600LSBと決めつけてアプリケーションを作ってしまうと、特性の異なる個体では正常動作しない恐れもあります。使い方には要注意です。「バイアスは1,600LSB付近になる」と言う、理論的裏付けがあれば別ですが、ESP32のデータシート(I2Sユニット)を読んでも、デジタルフィルタ(LPF)の仕様に関しての記述は見当たりませんでした。
読み出したPCM値をWAVファイルに変換して、再生してみました。
M5StickC Plusには、マイクは付いているのだけど、音声の出力デバイスが無いので応用が難しいです。取り込んで音声データをどうやって使うの?って事です。次回、何かに応用して使って見たいと思います。なので、具体的なサンプルプログラム等は、次回です。