Arduino-UNOでtone()の挙動を調べる
「シャカシャカ・ホイップ」(ピコピコ+ボコスカ)をArduino-UNO
でまとめるにあたり、変更したのが映像信号発生のための割り込み。
当初、これにタイマー1(16bit)を使っていたのですが、タイマー0
(delayなどを司るシステムタイマー)に変えました。
そしてタイマー1を音程(ブザー音)の発生に割り当てたのです。
その際、気になって調べたのがタイトルにした「tone()命令」です。
何が気になったかというと・・・
・どのポートでも音を出せること。
tone()はピン番号と周波数を指定すると、そのピンが出力になって、
方形波(デューティ50%)が出ます。
ATmega328Pマイコンが持つ3つのタイマーを使って、任意周波数の
方形波を出力するとなると・・・
・タイマー0はシステムで使ってるんで、周波数は変えられない。
OC0A=PD6(D6)とOC0B=PD5(D5)でPWM波は出せるけど。
・タイマー1はOC1A=PB1(D9)とOC1B=PB2(D10)が出力になる。
・タイマー2はOC2A=PB3(D11)とOC2B=PD3(D3)が出力になる。
(これらもPWM出力は出せる)
任意のピンに方形波を出せる「tone()」、どのようにやっているのか
を調べてみました。 (Tone.cppで処理されてます)
方形波を出しているのは「タイマー2のコンペアA割り込み」でした。
tone()で設定した周波数で割り込みがかかって、指定したピンを
トグルさせています。
ハードウェアではなく、ソフトでパルスを出力しているのです。
ですので、タイマーのパルス出力機能は使っていません。
ISR(TIMER2_COMPA_vect){
if (timer2_toggle_count != 0) {
*timer2_pin_port ^= timer2_pin_mask; // (1)ピンをトグル
if (timer2_toggle_count > 0) // 出力回数をチェック
timer2_toggle_count--; // (2)マイナスの時はカウントしない
}
else { // countが0になったらおわり
noTone(tone_pins[0]);
}
}
(1) がデューティ50%の方形波を出している操作です。
XORで、指定したピンのH/Lを反転させます。
(2) timer2_toggle_countがマイナスの時はカウントダウンしない
ので、ず~とパルスが出ます。
tone()の処理、タイマーが持つパルス出力機能は使わず、
割り込みでパルスを出しているのが分かりました。
そこで、実際にどんなパルスを出すのかオシロで見てみました。
使ったのはこんなスケッチ。
/***** Arduino-UNOで「tone()」のテスト *****/
// タイマー0と1が8bit、タイマー2が16bit
// タイマー0はシステムが使っている
// OC0A PD6 D6
// OC0B PD5 D5
// OC1A PB1 D9
// OC1B PB2 D10
// OC2A PB3 D11
// OC2B PD3 D3
#define PB5_X (PINB |= (1 << PB5)) // LEDポートをトグル出力
#define PB4_H (PORTB |= (1 << PB4)) // PB4 H/L
#define PB4_L (PORTB &= ~(1 << PB4))
/***** SETUP *****/
void setup() {
DDRB = 0b00111111; // PB5~0を出力に
tone(11, 8000); // (1)PB3:OC2Aに8kHzを出力
}
/***** LOOP *****/
void loop() {
unsigned long d, tm1; // ミリ秒タイマー
tm1 = millis();
while(1){
PB5_X; // (2)LEDポートトグル
d = millis();
if(tm1 != d){ // タイマー変化あり
PB4_H; // (3)H,Lパルス出力
tm1 = d;
PB4_L;
}
}
}
(1) tone()で8kHzをD11ピンにずっと出力。
(2) D13ピン(LED出力)をトグル。
(3) millis()に変化があったらD12ピンにパルス。
割り込みが入ったら(2)のパルスが止まります。
millis()はタイマー0割り込みで計時していますので、
この時も(2)のパルスが止まります。
さて・・・
こんなタイミングで処理が行われています。
PB5(D13)パルスに注目すると、割り込みの様子が見えてきます。
また、8kHzの↓エッジでトリガーをかけて、
デジタルオシロを「ずっと記録モード」にすると、
8kHz方形波に現れるジッターが観察できます。
割り込み処理、システムタイマーであるタイマー0より
tone()を処理しているタイマー2のほうが優先されるので、
同タイミングで割り込みがかかってもtone()の割り込みが
先に行われ、波形の乱れが少なくなるようになっています。
しかし・・・
tone()の方形波出力は、割り込み内でのポート操作。
(1) ポートを読んで
(2) 指定ビットを反転して
(3) ポートに書き戻す
これが割り込み内で行われてます。
もし、メインルーチン側でtone()と同じポートに対して
読み書き操作(ポートの別ビットをH/L)をしていたら・・・
「アトミック操作」 をしておかないと、出力波形が乱れて
しまいます。
上のスケッチに★の3行を追加してみます。
if(tm1 != d){ // タイマー変化あり
PB4_H; // H,Lパルス出力
tm1 = d;
a = PORTB; // ★ポートBをメイン側で操作
a ^= (1 << PB2); // | PB2をトグル
PORTB = a; // | 書き戻す
PB4_L;
}
こんなパルスが出てきます。
メインの読み書きと、割り込みでのポート操作が競合すると
8kHzの波形に乱れが発生します。
乱れるのはtone()で出しているほうのパルスですので、
「音」としての用途なら気も付かないでしょう。
しかし、一定周波数のパルスとして(便利に)使っていたら、
何らかの不具合が出るかもしれません。
このあたりの解説というか注意書き、残念ながら
見当たりません。
割り込み禁止と再許可の処理、重要なのが目に見えるかと。
※補足
PB2のトグル操作、いったん「a」に読み込んでから
XORしましたが、「 PORTB ^= (1 << PB2);」と
記しても同じです。
I/Oレジスタへのビット操作、SBI、CBI命令だと
「読み込み・操作・書きもどし」
が1命令内で行われますが、XORを直接記述する命令は
無いので、3行に書いたのと同様の処理(いったんレジスタ
に読み込んでから書き戻す)が行われます。
マクロ、PB5_X、PB4_H、PB4_Lは「SBI、CBI」命令に
展開されます。
ですので、これらの命令に対してのアトミック操作は不要
なのです。
| 固定リンク
「Arduino」カテゴリの記事
- Arduino UNO R3のクロック精度を1MHzパルスで確かめる(2025.04.28)
- Arduino、analogWriteは捨てちゃえ。ちゃんとしたPWMの例(2025.03.22)
- パルスジェネレータをI2C液晶で動かす(2025.01.28)
- EEPROMを使ったシリアル受信バッファ 512kバイトに増設(2024.12.26)
- 1/nカウント方式とDDS方式の2相パルス発生回路(2024.10.13)
「重箱の隅」カテゴリの記事
- 1/1023監視団 活動中!(2025.03.10)
- DIPのLMC6482えらい高くなった(2025.03.07)
- 因縁のボリューム記号 トランジスタ技術2025年3月号(2025.02.17)
- ボリューム記号のボヤキ、トラ技2025年3月号の別冊付録に再掲載(2025.02.10)
- NECは3段タイプの発振回路をすすめてる(2025.01.31)
「割り込み処理」カテゴリの記事
- 1クロックでも速くしたい 割込を「ISR_NAKED」で(2024.09.30)
- 1クロックでも速くしたい DDS方式の2相パルス発生器(2024.09.27)
- 最適化処理のせいで悩んだぞ 呪文volatile再び(2024.06.06)
- I2C液晶のアクセス、割り込みで処理しないようにすると(2024.04.12)
- I2C液晶のアクセス、割り込み処理で遅れる原因らしきもの(2024.04.07)
コメント