Arduinoの標準的なライブラリ関数を使わずに、タイマー割込みと直接的なレジスタ操作を通じて、音の生成とタイミングの正確な管理を行いドレミファソラシドを1秒ごとに演奏するプログラムを作ってみます。
標準関数 tone()を使ったプログラム
まずは、一般的なArduinoの関数であるtoneを使ったソースコードです。このコードは、どのArduinoのボードでも実行が可能です。
int speakerPin = 9; // スピーカーを接続するピン
int notes[] = {262, 294, 330, 349, 392, 440, 494, 523}; // ドレミファソラシドの周波数 (Hz)
int noteDuration = 1000; // 各音の持続時間 (ミリ秒)
void setup() {
// スピーカーピンを出力として設定
pinMode(speakerPin, OUTPUT);
}
void loop() {
// ドレミファソラシドの音を順に再生
for (int i = 0; i < 8; i++) {
tone(speakerPin, notes[i], noteDuration); // 音を出力
delay(noteDuration + 100); // 指定した時間だけ待ってから次の音へ
}
// オプションでループを終了させる場合は以下のコメントを解除
// while(true); // これ以上の音を鳴らさない
}
プログラムの説明
- 変数定義:
speakerPin
はスピーカーが接続されるピン、notes
配列にはドレミファソラシドの音階の周波数が格納されています。noteDuration
は音が持続する時間を指定します。 - setup()関数: スピーカーピンを出力としてセットアップします。
- loop()関数:
for
ループを使って、配列notes
の中の各音階を順に再生します。tone()
関数により指定されたピンから周波数に応じた音が出力され、その後delay()
関数で音が1秒間再生された後に追加の小休止を挟みます。
このプログラムは簡単に理解できるため、Arduino初心者にもおすすめですが、delay()
の使用によりその間プログラムの他の処理がブロックされるため、複数のタスクを同時に実行する場合には適していません。
tone関数
tone(pin, frequency)
tone(pin, frequency, duration)
Arduinoのtone()
関数は、指定したピンから特定の周波数の正方形波を生成するために使用される関数です。これにより、簡単にビープ音や他の音階を生成することができます。
- pin: 出力ピンの番号。PWMが可能なピンを使用する必要があります。
- frequency: 生成する音の周波数(単位はヘルツ)。この関数で設定できる周波数の範囲は、通常は31Hzから約65535Hzまでです。
- duration: オプションのパラメータで、音を鳴らす持続時間をミリ秒単位で指定します。このパラメータを設定すると、指定した時間が経過すると自動的に音が停止します。
音階と周波数表
Octave | C | C# | D | D# | E | F | F# | G | G# | A | A# | B |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 16 | 18 | 18 | 20 | 20 | 22 | 24 | 24 | 26 | 28 | 30 | 30 |
1 | 32 | 34 | 36 | 38 | 40 | 44 | 46 | 48 | 52 | 54 | 58 | 62 |
2 | 66 | 70 | 74 | 78 | 82 | 88 | 92 | 98 | 104 | 110 | 116 | 124 |
3 | 130 | 138 | 146 | 156 | 164 | 174 | 186 | 196 | 208 | 220 | 234 | 246 |
4 | 262 | 276 | 294 | 312 | 330 | 350 | 370 | 392 | 416 | 440 | 466 | 494 |
5 | 524 | 554 | 588 | 622 | 660 | 698 | 740 | 784 | 832 | 880 | 932 | 988 |
6 | 1046 | 1108 | 1174 | 1244 | 1318 | 1396 | 1480 | 1568 | 1662 | 1760 | 1864 | 1976 |
7 | 2092 | 2218 | 2348 | 2488 | 2636 | 2794 | 2960 | 3136 | 3324 | 3520 | 3728 | 3952 |
8 | 4186 | 4436 | 4698 | 4978 | 5274 | 5588 | 5920 | 6272 | 6644 | 7040 | 7460 | 7902 |
9 | 8372 | 8870 | 9398 | 9956 | 10548 | 11176 | 11840 | 12544 | 13290 | 14080 | 14916 | 15804 |
- 四捨五入して偶数に調整したたオリジナルの値とは少し異なる可能性があります。
標準関数を使わないプログラム
次に、Arduinoのtone
とnoTone
関数の代わりにタイマー割込みとレジスタ操作を用いて、ドレミファソラシドの音階を1秒ごとに切り替えるものです。このプログラムでは、Arduinoのタイマー1(16ビットタイマー)を使用してピンの状態をトグルし、音を生成します。また、プログラム内ではdelay
を使用せず、タイマー割込みによって時間の経過を管理します。
#include <avr/io.h>
#include <avr/interrupt.h>
volatile unsigned long timer1_millis;
volatile int currentNote = 0;
const int speakerPin = 9; // PWM出力ピン
const int notes[] = {262, 294, 330, 349, 392, 440, 494, 523}; // ドレミファソラシドの周波数
ISR(TIMER1_COMPA_vect) {
timer1_millis++; // タイマー割込み毎にミリ秒をカウントアップ
}
void setup() {
pinMode(speakerPin, OUTPUT);
// タイマー設定 (CTCモード、プリスケーラ64)
TCCR1A = 0;
TCCR1B = 0;
TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10); // CTC mode and 64 prescaler
OCR1A = 249; // 1ミリ秒ごとに割込み (16MHz / 64 / 1000)
TIMSK1 |= (1 << OCIE1A); // タイマー割込みを有効化
sei(); // 割込みを許可
timer1_millis = 0; // タイマー用のカウンタを0に
}
void loop() {
if (timer1_millis >= 1000) { // 1秒ごとに実行
timer1_millis = 0; // カウンタリセット
playTone(notes[currentNote], 1000); // 現在の音を1秒間再生
currentNote = (currentNote + 1) % 8; // 次の音へ
}
}
void playTone(int frequency, int duration) {
long toggleCount = 2 * frequency * duration / 1000;
int toggleDelay = 1000000 / (frequency * 2); // toggle every half period
for (long i = 0; i < toggleCount; i++) {
PORTB ^= (1 << PORTB1); // ピンの状態をトグル (ピン9)
_delay_us(toggleDelay); // 半周期待つ
}
}
プログラムの詳細説明
- タイマー割込みの設定: タイマー1をCTCモードで設定し、1ミリ秒ごとに
TIMER1_COMPA_vect
割込みを発生させています。これにより、timer1_millis
変数が1ミリ秒ごとにインクリメントされます。 - 音の再生:
playTone
関数では、指定された周波数で正確な期間音を再生します。これはピンの状態をトグルすることで実現し、そのためのディレイは_delay_us
を使用して精密に管理します。 - 音階の管理:
loop
関数では、timer1_millis
が1000ミリ秒に達するごとに音を切り替えています。音階は配列notes
に保存されており、ドレミファソラシドの周波数が格納されています。
このプログラムは、Arduinoの標準的なライブラリ関数を使わずに、タイマー割込みと直接的なレジスタ操作を通じて、音の生成とタイミングの正確な管理を行っています。これにより、より効率的でタイムクリティカルなアプリケーションに適した制御が可能となります。
toneを使わない処理のメリット
tone
関数を使わずに直接タイマー割込みとレジスタ操作を用いる方法と比較した場合、以下のメリットが考えられます:
1. 高速な実行とリアルタイム性
直接タイマー割込みとレジスタを操作する方法は、関数呼び出しのオーバーヘッドが少なく、より高速に実行できます。これにより、リアルタイム性が要求されるアプリケーションでの使用に適しています。tone
関数よりも細かいタイミング制御が可能で、割込みレイテンシーを最小限に抑えることができます。
2. リソースの節約
tone
関数は内部でタイマーを使用しますが、自分でタイマーを制御することで、使用するタイマーの数や方法を最適化でき、限られたリソースをより効果的に利用することが可能です。特に複数のタスクを同時に処理する必要がある場合に有効です。
3. 柔軟な制御
直接レジスタを操作することで、tone
関数ではアクセスできないタイマーの特定の機能や設定を利用できるようになります。例えば、異なるプリスケーラの設定やタイマーの特定のモードを活用することが可能です。これにより、より複雑な音のパターンやタイミングの制御が可能になります。
4. 複数のピンでの同時音出力の容易化
tone
関数は一度に一つのピンにのみ音を出力できますが、直接レジスタ操作による方法では、複数のピンに対して独立した制御を同時に行うことが可能です。これにより、より複雑な音楽や効果を生成することができます。
5. パワーマネジメントの最適化
直接レジスタ操作による方法では、必要な時だけタイマーを有効にし、不要な時は無効化することで消費電力を節約することができます。これは、特にバッテリー駆動のデバイスにとって重要です。
注意点
ただし、これらのメリットを享受するためには、マイクロコントローラの内部構造や動作について深い理解が必要です。間違ったレジスタ設定はシステムの不安定な動作や予期せぬ副作用を引き起こす可能性があります。また、tone
関数を使った方が開発はずっと簡単で、多くの場合において十分な性能を提供します。