google-site-verification: google3bd66dd162ef54c7.html
fc2ブログ

ラズピコを使ってステレオのスペクトラムアナライザを作る

◆まえがき
Raspberry Pi Pico (以下ラズピコ)を使って0.96インチOLED(SSD1306インターフェイス)に表示する、オーディオスペクトラムアナライザーを作ってみました。

◆概要
FFTをやってみたくなってArduino UNOを使ってキャラクタ液晶に表示するタイプの物を作ったりしていたのですが、もう少し本格的な物を作りたくなりました。最初はUNOでやっていたのですが、メモリーの制限が厳しいので断念しラズピコで作ることにしました。

◆回路図
回路図
ACカップルで入力し電位が1/2Vccになるように抵抗でバイアスしてADCに入力しています。普通ならR1-R4には1桁高い値を使うのですが、RP2040のADCの入力抵抗が80kΩ min.と言う仕様になっているので低めの抵抗値を使いました。

0.96インチのOLEDはI2Cインターフェイスで接続しています。

◆外観
パイピコでスペアナ

◆プログラム
0.96インチのOLEDの表示のライブラリはu8g2.hを使用。FFTのライブラリにはArduinoFFT.hを使いました。
プログラムはちょっと長いですが全体が見える形で公開します。
/* オーディオ信号を128x64のOLEDにスペクトル表示 20220603_PiPicoFftAnalyzer.ino
  ボードはRP picoを使用。ライブラリは ArduinoFFT.h
  2022/06/03 ラジオペンチ http://radiopench.blog96.fc2.com/
*/
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "arduinoFFT.h"

#define R_IN 26  // R-入力ピン
#define L_IN 27  // L-入力ピン

#define PX1 62   // 左画面(L)原点
#define PX2 65   // 右画面(R)原点
#define PY1 16   // 波形画面の下端
#define PY2 55   // スペクトル画面の下端(-50db)

arduinoFFT FFT = arduinoFFT();          // Create FFT object
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

const uint16_t samples = 128;  // サンプル数
double vReal_R[samples];       // FFTの計算領域(実際にはたぶん32ビット浮動小数点?)
double vImag_R[samples];
double vReal_L[samples];
double vImag_L[samples];
int16_t wave_R[samples];      // 波形の生データー
int16_t wave_L[samples];

void setup() {
  pinMode(16, OUTPUT);         // 実行時間測定用
  pinMode(25, OUTPUT);         // pico内蔵LED
  //  Wire.setClock(400000);       // これは効果無し
  Serial.begin(115200);
  analogReadResolution(12);    // ADCのフルスケールを12ビットに設定
  u8g2.begin();
  u8g2.setFont(u8g2_font_6x10_tf);
  u8g2.setDrawColor(1);
  u8g2.setFontPosTop();       // 左上を文字位置とする
  u8g2.clearBuffer();
  u8g2.drawStr( 0, 0, "Start FFT v0.4");
  u8g2.sendBuffer();
  delay(1000);
}

