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()