概要
今回はArduinoでFFT解析を試してみました。具体的にはRP2040を使用し、内蔵ADCでアナログ方式のMEMSマイクが出力する信号を処理します。精度は微妙ですが、とても簡単に試せました。
開発環境
RP2040はArduino開発環境で扱います。予めボードは追加しておく必要があります。
- Windows 11 Home 23H2
- Arduino IDE 2.3.0
- Arduino-Pico 3.8.1
- Arduino FFT 2.0.2
部品
まず、要となるRP2040の内蔵ADCは分解能が12ビット、SAR型です。少し精度は不安ですが、詳細については後で補足します。マイクは回路をシンプルに組むため、アンプが内蔵された製品を選びました。外部にコンデンサ、抵抗器を接続するだけで増幅率は変更できます。今回は最大の増幅率20dBに設定しました。また、マイクの信号に含まれる直流分はコンデンサに通すことで除去します。
販売コード | 名称 | 型番 | 単価 | 数量 | 小計 |
---|---|---|---|---|---|
117044 | Seeed Studio XIAO RP2040 | 102010428 | 850円 | 1個 | 850円 |
108940 | MEMSマイク | AE-SPU0414 | 220円 | 1個 | 220円 |
105156 | ブレッドボード | BB-601 | 170円 | 1個 | 170円 |
117891 | 電解コンデンサ 2.2uF | 50PX2R2MEFC5X11 | 10円 | 1個 | 10円 |
104620 | 電解コンデンサ 1uF | UFG1H010MDM | 10円 | 1個 | 10円 |
部品代は合計1260円でした。秋月電子通商から全て入手できます。
ADCの速度
サンプリング周波数を48kHzに設定するため、ADCの速度は1回当たり20us以内に収める必要があります。そこで以下のプログラムを書き込み、実際に掛かる時間を測定してみました。分解能は12ビットを指定します。また、クロック周波数はデフォルトの125MHzに設定しました。
#define MICROPHONE_PIN A0
const long repeat = 100000;
void setup() {
delay(5000);
Serial.begin(115200);
Serial.println("Started measuring...");
analogReadResolution(12); //12 bits
unsigned long startTime = micros();
for (long i = 0; i < repeat; i++) {
int result = analogRead(MICROPHONE_PIN);
}
unsigned long stopTime = micros();
Serial.println("Stopped measuring");
Serial.println(stopTime - startTime);
}
void loop() {
//do something
}
結果は10万回当たり約470ms、平均すると1回当たり約5usでした。問題なさそうです。
プログラム
今回のFFT解析について、サンプル数$N$が1024点、サンプリング周波数$f_s$が48kHzなので周波数分解能$\Delta f$は次式より約47Hzと求まります。サンプル数は必ず2の冪数で指定します。ここから時間窓長$T$も必然的に決まります。また、サンプリング定理よりナイキスト周波数はサンプリング周波数の半分なので24kHzと求まります。つまり、理論上は最大24kHzまで測定できることになります。
\Delta f=\frac{f_s}{N}=\frac{1}{T}
リーケージ誤差を減らすため、窓関数についても考えます。無難にハン窓を使いたいところですが、ライブラリにバグ1があるみたいなので今回はハミング窓を使っています。振幅の値が重要なら正規化や窓関数の補正を行う必要もありますが、今回は省略しました。
#include <ArduinoFFT.h>
#define MICROPHONE_PIN A2
const double samplingFrequency = 48000.0f; //48kHz
const int samples = 1024; //this value must always be a power of 2
double real[samples] = {0};
double imaginary[samples] = {0};
ArduinoFFT<double> fft = ArduinoFFT<double>(real, imaginary, samples, samplingFrequency);
void setup() {
Serial.begin(115200);
delay(1000);
pinMode(17, OUTPUT); //red
pinMode(16, OUTPUT); //green
pinMode(25, OUTPUT); //blue
pinMode(MICROPHONE_PIN, INPUT);
//turn off the onboard LED
digitalWrite(17, HIGH); //red
digitalWrite(16, HIGH); //green
digitalWrite(25, HIGH); //blue
analogReadResolution(12); //12 bits
delay(1000);
}
void loop() {
static const int samplingPeriod = floor(1000000.0f / samplingFrequency); //in microseconds
for (int i = 0; i < samples; i++) {
unsigned long time = micros();
real[i] = analogRead(MICROPHONE_PIN);
imaginary[i] = 0;
while (micros() < (time + samplingPeriod)) {
//do nothing
}
}
fft.dcRemoval();
fft.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
fft.compute(FFT_FORWARD);
fft.complexToMagnitude(); //overwrite the first half of the integer data array
for (int i = 0; i < samples / 2; i++) {
if (i > 0) {
Serial.print(",");
}
Serial.print(real[i]);
}
Serial.println();
delay(1000);
}
実行すると随時シリアルモニタにFFT解析の結果が出力されるはずです。これをコピーしてCSVファイルへ保存し、表計算ソフトに取り込めばグラフ化できます。
検証
こちらのアプリを使用して1kHzの純音を再生し、FFT解析の結果を確認します。
概ね1kHz付近にピークが現れることを確認できました。
RP2040の内蔵ADCは精度が低い可能性
ここでRP2040の内蔵ADCについて、少し補足します。こちらのデータシート、565ページ目を見ると微分非直線性誤差、及び積分非直線性誤差の値を確認できます。どちらも1LSB以下が望ましいものの、そこまで高い精度は期待できなそうです。さらに特定の値で不連続な特性を示すことも読み取れます。なお、用語に関してはこちらのページが参考になりました。
総括
ライブラリのおかげで簡単にFFT解析できました。ただし、今回はRP2040の内蔵ADCを利用したので精度は微妙です。高性能な外付けADCに変更するなど、改善の余地はありそうです。あるいはPDM方式、つまり、デジタル方式のMEMSマイクへ置き換えてみることも検討してみたいです。