使用定时器来计算时间
在电子琴这节中,我们已经讲述了蜂鸣器的原理,知道如何用蜂鸣器演示不同音调的音乐,本节改进根据频率计算周期的方法,改为定时器,精确度更高,且不再阻塞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分,有点贵)