CPUタイマー割り込みで並列処理をする | arduinoロボットプログラミング

2023年12月3日

arduinoLeonardoを使ったクムクムロボット入門モデル(R3J)にATmega32U4のCPUレジスターを操作したタイマー割込みを発生させて、クムクムロボットのLEDの点滅と顔のサーボモータを並列に動くようにプログラムを作ってみます。

タイマー割込みを使う意味

Arduinoでの一般的なプログラミングでは、loop() 関数で繰り返し実行される中でdelay() 関数などを使って待ち時間を作る方法を行います。しかしそのやり方では、delay()の間Arduinoは他の処理が何もできません。
例えば、LEDを点滅させながらサーボモータを動かす場合、delay()を使うと、LEDが点滅している間にサーボモータの制御を行うことができなくなります。
それを解決する方法として、タイマー割り込みを使用します。タイマー割り込みを使用すると、Arduinoは背後で時間を計っており、設定した時間が経過すると自動的に指定した関数(割り込みサービスルーチン)を実行することになります。
これにより、一つ一つの処理に影響されない個別の処理が実現できるようになります。ここではこの方法をとり、LEDの点滅とサーボモータの動作を正確にタイミング良く制御してみます。

作るプログラム

赤いLEDを0.5秒おきに点滅させながら、クムクムの顔のサーボモータを0-90度に回転させます。

Leonardo(ATmega32U4)の注意点

