東京理科大MICEの方のクラシックマウスが走りながら曲を鳴らしているのを見て自分でもやってみたくなりました。演奏関連の処理のうち初期設定以外を全てタイマ割り込み関数内で行うことで実現しました。
注意
この記事はマイコンのクロック設定とPWM設定とタイマ割り込み設定ができる人向けです。だからそれらに関する説明はしません。
今回はSTM32F303C8T6という48ピンのマイコンを使って鳴らしました。他のマイコンのことは知りませんが基本的な考え方は同じだと思います。
プログラムにC言語とHALライブラリを使っています。
スピーカー
UGCT7525AN4というスピーカーを使いました。秋月電子で手に入り、表面実装でき、小さくてそこそこ大きい音が鳴るからです。しかし騒音が大きい場所では音がかき消されてしまうかもしれません。
表面実装用ダイナミックスピーカー UGCT7525AN4: パーツ一般 秋月電子通商-電子部品・ネット通販
データシートによるとこの図のように配線するといいみたいです。Vinをマイコンのピンにつなげます。今回はダイオードと抵抗とトランジスタは適当に買ったものを使いました。このダイオードはスピーカーに逆電圧がかからないようにするためにつけるみたいです。
プログラムの説明
音楽の教科書などに載っている楽譜には音の高さや長さ、テンポや強弱など、たくさん記号が書かれていますが、曲をプログラムする際には音の高さと長さ以外の情報を全て無視します。
PB9ピンからスピーカーに入力するパルス波が出力されます。
ここでは曲の例として「ド・レ・ミ・ファ・ソ・ラ・シ・休み」を繰り返すプログラムを示します。
楽譜
ドレミファソラシの音の周波数をネットなどで調べてマクロにします。このマクロの単位は[Hz]です。休符はRESTとし、適当な数字な数字を割り当てます。0にすると音の周波数と被らずに済みます。
#define DO 261.626 #define RE 293.665 #define MI 329.628 #define FA 349.228 #define SOL 391.995 #define LA 440.000 #define SI 493.883 #define REST 0
次に音の長さのマクロです。八分音符の長さをLEN8というマクロで表します。長さは適当に1000[ms]とします。
#define LEN8 1000 // 八分音符の長さ[ms]
楽譜を配列として表します。二元配列を使い、1行目に音の高さと休符、2行目に音の長さを格納します。
const float music[2][8] = { {DO, RE, MI, FA, SOL, LA, SI, REST}, {LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8} };
PWM
スピーカーに入力するパルス波をマイコンのPWM機能を使って生成します。今回はTIM17というクロックをPWM用に使います。CLK_FREQはタイマクロックの周波数という意味です。今回はタイマクロックの周波数を1[MHz]に設定するので1000000にします。パルス波の周期をタイマカウンタのカウント上限を指定することによって自由に変更するためにFREQ2PWMPERIOD(X)というマクロを使って音の周波数からカウント上限を計算します。このマクロで-1している理由は、タイマカウンタが0から数え始めるためそのままだと1カウント多くなってしまうからです。
#define CLK_FREQ 1000000 #define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X) - 1)
タイマ割り込み
1[ms]ごとに関数を呼び出します。今回はTIM6というタイマを使い、TIM6_DAC1_IRQHandler()という関数をタイマ割り込みで呼び出します。この関数と、音の長さを1[ms]単位で計るsound_timerという変数と、楽譜配列のどの音符を鳴らしているかを示すnote_numという変数を使って演奏に必要な処理を行います。関数が呼び出されたとき、もし休符の最中でなければパルス波の周期を指定します。そしてその値のちょうど半分をコンペアマッチ用の値として使ってHIGHとLOWを反転させます。そうすると1周期の間のHIGHとLOWの時間は同じ長さになります。もし休符の最中であれば音が鳴らないようにします。関数が呼び出されたとき、もし指定した長さに音を鳴らし終わっていたらsound_timerを0にしてnote_numをインクリメントします。こうすることで次に関数が呼び出されたときに次の音を鳴らし始められます。関数が呼び出されたとき、もし楽譜配列の最後まで鳴らし終わっていたら再び楽譜配列の最初から鳴らし始めます。
uint16_t sound_timer = 0; uint8_t note_num = 0; void TIM6_DAC1_IRQHandler(void) { if( !(TIM6->SR & TIM_SR_UIF) ){ return; } sound_timer++; if(music[0][note_num] != REST) { TIM17->ARR = FREQ2PWMPERIOD(music[0][note_num]); TIM17->CCR1 = TIM17->ARR / 2; }else{ TIM17->CCR1 = 0; } if(sound_timer > music[1][note_num]) { sound_timer = 0; note_num++; } if(note_num == 8) { note_num = 0; } TIM6->SR &= ~TIM_SR_UIF; }
main関数
システムクロック設定とPWM設定とタイマ割り込み設定をして無限ループに入ります。演奏に必要な処理は全てタイマ割り込み関数内で行われるため、main関数が非常に簡潔になります。
int main(void) { init(); while(1); return 0; }
ソースコード
たとえ同じマイコンを使ったとしても開発環境が異なれば違うプログラムを書かなければいけないのでソースコードを丸ごと載せてもあまり参考にならないかもしれませんが一応載せておきます。コメントがぐちゃぐちゃと汚いですが、これは色々なブログなどからの寄せ集めだったり自分のメモだったりするからですね。PWMモード2を使っていますがこのコードだとモード1でも音は変わらないはずです。
2019.8.15追記 SW4STM32(開発環境)に最初からついてくるスタートアップファイル(?)を使うと、このコードをコンパイルできない可能性があります。参考にするだけにしてください。
#include "stm32f3xx.h" #define DO 261.626 #define RE 293.665 #define MI 329.628 #define FA 349.228 #define SOL 391.995 #define LA 440.000 #define SI 493.883 #define REST 0 #define LEN8 1000 // 八分音符の長さ[ms] #define CLK_FREQ 1000000 #define FREQ2PWMPERIOD(X) (int)(CLK_FREQ / (X)) - 1 const float music[2][8] = { {DO, RE, MI, FA, SOL, LA, SI, REST}, {LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8, LEN8} }; uint16_t sound_timer = 0; uint8_t note_num = 0; void init(); void sysclk_init(); void speaker_pwm_init(); void interrupt_init(); void TIM6_DAC1_IRQHandler(); int main(void) { init(); while(1); return 0; } void init() { sysclk_init(); speaker_pwm_init(); interrupt_init(); } void sysclk_init() { RCC->CFGR |= RCC_CFGR_PLLSRC_HSI_DIV2; // PLL <== HSI/2 = 4MHz RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = PLL / 2 = 32MHz //RCC->CFGR |= RCC_CFGR_PPRE2_DIV2; // APB2 = PLL / 2 = 32MHz RCC->CFGR |= ( (16 - 1) << RCC_CFGR_PLLMUL_Pos); //sysclk = 64MHz FLASH->ACR |= FLASH_ACR_LATENCY_1; // flash access latency for 48 < HCLK <= 72. This statement must be placed immediately after PLL multiplication. RCC->CR |= RCC_CR_PLLON; while(!(RCC->CR & RCC_CR_PLLRDY)); // wait until PLL is ready RCC->CFGR |= RCC_CFGR_SW_PLL; // PLL as system clock while( (RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_PLL ); // wait until PLL clock supply starts SystemCoreClockUpdate(); } void speaker_pwm_init(void) { RCC->AHBENR |= RCC_AHBENR_GPIOBEN; // IO portB clock enable // PB9 を Alternative Function に設定する GPIOB->MODER &= ~(1 << 18); GPIOB->MODER |= 1 << 19; // PB9 を AF1に設定する GPIOB->AFR[1] |= (GPIO_AF1_TIM17 << GPIO_AFRH_AFRH1_Pos); RCC->APB2ENR |= RCC_APB2ENR_TIM17EN; // Clock supply enable TIM17->CR1 = TIM_CR1_CEN; //タイマ有効 TIM17->CR2 = 0; TIM17->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0 | TIM_CCMR1_OC1PE; //PWMモード2,CH1 TIM17->CCER = TIM_CCER_CC1E; //TIM17_CH1出力有効 TIM17->BDTR = TIM_BDTR_MOE; TIM17->CNT = 0; //タイマカウンタ値を0にリセット TIM17->PSC = 64-1; //タイマのクロック周波数をシステムクロック/64=1MHzに設定 TIM17->ARR = 0; //タイマカウンタの上限値。取り敢えず TIM17->CCR1 = 0; //タイマカウンタの比較一致値 } void interrupt_init(void) { RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; // Clock supply enable TIM6->CR1 = TIM_CR1_CEN; //タイマ有効 TIM6->CR2 = 0; TIM6->DIER = TIM_DIER_UIE; //タイマ更新割り込みを有効に TIM6->CNT = 0; //タイマカウンタ値を0にリセット TIM6->PSC = 64-1; //タイマのクロック周波数をシステムクロック/64=1MHzに設定 TIM6->ARR = 1000-1; //タイマカウンタの上限値。1000に設定。ゆえに1msごとに割り込み発生 TIM6->EGR = TIM_EGR_UG; //タイマ設定を反映させるためにタイマ更新イベントを起こす NVIC_EnableIRQ(TIM6_DAC1_IRQn); //タイマ更新割り込みハンドラを有効に NVIC_SetPriority(TIM6_DAC1_IRQn, 1); //タイマ更新割り込みの割り込み優先度を設定 } void TIM6_DAC1_IRQHandler(void) { if( !(TIM6->SR & TIM_SR_UIF) ){ return; } sound_timer++; if(music[0][note_num] != REST) { TIM17->ARR = FREQ2PWMPERIOD(music[0][note_num]); TIM17->CCR1 = TIM17->ARR / 2; }else{ TIM17->CCR1 = 0; } if(sound_timer > music[1][note_num]) { sound_timer = 0; note_num++; } if(note_num == 8) { note_num = 0; } TIM6->SR &= ~TIM_SR_UIF; }