脳汁portal

アメリカ在住(だった)新米エンジニアがその日学んだIT知識を書き綴るブログ

ソフトウェアエンジニアがarduinoの割り込みタイマーライブラリを読んでみた

arduinoのタイマーを使って割り込み処理をするにはMsTimer2とflexitimer2があってそれらを読み込めば簡単に出来るのですが、中身を知らないのもあれだなーと思い今回ソースを呼んでUno用に簡略化してみた
MsTime2: https://github.com/PaulStoffregen/MsTimer2
flexitimer: https://github.com/wimleers/flexitimer2
MsTimer2はミリセカンド単位でタイマー指定できて、flexitimerはマイクロセカンドごとに指定できるらしい
(flexitimerの方はちゃんと読んでないのでもしかしたら厳密には違うかも)

Uno用に簡略化したソース

5V/16MHz駆動でarduino UNO(atmega328P)を利用する場合の割り込みタイマー処理

volatile unsigned long count; // overflowした回数
volatile char overflowing;  // overflow処理中かどうかのフラグ
volatile unsigned int tcnt2; // タイマーのオーバーフローのタイミング調整用変数
unsigned long msecs; // 何秒ごとに任意の処理を実行したいか
float prescaler; // 前置分周(F_CPUが1MHzから16MHzの場合は64.0が多い)


// 初期化や割り込みの許可等の設定
void setTimer2(){
  TIMSK2 &= ~(1<<TOIE2);
  TCCR2A &= ~((1<<WGM21) | (1<<WGM20));
  TCCR2B &= ~(1<<WGM22);
  ASSR &= ~(1<<AS2);
  TIMSK2 &= ~(1<<OCIE2A);
  TCCR2B |= (1<<CS22);
  TCCR2B &= ~((1<<CS21) | (1<<CS20));
  prescaler = 64.0;
}

// timerのスタート
void startTimer(unsigned long ms){
  msecs = ms;
  tcnt2 = 256 - (int)((float)F_CPU * 0.001 / prescaler);  
  count = 0;
  overflowing = 0;
  TCNT2 = tcnt2;
  TIMSK2 |= (1<<TOIE2);
}

// timerのセットアップ
void setup() {
  setTimer2();
  startTimer(1000); // これで1000ミリセカンド単位のタイマーをスタート
}

// 割り込み検知
ISR(TIMER2_OVF_vect) {
  TCNT2 = tcnt2;
  overflow();
}

// 割り込み処理
void overflow(){
  count += 1;

  if (!overflowing && count >= msecs) {
    overflowing = 1;
    count = count - msecs;
    flash();
    overflowing = 0;
  }
}

// 任意のメソッド(1000ミリセカンド毎に実行したい処理)
void flash(){  
  static boolean output = HIGH;
  digitalWrite(13, output);
  output = !output;
}

set timer

