Arduino(Seeeduino Xiao)で周波数カウンタをつくろう

 

こんなこと、やります。

  • Arduino互換機のSeeeduino Xiaoを使って、簡易版周波数カウンタをつくる
  • AD変換器をオペアンプで自作する
  • 数百kHz程度の高周波まで測れるように高速化する

Arduinoで周波数カウンタをつくろう
Arduinoで周波数カウンタをつくろう

Arduinoで周波数カウンタ【簡易版】

以前からArduinoで周波数カウンタを作ってみたかったのですが、なんだか難しそうと思って遠ざけてました。その後、調べてみると、割り込み処理でカンタンに作れることがわかりました。ここでは、割り込み処理による周波数カウンタの説明を行います。

つかうもの

ここでつかうものを説明します。

Arduino

Arduino互換機のSeeeduino XIAOを使用しました。

Seeeduino XIAOの使い方はSeeeduino XIAOでArduino開発をはじめようをご覧ください。

他のおすきなArduinoでも構いません。もしArduinoをお持ちでないようでしたら、オススメArduinoどれを選べばいい?Arduinoで電子工作をはじめる方へをご参考になさってみてください。

OLEDディスプレイ

周波数を表示させるために、OLEDディスプレイを使用しました。OLEDディスプレイの使い方はArduinoでOLEDをご覧ください。

アナログデジタル変換器

低電圧で動作するオペアンプNJM022を使いました。ただし、NJM022は20kHz程度の低速なオペアンプですので、後に変更します。詳しくは記事後半の改良版をご覧ください。

他に、100kΩ x 2、1MΩ x 1の固定抵抗と、0.1uFのフィルムコンデンサ、10uFの電解コンデンサを使用します。

その他の電子部品

ブレッドボードやジャンプワイヤをお持ちでない方は、そろえておくとよいでしょう。

アナログデジタル変換

周波数カウンタを作るにあたって、対象となるアナログ信号をArduinoのデジタルピンの基準電圧に揃えなければなりません。

コンパレータでAD変換
コンパレータでAD変換

測定する信号は?

どんなアナログ信号を入力するか考えなければなりませんが、ここではオーディオ信号のような±1Vppの小信号を想定します。

Arduinoではマイナス電圧は入力できません。また、1V程度のピークではデジタルピンのしきい値を越えることができません。

ちなみに、Arduinoのしきい値の詳しくはArduinoでコンデンサの静電容量測定をご覧ください。

コンパレータでAD変換

そこで、オペアンプで作るコンパレータで作ったコンパレータを使って、入力信号を0Vまたは3.3Vのデジタル信号へと変換させます。

コンパレータの回路図
コンパレータの回路図

ソースコード

こちらが周波数カウンタのソースコードになります。

#include <U8g2lib.h>
#define FREQ_PIN A2
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

float frequency = 0;
unsigned long lastTime = 0;
float freqz = 0;
float k = 0.5;

void setup() {
//  Serial.begin(9600);
  attachInterrupt(FREQ_PIN, frequencyCounter, RISING);
  u8g2.begin();
}

void loop() {
  displayOLCD(frequency);
  delay(100);  
}


void frequencyCounter() // interrupt handler
{
  unsigned long t = micros();
  unsigned long d = t - lastTime;
//  Serial.println(d);
  lastTime = t;
  float f = 1e6 / d;
  frequency = k * freqz + (1-k)*f;
  freqz = frequency;
}


void displayOLCD(float f) {
    char buf[15];
    snprintf(buf, 15, "%.1f [Hz]", f);

    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_crox3hb_tf);
    u8g2.drawStr(0, 16, buf);
    u8g2.sendBuffer();
}

ソースコードの解説

ソースコードの解説を行います。

割り込み処理

ソースコード内のattachInterrupt関数は、割り込み処理を行うための関数です。

attachInterrupt(FREQ_PIN, frequencyCounter, RISING);

割り込み関数にRISINGを指定すると、ピンの状態がLOWからHIGHに変わったとき、指定した関数frequencyCounterが呼び出されます。

割り込み処理から周期の算出
割り込み処理から周期の算出

Arduino UNOの場合、FREQ_PINに0を指定した場合、デジタルピン2番が使われますので注意してください。割り込み処理ができるピンは端末によって異なります。詳しくは Arduinoの公式リファレンス で確認してください。

周期から周波数の計算

frequencyCounterでは、現在の時間から前回トリガされた時間を引いて周期を計算します。信号の周期Tがわかれば、周波数fを導き出すことができます。

$$f=\frac{1}{T}$$

これで周波数カウンタは一応でき上がりですが、実際に周波数を測定してみるとセンサノイズにより周波数が変動しやすいです。そこで、ローパスフィルタをかけることにします。

ローパスフィルタ

よく使われるRCローパスフィルタです。RCローパスフィルタの数学的な証明は、デジタル信号におけるRCローパスフィルタをご覧ください。

frequency = k * freqz + (1-k)*f;

次の式のとおり、1つ前にサンプルした周波数と、現在の周波数に重みをかけて足し合わせているだけです。

