本节原来是想讲一讲无源蜂鸣器发声的原理,用于添加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了。当然也可以用判断语句来避免这种情况。
我还设想了很多功能,比如屏幕显示个乐谱,屏幕显示音调;按键亮起作为提示,然后按下对应的按键,发出声音。想法越来越多,我只好赶紧收手了,毕竟,,,我原来的计划是打地鼠掌机啊!电子琴只是为了讲蜂鸣器的原理啊!
放上两只老虎的简谱,来弹奏一曲吧。
上一篇:STM32掌机教程7,演奏音乐
下一篇:STM32掌机教程5,程序框架,随机,加命与升级
推荐阅读最新更新时间:2024-11-09 11:22
设计资源 培训 开发板 精华推荐
- 有奖直播:TI 低功耗 MCU 产品和 Zigbee 无线解决方案
- PI HiperPFS-4系列功率因数校正IC,了解产品特色,答题享好礼!
- 下载有礼|精华课件汇编:泰克半导体器件的表征及可靠性研究交流会暨吉时利测试测量年度研讨会
- ADI有奖直播:基准电压源产品技术及应用选择 7月25日上午10:00-11:30 不容错过~
- 有奖直播|Nexperia针对车联网应用的高效ESD解决方案
- 发现之旅,让你身边的电源无处可藏!
- 有奖直播|相约Keysight World 2019“汽车电子及新能源汽车测试”论坛
- MPS有奖活动|绿色AI“芯” 动力,探索节能低碳的AI供电方案!
- 直播:计算机视觉影像处理应用于智能驾驶的未来及挑战