/* オーディオ信号を128x64のOLEDにスペクトル表示 20220706_PiPicoFftAnalyzerV04.ino ボードはRP pico(RP2040)を使用。ライブラリは ArduinoFFT.h (1.5.6)、Arduino(1.8.16)でテスト FFTの変換感度を反映(2Vpp=68.889) 窓関数のコメント欄の名前修正 ハニング→ハミング 2022/07/01 ラジオペンチ http://radiopench.blog96.fc2.com/ */ #include #include #include #include "arduinoFFT.h" #define BOARD_LED 25 // 基板内蔵LED #define CHECK_PIN 16 // 動作タイミングチェックピン #define SIG_IN 26 // 信号入力ピン #define OF_LED 15 // 過大入力表示LEDピン #define UP_SW 9 // レンジUpボタン #define DN_SW 10 // レンジDownボタン #define ADC_COMPE 14 // ADCエラッタパッチ選択(LOWなら補正無し) #define PX2 0 // 画面(R)原点 #define PY1 16 // 波形画面の下端 #define PY2 52 // スペクトル画面の下端(-50db) #define NNN 256 // FFTのサンプル数 arduinoFFT FFT = arduinoFFT(); // Create FFT object //U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);; // 0.96inch OLED SSD1306 使用時に選択 U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); // 1.3inch OLED SH1106 使用時に選択 double vReal[NNN]; // FFTの計算領域(実際にはたぶん32ビット浮動小数点?) double vImag[NNN]; // イマジナリ値 int16_t wave[NNN]; // 波形の生データー int16_t peak[NNN / 2]; // スペクトルピーク int16_t adcOffset = 40; // ADCのオフセット補正量 自動調整されるが初期値を指定すると収束が速くなる uint16_t range = 8; // レンジ番号(3:100Hz, 4:200Hz, 5:50Hz, 6:1k, 7:2k, 8:5k, 9:10k, 10:20k, 11:50k) uint16_t dR = 1; // ピークホールド減衰量 void setup() { pinMode(CHECK_PIN, OUTPUT); // 実行時間測定用 pinMode(BOARD_LED, OUTPUT); // pico内蔵LED pinMode(OF_LED, OUTPUT); // オーバーフロー表示LED pinMode(UP_SW, INPUT_PULLUP); // UPボタン pinMode(DN_SW, INPUT_PULLUP); // Downボタン pinMode(ADC_COMPE, INPUT_PULLUP); // ADC補正指定ピン(HIGHで補正有り) Serial.begin(115200); // Rp pico はシリアルの起動が失敗することがある analogReadResolution(12); // ADCのフルスケールを12ビットに設定 u8g2.begin(); // (I2Cバス400kbpsで開始される) u8g2.setFont(u8g2_font_6x10_tf); u8g2.setDrawColor(1); u8g2.setFontPosTop(); // 左上を文字位置とする u8g2.clearBuffer(); u8g2.drawStr(0, 0, "Multi Range FFT v0.4"); u8g2.sendBuffer(); clearPeak(); // ピーク値メモリーを初期化 delay(1000); } void loop() { int olFlag; // 過大入力フラグ rangeSet(); // ボタンが押されていたら周波数レンジを設定 // 波形の読み取り digitalWrite(BOARD_LED, HIGH); // ボード内蔵LED点灯 digitalWrite(CHECK_PIN, HIGH); // タイミング測定ピンHigh readWave(); // 波形を読み取り digitalWrite(CHECK_PIN, LOW); // タイミング測定ピンLow digitalWrite(BOARD_LED, LOW); // ボード内蔵LED消灯 // FFT計算データ準備( ms) olFlag = 0; for (int i = 0; i < NNN; i++) { if ((wave[i] > 4050) || (wave[i] < 50)) { // オーバーフロー警告のために値をチェック olFlag = 1; // チェックに引っ掛かったらフラグ立てる } if (digitalRead(ADC_COMPE) == HIGH) { // ADコンバーターの補正ジャンパー無し(GND接続無し)だったら wave[i] = adcCompe(wave[i]); // ADCの非直線性(RP2040-E11)を補正 } wave[i] += adcOffset; // オフセット補正を実施 vReal[i] = (wave[i] - 2048) * 3.3 / 4096.0; // 電圧の値に変換(ADCの補正でフルスケールが32増えているが、とりあえず無視) vImag[i] = 0; // 虚数部はゼロ } digitalWrite(OF_LED, olFlag); // フラグに応じオーバーフローLEDを点灯 // FFTの計算 FFT.Windowing(vReal, NNN, FFT_WIN_TYP_HAMMING, FFT_FORWARD); // 窓関数(ハミング)適用(9ms) FFT.Compute(vReal, vImag, NNN, FFT_FORWARD); // FFTの計算(26ms) offsetAdj(); // オフセット補正量を計算 FFT.ComplexToMagnitude(vReal, vImag, NNN); // 絶対値の算出 (4.3ms) u8g2.clearBuffer(); // 画面バッファクリア (22us) showWaveform(); // 波形表示 (0.72ms) showSpectrum(); // スペクトラム表示 (11~12ms) showBackGround(); // 目盛線等の背景を表示 (1.4ms) u8g2.sendBuffer(); // データーを転送して表示更新 (31ms) delay(1); // 書き込み不良対策のおまじない } // loop (83~1000ms) void rangeSet() { // レンジスイッチが押されていたらレンジ切り替え if (digitalRead(UP_SW) == LOW) { // UPボタンが押されていたら digitalWrite(OF_LED, HIGH); // 動作確認用にLED点灯 range++; clearPeak(); // レンジが変わったのでピークメモリーをクリア delay(30); if ( range > 11) range = 11; dR = decayRate(range); // レンジに応じたピーク値デケイ量を設定 } while (digitalRead(UP_SW) == LOW) { // ボタンが離されるまで待つ } digitalWrite(OF_LED, LOW); if (digitalRead(DN_SW) == LOW) { // DOWNボタンが押されていたら digitalWrite(OF_LED, HIGH); range--; clearPeak(); delay(30); if (range < 3) range = 3; dR = decayRate(range); // レンジに応じたピーク値デケイ量を設定 } while (digitalRead(DN_SW) == LOW) { // ボタンが離されるまで待つ } digitalWrite(OF_LED, LOW); } void clearPeak() { // ピーク値メモリをゼロクリア for (int i = 0; i < (NNN / 2 ); i++) { peak[i] = 0; } } uint16_t decayRate(uint16_t d) { // レンジに応じたディケイレートを設定 uint16_t x; if (d >= 8) x = 1; else if (d >= 6) x = 2; else if (d >= 4) x = 3; else x = 4; return x; } void readWave() { // 該当するレンジ速度で波形を読み込む switch (range) { case 3: // 100Hzレンジ(実行時間:1000ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 if (digitalRead(UP_SW) == LOW || digitalRead(DN_SW) == LOW) { // レンジボタンが押されていたら処理中断 break; } delayMicroseconds(3886); // サンプリング周期調整 } break; case 4: // 200Hzレンジ(実行時間:500ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 if (digitalRead(UP_SW) == LOW || digitalRead(DN_SW) == LOW) { // レンジボタンが押されていたら処理中断 break; } delayMicroseconds(1942); // サンプリング周期調整 } break; case 5: // 500Hzレンジ(実行時間:200ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 if (digitalRead(UP_SW) == LOW || digitalRead(DN_SW) == LOW) { // レンジボタンが押されていたら処理中断 break; } delayMicroseconds(772); // サンプリング周期調整 } break; case 6: // 1kレンジ(実行時間:100ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 delayMicroseconds(383); // サンプリング周期調整 } break; case 7: // 2kレンジ(実行時間:50ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 delayMicroseconds(186); // サンプリング周期微調整 } break; case 8: // 5kレンジ(実行時間:20ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 delay80ns(877); // 周期微調整(引数1で約80ns) } break; case 9: // 10kレンジ(実行時間:10ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 delay80ns(389); // 微調整 } break; case 10: // 20kレンジ(実行時間:5ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 delay80ns(144); // 周期微調整(1で約80ns) } break; case 11: // 50kレンジ(実行時間:2ms) for (int i = 0; i < NNN; i++) { wave[i] = analogRead(SIG_IN); // 波形データー取得 // delay80ns(1); // 周期整調無しでギリギリOK } break; default: break; } } void delay80ns(int t) { // 引数*80ns+400nsくらい待つ volatile uint32_t x; // 時間稼ぎ変数(最適化で削除されないようにvolatileで宣言) for (int i = 0; i < t; i++) { x++; // 32bitのインクリメントなので時間が余計にかかるはず } } void offsetAdj() { // ADCのオフセット補正 if (vReal[0] > 0) { // FFT結果の要素0(DC成分)が正なら adcOffset -= 1; // オフセットマイナス } else { adcOffset += 1; // プラス } Serial.println(adcOffset); // 現在のオフセット値(ここで表示される値をadcOffsetの初期値に設定すると収束が速くなる) } int adcCompe(int x) { // RP2040のADCの非直線性補正(RP2040-E11対策) int y; if (x >= 3584) y = x + 32; // フルスケールの7/8のポイントのオフセット補正 else if (x == 3583) y = x + 29; // 遷移のステップを少し埋める else if (x == 3582) y = x + 27; // 同上 else if (x >= 2560) y = x + 24; // fsの5/8 else if (x == 2559) y = x + 21; else if (x == 2558) y = x + 19; else if (x >= 1536) y = x + 16; // fsの3/8 else if (x == 1535) y = x + 13; else if (x == 1534) y = x + 11; else if (x >= 512) y = x + 8; // fsの1/8 else if (x == 511) y = x + 5; else if (x == 510) y = x + 3; else y = x; // 該当しなければそのままの値 return y; } void showWaveform() { // 入力波形を表示 int last_y, new_y; u8g2.setDrawColor(1); // 白で書く last_y = PY1 - (wave[0]) / 256; for (int i = 0; i < 254; i += 2) { new_y = PY1 - (wave[i + 2] / 256); u8g2.drawLine(PX2 + i / 2, last_y, PX2 + i / 2 + 1, new_y); // 波形プロット last_y = new_y; } } void showSpectrum() { // スペクトラム表示 int d; u8g2.setDrawColor(1); // 白で書く for (int xi = 0; xi < 128; xi++) { // スペクトラム表示 d = barLength(vReal[xi]); u8g2.drawVLine(xi + PX2, PY2 - d, d); // スペクトラムバー表示 u8g2.drawPixel(xi + PX2, PY2 - peak[xi]); // ピーク値プロット if (peak[xi] < d) { // 現在値がピーク値以上だったら peak[xi] = d; // ピーク値を更新 } peak[xi] -= dR; // ピーク値を指定量減衰させる if (peak[xi] < 0) { peak[xi] = 0; } } } int barLength(double d) { // グラフのスペクトルの長さを計算 float fy; int y; fy = 10.0 * (log10(d) + 1.46344); // 20dBで10画素 2VppをFFTすると68.889だったので、1mVのLog10の値で換算 y = fy; y = constrain(y, 0, 56); return y; } void showBackGround() { // グラフの修飾(目盛他の作画) // 領域区分線 u8g2.setDrawColor(1); // 白で書く u8g2.drawVLine( 0, 6, 6); // 時間軸左端 縦線 u8g2.drawVLine( 63, 6, 6); // 時間軸1/2 u8g2.drawVLine(127, 6, 6); // 時間軸右端 u8g2.drawVLine( 1, 8, 2); // 時間軸左端 中心線 u8g2.drawVLine( 63 - 1, 8, 2); // 時間軸1/2 u8g2.drawVLine( 63 + 1, 8, 2); // 時間軸1/2 u8g2.drawVLine(127 - 1, 8, 2); // 時間軸右端 u8g2.drawHLine(PX2, PY2, 128); // スペクトル下端線 // 周波数目盛(下の横軸) for (int xp = PX2; xp < 127; xp += 10) { // 等間隔目盛 u8g2.drawVLine(xp, PY2 + 1, 2); } u8g2.drawBox(PX2 , PY2 + 2, 2, 2); // 0k太い目盛(2画素) u8g2.drawBox(PX2 + 49, PY2 + 2, 3, 2); // 10k太い目盛(3画素) u8g2.drawBox(PX2 + 99, PY2 + 2, 3, 2); // 20k太い目盛 freqScale(); // レンジに対応した周波数目盛を表示 // スペクトルレベル目盛(縦軸) u8g2.setDrawColor(2); // 重なっても見えるようにXORで書く for (int y = PY2 - 10; y > 16; y -= 10) { // dB目盛線(横の点線) u8g2.drawHLine(0, y, 2); // ゼロの傍の目盛 for (int x = 9; x < 127; x += 10) { u8g2.drawHLine(x, y, 3); } } for (int y = PY2 - 10; y > 16; y -= 10) { // (-60dB対応)交点マーク(+)の縦の線 for (int x = 0; x < 110; x += 50) { u8g2.drawPixel(x, y - 1); // 縦線では交点が消えるので点で描く u8g2.drawPixel(x, y + 1); } } u8g2.setFont(u8g2_font_micro_tr); // 小さな3x5ドットフォントで、 u8g2.setFontMode(0); u8g2.setDrawColor(1); u8g2.drawStr(117, 16, "0dB"); // スペクトル感度 u8g2.drawStr(117, 26, "-20"); u8g2.drawStr(117, 36, "-40"); u8g2.drawStr(117, 45, "-60"); } void freqScale() { // 周波数目盛表示 u8g2.setFont(u8g2_font_mozart_nbp_tr); // 5x7ドットフォント u8g2.drawStr( 0, 56, "0"); // 原点の0を表示 switch (range) { // レンジ番号に応じた周波数を表示 case 3: u8g2.drawStr(114, 0, "1s"); // サンプリング期間 u8g2.drawStr(45, 56, "50"); // 1/2周波数 u8g2.drawStr(92, 56, "100"); // フルスケール周波数 break; case 4: u8g2.drawStr(102, 0, "0.5s"); u8g2.drawStr(42, 56, "100"); u8g2.drawStr(92, 56, "200"); break; case 5: u8g2.drawStr(96, 0, "200ms"); u8g2.drawStr(42, 56, "250"); u8g2.drawStr(92, 56, "500"); break; case 6: u8g2.drawStr(96, 0, "100ms"); u8g2.drawStr(42, 56, "500"); u8g2.drawStr(95, 56, "1k"); break; case 7: u8g2.drawStr(102, 0, "50ms"); u8g2.drawStr(45, 56, "1k"); u8g2.drawStr(95, 56, "2k"); break; case 8: u8g2.drawStr(102, 0, "20ms"); u8g2.drawStr(41, 56, "2.5k"); u8g2.drawStr(95, 56, "5k"); break; case 9: u8g2.drawStr(102, 0, "10ms"); u8g2.drawStr(45, 56, "5k"); u8g2.drawStr(92, 56, "10k"); break; case 10: u8g2.drawStr(108, 0, "5ms"); u8g2.drawStr(42, 56, "10k"); u8g2.drawStr(92, 56, "20k"); break; case 11: u8g2.drawStr(108, 0, "2ms"); u8g2.drawStr(42, 56, "25k"); u8g2.drawStr(92, 56, "50k"); break; default: break; } }