ソフトウェアによる疑似タイマ割り込み


PICやAVRのようなワンチップマイコンだと、タイマ割り込みの数は限られていることが多く、かつその長さも1秒未満の短いカウントしか出来ないことが多い。


そこで、ラーメンタイマのようなものが複数欲しい時は、ソフトウェアで擬似的にタイマ割り込みを実現しなくてはならない。


私が書いたコードは、TIMERが0を取らないことを保証しておいて(割り込み処理中で if (++timer == 0) timer = 1; とか。)、idle中の疑似ソフトウェアタイマ割り込みは次のようなコードでやっている。



// このメソッドはidle中に定期的に呼び出されると仮定して良い。
void interruptCheck(void)
// 指定された時刻をすぎていれば void timerInterrupt(byte timerID)
// を呼び出す。
{
byte i;
int t,diff;
t = (int)getTimer();

for(i=0;i<ARRAY_SIZE_OF(timer_counter);++i)
{
if (timer_counter[i])
{
diff = (int)(t - timer_counter[i]);
if (diff > 0)
{
timer_counter[i] = 0;
timerInterrupt(i);
}
}
}
}


TIMERが0を取らないので、timer_counterの0を疑似ソフトウェアタイマがセットされていないという特殊な意味に使うことが出来る。(C#のnullable型のように扱うことが出来る)


(int)(t-timer_counter[i])の部分がよくあるテクニックで、タイマ(本来uint型)を次図のようなリング(円環)だとみなして差をとっている。diffがint.Max/2の範囲においては正常に動作する。いくら何でもint.Max/2 [ms] = 32.7秒ごとに1回はidleになるだろうという希望的観測のもとに書かれている。


※注1 引き算したときに、開き角は1周を0xffffとする単位で求まる。
※注2 実際には、0を(円環から)除外する必要があるので、0をまたぐ場合は、(本当に開き角を求めたいなら)引き算するときに1を引く必要がある。ただし、いまはタイマが指定した時刻を過ぎているかどうかを判定したいだけなので、この開き角の正負を調べるだけで良いのでその処理は不要。


以下、私がPIC16Fシリーズ用にPICC Liteで書いたソース。無保証だけど使いたければ自由にどうぞ。
(便宜上、1msのone shot timerと書いていたが、そこまで精度が必要なかったのでソースコード上は2msのone shot timerになっている。)



#ifdef __TIMER_ENABLE__
volatile uint timer_val; // timerの再設定値(1ms用)
volatile uint timer; // タイマをリングにして使う。timer=0は値としてとらないようにコーディングする。

// いつまで待つのかのタイマ値
bank1 uint timer_counter[4];
// 0だと設定されていないの意味。timerは0をとらない。

// 3.58MHz時に550msまでしか計測できない。
// 20MHz時は104.856msまでしか計測できない。
// もっと長いタイマーが欲しいならば、割り込みがかかった時に再度割り込みをかけて延長すべき。
void timer_init(ulong F_CPU)
{
// タイマーを設定する。この時間が経過するとTIMER_ON_FLGA==1になる。

// 1 timer_clock = 1/周波数 ×4 ×8
// (プリスケーラ1:8時)
// タイマーレジスタ設定値 = 目標時間 / 1 timer_clock
// = ms / 1000 * F_CPU / 32
// = ms * F_CPU / 32000
// = (ms * (F_CPU / 1000) ) >> 5

// プリスケーラ1:1のとき
// = ms / 1000 * F_CPU / 4
// = ms * (F_CPU/1000) >> 2
// ms==1なら
// = F_CPU/4000

byte i;

// timer_val = F_CPU / 4000;
// あまり割り込み頻度が高いとCPU負荷があるので2ms単位にしよう。
timer_val = F_CPU / 2000;

for(i=0;i<4;++i)
timer_counter[i] = 0;

timer = 1; // timerは0をとらない。

T1OSCEN = 0; // 専用発振回路→動作させない
T1SYNC = 0; // 外部入力を内部クロックに同期させるのか→Foscに同期させる :負論理
TMR1CS = 0; // clock = Fosc(内部クロック) /4

// タイマーのプリスケーラ1:1倍
T1CKPS0 = 0;
T1CKPS1 = 0;

timer_val = (uint)(-(int)timer_val); // count up型タイマなので逆算する必要あり。

TMR1L = timer_val & 0xff;
TMR1H = (timer_val >> 8);

TMR1IE = 1; // オーバーフロー割り込み許可
TMR1ON = 1; // カウント開始

// TMR1L,Hがoverflowしたときに割り込みがかかるだろう
}

uint getTimer(void)
{
uint t1,t2;
// di();
t1 = timer; // timerに対してatomicであることを保証しなければならない。
// ei();
// 割り込みを禁止してしまうとUSARTの文字を受信のときに取りこぼす。
// タイマだけ停止させられるが、そうするとタイマがずれる。

t2 = timer;
if (t1!=t2) // 2度読み出した値が同じならそれを採用し、異なるなら再度読み直した値を採用する。
t1 = timer;

return t1;
}

// ソフトウェアタイマ割り込み
void SetTimer(byte timerID,uint ms)
{
uint t;

timerID &= 0x3; // 異常値が渡されてはいけないのでマスクをとっておく。

t = getTimer() + (ms >> 1);

// これでoverflowしたなら+1する。(0を除外するため)
if (t < ms)
++t;

timer_counter[timerID] = t; // この時刻になれば割り込みがかかる。
}

// タイマーがセットされているのか。TIMERを再セットしてしまわないために。
bool isTimerSet(int timerID)
{
return timer_counter[timerID]!=0;
}

// タイマーを強制的に停止させる。
void StopTimer(byte timerID)
{
timer_counter[timerID] = 0;
}

// このメソッドは定期的に呼び出されると仮定して良い。
void interruptCheck(void)
// 指定された時刻をすぎていれば void timerInterrupt(byte timerID)
// を呼び出す。
{
byte i;
int t,diff;

t = (int)getTimer();

for(i=0;i<ARRAY_SIZE_OF(timer_counter);++i)
{
if (timer_counter[i])
{
diff = (int)(t - timer_counter[i]);

if (diff > 0)
{
timer_counter[i] = 0;
timerInterrupt(i);
}
}
}
}
#endif

// 送受信のための割り込みハンドラ
void interrupt serial_isr(void)
{
// ここにUSARTの送受信処理コードがあるが、省略。

#ifdef __TIMER_ENABLE__
if (TMR1IF)
{
// 割り込み中にもう一度割り込みがかかることは想定しなくて良いが、
// この間にタイマーが経過することは考慮が必要だと思う。

TMR1ON = 0; // タイマー停止

TMR1L = timer_val & 0xff;
TMR1H = (timer_val >> 8);

TMR1IF = 0; // オーバーランフラグのクリア
TMR1IE = 1; // オーバーフロー割り込みの許可

TMR1ON = 1; // タイマー再開

// タイマーを加算しておく。
if (++timer == 0)
timer = 1;
// タイマーは0をとらない。

// WDTをクリアしておく。(1msごとなのでちょうど良い)
CLRWDT();
}
#endif
}