Arduinoでタイマー割り込み

Arduinoでタイマー割り込み

前回の記事(Arduinoを使ってみよう)で簡単な時間計測プログラムを作成しましたが実は正確ではありません。
なぜならば、デッドラインを意識したリアルタイム設計が成されていないからです。

そこで、今回は、Arduinoのタイマー割り込みを使ってより正確な時刻計測を行うプログラムを作成してみましょう。


 目次:          

 ・ デッドラインとは?

 ・ タイマーで1秒間隔の割り込みを作るには? 

 ・ 正確なストップウォッチを作ってみる!



デッドラインとは?

なぜ、前回のタイマー処理では正確な時間計測ができないのでしょうか?
処理を時間で見てみると下記のようになっています。



LCDへの表示を行い、その後1ミリ秒のディレイを行っています。
1回の処理時間は下記のようになります。

 処理時間 = LCD表示処理時間 + 1ミリ秒ディレイ

LCDの表示にかかる処理時間は短いものですが、使い続けると誤差が大きくなってしまいます。

組み込み機器は、電源を入れられるとそのまま動き続けることが多いため、このような誤差も問題になってきます。
或いは、実際の競技計測のように厳密な計測を求められるような場合も同様です。

そこで、下記のように1秒の間隔の間でLCDの表示処理を行うようにする方法が考えられます。


このような処理形態とした場合、必ず1秒以内に処理を完了しなければなりません
この1秒という時間が『デッドライン』になります。

上記の図では、2つ目の1秒の間隔で処理が終わることができませんでした。
このような場合、次の1秒の処理に影響が出てしまうため、このような状況は避けなければなりません。
 だから、『デッドライン』といいます。

また、このデッドラインを意識した設計は、組み込み機器のリアルタイム性能に大きく影響を及ぼします。

さて、ではどうやって1秒の間隔を作り出せばよいのか?
このブログの以前の記事「STM32F3 Discovery、タイマーと割り込みのキホン」ではタイマーで割り込み駆動してLEDをソフトPWMで制御しました。

Arduinoでも同様に、タイマーによる割り込みが可能です。



タイマーで1秒間隔の割り込みを作るには

タイマーによる割り込みを使うためには、AVRマイコンのタイマーについて理解する必要があります。

Arduino Micro のマイコンは、ATmega32U4です。
 → ATmega32U4のデータシート

詳しくはデータシートを見ていただくとして、ATmega32U4は以下の4つのタイマーを持っています。
 Timer/Counter0, Timer/Counter1, Timer/Counter3, Timer/Counter4

このうち、Timer/Counter0は、標準ライブラリで提供される delay()やmillis()などで利用されているため使うことはできません。
このため、この記事では、Timer/Counter1 を使っていきます。

Timer/Counter1と3は16ビットのタイマーです。
そして、ATmega32U4でもコンペアマッチとしてA, BそしてCの3つを利用することができます。

今回は、1つのタイマーが必要なのでオーバーフローの割り込みでもイイのですが、せっかく?なのでコンペアマッチを使ってみましょう。

コンペアマッチで1秒周期の割り込みを作る。

const byte LED_PIN = 13;
byte ledState = LOW;

void setup() {
  pinMode(LED_PIN, OUTPUT);      // On-Board LED

  TCCR1A = 0b00000000;  // Mode4(MGM10=0,MGM11=0) CTC
  TCCR1B = 0b00001100;  // Mode4(MGM12=1,MGM13=0):clkI/256 (From prescaler)
  TIMSK1 = 0b00000010;  // OCIE1A: Timer/Countern, Output Compare A Match Interrupt Enable
  OCR1A = 62500;
}

ISR(TIMER1_COMPA_vect) {
  ledState = !ledState;         // LEDをトグル動作
  digitalWrite(LED_PIN, ledState);
}

void loop() {
}


タイマーのコントールレジスタは2つあります。TCCR1AとTCCR1Bの2つです。
今回、コンペアマッチでタイマーを使うので、モード4(Timer on Compare match (CTC) mode)に設定するため、TCCR1AのMGM10ビットとMGM11ビットを0に、TCCR1BのMGM12ビットを1にMGM13ビットを0に設定します。

また、1秒の周期を作り出すため、タイマーカウンタが1秒以上カウントできるようにするため、プリスケーラでタイマーカウントをクロック/256として、TCCR1BのCS12ビットを1にCS11・CS10ビットをそれぞれ0とします。

そして、Aレジスタによるコンペアマッチ割り込みを有効とするため、TIMSK1のOCIE1Aビットを1にします。

システムクロックは16MHzなので、プリスケーラが1/1であれば
  1/16,000,000 = 0.0000000625秒 = 62.5ナノ秒
