STM32掌机教程7,演奏音乐

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

使用定时器来计算时间

  在电子琴这节中,我们已经讲述了蜂鸣器的原理,知道如何用蜂鸣器演示不同音调的音乐,本节改进根据频率计算周期的方法,改为定时器,精确度更高,且不再阻塞CPU。
  首先,我们不再把蜂鸣器的控制引脚PB1作为普通IO,而是作为定时器的通道。在IO的初始化中,不应当继续操作PB1。通过查看数据手册,可以知道,PB1可以作为定时器3的通道4。(当然也可以作为定时器1和定时器8的通道,只不过定时器1和8是高级定时器,用起来稍微复杂一点点)。
通道的概念类似于道路。
在这里插入图片描述
在这里插入图片描述
  然后编写初始化函数。这段初始化函数可能比较复杂,我们暂时无需深究,只需要知道,这个定时器做了这么一件事情:
把原先这样的代码延时,交给了定时器自带的功能来实现:

	time_ON = F_us>>tvolum;
	time_OFF = F_us - time_ON;
	BEEP = 1;
	delay_us(time_ON);
	BEEP = 0;
	delay_us(time_OFF);

  定时器的用法是单片机里的重点和难点,不用想得太复杂,他就是个表嘛,只不过,他不能直接告诉你过了多少时间,他只知道数了多少个数字,也知道数一个数字用多长时间,两者结合,能算出过了多长时间。
在这里插入图片描述
  定时器3的初始化代码

//beep.c
void TIM3_PWM_Init(u16 arr,u16 psc)
{  
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	TIM_OCInitTypeDef  TIM_OCInitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);	
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);  
	 
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //TIM_CH4
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO
 
   //初始化TIM3   
	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 	
	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
	
	//初始化TIM3 Channel2 PWM模式	
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式2  每路产生
 	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
	TIM_OCInitStructure.TIM_Pulse=0;
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性,高低没有区别
	TIM_OC4Init(TIM3, &TIM_OCInitStructure);  //根据T指定的参数初始化外设TIM3 OC4

	TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable);  //使能TIM3在CCR4上的预装载寄存器
	TIM_ARRPreloadConfig(TIM3, ENABLE);
	TIM_Cmd(TIM3, ENABLE);  //使能TIM3
}

  我们在主函数中调用这个中断服务函数:

TIM3_PWM_Init(0xfffe,8); //蜂鸣器频率定时器初始化

  自动重装值决定什么时候定时器溢出,而分频系数决定定时器自加的频率,举例来说,定时器就像捉迷藏游戏中,负责捉人的小朋友,他要计数,好让大家躲起来。分频系数决定数数字的快慢,自动重装值决定数到几。
在这里插入图片描述

关于定时器溢出的时间计算,有个公式:

Tout = ((arr+1)*(psc+1))/Tclk

Tout 是溢出时间
arr是自动重装值,也可以称为周期或计数值,捉迷藏要数到10,那么代表arr就是9。默认情况下arr要+1,代表即便参数是0,也不会误操作。
psc就是分频系数,决定捉迷藏数多快。这个数字越大,代表数的越慢。
Tclk代表输入时钟,我们采用的是72Mhz
  定时器本质上是一个不断自加的计数器,只不过在自加的时候,能够自动比较计数值跟某个设定值而已。定时器+1用时多少?
  1/72000000,单位是秒。
  我想让数的慢一点,感觉72Mhz的时钟太快了,想用36Mhz可以吗?可以,2分频就行,这是+1的操作用时
  2/72000000,
  数100个数字用时多少?
  100 * 2/72000000
  在初始化的时候,我们传入的第一个参数是数多少个数字。第二个参数是数数字的速度。此处设置的频率是72/9=8Mhz。两个参数共同决定周期,由于第一个参数要根据频率改变,所以初始化的时候并不关心。

根据频率计算自动重装值

  接下来需要一个函数,把频率变为自动重装值。溢出时间 = 自动重装值+1/8000000,频率是时间的倒数,音调与频率有关,所以知道音调(频率)以后,可用以下方法计算自动重装值:

