Arduino 周波数/周期カウンタ (1)
VCO などの発振周波数の自動測定を目的として、AVR マイコンでオーディオ帯域信号の周期を測定するライブラリの作成をしています。
プロトタイピングのツールとして Arduino (Pakurino) を使い、簡単な周波数/周期カウンタとして機能するサンプル・スケッチ「fcounter」を作りました。
「周期」の測定は、16 ビットタイマである Timer1 のインプット・キャプチャ機能を利用しており、入力信号のエッジで割り込みをかけるため、数十 kHz 以上では処理が間に合わなくなります。
また、インプット・キャプチャは ICP1 端子からのディジタル入力だけでなく、アナログ・マルチプレクサを介した ADC0 〜 ADC5 のアナログ入力端子、およびアナログ・コンパレータ入力である AIN1 端子からも入力することができます。
長くなるので、詳しい説明は後に回し、簡単に説明します。
Arduino-IDE のバージョンは「0017」に対応しています。
サンプル・スケッチ「fcounter」の機能は、スケッチ中で「F_IN_PIN」に指定したピン (アナログ 0 番 〜 5 番、ディジタル 7 番、または 8 番) から入力したオーディオ帯域 (数十 kHz 以下) の信号の周波数と周期を、約 1 秒間隔で LCD に表示するのと同時にシリアルに出力するのを繰り返すだけです。
実行中に操作して、ふるまいを変えることはできません。
周波数は「Hz」単位、周期は「μs」単位で、小数点以下 2 桁の表示です。 小数点以下 2 桁というのは、LCD とシリアルのライブラリで使っている「print」クラスの制約によるものです。
LCD を使わない場合は、「fcounter.pde」の最初のほうにある
#define USE_LCD
をコメントアウト (行頭に 2 個スラッシュをつける) してください。
LCD を使う場合には、どのピンを LCD のどの信号に配線したのかを正しく設定してください。 また、LCD の R/W 端子はグラウンドに落としてください。
ちなみに、「CLCD-BOOSTER シールド」の場合には、
#define LCD_RS (4) #define LCD_EN (3) #define LCD_D7 (17)
に変えればいいはずです。(未確認)
サンプル・スケッチの実行方法としては、「スケッチブック」フォルダに「fcounter」フォルダを作成し、その中に、この後に続く3つのプログラム・リストから作成した、
- PeriodCounter.h
- PeriodCounter.cpp
- fcounter.pde
の3つのテキスト・ファイルを入れておきます。
あとは、Arduino-IDE を立ち上げ、メニューバーから File/Sketchbook/fcounter と選んで、「Upload」ボタンを押せば実行できるはずです。
信号源がなくても測定できるように、ディジタル 3 番ピンから約 8 秒ごとに約 31 kHz から約 30 Hz まで 7 段階に周波数が変わる信号を出してあります。
これは「analogWrite」による PWM 信号発生のプリスケール値を切り換えて実現しています。
"PeriodCounter.h"
/***********************************************/ /* Period measurement library for Arduino 0017 */ /* 2009/12/6 Created by pcm1723 */ /***********************************************/ #ifndef PeriodCounter_h #define PeriodCounter_h #include <inttypes.h> #include <avr/interrupt.h> namespace PeriodCounter { extern void start(int16_t ms); extern void stop( void ); extern volatile int16_t p_den; extern volatile uint32_t p_num; extern volatile uint8_t p_ready; extern volatile int8_t p_pin; extern volatile uint8_t p_mode; } // namespace PeriodCounter #endif // PeriodCounter_h
"PeriodCounter.cpp"
(12/20 追記: このプログラムでは、数千分の1の頻度でエラーを生じるので、12 月 19 日付けの記事のプログラムと差し替えてください)
/***********************************************/ /* Period measurement library for Arduino 0017 */ /* 2009/12/6 Created by pcm1723 */ /***********************************************/ #include "PeriodCounter.h" namespace PeriodCounter { // public volatile uint8_t p_ready; volatile int16_t p_den; volatile uint32_t p_num; volatile int8_t p_pin = 8; // digital 8 pin = ICP1 volatile uint8_t p_mode = 0x00; // private volatile int16_t max_ucnt; volatile int16_t icr1x; // software extended ICR1 volatile int16_t capt_cnt; volatile uint32_t capt, capt0; typedef volatile union tag_u32_u16_2_t { uint32_t u32; uint16_t u16[2]; } u32_u16_2_t; // // macro for 16 bit access #define U16_L(x) (((u32_u16_2_t *)&x)->u16[0]) #define U16_H(x) (((u32_u16_2_t *)&x)->u16[1]) #ifndef INT16_MAX #define INT16_MAX 0x7FFF #endif #define AIN1_PIN (7) void stop( void ) { noInterrupts(); TIMSK1 &= ~(_BV(ICIE1) | _BV(TOIE1)); // disable CAP/OVF int interrupts(); p_ready = 0; } // viod stop() void timer1_setup(void) { stop(); // disable Timer interrupts // Timer1 setup // FastPWM (WGM13:WGM12:WGM11:WGM10 = 1:1:1:1) // clk=CPUCLK (CS12:CS11:CS10 = 0:0:1) OCR1A = 0xFFFF; // MAX = 0xFFFF TCCR1A |= (_BV(WGM11) | _BV(WGM10)); // Fast PWM TCCR1B = (0xE0 & TCCR1B) | (_BV(WGM13) | _BV(WGM12) | _BV(CS10)); // clock = 16MHz // TCNT1 = 0x0000; // clear counter capt_cnt = 0; // clear capture count icr1x = 0; // clear ICR1 extension // AD MUX and comparator setup if ((0 <= p_pin) & (AIN1_PIN >= p_pin)) { // analog pin ADCSRA &= ~(_BV(ADSC) | _BV(ADIE)); // disable ADC interrupt if (AIN1_PIN == p_pin) { // AIN1 pin (digital 7 pin) ADCSRB &= ~(_BV(ACME)); // disable ACMP MUX DIDR1 |= _BV(AIN1D); // disable digital in for AIN1 } else { // ACMP MUX input ADCSRA &= ~(_BV(ADEN)); // disable ADC ADCSRB |= _BV(ACME); // enable ACMP MUX DIDR0 |= _BV(p_pin); // disable digital input ADMUX = (0xF0 & ADMUX) | p_pin; // set MUX } // if (AIN1_PIN == ... ACSR = (_BV(ACBG) | _BV(ACIC) | _BV(ACIS1)); // 1.1V BG ref, } // if noInterrupts(); TIFR1 |= _BV(ICF1); // clear input capture flag TIMSK1 |= (_BV(ICIE1) | _BV(TOIE1)); // enable CAP/OVF int interrupts(); } // void timer1_setup() void start(int16_t ms) { // convert millisec to OVF count max_ucnt = (ms / ((1000 * 0x10000UL) / F_CPU) ); timer1_setup(); } // void start() ISR(TIMER1_OVF_vect) { if (max_ucnt <= ++icr1x) { icr1x = 0; // reset upper count TIMSK1 = 0x00; // disable timer interrupts p_num = capt - capt0; p_den = (capt_cnt-1); p_ready = 1; } } // ISRITIMER1_OVF_vect) ISR(TIMER1_CAPT_vect) { U16_L(capt) = ICR1; U16_H(capt) = icr1x; if (0 == capt_cnt++) { // first time U16_L(capt0) = U16_L(capt); U16_H(capt0) = U16_H(capt); // remember capture value } else { if (INT16_MAX == capt_cnt) { // is max count? TIMSK1 &= ~(_BV(ICIE1)); // no more capture } // if (INT_MAX == } // if (0 == ... } // ISR(TIMER1_CAPT_vect) } // namespace PeriodCounter
"fcounter.pde"
/****************************************************/ /* Simple period/frequency counter for Arduino 0017 */ /* 2009/12/6 created by PCM1723 */ /****************************************************/ // Comment out following line to disable LCD #define USE_LCD #include <inttypes.h> #include <avr/pgmspace.h> #include <avr/sleep.h> #ifdef USE_LCD #include <LiquidCrystal.h> #endif #include "PeriodCounter.h" int16_t seq_no = 0; int16_t DA_buf; // Frequency input pin select // Analog comparator input: (Vth = 1.1 V) // 0..6 for analog 0 pin .. 6 pin (ADC0..ADC6) // 7 for digital 7 pin (AIN1/PD7) // // CMOS logic level input: (Vth = Vdd / 2) // 8 for digital 8 pin (ICP1/PB0) #define F_IN_PIN (4) // Serial Bit Per Second (Baud rate) select #define BPS (38400) // Gate time select in millisec #define GATE_ms (1000) // 16-bit PWM output pin (digital 10 pin, OC1B/PB2) #define PWM_PIN (10) #define LED_PIN (13) // Serial data line separator #define CSV_SEP ", " //#define CSV_SEP "\t" #ifdef USE_LCD #define LCD_ROWS (2) #define LCD_COLS (16) #define LCD_RS (6) #define LCD_EN (7) #define LCD_D7 (2) #define LCD_D6 (16) #define LCD_D5 (15) #define LCD_D4 (14) LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7); #endif void setup(void) { set_sleep_mode(SLEEP_MODE_IDLE); analogWrite(3, 128); // OC2B/PD3 pin pinMode(LED_PIN,OUTPUT); // change Timer2 prescaler TCCR2B = 1; Serial.begin(BPS); PeriodCounter::p_pin = F_IN_PIN; PeriodCounter::start(GATE_ms); DA_buf = 0x0001; #ifdef USE_LCD lcd.begin(LCD_COLS, LCD_ROWS); lcd.setCursor(0, 0); lcd.print("f = "); lcd.setCursor(0, 1); lcd.print("p = "); #endif } // void setup() void loop(void) { double f_Hz, p_us; using namespace PeriodCounter; if (p_ready) { // measurement finished? seq_no++; digitalWrite(LED_PIN, HIGH); // LED ON if (1 <= p_den) { // at least 1 cycle detected f_Hz = ((double)F_CPU * p_den / (p_num+1.0)); p_us = 1e6 / f_Hz; } else { // period undetermined f_Hz = 0.0; p_us = 0.0; } #ifdef USE_LCD lcd.setCursor(4, 0); lcd.print(f_Hz); lcd.print(" Hz "); lcd.setCursor(4, 1); lcd.print(p_us); lcd.print(" us "); #endif Serial.print(seq_no); Serial.print(CSV_SEP); Serial.print(p_num); Serial.print(CSV_SEP); Serial.print(p_den); Serial.print(CSV_SEP); Serial.print(f_Hz); Serial.print(CSV_SEP); Serial.print(p_us); Serial.println(); // change Timer2 prescaler TCCR2B = ((seq_no >> 3) % 7) + 1; noInterrupts(); analogWrite(PWM_PIN, DA_buf); interrupts(); DA_buf += 0x00FF; start(GATE_ms); digitalWrite(LED_PIN, LOW); // LED OFF } else { // if (p_ready) ... sleep_mode(); // sleep until timer interrupt } // if (p_ready) ... else ... } // void loop()