で+1されますが、ププスケーラを1/256としたので、タイマーカウントは、
  0.0000000625 × 256 = 0.000016 = 16マイクロ秒
で+1されるようになります。
Timer/Counter1は16ビットなので、
  0.000016 × 65536 = 1.048576秒
までカウントできます。1秒ピッタリにするカウント数は、
  1秒 ÷ 0.000016 = 62500
でちょうど1秒になります。この値を、OCR1Aに設定します。

LEDのON/OFFは割り込みハンドラ内で行っています。

ISR(TIMER1_COMPA_vect) {
  ledState = !ledState;         // LEDをトグル動作
  digitalWrite(LED_PIN, ledState);
}

割り込みハンドラは、ISR(割り込みベクタ名)で記述します。
 → 割り込みベクタ名定義はこの資料を参考に



正確なストップウォッチを作ってみる!

さてさて~
それでは、いよいよストップウォッチの制作をしてみたいと思います。

先程は、1秒間隔のタイマーとしましたが、ストップウォッチなので1/1000秒でカウントしたいと思います。

実際の動作の様子



上の黒いのは単3x2-USB給電(ビックカメラで98円)

前回の記事(Arduinoを使ってみよう)のLCD回路に、スイッチ割り込みを追加しています。
前回の記事(Arduinoでタイマー割り込み)では、5V系の電源でしたがLCDに合わせて3Vとしていますが、実力で動作しています。

ストップウオッチのプログラム

/*!
 * Copyright <2020> <LightningBrains> 
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include <Wire.h>
// Const
const byte LED_PIN = 13;
const byte INPUT_PIN = 7;
const char LIGHTNING_BRAINS[] = {"LightningBrains"};
const uint8_t AQM1602 = 0x3E;
// Global
byte ledState = LOW;
unsigned long msTime;

int hh = 0;
int mm = 0;
int ss = 0;
int ms = 0;

/*
 * sendCommand(uint8_t cmd)
 *   AQM1602 Command data send
 *
 *   cmd: command data
 */
void sendCommand(uint8_t cmd) {
  Wire.beginTransmission(AQM1602);
  Wire.write(0x00);
  Wire.write(cmd);
  Wire.endTransmission();
  return;
}

/*
 * sendData(uint8_t data)
 *   AQM1602 Display data send
 *
 *   data: display character code
 */
void sendData(uint8_t data) {
  Wire.beginTransmission(AQM1602);
  Wire.write(0x40);
  Wire.write(data);
  Wire.endTransmission();
  return;
}

/*
 * clearLCD()
 *   Clear the screen of AQM1602
 */
void clearLCD() {
  sendCommand(0x38);
  sendCommand(0x0C);
  sendCommand(0x01);
  delay(1);
  return;
}

/*
 * initLCD()
 *   Initialize AQM1602 LCD module
 */
void initLCD() {
  delay(40);          // 40ミリ秒待つ
  sendCommand(0x38);
  sendCommand(0x39);
  sendCommand(0x14);
  sendCommand(0x73);
  sendCommand(0x56);
  sendCommand(0x6C);
  delay(200);         // 200ミリ秒待つ
  clearLCD();         // Clear screen
  return;
}

/*
 * sendText(char* txt, int size)
 *   Text send to AQM1602 screen
 *
 *   txt: display text
 *   size: text length
 */
int sendText(char* txt, int size) {
  int sent = 0;
  // 全てのデータを1バイトづつ出力
  for(sent = 0; sent < size; sent ++) {
    sendData((uint8_t)*(txt +sent));
  }
  return sent;
}

/*
 * setCursor(int x, int y)
 *   Set display location in AQM1602 screen
 *
 *   x: X position, 0-15
 *   y: Y position, 0-1
 */
bool setCursor(int x, int y) {
  uint8_t cursor = 0x80;    // Set DDRAM Address command
  if(x > 15)                // 16桁
    return false;
  if(y > 1)                 // 2行
    return false;
  // 下位7ビットにDDRAMアドレスを設定
  cursor |= x;
  if(y) {
    cursor |= 0x40;
  }
  sendCommand(cursor);      // コマンド送信
  return true;
}

void setup() {
  // put your setup code here, to run once:
  pinMode(LED_PIN, OUTPUT);      // On-Board LED
  pinMode(INPUT_PIN, INPUT);

  Wire.begin();             // I2C有効
  initLCD();                // AQM1602初期化

  setCursor(1, 0);          // 表示位置 1, 0
  sendText(LIGHTNING_BRAINS, strlen(LIGHTNING_BRAINS));

  TCCR1A = 0b00000000;  // Mode4(MGM10=0,MGM11=0) CTC
  TCCR1B = 0b00001011;  // Mode4(MGM12=1,MGM13=0):clkI/64 (From prescaler)
  TIMSK1 = 0b00000000;
  OCR1A = 250;

  // 割り込み設定:入力ピン、割り込みハンドラ、割り込み条件はHIGH→LOW
  attachInterrupt(digitalPinToInterrupt(INPUT_PIN), switchON, FALLING);
  msTime = millis();
}