$$y[n]=ky[n-1]+(1-k)x[n]$$

kの値を大きくすると周波数の変動が安定しますが、安定するまでに時間がかかります。いろいろテストして、\(k=0.5\)に決めました。

Arduinoで周波数カウンタ【高速版】

さきほどの周波数カウンタでは、20kHz程度の信号までしか測定できませんでした。そこで、高周波まで測定できるように高速化してみました。

Arduinoで周波数カウンタ【高速版】
Arduinoで周波数カウンタ【高速版】

AD変換器の改良

オペアンプをTL072の高速なものに変えました。NJM022のスルーレートが0.5V/usだったのに対し、TL072では13V/usとなります。

アナログデジタル変換器の回路図
アナログデジタル変換器の回路図

この回路で、数百キロHz程度のの高周波を入力できます。詳しくはコンパレータの高速化・オペアンプで作るアナログデジタル変換器をご覧ください。

ソースコード

こちらが、改良版周波数カウンタのソースコードになります。

#include <U8g2lib.h>
#define FREQ_PIN A2
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

int sampleCount = 9;

void setup() {
  Serial.begin(9600);
  u8g2.begin();
  pinMode(FREQ_PIN, INPUT);
}


void loop() {
  float f = countFreq();
  displayOLCD(f);
  delay(1000);
}


float countFreq() {
  float s[sampleCount];
  for(int i=0; i < sampleCount; i++) {
    int pulseHigh = pulseIn(FREQ_PIN, HIGH);
    int pulseLow = pulseIn(FREQ_PIN, LOW);
    s[i] = pulseHigh + pulseLow; // Time period of the pulse in microseconds
  }
  return 1e6/medianFilter(s);
}


float medianFilter(float s[]) {
  int total = sizeof(s) / sizeof(float);

  for (int i=0; i<total; ++i) {
    for (int j=i+1; j<total; ++j) {
      if (s[i] > s[j]) {
        float tmp =  s[i];
        s[i] = s[j];
        s[j] = tmp;
      }
    }
  }
  int m = total / 2;
  return s[m];
}


void displayOLCD(float f) {
    char buf[15];
    if(f>10000) {
     snprintf(buf, 15, "%.1f [kHz]", f/1000);
    } else if(f>1000) {
     snprintf(buf, 15, "%.2f [kHz]", f/1000);
    } else {
      snprintf(buf, 15, "%.1f [Hz]", f);
    }
    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_crox3hb_tf);
    u8g2.drawStr(0, 16, buf);
    u8g2.sendBuffer();
}

ソースコードの解説

前回の周波数カウンタでは、割り込み処理によるものでした。割り込み処理の呼び出し速度には限界があるようです。

そこで、今回のプログラムでは、pulseIn関数を使ってみました。

pulseIn

pulseIn(ピン番号, HIGH)は、信号がHIGHになっている間の時間を、マイクロ秒で返されます。 また、pulseIn(ピン番号, LOW)とすれば、信号がLOWになっている時間を測定できます。よって、HIGHとLOWのそれぞれの時間を足し合わせると、1周期の時間がわかります。

pulseIn関数の詳しくは、 公式リファレンス をご覧ください。

メディアンフィルタ

プログラム中のmedianFilterはメディアンフィルタです。ローパスフィルタと同様、センサノイズなどの除去に有効なフィルタです。

メディアンフィルタは、サンプルを小さい順に並べ、真ん中の値を採用します。詳しくはArduinoで非接触温度センサGY-906で解説しています。

プログラムでは、周期のサンプルを9回取って、それを昇順に並べ変え、その中央値を信号の周期として採用しています。

実験結果

作った周波数カウンタに、約130kHzほどの正弦波を入力してみました。

Arduinoで周波数カウンタ【Seeeduino Xiao】
Arduinoで周波数カウンタ【Seeeduino Xiao】

周波数が高くになるにつれ、どうしても誤差が出てしまいます。それでも最初に紹介した周波数カウンタから比べると、大分マシになりました。

ちなみに、ここで使った周波数発振器はコルピッツ発振器の製作で作ったものです。

このほかにも、Arduino Unoを使って、数Mまで測定できる周波数カウンタを作ってらっしゃる方がいます。そのうち機会があれば、そちらも試してみようと思います。

記事に関するご質問などがあればTwitterへお返事ください。
この記事で紹介した商品
人気のArduino互換機
Arduinoで人気の周辺パーツ
Arduinoのオススメ参考書

▼ Arduino初心者向きの内容となっています。ほかのArduino書籍と比べて図や説明がとてもていねいで、読みやすかったです。Arduinoで一通りのセンサーが扱えるようになります。

▼ 外国人が書いた本を翻訳したものです。この手の書籍は、目からうろこな発見をすることが多いです。

▼ Arduinoの入門書を既に読んでいる方で、次のステップを目指したい人向きの本です。C言語のプログラミングの内容が中心です。ESP32だけでなく、ふつうのArduinoにも役立つ内容でした。

関連記事