void loop() {
  // 波形の読み取り(5ms)
  //  digitalWrite(16, HIGH);
  digitalWrite(25, HIGH);               // サンプリング中は内蔵LED点灯
  for (int i = 0; i < samples; i++) {
    wave_R[i] = analogRead(R_IN);       // 波形データー取得(fs:4096)
    wave_L[i] = analogRead(L_IN);
    delayMicroseconds(21);              // サンプリング周期調整(1サイクル39us)
  }                                     // (5ms)
  digitalWrite(25, LOW);
  //  digitalWrite(16, LOW);

  // FFT計算データ準備(1.7ms)
  for (int i = 0; i < samples; i++) {
    vReal_R[i] = (wave_R[i] - 2048) * 3.3 / 4096.0; // 電圧に換算
    vReal_L[i] = (wave_L[i] - 2048) * 3.3 / 4096.0;
    vImag_R[i] = 0;
    vImag_L[i] = 0;
  }

  // FFTの計算  (33ms)
  FFT.Windowing(vReal_R, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); // 窓関数(ハミング)適用
  FFT.Windowing(vReal_L, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD);

  FFT.Compute(vReal_R, vImag_R, samples, FFT_FORWARD);               // FFT
  FFT.Compute(vReal_L, vImag_L, samples, FFT_FORWARD);

  FFT.ComplexToMagnitude(vReal_R, vImag_R, samples);                 // 絶対値の算出
  FFT.ComplexToMagnitude(vReal_L, vImag_L, samples);

  u8g2.clearBuffer();           // 画面バッファクリア(22us)
  showWaveform();               // 波形表示(6ms)
  showSpectrum();               // スペクトラム表示(9.4ms)
  showOthers();                 // 目盛線他の表示(1.6ms)
  u8g2.sendBuffer();            // (30ms)
  delay(1);                     // 書き込み不良対策のおまじない
}   //(ループ実行時間:82ms)

void showWaveform() {          // 入力波形表示
  for (int i = 0; i < 52; i++) {
    u8g2.drawLine(PX2 + i, PY1 - (wave_R[i * 2]) / 256, PX2 + i + 1, 16 - (wave_R[i * 2 + 1] / 256)); // R-ch波形プロット
    u8g2.drawLine(PX1 - i, PY1 - (wave_L[i * 2]) / 256, PX1 - i - 1, 16 - (wave_L[i * 2 + 1] / 256)); // L-ch波形プロット
  }
}

void showSpectrum() {        // スペクトラム表示
  int d;
  static int peak_R[64];     // 過去ピーク値
  static int peak_L[64];

  for (int xi = 1; xi < 60; xi++) {     // スペクトラム表示(0は飛ばす)
    d = barLength(vReal_R[xi]);
    u8g2.drawVLine(xi + PX2, PY2 - d, d);          // 右側(R-ch)スペクトラム
    u8g2.drawVLine(xi + PX2, PY2 - peak_R[xi], 1); // 右側ピーク
    if (peak_R[xi] < d) {                          // 最新値がピーク値以上だったら
      peak_R[xi] = d;                              // ピーク値の更新
    }
    if (peak_R[xi] > 0) {
      peak_R[xi] --;                               // ピーク値をディケイ
    }

    d = barLength(vReal_L[xi]);                    // L側スペクトラム表示
    u8g2.drawVLine(PX1 - xi, PY2 - d, d);          // 左側(L-ch)スペクトラム
    u8g2.drawVLine(PX1 - xi, PY2 - peak_L[xi], 1); // 右側ピーク
    if (peak_L[xi] < d) {                          // 最新値がピーク値以上だったら
      peak_L[xi] = d;                              // ピーク値更新
    }
    if (peak_L[xi] > 0) {
      peak_L[xi] --;                               // ピーク値をディケイ
    }
  }
}

int barLength(double d) {            // スペクトルグラフの長さを計算
  float fy;
  int y;
  fy = 14.0 * (log10(d) + 1.5);      // 10倍(20dB)で14画素
  y = fy;
  y = constrain(y, 0, 56);
  return y;
}