void loop() {
  // put your main code here, to run repeatedly:
  char  buf[18];            // 表示バッファ

  // 経過時刻表示
  sprintf(buf, "%02d:%02d:%02d.%03d", hh, mm, ss, ms);
  setCursor(2, 1);
  sendText(buf, strlen(buf));
}

// タイマー割り込みハンドラ
//   IMPUT_PIN HIGH→LOW
//
ISR(TIMER1_COMPA_vect) {
  // 経過時刻更新
  ++ms;
  if(ms > 999) {
    ledState = !ledState;         // LEDをトグル動作
    digitalWrite(LED_PIN, ledState);
    ms = 0;
    ++ss;
    if(ss > 59) {
      ss = 0;
      ++mm;
      if(mm > 59) {
        mm = 0;
        ++hh;
        if(hh > 23) {
          hh = 0;
          mm = 0;
          ++hh;
          if(hh > 23)
            hh = 0;
        }
      }
    }
  }
}

// スイッチ割り込みハンドラ
//   IMPUT_PIN HIGH→LOW
//
void switchON() {
  if((millis() - msTime) > 50) {
    if(TIMSK1 == 0b00000000) {
      TIMSK1 = 0b00000010;  // OCIE1A: Timer/Countern, Output Compare A Match Interrupt Enable
    }
    else {
      TIMSK1 = 0b00000000;  // OCIE1A: Timer/Countern, Output Compare A Match Interrupt Disable
    }
    TCNT1 = 0;
    msTime = millis();
  }
}


ミリ秒での計測が可能なように、タイマーを設定しています。

プリスケーラを1/64としています。このため、タイマーカウントは、
  0.0000000625 × 64 = 0.000004 = 4マイクロ秒
で+1されるようになります。

1ミリ秒ピッタリにするカウント数は、
  0.001秒 ÷ 0.000004 = 250
でちょうど1ミリ秒になります。この値を、OCR1Aに設定します。

また、setup()では割り込みイネーブルとはしていません。

void switchON() {
  if((millis() - msTime) > 50) {
    if(TIMSK1 == 0b00000000) {
      TIMSK1 = 0b00000010;  // OCIE1A: Timer/Countern, Output Compare A Match Interrupt Enable
    }
    else {
      TIMSK1 = 0b00000000;  // OCIE1A: Timer/Countern, Output Compare A Match Interrupt Enable
    }
    TCNT1 = 0;
    msTime = millis();
  }
}

リアルタイム性を上げるため、スイッチ動作を割り込みで受けて、割り込み動作のイネーブル/ディセーブルを切り替えることで、ストップウオッチのスタート/ストップを行っています。
スイッチの割り込みハンドラで直接レジスタ操作を行っているのは、リアルタイム性を上げるためです。

また、タイマーの割り込みハンドラでは、計測時刻のカウントアップを行っていますが、LCDへの表示は行っていません。
これは、計測時刻から文字列を作って、I2C通信を行うような重たい処理を割り込みハンドラに入れたくなかったからです。

LCDへの表示は、loop()で行っています。割り込みハンドラでの計測時刻のカウントアップから実際の表示までタイムラグがあることになりますが、割り込みハンドラでのカウントを正確にし、表示はその後、人間が確認するのでタイムラグが問題になることは無いでしょう。

それでも、このLCDモジュールでは表示が追いつかないくらいになっています。

なお、このストップウオッチは、ボタンでスタート/ストップしますが、もう一度押すとリスタートになります。
リセットするには、電源OFFするかボードリセットです。

スタートのときに計測時刻の変数をクリアすれば、毎回0からの開始にもできます。
あるいは、もう一つボタンを増やしてもいいでしょう。
そんなコトが手軽にできるのはArduino のイイところですから。

正確さは如何程?
実際にはボード上のクロックに左右されます。
クォーツ式の時計などでは”枯れた”水晶発振器を搭載していますが、パソコンやスマホでさえあまり精度のいい発振器は使っていません。(ネットワーク補正できるということもありますが)
なので、コレ以上の精度を求めるなら、外部にRTCを接続したほうがイイでしょうネ。





Have a Happy Hucking!!
Lightning Brains

コメント

このブログの人気の投稿

Linuxシステムコール、共有メモリの使い方

Linuxシステムコール、メッセージキューの使い方

Linuxシステムコール、セマフォの使い方