本节原来是想讲一讲无源蜂鸣器发声的原理,用于添加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; //特定频率对应的周期时间,单位us
u32 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; //特定频率对应的周期时间,单位us
u32 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; //特定频率对应的周期时间,单位us
u32 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了。当然也可以用判断语句来避免这种情况。
我还设想了很多功能,比如屏幕显示个乐谱,屏幕显示音调;按键亮起作为提示,然后按下对应的按键,发出声音。想法越来越多,我只好赶紧收手了,毕竟,,,我原来的计划是打地鼠掌机啊!电子琴只是为了讲蜂鸣器的原理啊!
放上两只老虎的简谱,来弹奏一曲吧。