void showOthers() {                           // グラフの修飾(目盛他の作画)
  // 領域区分線
  u8g2.drawVLine(PX1, 0, 64);                 // L画面原点線
  u8g2.drawVLine(PX2, 0, 64);                 // R画面原点線
  u8g2.drawHLine(0, PY2, 128);                // スペクトル下端線

  // 周波数目盛(横軸)
  for (int xp = PX1; xp > 0; xp -= 5) {       // L側 1kHz間隔目盛
    u8g2.drawVLine(xp, PY2 + 1, 1);
  }
  u8g2.drawVLine(PX1 - 25, PY2 + 1, 2);       // L 5k目盛
  u8g2.drawVLine(PX1 - 50, PY2 + 1, 2);       // L 10k目盛

  for (int xp = PX2; xp < 127; xp += 5) {     // R側 1kHz間隔目盛
    u8g2.drawVLine(xp, PY2 + 1, 1);
  }
  u8g2.drawVLine(PX2 + 25, PY2 + 1, 2);       // R 5k目盛
  u8g2.drawVLine(PX2 + 50, PY2 + 1, 2);       // R 10k目盛

  u8g2.setFont(u8g2_font_micro_tr);           // 小さなフォント(3x5)で、
  u8g2.setCursor(  7, 58); u8g2.print("10k"); // L側周波数表示
  u8g2.setCursor( 34, 58); u8g2.print("5k");
  u8g2.setCursor( 58, 58); u8g2.print("0");
  u8g2.setCursor( 67, 58); u8g2.print("0");   // R側周波数表示
  u8g2.setCursor( 87, 58); u8g2.print("5k");
  u8g2.setCursor(110, 58); u8g2.print("10k");

  // スペクトルレベル目盛(縦軸)
  for (int y = PY2 - 7; y > 16; y -= 14) {    // dB目盛線(横の点線)
    for (int x = 13; x < 128; x += 5) {
      u8g2.drawHLine(x, y, 2);
    }
  }
  u8g2.setCursor(0, 17); u8g2.print("0dB");   // スペクトル感度
  u8g2.setCursor(0, 30); u8g2.print("-20");   //
  u8g2.setCursor(0, 45); u8g2.print("-40");   //

  // LR表示
  //  u8g2.setFont(u8g2_font_6x10_tf);            // 少し大きなフォントで
  //  u8g2.setCursor(0, 4); u8g2.print("L");      // チャンネル表示
  //  u8g2.setCursor(121, 4); u8g2.print("R");    //
  u8g2.setFont(u8g2_font_crox1cb_tf);         // 少し豪華なフォントで
  u8g2.setCursor(0, 3); u8g2.print("L");      // チャンネル表示
  u8g2.setCursor(119, 3); u8g2.print("R");    //
}
FFTの条件としてはサンプル数128、サンプリング周期39μsのステレオ入力になっていて、この条件で上限周波数12.8kHz、バンド幅200Hz/64チャンネルのFFTが可能になります。

サンプリングから表示までの1サイクルを82msで回るようになっています。(個別の処理時間はコメント内に記載)

◆画面
画面

画面の左右にステレオのL/Rチャンネルの波形とスペクトラムを表示します。

この画面はファンクションジェネレーターから5kHzの正弦波を入力した状態です。正確な位置にスペクトルが表示されていることが判ります。

ちょっと気になるのはメインの5kHzのピークの周りにスプリアスのようなものがまとわりついていることです。ひょっとしたら、RP2040のADCのINLの悪さ(エラッタ E11)が原因なのかも判りません。

あと、こうやって単一周波数の信号を入力すると、エイリアシング現象で12.8kHzより上の信号が折り返されて表示されるのが良く判ります。用途によっては簡単なローパスフィルタを入れておいた方が良さそうです。

◆動画


◆まとめ
簡単な物ですが動かすと面白いです。こうやって実際にライブラリのレベルから動かしてみると、FFTに対する理解が深まります。

今回はステレオで作りましたが、1チャンネルならもっと高性能なスペアナを作ることも可能です。時間がある時にやって見たいと思います。

私はラズピコを使った経験があまり無く、Arduino で使う場合の言語リファレンスの所在も判らないので先人の方のコードを真似しながら書きました。そんなことで変なコードになっているところがあるかも知れませんが、お気付きの点などあればコメントなどで教えて頂くと幸いです。
関連記事

コメントの投稿

管理者にだけ表示を許可する

真似してみます

こりゃ遊べそうです。
OLED、買ってきますわ。

RP2040でFFTの件

こんにちは。

