#analog
#norelated
#contents
* 電子オルゴールサンプル [#u83badf4]

スピーカーを繋いで、タイマーによる周波数出力で音楽を演奏させる。~
色々考えてみたけど、和音は一筋縄ではいかなさそうなので、とりあえず単音で。~

** 回路図 [#xe36eef5]

#ref(BeepTest.png,left,nowrap,電子オルゴールサンプル)

基本的にはスピーカーのみの接続です。~
[[部品購入1回目]]で、秋月で買った圧電スピーカーね。~
後で、RTCと組み合わせたり、他のセンサと組み合わせることによって、演奏のトリガを作っても面白いかも。~

** ソースコード [#x29c38ec]

いつものライブラリ。~
今回はユーティリティ(delay)のみ。~

&ref(util.h,left,nowrap,ユーティリティヘッダ);&ref(util.c,left,nowrap,ユーティリティソース);~

まずは、音階の周波数定義と、音符の長さ定義用のファイルを、「music_def.h」として作成します。~
内容は以下の通り。~

 #ifndef MUSIC_DEF_H_
 #define MUSIC_DEF_H_
 
 typedef enum _tagNOTE_HZ{	// 音階リスト
 	NOTE_HZ_NONE,
 	NOTE_HZ_C3,
 	NOTE_HZ_C3S,
 	NOTE_HZ_D3,
 	NOTE_HZ_D3S,
 	NOTE_HZ_E3,
 	NOTE_HZ_F3,
 	NOTE_HZ_F3S,
 	NOTE_HZ_G3,
 	NOTE_HZ_G3S,
 	NOTE_HZ_A3,
 	NOTE_HZ_A3S,
 	NOTE_HZ_B3,
 	NOTE_HZ_C4,
 	NOTE_HZ_C4S,
 	NOTE_HZ_D4,
 } NOTE_HZ;
 
 typedef enum _tagNOTE_LEN{	// 音符リスト
 	NOTE_LEN_ZEN,
 	NOTE_LEN_2BU,
 	NOTE_LEN_2_BU,
 	NOTE_LEN_4BU,
 	NOTE_LEN_4_BU,
 	NOTE_LEN_8BU,
 	NOTE_LEN_16BU,
 } NOTE_LEN;
 
 typedef struct _tagNOTE{	// 音符データ構造体
 	uint8_t	hz;
 	uint8_t	len;
 } NOTE;
 
 uint8_t note_hz[] = {	// 音階周波数リスト(CPUクロック1MHz/プリスケーラ8)
 		0,									// 休符
 		( 1000000 / 2 / 8 ) / 261.62,	// C3
 		( 1000000 / 2 / 8 ) / 277.18,	// C3#
 		( 1000000 / 2 / 8 ) / 293.66,	// D3
 		( 1000000 / 2 / 8 ) / 311.12,	// D3#
 		( 1000000 / 2 / 8 ) / 329.62,	// E3
 		( 1000000 / 2 / 8 ) / 349.22,	// F3
 		( 1000000 / 2 / 8 ) / 369.99,	// F3#
 		( 1000000 / 2 / 8 ) / 391.99,	// G3
 		( 1000000 / 2 / 8 ) / 415.30,	// G3#
 		( 1000000 / 2 / 8 ) / 440.00,	// A3
 		( 1000000 / 2 / 8 ) / 466.16,	// A3#
 		( 1000000 / 2 / 8 ) / 493.88,	// B3
 		( 1000000 / 2 / 8 ) / 523.25,	// C4
 		( 1000000 / 2 / 8 ) / 554.36,	// C4#
 		( 1000000 / 2 / 8 ) / 587.32,	// D4
 };
 
 uint16_t note_len[] = {	// 音符長リスト
 		60000 / BPM * 4,	// 全音符
 		60000 / BPM * 2,	// 二分音符
 		60000 / BPM * 3,	// 附点二分音符
 		60000 / BPM,		// 四分音符
 		60000 / BPM * 1.5,	// 附点四分音符
 		60000 / BPM / 2,	// 八分音符
 		60000 / BPM / 4,	// 十六分音符
 };
 
 #endif /* MUSIC_DEF_H_ */

見ての通り、音階リストの列挙型と音階周波数リストが、音符リストの列挙型と音符長リストが、それぞれ対応しています。~

周波数リストは、CPUの周波数と、実際の音階周波数を元に算出している、8bitタイマ/カウンタの比較レジスタに設定する値です。~
今回、音をスピーカーから出力するために、8bitタイマの比較一致のトグル出力を使用します。~
8bitタイマが0〜比較レジスタに格納した値までカウントを行い、カウンタが比較レジスタに格納した値に達した時点で、ピンのLow/Highを切り替えます。~

例えば、A3「ラ」の音の周波数は440Hzなので、ピンのON/OFF1セットの回数を、1秒間に440回のサイクルで行ってあげる必要があります。~
ということは、ON/OFFの切り替えが実質2回の処理なので、1秒間に880回のサイクルでONとOFFを繰り返せばいいことになりますが、CPUの1MHzを880回に分けると、1回のON/OFFの切り替えのタイミングは、CPUのクロックが約1136回の時点となります。~

8bitタイマは、基本的には、CPUのクロックが0〜最大255回までをカウントして、あるタイミングで出力のLow/Highを切り替えるものなので、1136回もカウントはできません。~
というわけで、タイマ機能の前置分周を8に設定し、CPUクロックが8毎にタイマカウンタが1上がるように設定します。~
こうすれば、1136/8で142回となるので、8bitタイマの比較レジスタに142を入れ、タイマをスタートし、カウンタが142となった時点で出力のLow/Highが切り替わるという動作が続けば、その出力ピンに繋いでいるスピーカーからは、440Hzの「ラ」の音が出ることになります。~
まぁ、16bitタイマを使えば、前置分周とかは考えなくてもいいような気もしますが……。~

#ref(Timing.png,left,nowrap,タイミング)

あと、音符長リストには、BPMという定数を使った計算をしています。~
これは、後でメインソースの方で定義しますが、曲のテンポです。~
1分間に四分音符が何個かという単位なので、それをミリ秒単位に変換しています。~

次に、楽譜ファイル。~
サンプルで作ってみたのは、「ドレミの歌」。~
music_def.hで定義した音符データ構造体の配列に、音階と音符のペアで、楽譜を記述しています。~

 #ifndef DOREMI_H_
 #define DOREMI_H_
 
 NOTE	score[] ={	// ドレミの歌
 		{ NOTE_HZ_C3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_D3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_C3,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_E3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_C3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_D3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_D3,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_F3,	NOTE_LEN_2_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_E3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_G3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_F3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_A3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_A3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_A3,	NOTE_LEN_2_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_G3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_C3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_D3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_A3,	NOTE_LEN_2_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_A3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_D3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3S,	NOTE_LEN_8BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_A3,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_B3,	NOTE_LEN_2_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_B3,	NOTE_LEN_4_BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3S,	NOTE_LEN_8BU },
 		{ NOTE_HZ_G3S,	NOTE_LEN_8BU },
 		{ NOTE_HZ_A3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_C4,	NOTE_LEN_2BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_A3S,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_A3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_C4,	NOTE_LEN_2_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 		{ NOTE_HZ_C3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_D3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_F3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_A3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_C4,	NOTE_LEN_4BU },
 		{ NOTE_HZ_C4,	NOTE_LEN_4BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_A3,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_G3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_D3,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_C3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_E3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_D3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_A3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_4BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_C3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_8BU },
 		{ NOTE_HZ_E3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_G3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_D3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_8BU },
 		{ NOTE_HZ_A3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_8BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_8BU },
 
 		{ NOTE_HZ_G3,	NOTE_LEN_2BU },
 		{ NOTE_HZ_C3,	NOTE_LEN_2BU },
 
 		{ NOTE_HZ_A3,	NOTE_LEN_2BU },
 		{ NOTE_HZ_F3,	NOTE_LEN_2BU },
 
 		{ NOTE_HZ_E3,	NOTE_LEN_2BU },
 		{ NOTE_HZ_C3,	NOTE_LEN_2BU },
 
 		{ NOTE_HZ_D3,	NOTE_LEN_2_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 
 		{ NOTE_HZ_G3,	NOTE_LEN_2BU },
 		{ NOTE_HZ_C3,	NOTE_LEN_2BU },
 
 		{ NOTE_HZ_A3,	NOTE_LEN_2BU },
 		{ NOTE_HZ_B3,	NOTE_LEN_2BU },
 
 		{ NOTE_HZ_C4,	NOTE_LEN_2BU },
 		{ NOTE_HZ_D4,	NOTE_LEN_2BU },
 
 		{ NOTE_HZ_C4,	NOTE_LEN_2_BU },
 		{ NOTE_HZ_NONE,	NOTE_LEN_4BU },
 };
 
 #endif /* DOREMI_H_ */

そしてメイン関数。~
メインは短め。~

 #include <avr/io.h>
 #include "util.h"
 
 #define BPM 140	// テンポ設定
 #include "music_def.h"
 #include "doremi.h"
 
 int main( void )
 {
 	uint8_t	i = 0;	// ループ変数
 
 	DDRD	= 0b01000000;	// PD6(OC0A)を出力に設定
 	PORTD	= 0b00000000;	// ポートDをLowに初期化
 
 	while( 1 ){
 		for( i = 0; i < sizeof( score ) / sizeof( NOTE ); i++ ){
 			if( NOTE_HZ_NONE == score[i].hz ){
 				TCCR0A	= 0b00000000;	// タイマー停止
 			} else {
 				OCR0A	= note_hz[score[i].hz];	// 周波数設定
 				TCCR0A	= 0b01000010;	// 比較一致トグル出力
 				TCCR0B	= 0b00000010;	// 前置分周8
 			}
 			delay_ms( note_len[score[i].len] );
 		}
 	}
 
 	return 0;
 }

BPM(テンポ)の定義をしてからmusic_def.hをincludeすることで、先程の音符長の値が決定します。~
先にincludeしてしまうと、定義が解決出来ないので注意。~

タイマの出力をピンに反映させるためには、ポート方向レジスタを、出力に設定する必要があります。~
ポートの初期化後に、メインループに入ります。~

無限ループ内に曲のループがあり、音符データ構造体を一つずつ処理しています。~
休符設定の時は、タイマを止め、指定音符長だけ待ちます。~
それ以外の時は、比較レジスタ値を設定し、タイマを動作させた状態で、指定音符長だけ待ちます。~

** 基本動作 [#k21d9ac5]

基本、楽譜通りに鳴らしているだけですが、同じ音が複数続いた場合、繋がって聞こえてしまうという問題があります。~
音符の合間で一旦止めているわけではないので当然ですが。~
今回鳴らす音が、次回鳴らす音と同じであれば、数ミリ秒演奏時間を短くして、慣らし終わった後に短くした分の停止時間を入れるとかするといいのかもしれません。~
意図して繋げたい場合は、スラーを示すフラグを用意するとか……?~

あと、もうひとつ、比較レジスタ値を途中で変えていますが、これを変えたタイミングで、既にカウンタが後から設定した値を超えていると、一致するタイミングが無くて、一旦最大値(255)までカウントしてしまってから次のサイクルに入ってしまうという問題があります。~
このため、場合によっては一瞬(半波長)だけ低い音の周波数の波が出来てしまいますが、可聴レベルでの違いが出るのかどうかはよくわかりません……。~

圧電スピーカーは音が甲高くて耳につきますね……。~
超小型スピーカーの方がよかったかも。~

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS