« 「シャカシャカ・ホイップ」予備実験 #4 | トップページ | toneパルスの異常 もうちょっと掘り下げて »

2022年1月30日 (日)

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)のパルスが止まります。

さて・・・
こんなタイミングで処理が行われています。
C007
PB5(D13)パルスに注目すると、割り込みの様子が見えてきます。

また、8kHzの↓エッジでトリガーをかけて、
デジタルオシロを「ずっと記録モード」にすると、
8kHz方形波に現れるジッターが観察できます。
C008

割り込み処理、システムタイマーであるタイマー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;
}

こんなパルスが出てきます。
D000
メインの読み書きと、割り込みでのポート操作が競合すると
8kHzの波形に乱れが発生します。

乱れるのはtone()で出しているほうのパルスですので、
「音」としての用途なら気も付かないでしょう。
しかし、一定周波数のパルスとして(便利に)使っていたら、
何らかの不具合が出るかもしれません。

このあたりの解説というか注意書き、残念ながら
見当たりません。
割り込み禁止と再許可の処理、重要なのが目に見えるかと。

※補足
PB2のトグル操作、いったん「a」に読み込んでから
XORしましたが、「 PORTB ^= (1 << PB2);」と
記しても同じです。
I/Oレジスタへのビット操作、SBI、CBI命令だと
 「読み込み・操作・書きもどし」
が1命令内で行われますが、XORを直接記述する命令は
無いので、3行に書いたのと同様の処理(いったんレジスタ
に読み込んでから書き戻す)が行われます。
マクロ、PB5_X、PB4_H、PB4_Lは「SBI、CBI」命令に
展開されます。
ですので、これらの命令に対してのアトミック操作は不要
なのです。

|

« 「シャカシャカ・ホイップ」予備実験 #4 | トップページ | toneパルスの異常 もうちょっと掘り下げて »

Arduino」カテゴリの記事

重箱の隅」カテゴリの記事

割り込み処理」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)




« 「シャカシャカ・ホイップ」予備実験 #4 | トップページ | toneパルスの異常 もうちょっと掘り下げて »