TIMSK2 &= ~(1<
  • TIMSK2: タイマ/カウンタ2割り込み許可レジスタ(Timer/Counter 2 Interruput Mask Register)
  • TOIE2: タイマ/カウンタ2溢れ割り込み許可(Timer/Counter 2 Overflow Interrupt Enable)
  • OCIE2A: タイマ/カウンタ比較A割り込み許可(TImer/Counter2 output Compare Match A Interrupt Enable)

f:id:portaltan:20180125120321p:plain

TCCR2A &= ~*1
  • TCCR2A: タイマ/カウンタ制御レジスタA(Timer/Counter 2 Control Register A)
  • WGM21,20: 波形生成種別(Waveform Generation Mode)

f:id:portaltan:20180125114814p:plain

TCCR2B &= ~(1<*2;
  • TCCR2B: タイマ/カウンタ制御レジスタB(Timer/Counter 2 Control Register B)
  • WGM22: 波形生成種別 (Waveform Generation Mode bit 2)
  • CS22,21,20: クロック選択(Clock Select)

f:id:portaltan:20180125114933p:plain

ASSR &= ~(1<
  • ASSR: タイマ/カウンタ2非同期状態レジスタ(TImer/Counter 2 Asynchronous Status Register)
  • AS2: タイマ/カウンタ2非同期動作許可(Asynchronous Timer/Counter2)

f:id:portaltan:20180125115102p:plain

prescaler = 64.0;
  • 前置分周(次の項目で説明)

startTimer

tcnt2 = 256 - (int)((float)F_CPU * 0.001 / prescaler)

指定したミリセカンドぴったりで処理が実行されるようにするための調整値を求めている
F_CPUが16MHzだとして計算を一つ一つ説明

説明①

  • F_CPU(16,000,000Hz) * 0.001 = 16,000(Hz)
    • まずは秒単位ではなくミリセカンド単位で扱うために1/1000している
    • 16MHzなので1秒で16,000,000カウント、1ミリセカンドで16,000カウントされることとなる
    • これだと8bit(256)のカウント内に収まらないため、1ミリセカンド分もカウントできない(カウントできる時間間隔が短すぎる)
    • そこでCPU側で1Hzごとにカウントアップするのではなく、何Hzかごとにまとめて1カウントとするようにしている(この値をプリスケーラといい、チップや駆動電圧によって異なる)
  • 16,000(Hz) / prescaler(64.0) = 250(count)
    • atmega328Pで16MHzの場合prescalerの値は64.0
    • prescalerなしだと1msecで16,000Hz(16,000カウント)だったが、prescaler処理をいれると64Hzに1回しかカウントしなくなるので、1msecで250カウントとなる
    • これで8bit(256)カウント内でおさまるようになった
    • しかし、8bit(256) timerはカウント250回ではなく256回でオーバーフローするため、 毎回6カウントずつずれてしまう。これの調整を以下で行う
  • 256 - 250 = 6
    • ようは6からカウントを開始すれば250カウント経過で256になり、ちょうど1msecでオーバーフローする
    • そのためカウントの開始地点(初期カウント数)をここで求めている
    • 実際にISRで割り込んだ後も毎回カウントの初期値に6をセットしている


説明②(上記と同じ説明を順番を変えて考えただけ)

  • 16,000,000Hz / prescaler(64.0) = 250,000
    • 通常16MHz(16,000,000Hz)なので16,000,000カウントで1秒
    • arduinoにのっているtimerは8bit(256)か16bit(65536)なのでさすがにカウントが速すぎて使いにくい
    • そこでCPU側で毎Hzごとにカウントアップするのではなく、何Hzかごとに1カウントアップするようにしている(この値をプリスケーラといい、チップや駆動電圧によって異なる)
    • atmega328Pの場合は通常64.0
    • これによって実際の16,000,000Hz(16,000,000カウント)毎ではなく、250,000カウント毎に1秒という計算になる
  • 250,000 * 0.001 = 250
    • しかしまだ8bit(256)タイマーでも16bit(65536)タイマーでも値が足らずに1秒間もカウントできない(その前にオーバーフローしてしまう)
    • しかしそもそもマイコンの世界のタイマーで最小単位が秒単位というのは逆に長すぎるので、ミリセカンド単位で管理できるようになればよいので、1/1000している
    • 1秒では250,000カウントだったが、1msecでは250カウントなので、8bitカウンタ(256)でもカウントできるようになった
  • 256 - 250 = 6
    • しかし、8bit(256) timerはカウント250回ではなく256回でオーバーフローするため、 毎回6カウントずつずれてしまう。これの調整を以下で行う
    • ようは6からカウントを開始すれば250カウントで256になり、ちょうど1msecでオーバーフローする
    • そのためカウントの開始地点(初期カウント数)をここで求めている
    • 実際にISRで割り込んだ後も毎回カウントの初期値に6をセットしている
TCNT2 = tcnt2(6)
  • TCNT2: Timer/Counter 2 Counter Value Register

f:id:portaltan:20180125153553p:plain
上記のとおりカウントの値を書き換えて、初期値を0ではなく6にしている

TIMER2_OVF_vect

ISR(TIMER2_OVF_vect) {
  TCNT2 = tcnt2;
  overflow();
}
  • ISR: interrupt service routine
  • TIMER2_OVR_vect: タイマ2のオーバーフロー割り込み
  • 割り込みを検知する処理
  • 上記の設定の結果、1msec毎にtimer2がオーバーフローし、それを検知してこの処理が実行される
  • オーバーフローする毎にカウントが0に戻るので、初期値(6)を毎回代入している

void overflow

void overflow(){
  count += 1;

  if (!overflowing && count >= msecs) {
    overflowing = 1;
    count = count - msecs; // subtract ms to catch missed overflows. set to 0 if you don't want this.

    flash();
    overflowing = 0;
  }
}
  • 1msecごとに実行されてcountを1ずつincrementしていく
    • (このように平行した処理内で値が変わる可能性のある変数は宣言時にvolatile属性をつけておく必要がある)
  • 1msec毎にカウントして、指定のmsec回数を越えたら実際に実行したい処理(このコードではflash();)を実行する
  • 上記の処理中はoverflowingフラグをあげることで、処理が1ミリセカンド以上かかったとしても二重で処理が実行されるのを防ぐことができる
  • しかしこの間もcountのincrementは続いているので、実行する処理が数ミリセカンドかかったとしてもその分次回の処理がずれていくということはない
    • ただし指定したミリセカンド秒数を越える処理遅延が発生する場合は対策が必要(1000msecごとに行いたい処理があるのに、その処理自体が1000msec以上かかる場合など)

*1:1<

*2:1<