STM32掌机教程8,背景音乐

版权声明:希望能帮助你,也希望你帮助别人,哪怕一点点 https://blog.csdn.net/geek_monkey/article/details/87283580

再用一个定时器

  在上一节,我们使用了一个定时器来计算频率。需要某个音符持续一定的时间的话,仍然使用的是延时函数delay_ms,这会导致CPU阻塞,程序运行到这里,CPU只会去数数字,你按下按键,他也检测不到——忙着数数字呢。接下来把这个延时也改成定时器,让定时器像个闹钟一样工作,让CPU该干什么干什么,时间到了以后,让定时器来提醒CPU。换句话说,播放的是背景音乐

//改进此函数中的延时
void musicPlay(int length,unsigned char volume_level)
{
	u8 i=0;
	while(i<length)
	{
		buzzerSound(AllBGM[i].mName,volume_level);
		delay_ms(AllBGM[i].mTime);
		i++;
	}
}

在这里插入图片描述
  定时器5的应用比定时器3还简单点——不需要输出PWM,只做计时用途。

//beep.c
void TIM5_Int_Init(u16 arr,u16 psc)
{
  TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //时钟使能
	
	//定时器TIM5初始化
	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
  TIM_ClearITPendingBit(TIM5, TIM_IT_Update  );  //清除TIMx更新中断标志 
	TIM_ITConfig(TIM5,TIM_IT_Update,ENABLE ); //使能指定的TIMx中断,允许更新中断

	//中断优先级NVIC设置
	NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;  //TIMx中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;  //先占优先级3级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;  //从优先级2级
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_Init(&NVIC_InitStructure);  //初始化NVIC寄存器

	TIM_Cmd(TIM5, ENABLE);  //使能TIMx					 
}

  我们把定时器5的初始化放在主函数的最前边,并且把音量也初始化,让掌机一上电就唱歌。死循环暂时什么都不写。

int main(void)
{
	BGM_volum = 6;//音量
	TIM5_Int_Init(9,7199);   //上电先播放背景音乐
    ...
   }

定时器5的中断服务

  定时器的中断服务函数无需调用,只要我们设定的时间到了以后,就会自动跳转到此函数中来执行。在分频系数为7199时,(自动重装值+1)/10=ms,所以,如果知道了需要延时的时间ms以后,ms * 10-1=自动重装值。
  我们可以把每一个包含了音调与时间的音符都看作一个珍珠,整个乐曲就像是珍珠项链。前一个音符根据频率与音量算出定时器3需要的自动重装值与比较值(定时器3的分频系数是确定的,9),根据延时的时间设置好定时器5的自动重装值(定时器5的分频系数也是确定的,7200,且无须比较值),然后开启定时器。
  CPU该干嘛干嘛。
  等到定时器5的时间到了,播放下一个音符,周而复始。思路与之前一样。显而易见,我们需要一个变量来记录当前播放到哪一个音符。我用了一个局部的静态变量,即便中断服务函数执行完了,静态变量的值也不会丢失。只要没有把乐谱播放完,就播放下一个音符。
在这里插入图片描述

//beep.c
//定时器5中断服务程序
void TIM5_IRQHandler(void)   
{
	static u16 i = 0;
	if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)  //检查TIM5更新中断发生与否
		{
			TIM_ClearITPendingBit(TIM5, TIM_IT_Update );  //清除TIMx更新中断标志 
			if(i<TWO_TIGER_LENGTH)
			{
				buzzerSound(AllBGM[i].mName,BGM_volum);
				TIM_SetAutoreload(TIM5,AllBGM[i].mTime*10-1);
				TIM_SetCounter(TIM5,0);
				i++;
			}
			else
				i = 0;
		}
}

  完成这些代码以后,即便主函数的死循环为空,歌曲也能后台播放了。

更多的BGM

  考虑到我要用的音效有好几个,所以我需要更多的BGM。其中,打中地鼠与生成地鼠的音乐还是我原创的,哈哈哈。这是我的乐谱:

