AVRのマイコンにはたいてい、8ビットタイマーと16ビットタイマーの2つが用意されています。
私の場合、8ビットタイマーはPWMに使って、16ビットタイマーは時間の測定に使うことが多いです。
その、16ビットタイマーで時間の測定をするメモです。
時間の測定ということは、あるタイミングでカウンタをカウントしていくわけですが、16ビットタイマーでは当然16ビット分しかカウントできません。
カウントするタイミングは、最大1024分周、つまり1024クロックごとに1カウントという設定ができますから、
最大で計れる時間は、例えばクロックが16MHZの場合、
65535 * 1024 / 16,000,000 = 4.19424(秒)
4秒程度しか計れません。これではとても役に立たない。
それではどうするかというと、タイマカウンタが65535に達して、次に桁あふれした際に、割り込みを起こすことができます。
その割り込みを起こす度に、4.19424秒を、それまでの経過時間として、どこかに足し込んでいけばいいわけです。
具体的にはこんな感じです。
以下、ATMega328Pの16ビットタイマである、タイマ1を使う前提での説明です。
//CPUのクロック周波数
#define F_CPU 16000000
int32_t msec;//ミリ秒で保存します。
ISR(TIMER1_OVF_vect) //タイマ1割り込みの定義
{
msec += (65536L * 1024L * 1000L) / F_CPU ;
}
これは、ミリ秒にするために1000を掛けていますが、これでは大きくなりすぎて、32ビット数値を桁あふれしてしまいます。
そこで、予めCPU周波数を1000で割った数字を用意し、これで割りましょう。
//CPUのクロック周波数を1000で割ったもの
#define F_CPU_DIV1000 16000
int32_t msec;//ミリ秒で保存します。
ISR(TIMER1_OVF_vect) //タイマ1割り込みの定義
{
msec += (65536L * 1024L) / F_CPU_DIV1000 ;
}
そして、あとはタイマ関係のレジスタに設定してやれば、その通り動きます。
クラスにしてみました。ATMega328P、Atmel Studio 6.0で確認済み。
Timer.h
#include <stdint.h>
class Timer {
public:
//タイマを設定し、起動する。
void StartTimer();
//タイマを起動してからの経過時間をミリ秒で返す。
int32_t GetTime();
//タイマを終了する。
void EndTimer();
//時間を0にリセットする。
void ResetTime();
};
Timer.cpp
#include "Timer.h"
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/common.h>
#define F_CPU 16000000L
#define F_CPU_DIV1000 16000L
volatile int32_t current_msec;
void Timer::StartTimer()
{
current_msec = 0;
//割り込み禁止
cli();
//タイマ/カウンタ1制御レジスタAの設定
//(タイマ用ピンとして使わない)
TCCR1A = 0x00; //タイマ関係のピンは標準ポート動作とする
//タイマ/カウンタ1制御レジスタBの設定
TCCR1B = (1<<CS12)|(1<<CS10);//1024分周でタイマON
//タイマ/カウンタ1割り込みマスク レジスタ設定
TIMSK1 = (1<<TOIE1);//タイマ1オーバーフロー割り込み許可
//割り込み許可
sei();
}
int32_t Timer::GetTime()
{
//16ビットレジスタの読み書きの際には、テンポラリレジスタを使用する。
//このため、割り込み禁止操作が必要。
//ステータスレジスタを一時保存する変数
uint8_t sreg;
//ステータスレジスタを保存
sreg = SREG;
//割り込み禁止
cli();
//現在のタイマ値を取得
uint16_t t = TCNT1;
//SREGを戻す。これによって割り込み禁止状態が戻る。(SREGのIビットを戻すから)
SREG = sreg;
int32_t tw = (int32_t)t;
return current_msec + (tw * 1024) / F_CPU_DIV1000;
}
void Timer::ResetTime()
{
uint8_t sreg;
sreg = SREG;
cli();
//タイマを0にする
TCNT1 = 0;
SREG = sreg;
//経過時間も0にする
current_msec = 0;
}
void Timer::EndTimer()
{
TCCR1B = 0;//タイマoff
}
ISR(TIMER1_OVF_vect) //タイマ割り込み
{
current_msec += (65536L * 1024L)/ F_CPU_DIV1000;
}
使い方は、どこかにTimerの実体を定義しておいて、
#include "Timer.h"
Timer timer;
//タイマ起動時に一回だけ実行
timer.StartTimer();
...
...
//時刻を知りたいとき
int32_t time_elapsed = timer.GetTime();
...
...
//時間をリセットしたいとき
timer.ResetTime();
...
...
//タイマーが不要になったとき
timer.EndTimer();
こんな感じですかね
あ、そうそう、SREGはavr/common.hに定義されていますので、avr/interrupt.hと合わせてインクルードしておきましょう。
それと、当然ですが、この方法でもミリ秒で符号付き32ビットがいっぱいになるまでしかカウントできませんので、
0x7FFFFFFF / 1000 / 3600 / 24 = 24日程度でいっぱいになります。まあ私の用途では十分です。