哪个企业做网站/搜狗链接提交入口
本节原来是想讲一讲无源蜂鸣器发声的原理,用于添加BGM功能。为了讲原理,就写了一些通俗的代码,没想到越写越多,后来,干脆就形成了一个小小的项目吧——基于STM32与无源蜂鸣器的电子琴。
灯光效果
首先想到的是做一个灯光的效果,按下哪个按键,哪个按键的灯要亮;松手后,灯灭掉。顺带,检测一下带松手检测的按键功能好不好用。后续还可以做成通过亮灯提示需要按下那个按键,类似于节奏大师的功能——哪里要响点哪里。
我去掉了无关的代码,主函数里通过死循环,来确保按键按下的时候,灯是亮起来的 :
//main.c
while(1){AllLED_OFF();while(!SKEY1){SLED1 = LED_ON;}while(!SKEY2){SLED2 = LED_ON;}while(!SKEY3){SLED3 = LED_ON;}while(!SKEY4){SLED4 = LED_ON;}while(!SKEY5){SLED5 = LED_ON;}while(!SKEY6){SLED6 = LED_ON;}while(!SKEY7){SLED7 = LED_ON;}while(!SKEY8){SLED8 = LED_ON;}if(PAUSE_PRES == KEY_Scan(0)){LED1 = !LED1;}}
下载程序并看看现象。
无源蜂鸣器的音调控制
音调和频率是息息相关的,可以在网上查找到频率和音调对应的表格。本文的代码参考了这篇文章,表示感谢
根据图片,可以做宏定义,中音的C调:
#define CM1 523
#define CM2 587
为了讲清楚原理,这里蜂鸣器先当做LED用。引脚给高电平,蜂鸣器就能响(只有一瞬间有声音)。然而,只给高电平,无源蜂鸣器不能自己持续发出声音;需要马上给低电平,然后再给一个高电平。即在一个很短的周期内,无源蜂鸣器在高电平持续器件工作,在低电平持续器件休息。周期的倒数就是频率。
蜂鸣器的引脚是PB1,初始化跟LED一样,我直接写在了LED的初始化函数里。
接下来先写两个按键的功能,用按键1和2来演奏C调的哆和唻。我定义了一个变量,是us为单位的时间,这是蜂鸣器的一个周期。它的值就是1000000us(1百万us就是1s)除以频率。频率是查表得到的。在周期内,高电平持续的时间和低电平持续的时间各占一半。
//main.c
u32 F_us; //特定频率对应的周期时间,单位us
while(!SKEY1)
{SLED1 = LED_ON;F_us = 1000000/CM1;BEEP = 1;delay_us(F_us/2);BEEP = 0;delay_us(F_us/2);
}
while(!SKEY2)
{SLED2 = LED_ON;F_us = 1000000/CM2;BEEP = 1;delay_us(F_us/2);BEEP = 0;delay_us(F_us/2);
}
下载程序,按下按键1或2,就可以听到不同的音调。原理就是这么简单。
音量控制
无源蜂鸣器可以用高电平持续的时间调整音量,在一个周期中,高电平持续的时间越长,蜂鸣器声音越大;高电平持续的时间越短,蜂鸣器的声音越小。这句话还有一个时髦的描述方法——脉宽调制。
原理很简单,实现起来也不复杂。在上一个案例的基础上,我把高电平持续的时间由50%改成了通过变量volum来计算。如果volum=1,那么高电平持续的时间就是周期的一半(右移一位等于除以2);如果volum=5,那么高电平持续的时间就是周期的64分之1,(右移n位等于除以2的n次方)。为了方便比较,我先让按键1和2的音调一样,音量不一样。
//main.c
u32 F_us; //特定频率对应的周期时间,单位us
u32 time_ON; //蜂鸣器响的时间
u32 time_OFF; //蜂鸣器不响的时间
u8 volum; //音量
while(!SKEY1)
{SLED1 = LED_ON;F_us = 1000000/CM1;volum = 1;time_ON = F_us>>volum;time_OFF = F_us - time_ON;BEEP = 1;delay_us(time_ON);BEEP = 0;delay_us(time_OFF);
}
while(!SKEY2)
{SLED2 = LED_ON;F_us = 1000000/CM1;volum = 6;time_ON = F_us>>volum;time_OFF = F_us - time_ON;BEEP = 1;delay_us(time_ON);BEEP = 0;delay_us(time_OFF);
}
按下两个按键,可以听出响度是不一样。事实上我调整过比例,感觉,50%的占空比可能是最大的声音了,,volum < 4之前都听不大出来。
提取函数
既然按键1和按键2都既能控制音调,用能控制音量了,别的按键把代码复制粘贴就能实现功能了。只不过,复制粘贴是代码不好的表现。
所以,再次提取出一个函数,传入音调和音量,就能发出声音。
void play(u32 tone,u8 tvolum)
{u32 F_us; //特定频率对应的周期时间,单位usu32 time_ON; //蜂鸣器响的时间u32 time_OFF; //蜂鸣器不响的时间F_us = 1000000/tone;time_ON = F_us>>tvolum;time_OFF = F_us - time_ON;BEEP = 1;delay_us(time_ON);BEEP = 0;delay_us(time_OFF);
}
然后修改死循环。我有8个带灯按键,但是音调只有7个,所以预留8和pause用于升降调,这两个按键无需松手检测。其它按键按下时,调用play函数。
while(1){key = KEY_Scan(0);if(KEY8_PRES == key){LED2 = !LED2;}else if(PAUSE_PRES == key){LED1 = !LED1;}else{AllLED_OFF();}while(!SKEY1){SLED1 = LED_ON;play(CM1,volum);}while(!SKEY2){SLED2 = LED_ON;play(CM2,volum);}while(!SKEY3){SLED3 = LED_ON;play(CM3,volum);}while(!SKEY4){SLED4 = LED_ON;play(CM4,volum);}while(!SKEY5){SLED5 = LED_ON;play(CM5,volum);}while(!SKEY6){SLED6 = LED_ON;play(CM6,volum);}while(!SKEY7){SLED7 = LED_ON;play(CM7,volum);}}
至此,就已经实现了最简单的电子琴的功能。
升调和降调功能
默认情况下,我们演奏的都是C调中间那个音阶。我定义按键8升调,按键PAUSE为降调(其实调整的不是音调而是音阶)。然后定义个变量用于储存当前是C调还是F调,也就是音阶?
while(1){key = KEY_Scan(0);if(KEY8_PRES == key){LED2 = !LED2;tone_level++;}else if(PAUSE_PRES == key){LED1 = !LED1;tone_level--;}else{AllLED_OFF();}。。。}
修改play函数,根据音阶与音调来计算周期。
void play(u32 tone,u8 tvolum)
{u32 F_us; //特定频率对应的周期时间,单位usu32 time_ON; //蜂鸣器响的时间u32 time_OFF; //蜂鸣器不响的时间if(tone_level<1)tone_level = 1;else if(tone_level>12)tone_level = 12;if(1 == tone_level){switch(tone){case 1:F_us = 1000000/CL1;case 2:F_us = 1000000/CL2;case 3:F_us = 1000000/CL3;case 4:F_us = 1000000/CL4;case 5:F_us = 1000000/CL5;case 6:F_us = 1000000/CL6;case 7:F_us = 1000000/CL7;}}else if(2 == tone_level){switch(tone){case 1:F_us = 1000000/CM1;case 2:F_us = 1000000/CM2;case 3:F_us = 1000000/CM3;case 4:F_us = 1000000/CM4;case 5:F_us = 1000000/CM5;case 6:F_us = 1000000/CM6;case 7:F_us = 1000000/CM7;}}//F_us = 1000000/tone;time_ON = F_us>>tvolum;time_OFF = F_us - time_ON;BEEP = 1;delay_us(time_ON);BEEP = 0;delay_us(time_OFF);
}
这段代码太糟糕了,才写了两种音阶我就受不了了。之前音调的信息都是宏定义,为了方便调用,我改成数组。
//beep.c
u16 CL[7]={262,294,330,349,392,440,494};
u16 CM[7]={523,587,659,698,784,880,988};
u16 CH[7]={1047,1175,1319,1397,1568,1760,1976};
u16 DL[7]={294,330,370,392,440,494,554};
u16 DM[7]={587,659,740,784,880,988,1109};
u16 DH[7]={1175,1319,1480,1568,1760,1976,2217};
u16 EL[7]={330,370,415,440,494,554,622};
u16 EM[7]={659,740,831,880,988,1109,1245};
u16 EH[7]={1319,1480,1661,1760,1976,0,0};
u16 FL[7]={349,392,440,466,523,587,659};
u16 FM[7]={698,784,880,932,1047,1175,1319};
u16 FH[7]={1397,1568,1760,1865,0,0,0};
然后修改演奏函数。
void play(u32 tone,u8 tvolum)
{u32 F_us; //特定频率对应的周期时间,单位usu32 time_ON; //蜂鸣器响的时间u32 time_OFF; //蜂鸣器不响的时间if(tone_level<1)tone_level = 1;else if(tone_level>12)tone_level = 12;switch(tone_level){case 1: F_us = 1000000/CL[tone];break;case 2: F_us = 1000000/CM[tone];break;case 3: F_us = 1000000/CH[tone];break;case 4: F_us = 1000000/DL[tone];break;case 5: F_us = 1000000/DM[tone];break;case 6: F_us = 1000000/DH[tone];break;case 7: F_us = 1000000/EL[tone];break;case 8: F_us = 1000000/EM[tone];break;case 9: F_us = 1000000/EH[tone];break;case 10:F_us = 1000000/FL[tone];break;case 11:F_us = 1000000/FM[tone];break;case 12:F_us = 1000000/FH[tone];break;}//F_us = 1000000/tone;time_ON = F_us>>tvolum;time_OFF = F_us - time_ON;BEEP = 1;delay_us(time_ON);BEEP = 0;delay_us(time_OFF);
}
主函数调用的部分也修改了。注意,数组的索引是从零开始的。
while(1){key = KEY_Scan(0);if(KEY8_PRES == key){LED2 = !LED2;tone_level++;}else if(PAUSE_PRES == key){LED1 = !LED1;tone_level--;}else{AllLED_OFF();}while(!SKEY1){SLED1 = LED_ON;play(0,volum);//数组的第一个元素是0}while(!SKEY2){SLED2 = LED_ON;play(1,volum);}while(!SKEY3){SLED3 = LED_ON;play(2,volum);}while(!SKEY4){SLED4 = LED_ON;play(3,volum);}while(!SKEY5){SLED5 = LED_ON;play(4,volum);}while(!SKEY6){SLED6 = LED_ON;play(5,volum);}while(!SKEY7){SLED7 = LED_ON;play(6,volum);}}
也可以把音阶的信息作为一个变量传入参数,避免使用全局的变量。
实际演奏时,还发现了小小的BUG,E和F的高音,数组不够7个,如果传入的参数是0,那么F_us的时候分母是0,程序可能卡死,所以把0音调改成1了。当然也可以用判断语句来避免这种情况。
我还设想了很多功能,比如屏幕显示个乐谱,屏幕显示音调;按键亮起作为提示,然后按下对应的按键,发出声音。想法越来越多,我只好赶紧收手了,毕竟,,,我原来的计划是打地鼠掌机啊!电子琴只是为了讲蜂鸣器的原理啊!
放上两只老虎的简谱,来弹奏一曲吧。