2008年09月10日 [ColdFire] ColdFire×P2P地震情報(A) PWMと圧電ブザーで音を鳴らす
_ [ColdFire] ColdFire×P2P地震情報(A) PWMと圧電ブザーで音を鳴らす
Interface 9月号付録基板を使い,P2P地震情報のサービスを作ってしまう勝手な企画です.が,番号がアルファベットのものはおまけです.
二番煎じに関する注意事項
これは新適当マイコン電子工作研究所で行われていることの二番煎じです.が,PWMの出力をクリスタルイヤホンで… などと見ていると,「これだ」と思ってそのまま突っ走ってしまったわけです.お付き合いください.
経緯
- 「PWMの出力を… クリスタルイヤホン?」というわけで,検索する.
- 「ふむふむ,圧電効果とかいうのか…」
- 「圧電? 圧電ブザーってあるね? つーか同じじゃね?」
- 「じゃあ圧電ブザー繋げて同じことすれば音鳴るんじゃね?」
材料
- ColdFire基板 (Interface 9月号付録,RJ-45とDCジャックを実装済み)
- 何に使ったか覚えていない導線
- 古いマシンから切り取った圧電ブザー
- "HYCOM HY-05"と書かれていて,どうやら3.0 V〜8.0 Vで動作するらしい.
- 私の時間
手順1: 本当に鳴るのか
まずは,MCF52233付録基板 - SilentCが使っていないモジュールを探せ(4)に記載されているとおりにやってみます.
(PDF)リファレンスマニュアルや(PDF)回路図を見ると,PNQ[1]=IRQ1のようなので,次のように接続します.
すると,確かに「プツッ,プツッ,プツッ…」と3.6 Hzくらいで鳴っているのが聞こえます.いけるようです.
手順2: プリスケーラ? スケーラ? デューティ? 周波数を設定するには…
圧電ブザーは高速でON/OFFを繰り返すパルス信号によって,その信号の周波数の音を出すようです.ですから,この周波数さえ設定できれば音の高低を自由自在に操れるわけです.
が,WindowsのBeep関数のように「周波数」という設定項目はありません.あるのは,プリスケーラ,スケーラ,デューティ,意味の分からない項目ばかりです.じっくり向き合う必要があるようです.
手順3: ColdFireの"Pulse Width Modulation(PWM)"って?
と言いつつ,PWMの解説は他所に譲ります.ここでは,ColdFireで用意されている設定項目について見ていきます.リファレンスマニュアルをご用意ください.
- PWM Clock Select Register (PWMCLK) - 29.2.3/534ページ
- PWMのクロック源を選ぶ.AやBを指定するとプリスケーラ(PWMPRCLK)のみ,SAやSBを指定するとプリスケーラとスケーラ(PWMSCLA/PWMSLCB)の両方を通る.
- PWM Prescale Clock Select Register (PWMPRCLK) - 29.2.4/535ページ
- プリスケーラの分周比を選ぶ.簡単に言うと,内部バスクロックの何分の1のクロックで動くようにするかを指定する.ただし,分母は2の0乗,1乗,2乗,…,7乗.
- PWM Scale A/B Register (PWMSCLA/B) - 29.2.7〜/538ページ〜
- プリスケーラクロックの何分の1で動くようにするかを指定する.ただし,分母は2,4,6,…,512.
- PWM Channel Period Registers (PWMPERn) - 29.2.10/540ページ
- PWMCLKなどで指定したクロックを元にカウントを行うが,そのカウンタの上限値,1サイクルに掛かるクロック数.
- PWM Channel Duty Registers (PWMDTYn) - 29.2.11/541ページ
- 1サイクルに掛かるクロック数に対し,信号がONになるクロック数.PWMPERnと同じにすれば常にONだし,半分ならON/OFFがピッタリ交互だし,0ならOFFのまま.
プリスケーラ,スケーラ,ピリオド(デューティ)を組み合わせることで,やっと周波数を指定出来るというわけです.これはなかなかしんどい.
スケーラを通る(PWMCLKにSA/SBを指定した)場合の周波数の計算式を置いておきます.ただし,デューティ比(信号ONの割合)は50 %とします.
= 60*1000*1000 / ( 2 ^ PWMPRCLK ) / ( 2 * PWMSCLA/B ) / PWMPERn 内部バスクロック プリスケーラ スケーラ ピリオド
手順4: 音階に対応する組み合わせ
早速組み合わせてみました.平均律を用いています.0.6 Hzくらいズレているところもありますが,お遊びということで.
プリスケーラ | スケーラ | ピリオド | 算出 周波数 | 平均律 周波数 | ズレ(絶対値) | ||
B 5 | シ | 2 | 211 | 36 | 987.3617694 | 987.7666 | 0.404830648 |
A#5 | シ♭ | 2 | 149 | 54 | 932.1401939 | 932.3275 | 0.187306115 |
A 5 | ラ | 2 | 147 | 58 | 879.6622097 | 880 | 0.337790289 |
G#5 | ソ♯ | 2 | 215 | 42 | 830.5647841 | 830.6094 | 0.044615947 |
G 5 | ソ | 5 | 46 | 26 | 783.8628763 | 783.9909 | 0.128023746 |
F#5 | ファ♯ | 3 | 181 | 28 | 739.9368587 | 739.9888 | 0.051941279 |
F 5 | ファ | 3 | 179 | 30 | 698.3240223 | 698.4565 | 0.132477654 |
E 5 | ミ | 3 | 39 | 146 | 658.5879874 | 659.2551 | 0.667112645 |
D#5 | ミ♭ | 3 | 137 | 44 | 622.0968812 | 622.254 | 0.157118779 |
D 5 | レ | 3 | 103 | 62 | 587.2220482 | 587.3295 | 0.107451769 |
C#5 | ド# | 3 | 199 | 34 | 554.2417972 | 554.3653 | 0.123502779 |
C 5 | ド | 4 | 256 | 14 | 523.1584821 | 523.2511 | 0.092617857 |
ポイントは,「プリスケーラを1減らすと,周波数が2倍=1オクターブ上になる」こと,そして逆もまたあるということです.上記のような1オクターブ分の音階表を持っておけば,あとはプリスケーラの値だけで対応できます.
プリスケーラは0〜7まで指定できるので,この場合だとG#2〜G 8までは自由に出せることになります.この範囲を外れてしまうと所々音が鳴らなくなります.
手順5: 実装
mel(){ // char *meloは可読性のため改行を入れたので,実行時には削除すること char *melo="5E-85D#85E-85D#85E-84B-85D-85C-84A-83E-83A-84C-84E-84A-84B-83E-8 3G#84E-84G#84B-85C-83E-83A-84E-85E-85D#85E-85D#85E-84B-85D-85C-8 4A-83E-83A-84C-84E-84A-84B-83E-83G#84D-85C-84B-84A-83E-83A-8SS-8 EE"; char *pt=melo; int bt,slp; bt=10000/(16000/60); // テンポを指定し,4分音符の長さを10 ms単位で計算 // "10000 / (テンポ*10 / 60)" // 小数計算が出来ないため簡易精度確保 int *pnq = 0x40100068; // PNQ[1] char *pwm = 0x401b0000; // PWM *pnq |= 12; pwm[2]=2; // PNQ[1]とPWM1を接続,PWM1を有効に #stop 0 for(;;){ if(Getc(0)!=0)break; if(pt[0]==69)break; if(pt[1]==67&&pt[2]==45){pwm[3]=4;pwm[8]=256;pwm[21]=14;pwm[29]=7;} //C if(pt[1]==67&&pt[2]==35){pwm[3]=3;pwm[8]=199;pwm[21]=34;pwm[29]=17;} //C# if(pt[1]==68&&pt[2]==45){pwm[3]=3;pwm[8]=103;pwm[21]=62;pwm[29]=31;} //D if(pt[1]==68&&pt[2]==35){pwm[3]=3;pwm[8]=137;pwm[21]=44;pwm[29]=22;} //D# if(pt[1]==69&&pt[2]==45){pwm[3]=3;pwm[8]=39;pwm[21]=146;pwm[29]=73;} //E if(pt[1]==70&&pt[2]==45){pwm[3]=3;pwm[8]=179;pwm[21]=30;pwm[29]=15;} //F if(pt[1]==70&&pt[2]==35){pwm[3]=3;pwm[8]=181;pwm[21]=28;pwm[29]=14;} //F# if(pt[1]==71&&pt[2]==45){pwm[3]=5;pwm[8]=46;pwm[21]=26;pwm[29]=13;} //G if(pt[1]==71&&pt[2]==35){pwm[3]=2;pwm[8]=215;pwm[21]=42;pwm[29]=21;} //G# if(pt[1]==65&&pt[2]==45){pwm[3]=2;pwm[8]=147;pwm[21]=58;pwm[29]=29;} //A if(pt[1]==65&&pt[2]==35){pwm[3]=2;pwm[8]=149;pwm[21]=54;pwm[29]=27;} //A# if(pt[1]==66&&pt[2]==45){pwm[3]=2;pwm[8]=211;pwm[21]=36;pwm[29]=18;} //B if(pt[1]==83){*pwm=0;}else{*pwm=2;} pwm[3]-=pt[0]-51; //所詮ビープなので,指定より2オクターブ高く出して聞こえやすく if(pt[3]==52){slp=bt;} if(pt[3]==56){slp=bt/2;} if(pt[3]==57){slp=bt/3;} if(pt[3]==50){slp=bt*2;} if(pt[3]==49){slp=bt*4;} Sleep(slp); pt+=4; } *pwm=0; PrStr("E.\r\n"); }
char *meloでメロディを作ります.4バイトで1個の音を奏でる独自形式です.
[オクターブ][音階][シャープ][長さ] ※音階はA〜GまたはS(休み)で,シャープは#または-(ありまたはなし) ※長さは 1(全音符),2(2分音符),4,8,9(3連8分音符)のみ
forループ内で次のような処理を繰り返します.
- オクターブが E であれば,ループを抜ける.
- 音階とシャープを見て,プリスケーラなどを設定
- 休みであればPWMオフ,そうでなければPWMオン
- オクターブを見て,プリスケーラを加減
- 長さを見て,演奏時間を計算してスリープ
- 次の位置へポインタをセット
若干順番が乱れていますが,気にしないことにします.かなり強引にやってしまいましたが,これを実行すると「エリーゼのために」が流れます.
制限として,約256バイトに収める必要があるようです.それを超えたり,終了命令(オクターブに E)を忘れたりすると,範囲外にまで演奏を続行して変な音になります.
手順6: 実演
- ニコニコ動画: ColdFire基板と圧電ブザーで音を鳴らしてみた(YouTube版)
SilentCからここまで操作できちゃうとは.プログラミングガイドを読んで「こんなものか」と思っていたのがバカみたいです.スバラシイ.