Autoreload=(8000000/usFraq)-1;  //频率变为自动重装值

  特别的,当音调是0的时候,不发出任何声音。我们可以采取关闭定时器的方法来静音,但是不方便。比较方便的方法就是设置比较值,当自动重装值小于比较值的时候,引脚输出高电平,否则,引脚输出低电平。按理说,把比较值设置为0xffff,则无论如何自动重装值都小于比较值就可以保持静音了,不过实测,此时会有噪音,尝试后发现,设置比较值为0的时候,可以静音,且没有噪音。可以调用库函数设置比较值。在频率很小或者大于20000时,人类都听不到,此时可以静音。
在这里插入图片描述

//beep.c
//蜂鸣器停止发声
void buzzerQuiet(void)
{
	TIM_SetCompare4(TIM3,0);
}

  在频率小于122的时候,我们也认为应当静音,是因为一方面,各音调的频率都大于122;另一方面,自动重装值最大是65535,此时对应的频率就是122。
  我们已经知道,音量由占空比决定,根据频率算出自动重装值以后,把它右移(相当于除以2的倍数)若干位,可以调整音量。

//beep.c
//蜂鸣器发出声音 
//usFreq即发声频率,volume_level是音量等级,1最高,到9几乎就听不到声音了。
void buzzerSound(unsigned short usFraq,unsigned char volume_level)   //usFraq是发声频率,即真实世界一个音调的频率。
{
	unsigned long Autoreload;
	if((usFraq<=122)||(usFraq>20000))
	{
		buzzerQuiet();
	}
	else
	{
		Autoreload=(8000000/usFraq)-1;  //频率变为自动重装值
		TIM_SetAutoreload(TIM3,Autoreload);
		TIM_SetCompare4(TIM3,Autoreload>>volume_level);   //音量  
	}
}

  特别强调一点,比较值与自动重装值比较大小,这个功能是定时器自带的,不需要在中断服务里比较。

主函数循环

  调用函数来演奏某个音乐是很简单的。我们先在主函数中演奏两只老虎的开头。

//main.c
int main(void)
{
	LED_Init();
	KEY_Init();
	delay_init();
	initIIC();
	initOLED();
	
	TIM3_PWM_Init(0xfffe,8); //蜂鸣器频率定时器初始化
	while(1)
	{
		buzzerSound(CM1,volum);
		delay_ms(250);
		buzzerSound(CM2,volum);
		delay_ms(250);
		buzzerSound(CM3,volum);
		delay_ms(250);
		buzzerSound(CM1,volum);
		delay_ms(250);
		buzzerSound(0,volum);
		delay_ms(250);
	}
}

从简谱到数组

  接下来我们尝试用数组来储存乐谱。我们要知道每个音符的音调和持续的时间,所以可以定义一个新的结构体:

typedef struct
{
	short mName; //音名
	short mTime; //时值,全音符,二分音符,四分音符
}tNote;

  然后我把两只老虎的乐谱改写如下:

const tNote AllBGM[]=
{
	//两只老虎  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},
};

  我们采用C调,中间的那个音阶,所有的音调都是CMx。每一小节都分4个音符,所以每个音符持续的时间都是某个常数的四分之一。这个常数我定义为TT,其实它与现实世界的没有直接的对应关系,数字小一点,唱的就快一点。如果音符有个下划线,那么持续的时间就是八分之一,再有个下划线,时间就是十六分之一。
  用于演奏音乐的函数如下:

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

  我传入的参数是数组的长度与音量,其实应当传入某个乐谱的指针,只是担心指针与结构体一起用,容易懵逼,因此乐谱作为了全局的变量。然后主函数值调用这个播放函数就可以了。

	while(1)
	{
		musicPlay(36,volum);
	}

代码放在这里,需要的就那去吧。(积分忘了调了,5分,有点贵)

猜你喜欢

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