タイマー割込みを使ってドレミを鳴らす | arduinoロボットプログラミング

2023年12月5日

クムクムロボット入門モデル(R3J)のBEEP音機能とタイマー割込みを使って、一定の長さの音を一定の間隔で鳴らすプログラムを作ります。また、音を鳴らすために、通常だとArduinoの標準関数 toneを使いますが、このプログラムではtoneを使わずに波形を生成して自前の処理で鳴らしてみましょう。

toneを使わないプログラムの基本方針

toneを使わない場合、ArduinoのCPUから自前の処理で音を鳴らすための波形データを生成する必要があります。そのためにCPUのタイマー機能を使用します。
また、一定の間隔と一定の長さの音を鳴らすためにも、delay関数を使用しないもう一つのタイマーでの制御を試みますので、今日のプログラムでは2つのタイマーを使って制御します。
クムクム入門モデル(R3J)でプログラミングを行いますので、対象とするCPUはATmega32U4となります。
もしUNOなどで実現しようとするなら、タイマーのレジスターセットの扱いがかわりますので、このプログラムは正しく動作しません。

プログラム

const int speakerPin = 12; // スピーカーまたは圧電ブザーを接続するピン

// 音階の周波数
const float notesFrequency[] = {261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88};
volatile int currentNote = 0; // 現在の音階を指すインデックス
volatile bool isPlaying = false; // 音が鳴っているかのフラグ
volatile unsigned long playStartTime = 0; // 音が鳴り始めた時間

void setup() {
  pinMode(speakerPin, OUTPUT);
  setupTimer3(); // 音を生成するためのタイマー
  setupTimer1(); // 音階を切り替えるためのタイマー
}

void loop() {
  // メインループは空
}

void setupTimer3() {
  noInterrupts();
  TCCR3A = 0;
  TCCR3B = 0;
  TCNT3 = 0;
  TCCR3B |= (1 << WGM32); // CTCモード
  TCCR3B |= (1 << CS31); // 8分周
  TIMSK3 |= (1 << OCIE3A); // タイマー3 Aの比較一致割込みを有効化
  interrupts();
}

void setupTimer1() {
  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  TCCR1B |= (1 << WGM12); // CTCモード
  TCCR1B |= (1 << CS12) | (1 << CS10); // 1024分周
  OCR1A = 15624; // 1秒間隔
  TIMSK1 |= (1 << OCIE1A); // タイマー1 Aの比較一致割込みを有効化
  interrupts();
}

ISR(TIMER3_COMPA_vect) {
  if (isPlaying && (millis() - playStartTime) < 300) {
    digitalWrite(speakerPin, digitalRead(speakerPin) ^ 1); // ピンの状態を切り替え
  } else {
    isPlaying = false;
    digitalWrite(speakerPin, LOW); // 音を停止
  }
}

ISR(TIMER1_COMPA_vect) { // 音階を切り替えるためのタイマー割込み
  currentNote = (currentNote + 1) % 7; // 次の音階に進む
  OCR3A = (int)(16000000 / (8 * 2 * notesFrequency[currentNote]) - 1); // 新しい周波数を設定
  TCNT3 = 0; // タイマー3のカウンタをリセット
  playStartTime = millis(); // 再生開始時間を記録
  isPlaying = true; // 音の再生を開始
}

プログラムの動き

音は0.3秒間鳴り、次の音階までの間は0.7秒間無音です。この動作を実現するために、プログラムでは「タイマー割込み」という特別な機能を使用しています。
これは、特定の時間が経過するたびに自動的に行われる処理です。

タイマー割込みの設定

タイマー割込みは、一定の時間が経過するごとに特定のコード(割込みサービスルーチン)を自動的に実行する機能です。
このプログラムでは、2つのタイマー割込みを使用しています。タイマー1は音階の切り替え、タイマー3は音の生成に使用されます。

タイマー1

タイマー1は、1秒ごとに割込みを発生させるように設定されています。
この割込みが発生すると、プログラムは次の音階に切り替え、タイマー3をリセットし、音の再生を開始します。

タイマー3

タイマー3は、音を生成するために使用されます。
このタイマーは、設定された周波数で割込みを発生させ、スピーカーピンの状態を切り替えることで音を生成します。

音の再生時間の管理

音は0.3秒間だけ鳴ります。これを実現するために、isPlayingフラグとplayStartTime変数を使用します。
isPlayingは、音が現在鳴っているかどうかを示すフラグです。
playStartTimeは、音の再生が開始された時刻を記録します。
タイマー3の割込み内で、音が0.3秒間鳴ったかどうかをチェックし、0.3秒を超えた場合は音を停止します。

なぜdelayを使わないか

一般的にArduinoプログラミングではdelay()関数を使って一定時間待機しますが、これはプログラムの他の部分の実行を停止します。
このプログラムでは、delay()の代わりにタイマー割込みを使用しています。これにより、プログラムは待機時間中も他の処理を行うことができます。
このように、タイマー割込みを使用することで、プログラムはより効率的になり、待機時間中も他のタスクを実行できるようになります。
これは、ロボットの制御などリアルタイムでの操作や複数のタスクを同時に処理する必要がある場合に特に有用です。

ATmega32U4タイマー

このタイマーについて詳しく書かれたサイトがあったので紹介をします。
kumikomi-yitjc @ ウィキ さんの ページへリンクします。