STM32掌机教程6,电子琴

  本节原来是想讲一讲无源蜂鸣器发声的原理,用于添加BGM功能。为了讲原理,就写了一些通俗的代码,没想到越写越多,后来,干脆就形成了一个小小的项目吧——基于STM32与无源蜂鸣器的电子琴。

灯光效果

  首先想到的是做一个灯光的效果,按下哪个按键,哪个按键的灯要亮;松手后,灯灭掉。顺带,检测一下带松手检测的按键功能好不好用。后续还可以做成通过亮灯提示需要按下那个按键,类似于节奏大师的功能——哪里要响点哪里。
STM32掌机教程6,电子琴_第1张图片
STM32掌机教程6,电子琴_第2张图片

  我去掉了无关的代码,主函数里通过死循环,来确保按键按下的时候,灯是亮起来的 :

//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;
		}
	}

  下载程序并看看现象。

无源蜂鸣器的音调控制

  音调和频率是息息相关的,可以在网上查找到频率和音调对应的表格。本文的代码参考了这篇文章,表示感谢
STM32掌机教程6,电子琴_第3张图片
  根据图片,可以做宏定义,中音的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,就可以听到不同的音调。原理就是这么简单。

音量控制

  无源蜂鸣器可以用高电平持续的时间调整音量,在一个周期中,高电平持续的时间越长,蜂鸣器声音越大;高电平持续的时间越短,蜂鸣器的声音越小。这句话还有一个时髦的描述方法——脉宽调制。
STM32掌机教程6,电子琴_第4张图片
  原理很简单,实现起来也不复杂。在上一个案例的基础上,我把高电平持续的时间由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都既能控制音调,用能控制音量了,别的按键把代码复制粘贴就能实现功能了。只不过,复制粘贴是代码不好的表现。
STM32掌机教程6,电子琴_第5张图片
  所以,再次提取出一个函数,传入音调和音量,就能发出声音。

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掌机教程6,电子琴_第6张图片
STM32掌机教程6,电子琴_第7张图片

你可能感兴趣的:(掌机,STM32)