//beep.c
const tNote AllBGM[]=
{
	//击中   5    BAD_BGM
	{CM1,TT1/4},{CL4,TT1/4},{CL3,TT1/8},{CL1,TT1/8},{0,TT1/8},
	// 生成  4   GOOD_BGM
	{CH1,TT1/4},{CH2,TT1/4},{CH3,TT1/4},{0,TT1/8},
	//两只老虎  36  BEGIN_BGM
	{CM1,TT/4},{CM2,TT/4},{CM3,TT/4},{CM1,TT/4},
	{CM1,TT/4},{CM2,TT/4},{CM3,TT/4},{CM1,TT/4},
	{CM3,TT/4},{CM4,TT/4},{CM5,TT/4},{0,TT/4},
	{CM3,TT/4},{CM4,TT/4},{CM5,TT/4},{0,TT/4},
	{CM5,TT/8},{CM6,TT/8},{CM5,TT/8},{CM4,TT/8},{CM3,TT/4},{CM1,TT/4},
	{CM5,TT/8},{CM6,TT/8},{CM5,TT/8},{CM4,TT/8},{CM3,TT/4},{CM1,TT/4},
	{CM1,TT/4},{CL5,TT/4},{CM1,TT/4},{0,TT/4},
	{CM1,TT/4},{CL5,TT/4},{CM1,TT/4},{0,TT/4},
	//小猪佩奇  12   LIFE_BGM
	{CH5,TTS/4},{CH3,TTS/8},{CH1,TTS/8},{CH2,TTS/4},{CM5,TTS/4},
	{CM5,TTS/8},{CM7,TTS/8},{CH2,TTS/8},{CH4,TTS/8},{CH3,TTS/4},{CH1,TTS/4},{0,TT/4},
	//坦克大战 33   LEVEL_BGM
	{CM1,TT1/4},{CM2,TT1/4},{0,TT1/4},{CM3,TTS/4},
	{CM1,TT1/4},{CM2,TT1/4},{0,TT1/4},{CM3,TTS/4},
	{CM3,TT1/4},{CM4,TT1/4},{0,TT1/4},{CM5,TTS/4},
	{CM3,TT1/4},{CM4,TT1/4},{0,TT1/4},{CM5,TTS/4},
	{CM4,TT1/4},{CM5,TT1/4},{0,TT1/4},{CM6,TTS/4},
	{CM4,TT1/4},{CM5,TT1/4},{0,TT1/4},{CM6,TTS/4},
	{CM5,TT1/4},{CM7,TT1/4},{0,TT1/4},{CH1,TTS/4},
	{CM5,TT1/4},{CM7,TT1/4},{0,TT1/4},{CH1,TTS/4},{0,TTS/4},
};

  如何切换歌曲呢?可以每首歌都是不同的结构体数组,然后用判断语句切换歌曲。我选择把所有的歌都放在一个结构体数组内,记录歌曲的开头与结尾的位置,并且为每首歌都按顺序编号,所以我还需要一些变量:

//beep.c
u8 BGM = 0;
u8 BGM_LENGTH[6] = {0,5,4,36,12,33};
u8 BGM_change_flg = 0;
u8 BGM_volum; 

  通过遍历BGM_LENGTH的数组,就可以找到每一首歌的开头和结尾的坐标了。而BGM_change_flg可用于标记背景音乐有没有改变。

按键切换BGM

  首先要修改定时器5的中断服务,来判断有没有换歌,如果没有,接着播放下一个音符;如果换了,遍历LENGTH数组来寻找新的数组的开头与结尾。
  当歌曲播放完成以后,我通过把BGM_change_flg与old_BGM清零,来实现这个需求:按下按键1可以播放音乐1,音乐1 播放完以后,再次按下按键1,仍然可以播放音乐1。

//beep.c
//定时器5中断服务程序
void TIM5_IRQHandler(void)   //TIM3中断
{
	static u16 i = 0;
	static u8 old_BGM = 0;
	static u16 END = 0;
	if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
		{
			TIM_ClearITPendingBit(TIM5, TIM_IT_Update );  //清除TIMx更新中断标志 
			if(old_BGM != BGM)//改变了BGM
			{
				u16 temp;
				i = 0;
				for(temp = 0;temp<BGM;temp++)//计算音乐的开头
				{
					i += BGM_LENGTH[temp];
				}
				END = i+BGM_LENGTH[temp];//计算音乐的开头
				old_BGM = BGM;
				BGM_change_flg = 0;//此处为1可以设为单曲循环。
			}
			if(i<END)
			{
				buzzerSound(AllBGM[i].mName,BGM_volum);
				TIM_SetAutoreload(TIM5,AllBGM[i].mTime*10-1);
				TIM_SetCounter(TIM5,0);
				i++;
			}
			else
			{
				buzzerSound(0,BGM_volum);//停止。
				if(BGM_change_flg)
				{
					BGM_change_flg = 0;
					old_BGM = 0;
				}
			}
		}
}

  主函数先写一段测试代码,按下不同的按键,可以切换不同的BGM。

//main.c
int main(void)
{
	BGM_volum = 6;//音量
	TIM5_Int_Init(9,7199);   //上电先播放背景音乐
	LED_Init();
	KEY_Init();
	delay_init();
	initIIC();
	initOLED();
	
	TIM3_PWM_Init(0xfffe,8); //蜂鸣器频率定时器初始化
	while(1)
	{
		key = KEY_Scan(0);
		if(key)       //如果按下按键
		{
			BGM_change_flg = 1;
			if(key == KEY1_PRES)//正确打中地鼠   加分,生成下一个地鼠
			{
				BGM = BAD_BGM;				
			}	
			else if(key == KEY2_PRES)
			{
				BGM = GOOD_BGM;
			}
			else if(key == KEY3_PRES)
			{
				BGM = BEGIN_BGM;
			}
			else if(key == KEY4_PRES)
			{
				BGM = LIFE_BGM;
			}
			else if(key == KEY5_PRES)
			{
				BGM = LEVEL_BGM;
			}
			else
			{
				BGM = 0;
			}
		}

	}
}

  到目前为止,背景音乐功能都写好了,最起码,掌机可以实现点唱机的功能了。应付下本科生的毕设,应该是没问题了。

猜你喜欢

转载自blog.csdn.net/geek_monkey/article/details/87283580