Seeeduino Xiaoで周波数カウンタをつくろう【Arduino】
こんなこと、やります。
- Arduino互換機のSeeeduino Xiaoを使って、簡易版周波数カウンタをつくる
- AD変換器をオペアンプで自作する
- 数百kHz程度の高周波まで測れるように高速化する
Arduinoで周波数カウンタ【簡易版】
以前からArduinoで周波数カウンタを作ってみたかったのですが、なんだか難しそうと思って遠ざけてました。その後、調べてみると、割り込み処理で簡単に作れることがわかりました。ここでは、割り込み処理による周波数カウンタの説明を行います。
つかうもの
ここでつかうものを説明します。
Arduino
Arduino互換機の Seeeduino XIAO を使用しました。
Seeeduino XIAOの使い方は Seeeduino XIAOでArduino開発をはじめよう をご覧ください。▼ もちろん、ほかのArduinoをお使いになってもらっても構いません。
もしまだArduinoをお持ちでないようでしたら、 おすすめArduinoどれを選べばいい?Arduinoで電子工作をはじめる方へ をご覧ください。
OLEDディスプレイ
周波数を表示させるために、OLEDディスプレイを使用しました。OLEDディスプレイの使い方は 【Arduino】OLEDに文字表示【Seeeduino】 をご覧ください。
LCDの液晶ディスプレイでやりたい場合は、 ArduinoでLCDに文字表示 をご覧ください。
アナログデジタル変換器
低電圧で動作するオペアンプNJM022を使いました。ただし、NJM022は20kHz程度の低速なオペアンプですので、後に変更します。詳しくは記事後半の改良版をご覧ください。
他に、100kΩ x 2、1MΩ x 1の固定抵抗と、0.1uFのフィルムコンデンサ、10uFの電解コンデンサを使用します。
その他の電子部品
ブレッドボードやジャンプワイヤをお持ちでない方は、そろえておくとよいでしょう。
アナログデジタル変換
周波数カウンタを作るにあたって、対象となるアナログ信号をArduinoのデジタルピンの基準電圧に揃えなければなりません。
測定する信号は?
どんなアナログ信号を入力するか考えなければなりませんが、ここではオーディオ信号のような±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が呼び出されます。
周期から周波数の計算
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程度の信号までしか測定できませんでした。そこで、高周波まで測定できるように高速化してみました。
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 Unoを使って、数Mまで測定できる周波数カウンタを作ってらっしゃる方がいます。そのうち機会があれば、そちらも試してみます。