Arduinoボードにはさまざまなモデルがあり、それぞれに異なるチップが使われています。例えば、Arduino UnoはATmega328Pを、Arduino LeonardoはATmega32u4を使用しています。これらのチップは、使用できるタイマーの種類や数が異なります。Arduino Unoでは、 Timer1 がよく使われますが、Arduino Leonardoでは どうもTimer1が他のライブラリーでバッティングしているようででコンパイル時に、「(.text+0x0): multiple definition of `__vector_17’」などのエラーが発生します。これは、割り込みベクタ(この場合はTIMER1_COMPA_vect)が複数回定義されていることを意味しており、この問題は、他のライブラリ(例えばServo.h)が同じ割り込みベクタを使用している場合に発生している可能性があります。
よってこのクムクム入門モデルではTimer3 を利用します。これらのタイマーは、プログラムによって設定された特定の時間間隔で割り込みを発生させることができます。

クムクム入門モデル(R3J)で正しく動くソースコード

#include <Servo.h>

Servo servo;
int ledPin = A0;
int servoPin = 7; // Arduino LeonardoのPWM対応ピン
volatile bool toggleLed = false;
volatile bool moveServo = false;
int servoPosition = 0;

void setup() {
  pinMode(11, OUTPUT);
  pinMode(ledPin, OUTPUT);
  servo.attach(servoPin);
  digitalWrite(11,HIGH);

  // タイマー3の設定 (Arduino Leonardo用)
  noInterrupts(); // 割り込みを無効化
  TCCR3A = 0; // タイマー/カウンター3の制御レジスタAをリセット
  TCCR3B = 0; // タイマー/カウンター3の制御レジスタBをリセット
  TCNT3 = 0; // カウンターの初期値を0に設定
  OCR3A = 6250; // 比較マッチレジスタを設定
  TCCR3B |= (1 << WGM32); // CTCモード
  TCCR3B |= (1 << CS32); // 256分周
  TIMSK3 |= (1 << OCIE3A); // タイマー比較割り込みを有効化
  interrupts(); // 割り込みを有効化
}

ISR(TIMER3_COMPA_vect) { // タイマー3割り込みサービスルーチン
  static int ledCounter = 0;
  static int servoCounter = 0;

  ledCounter++;
  servoCounter++;

  if (ledCounter >= 1) { // 0.25秒ごと
    toggleLed = !toggleLed;
    ledCounter = 0;
  }

  if (servoCounter >= 8) { // 1秒ごと
    moveServo = !moveServo;
    servoCounter = 0;
  }
}

void loop() {
  if (toggleLed) {
    digitalWrite(ledPin, digitalRead(ledPin) == LOW ? HIGH : LOW);
    toggleLed = false;
  }

  if (moveServo) {
    servoPosition = servoPosition == 0 ? 90 : 0;
    servo.write(servoPosition);
    moveServo = false;
  }
}

コードのポイント

このプログラムのポイントは21行目の OCR3A = 6250; // 比較マッチレジスタを設定 にあります。
前回行った toneによる1秒割り込みは 31250 を設定していたので、最初はそのまま 31250 で行ったのですがどうもうまくタイミングが取れませんでした。

OCR3A = 31250 と 6250の違い

OCR3Aは、Arduinoのタイマー割り込みで使用される比較マッチレジスタです。
このレジスタの値は、タイマー割り込みが発生する間隔を決定します。具体的には、タイマーのカウンタがOCR3Aに設定された値に達すると、割り込みが発生することになります。
Arduino Leonardo(ATmega32u4を使用)のタイマーは16MHzのクロック周波数で動作し、タイマーのカウント速度は、クロック周波数に加えてプリスケーラの設定に依存します。プリスケーラは、タイマーのカウント速度をクロック周波数の分数に減速させるものです。

OCR3A = 31250

この設定では、プリスケーラが256と設定されていると仮定します(TCCR3B |= (1 << CS32);)。

  • クロック周波数: 16,000,000 Hz
  • プリスケーラ: 256
  • タイマーのカウント速度: 16,000,000 / 256 = 62,500 Hz
  • OCR3A: 31250

タイマーは62,500Hzでカウントし、31,250のカウントで1回割り込みが発生します。つまり、割り込みの間隔は31,250 / 62,500 = 0.5秒(500ミリ秒)になります。

OCR3A = 6250

ここではプリスケーラが8と設定されていると仮定します(TCCR3B |= (1 << CS31);)。

  • クロック周波数: 16,000,000 Hz
  • プリスケーラ: 8
  • タイマーのカウント速度: 16,000,000 / 8 = 2,000,000 Hz
  • OCR3A: 6250

タイマーは2,000,000Hzでカウントし、6,250のカウントで1回割り込みが発生します。つまり、割り込みの間隔は6,250 / 2,000,000 = 0.003125秒(3.125ミリ秒)になります。

プリスケーラが8と設定されていると仮定するという意味

「プリスケーラが8に設定されている」という表現は、タイマーのクロック速度を分割することを意味します。プリスケーラは、マイクロコントローラの基本クロック信号を遅らせるために使用される分周器で、マイクロコントローラである Arduino Leonardo用のATmega32U4のタイマーは、その基本クロック速度(16MHz)でカウントを行います。しかし、この速度は多くのタイミング関連のタスクには速すぎます。そこでプリスケーラを使用して、タイマーのカウント速度を遅くします。

プリスケーラの役割

プリスケーラの値が「8」と設定されている場合、タイマーのカウント速度は基本クロック速度を8で割ったものになります。つまり、以下のように計算されます:

  • 基本クロック速度 = 16MHz (16,000,000 Hz)
  • プリスケーラ = 8
  • タイマーのカウント速度 = 16MHz / 8 = 2MHz (2,000,000 Hz)

これは、タイマーが1秒間に2,000,000回カウントすることを意味します。プリスケーラが大きいほど、カウント速度は遅くなります。

タイマー割り込みとプリスケーラ

タイマー割り込みを使用する際には、プリスケーラの値によって割り込みの頻度が変わります。OCR(Output Compare Register)の値がタイマーのカウントと一致するときに割り込みが発生します。プリスケーラが小さいほど、タイマーは高速にカウントし、割り込みはより頻繁に発生します。

例えば、プリスケーラを8に設定し、OCR3Aを6250に設定すると、割り込みは以下のように計算されます:

  • タイマーのカウント速度 = 2MHz (2,000,000 Hz)
  • OCR3A = 6250
  • 割り込み間隔 = 6250 / 2,000,000秒 = 0.003125秒(または3.125ミリ秒)

結果

割り込みを約3.125ミリ秒ごとに発生させることで、タイマーを使用して精密なタイミングを制御するように改造しました。

ソースコードの説明

1.サーボモータライブラリーをインクルードします。

#include <Servo.h>

2.変数を宣言します

Servo servo;
int ledPin = A0;
int servoPin = 9;
volatile bool toggleLed = false;
volatile bool moveServo = false;
int servoPosition = 0;

Servo servo;: サーボモータを制御するためのServoオブジェクト。
int ledPin = A0;: LEDが接続されているピン。
int servoPin = 9;: サーボモータが接続されているピン。
volatile bool toggleLed = false;: LEDの点滅を制御するためのフラグ。割り込みで使用されるためvolatileを指定。
volatile bool moveServo = false;: サーボモータの動きを制御するためのフラグ。割り込みで使用されるためvolatileを指定。
int servoPosition = 0;: サーボモータの現在の位置。

3.setup関数

pinMode(11,OUTPUT); はクムクムロボット入門モデル専用の処理で、以下 digitalWrite(11,HIGH);でモータに電源供給を開始します。
それ以外はLEDピンの設定やタイマーの設定、割込み処理の書き方です。

void setu(){
  pinMode(11, OUTPUT);      //モーター電源供給
  pinMode(ledPin, OUTPUT);  //LED
  servo.attach(servoPin);   //モータアタッチ
  digitalWrite(11,HIGH);    //モータ電源供給ON
  // タイマー3の設定 (Arduino Leonardo用)
  noInterrupts(); // 割り込みを無効化
  TCCR3A = 0; // タイマー/カウンター3の制御レジスタAをリセット
  TCCR3B = 0; // タイマー/カウンター3の制御レジスタBをリセット
  TCNT3 = 0; // カウンターの初期値を0に設定
  OCR3A = 6250; // 比較マッチレジスタを設定(1秒間隔)
  TCCR3B |= (1 << WGM32); // CTCモード
  TCCR3B |= (1 << CS32); // 256分周
  TIMSK3 |= (1 << OCIE3A); // タイマー比較割り込みを有効化
  interrupts(); // 割り込みを有効化
}

4.ISR割込み処理ルーチン

この関数は、タイマー3による割り込みが発生するたびに自動的に呼び出されます。LEDとサーボモータの操作に使用するカウンタを増やし、指定された回数に達した場合にフラグを切り替えています。

ISR(TIMER3_COMPA_vect) {
  static int ledCounter = 0;
  static int servoCounter = 0;

  ledCounter++;
  servoCounter++;

  if (ledCounter >= 2) {
    toggleLed = !toggleLed;
    ledCounter = 0;
  }

  if (servoCounter >= 4) {
    moveServo = !moveServo;
    servoCounter = 0;
  }
}

5.loop関数の処理

LEDの点滅とサーボモータの動作を制御しています。

  • if (toggleLed) { ... }: この部分では、toggleLedフラグがtrueの場合にLEDの状態を変更します。
    digitalWrite()関数を使用してLEDのON/OFFを切り替え、その後でtoggleLedfalseに設定して、次の割り込みまで待機します。
  • if (moveServo) { ... }: ここでは、moveServoフラグがtrueの場合にサーボモータの位置を変更します。
    サーボモータの現在位置が0度なら90度に、90度なら0度に移動させ、servo.write()関数を使用してサーボモータを制御します。
    その後でmoveServofalseに設定しています。
void loop() {
  if (toggleLed) {
    digitalWrite(ledPin, digitalRead(ledPin) == LOW ? HIGH : LOW);
    toggleLed = false;
  }

  if (moveServo) {
    servoPosition = servoPosition == 0 ? 90 : 0;
    servo.write(servoPosition);
    moveServo = false;
  }
}

動き