ご無沙汰しています。
小生もRP2040でADCを使うものをいくつか製作しましたので、お節介かとも思いましたがアドバイスさせていただきます。

1)念のため「直流成分の除去」をやって置いた方がよいと思います。
具体的には信号を入れないときのADCの読みを確認し、ACに変換するときに引くゲタを校正しておくことでOKです。
ゼロHz寄りのfrequency bin(日本語訳が思いつきません)の値にも少なからず影響が現れます(窓関数由来)。
私が試作したときはRP2040自体の特性なのか、分圧抵抗の誤差のせいなのか深くは調べなかったのですが、オフセットが意外に大きかったと記憶しています。

2)DMAを使うと500kSa/sの変換速度が得られます(確認済)。
Earle F. Philhowerさんのボードマネージャーをインストールするとその中に含まれるライブラリ(adc.h, dma.h)が利用できます。

re:真似してみます

注目して頂いて光栄です。
OLEDはAliExpressで買うと安いですが、届くのに時間がかかるのが難点です。

re:RP2040でFFTの件

ご無沙汰してます、アドバイスありがとうございます。

2枚目の5kHzのスペクトルの写真で、最初の成分(DCの次の成分)のレベルが-30dBくらいあるのは、やはりオフセットの影響なんですね。対策を考えたいと思います。

あと、サンプリングレート500kHzのやり方了解です。時間がある時にやって見たいと思います。ちなみに今回の記事では使っていませんが、Earle F. Philhowerバージョンも使える状態になっています。

re:RP2040でFFTの件

Arduino Mbed OS RP2040 Boards 3.1.1 でもDMA使えますよ。設定を正しくすれば12ビットデータでもDMA可能です。2チャンネルの場合はラウンドロビンの設定をすれば250kspsで行けるはずです。

re:RP2040でFFTの件

siliconvalley4066さん、初めまして。

驚きました。正に今日ネットを検索をしていて、「DMAを使い、500kSa/sのオシロを作成した」という貴殿の記事を見つけたところでした。MBED OSのボードマネージャーでも可能なんですね!

ちなみに私の方は、1ch500kSa/sADCの動作確認の後2週間位前にブレッドボードの回路をバラしてしまい、AD9214BRSZ(秋月)とRP2040のPIOを使った80MSa/sのオシロを作るというヌマに移っています。使い方をマスターできれば、RP2040のPIOはいろんなことにとても役にたちそうなんですが...。

re:RP2040でFFTの件

siliconvalleyさん

貴殿の記事の中にADCの入力インピーダンスに触れられている部分がありましたので改めてRP2040のデータシートを眺めてみたら、P641に「ADCの入力インピーダンスが最小100kΩ」とだけ記述がありました。
仮想GND(中点電位)にオフセットがあったのは入力インピーダンスがさほど大きくないことが原因だったのですね(データシートは最初に確認するべきでした!)。

re:RP2040でFFTの件

siliconvalley4066さん、情報ありがとうございます。
貼っていただいたURLも拝見しました、今後も参考にさせていただきます。

vabenecosiさん
RP2040のADCの入力抵抗が低めなので私の回路でもバイアス抵抗を51kΩにしています。実際の電圧を見るとそんなに低くは無いようですが、バラツキを考えると安心できないです。
カレンダー
03 | 2024/04 | 05
- 1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 - - - -
プロフィール

ラジオペンチ

Author:ラジオペンチ
電子工作を中心としたブログです。たまに近所(東京都稲城市)の話題など。60過ぎて視力や器用さの衰えを感じつつ日々挑戦!
コメントを入れる時にメールアドレスの記入は不要です。なお、非公開コメントは受け付けていません。
記事の内容のご利用は読者の自己責任でお願いします。

記事が気に入ったらクリックを!
最新記事
カテゴリ
最新コメント
リンク
FC2カウンター
検索フォーム
月別アーカイブ
RSSリンクの表示
QRコード
QRコード