历史上的今天
返回首页

历史上的今天

今天是:2025年05月31日(星期六)

2019年05月31日 | STM32掌机教程8,背景音乐

2019-05-31 来源:eefocus

再用一个定时器

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


//改进此函数中的延时

void musicPlay(int length,unsigned char volume_level)

{

u8 i=0;

while(i {

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 {

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 {

i += BGM_LENGTH[temp];

}

END = i+BGM_LENGTH[temp];//计算音乐的开头

old_BGM = BGM;

BGM_change_flg = 0;//此处为1可以设为单曲循环。

}

if(i {

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;

}

}


}

}


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


推荐阅读

史海拾趣

问答坊 | AI 解惑

求BAV99这个二极管

如题,这个BAV99搞了很久还是没懂它到底是怎么用,网上查了是TVS,但是他有3个脚,具体要怎么接,多一个公共端,搞不懂要怎么用,,清高手指点一下…

查看全部问答>

降低FPGA功耗的设计技巧

使用这些设计技巧和ISE功能分析工具来控制功耗    新一代 FPGA的速度变得越来越快,密度变得越来越高,逻辑资源也越来越多。那么如何才能确保功耗不随这些一起增加呢?很多设计抉择可以影响系统的功耗,这些抉择包括从显见的器件选择到细小的 ...…

查看全部问答>

有没CE5.0下的半透明按钮控件(C#的)

EVC的不怎么看得懂(.net搞多了,变懒了!) …

查看全部问答>

Ctree 控件如何使头结点点击不收缩啊??

Ctree 控件如何使头结点点击不收缩啊??…

查看全部问答>

mc55 接受短信问题

当 给 mc55模块 发送 短信时,模块 为什么 无法 接受到 +CMT: \"+491795289609\",,\"03/08/14,14:44:49+08\"是甚么 原因造成的呢。但用AT+CMGR=28 可以 读取到短信。…

查看全部问答>

请教tilcon下如何设置显示数据的格式?

tilcon下可以在edit输入框窗口右键属性里Mask标签页的Format下输入如##.0来设置数据显示的格式,但是遇到负数只能显示正数,虽然在程序里读取的值是负数,如果在设置格式的同时也能显示负数,该怎么样做,请教这方面的高手,是在是困扰多日,谢谢 ...…

查看全部问答>

C语言深度剖析-----个人觉得挺好的

关于C语言的,个人觉得还行!~…

查看全部问答>

vmware 中怎么使用sharefolder

在虚拟机中安装了vmtools ,在虚拟机设置里也添加了共享文件夹,而且也在虚拟机linux中运行了vmware-config-tools.pl,但是我设置的那个文件夹似乎还是没有和windows7实现直接共享,无论在哪个系统中的共享文件夹中有什么操作,另一个系统的共享文件 ...…

查看全部问答>

邮箱问题

我从网上下了移到MSP430芯片上的操作系统,运行后一切正常,当我用到邮箱的时候就不对了,从中断发出的邮箱消息,在任务里收到值就不对了,发现邮箱的值是固定变他的,也就是说任务中收到的邮箱消息值是固定的几个数据,看了邮箱消息的物理地址,发 ...…

查看全部问答>