再用一个定时器
在上一节,我们使用了一个定时器来计算频率。需要某个音符持续一定的时间的话,仍然使用的是延时函数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;
}
}
}
}
到目前为止,背景音乐功能都写好了,最起码,掌机可以实现点唱机的功能了。应付下本科生的毕设,应该是没问题了。