51 单片机[11]:蜂鸣器播放提示音和音乐
1. 蜂鸣器
蜂鸣器是一种将电信号转换为声音信号的器件,常用来产生设备的按键音、报警音等提示信号。
蜂鸣器按驱动方式可分为有源蜂鸣器和无源蜂鸣器。
- 有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定。
- 无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音。
51开发板原理图上的蜂鸣器:
51单片机开发板上,蜂鸣器连接在ULN2003D上。这种连接方式属于集成电路驱动,比较少见,一般用三极管驱动。下图展示了用三极管驱动蜂鸣器的原理图。
ULN2003D 是单片集成的高耐压、大电流达林顿管阵列,电路内部包含 7 个独立的达林顿管驱动通道。电路内部设计有续流二极管,可用于驱动继
电器、步进电机等电感性负载。单个达林顿管集电极可输出 500mA 电流,将多个通道并联可实现更高的输出电流能力。该电路可广泛应用于继电器驱动、照明驱动、显示屏驱动(LED)、步进电机驱动和逻辑缓冲器。
达林顿管驱动电路图如下图所示。
ULN2003D数据手册中的逻辑图如下图所示。
由逻辑图和原理图可知,只要控制 U14 的引脚5,就可以控制蜂鸣器。
注意:无源蜂鸣器不能一直通电,容易被烧毁。无源蜂鸣器通常由一个线圈和一个振动膜组成,当给无源蜂鸣器施加变化的电压信号时,线圈中的电流会产生变化的磁场,这使得与线圈相连的振动膜振动,从而产生声音。
由开发板的原理图可知,蜂鸣器是默认被使用的。因为单片机上电后P2口默认是高电平,那么P25为1,BEEP就为0。这个开发板因为有限流电阻,所以没有烧。
2. 乐理
在让蜂鸣器播放声音之前,需要补充一些乐理知识:
3. 蜂鸣器播放提示音
3.1 独立按键控制蜂鸣器播放提示音
新建项目,把定时器时钟项目的Delay.c和Delay.h文件,按键控制LED流水灯模式项目的Key.c和Key.h和模块化编程项目的Digital.c和Digital.h复制过来,然后添加进项目。
在main.c中包含头文件:
#include <REGX52.H>
#include "Delay.h"
#include "Digital.h"
#include "Key.h"
先写一个检测独立按键键码的程序。按下K1,数码管显示1,按下K2,数码管显示2……
unsigned char KeyNum;
void main()
{
digital(1, 0);//什么都不按,显示0
while(1)
{
KeyNum = Key();//返回键码值
if(KeyNum)
{
digital(1, KeyNum);//在数码管第一位显示KeyNum
}
}
}
下面实现显示键码的同时,蜂鸣器发声。定义一个Buzzer
控制蜂鸣器,定义一个循环次数i
控制蜂鸣器振动时长。
sbit Buzzer = P2^5;
unsigned char KeyNum;
unsigned int i;
void main()
{
digital(1, 0);//什么都不按,显示0
while(1)
{
KeyNum = Key();//返回键码值
if(KeyNum)
{
for(i=0;i<100;i++)//以500Hz的频率响100毫秒
{
Buzzer = !Buzzer;//翻转100次
Delay(1);
}
digital(1, KeyNum);//在数码管第一位显示KeyNum
}
}
}
为什么是500Hz?
高电平1ms、低电平1ms, 1 ÷ ( 0.001 s + 0.001 s ) = 500 Hz 1÷(0.001 \text{s} + 0.001 \text{s})=500\text{Hz} 1÷(0.001s+0.001s)=500Hz
3.2 程序模块化
新建Buzzer.c和Buzzer.h文件。
Buzzer.h:
#ifndef __BUZZER_H__
#define __BUZZER_H__
void Buzzer_Time(unsigned int ms);
#endif
在Buzzer.c文件中,定义Buzzer_Time
函数,把刚刚的for循环封装到Buzzer_Time
函数里
void Buzzer_Time(unsigned int ms) //@12.000MHz
{
for(i=0;i<ms;i++)//以500Hz的频率响ms毫秒
{
Buzzer = !Buzzer;//翻转ms*2次
Delay(1);
}
}
新定义一个函数,替换一下Delay(1);
在STC-ISP上的“软件延时计算器”中生成Delay500us()
函数
给函数名加个蜂鸣器前缀,把这个函数作为蜂鸣器私有函数。
void Buzzer_Delay500us() //@12.000MHz
{
unsigned char i;
_nop_();
i = 247;
while (--i);
}
注意,调用_nop_();
函数,需要包含头文件INTRINS.H
现在可以修改一下Buzzer_Time
函数
void Buzzer_Time(unsigned int ms) //@12.000MHz
{
for(i=0;i<ms*2;i++)//以1000Hz的频率响ms毫秒
{
Buzzer = !Buzzer;//翻转ms*2次
Buzzer_Delay500us();//每次循环延时500us
}
}
独立按键按一下响100毫秒。
4. 蜂鸣器播放音乐
本节用定时器中断来控制蜂鸣器的状态
需要定时器时钟项目中的Delay.h、Delay.c、Timer0.c和Timer0.h。
先做一下准备工作,在main.c中包含头文件
#include <REGX52.H>
#include "Delay.h"
#include "Timer0.h"
声明一下位端口,搭好主函数
sbit Buzzer = P2^5;
void main()
{
Timer0Init();
while(1)
{
}
}
其中Timer0Init()
是1毫秒的定时器中断(在Timer0.c文件中)。
把Timer0.c中的定时器中断函数模板复制过来。定时器0溢出后触发中断。Timer0_Rountine()
函数体内部是中断后执行的操作。
void Timer0_Rountine() interrupt 1
{
static unsigned int T0Count; //静态变量,退出函数后不丢失值
T0Count++;
TL0 = 0x18; //重新赋初值,防止溢出后从0开始计数
TH0 = 0xFC;
if(T0Count>=1000) //每隔一秒执行一次
{
T0Count = 0;
}
}
把函数修改为中断后翻转蜂鸣器状态:
void Timer0_Rountine() interrupt 1
{
TL0 = 0x18; //重新赋初值,防止溢出后从0开始计数
TH0 = 0xFC;
Buzzer = !Buzzer;
}
编译一下,下载程序,给单片机上电,发现蜂鸣器一直响。
为什么蜂鸣器一直响?
因为主函数中Timer0Init()
每隔1毫秒触发一次定时器中断,每次中断执行的任务都是翻转Buzzer的状态,这就相当于给蜂鸣器输入了一个频率为 500Hz 的方波,导致它持续发声。
8051单片机的机器周期是晶振频率的1/12,因此在12MHz晶振下,机器周期为
机器周期 = 1 12 × 1 0 6 = 1 μ s 机器周期=\frac{1}{12\times10^6}=1\mu \text{s} 机器周期=12×1061=1μs
0xFFFF − 0xFC18 = 0x3E7 = 999 \text{0xFFFF} - \text{0xFC18} = \text{0x3E7} = 999 0xFFFF−0xFC18=0x3E7=999,即计数器需要从0xFC18计数999次才能溢出。
每次计数时间为1微秒, 999 × 1 μ s ≈ 1 ms 999\times 1\mu \text{s} \approx 1\text{ms} 999×1μs≈1ms,所以计数器每过1毫秒就会溢出一次,触发中断。
高电平 1 ms + 低电平 2 ms = 一个周期 2 ms 高电平1\text{ms}+低电平2\text{ms}=一个周期2\text{ms} 高电平1ms+低电平2ms=一个周期2ms
1 ÷ 0.002 s = 500 Hz 1÷0.002\text{s}=500\text{Hz} 1÷0.002s=500Hz
main()
中的 while(1)
循环是空的,没有任何条件来控制蜂鸣器的开关。
每次定时器中断都会反转蜂鸣器的状态,因此蜂鸣器持续工作。
如何调整蜂鸣器振动频率?
改变 TL0
和 TH0
可以调整定时器溢出的速度,从而改变蜂鸣器的振荡频率。注意是改变Timer0_Rountine()
中的 TL0
和 TH0
,而不是改变Timer0Init()
中的 TL0
和 TH0
。Timer0Init()
中的 TL0
和 TH0
只决定了第一次中断的时间,以后的中断时间由Timer0_Rountine()
中的 TL0
和 TH0
决定。
如果Timer0Init()
中的 TL0
和 TH0
变大,那溢出的时间就变短,周期就变短,频率就变高。
如果要播放中央C音符,它的重装载值是64580。
只需要给Timer0Init()
中的 TL0
和 TH0
赋新值即可
TL0 = 64580%256;//取余,放在低位
TH0 = 64580/256;//取整,放在高位
为了方便找音符对应的重装载值,把低、中、高音放在数组中
unsigned int FreqTable[] = {
63628,63731,63835,63928,64021,64103,64185,64260,64331,64400,64463,64528,//低音
64580,64633,64684,64732,64777,64820,64860,64898,64934,64968,65000,65030,//中音
65058,65085,65110,65134,65157,65178,65198,65217,65235,65252,65268,65283//高音
};
这样就能快速索引到想要的音符,如低音1直接写成FreqTable[0]
即可:
TL0 = FreqTable[1]%256;
TH0 = FreqTable[1]/256;
试试好不好用,定义一个频率选择变量FreqSelect
unsigned char FreqSelect;
void main()
{
Timer0Init();
while(1)
{
FreqSelect++;
Delay(500);
}
}
void Timer0_Rountine() interrupt 1
{
//重新给TL0和TH0赋初值,防止溢出后从0开始计数
TL0 = FreqTable[FreqSelect]%256;
TH0 = FreqTable[FreqSelect]/256;
Buzzer = !Buzzer;
}
可以听到,音调越来越高。
如何指定音调,响出曲子《小星星》呢?定义一个数组Music[]
,根据索引表格把数填进去,再定义一个MusicSelect
去选Music[]
里的数。
unsigned char FreqSelect, MusicSelect;
unsigned char Music[] = {12,12,19,19,21,21,19,17,17,16,16,14,14,12};
void main()
{
Timer0Init();
while(1)
{
FreqSelect = Music[MusicSelect];
MusicSelect++;
Delay(500);
}
}
void Timer0_Rountine() interrupt 1
{
//重新给TL0和TH0赋初值,防止溢出后从0开始计数
TL0 = FreqTable[FreqSelect]%256;
TH0 = FreqTable[FreqSelect]/256;
Buzzer = !Buzzer;
}
编译一下,发现这些音之间没有停顿,需要通过控制定时器TR0加一个停顿。
void main()
{
Timer0Init();
while(1)
{
FreqSelect = Music[MusicSelect];
MusicSelect++;
Delay(500);
//断奏
TR0 = 0;//关掉定时器
Delay(5);
TR0 = 1;//打开定时器
}
}
虽然这下有了停顿,但是像“5 - ”这种带延时符号的,时长不对。可以再定义一个数组控制音符时长,也可以更改Music[]
,音符后面紧跟音符时长。这里选择后者。
unsigned char Music[] = {
13,4,
13,4,
20,4,
20,4,
22,4,
22,4,
20,4+4,
18,4,
18,4,
17,4,
17,4,
15,4,
15,4,
13,4+4,
};
音符的时长,可以以最短的时值作为1来写。如果最短的是四分音符,那么把四分音符当作1,二分音符就为2,全音符就为4。最后只需要用一个系数乘以这些1、2、4就能表示时长。在这里选用125(十六分音符的时长)作为这个系数。
void main()
{
Timer0Init();
while(1)
{
FreqSelect = Music[MusicSelect];//取的是音调
MusicSelect++;//此时Music[MusicSelect]是时长
Delay(125*Music[MusicSelect]);
MusicSelect++;
//断奏
TR0 = 0;//关掉定时器
Delay(5);
TR0 = 1;//打开定时器
}
}
为了方便更改系数,做一个宏定义
#define SPEED 500
把Delay(125*Music[MusicSelect]);
改为
Delay(SPEED/4*Music[MusicSelect]);
这样每次只需要调整SPEED既可以调整速度。
现在没有休止符0,需要定义休止符,只需要在FreqTable[]
中加入0即可。注意更改Music[]
中的索引值。
现在还有一个问题,播完设定的音乐后,蜂鸣器就乱响了,需要加一个终止判断。在Music[]
的最后加一个0xFF
,然后在主函数中加一个终止判断:
void main()
{
Timer0Init();
while(1)
{
if(Music[MusicSelect]!=0xFF)
{
FreqSelect = Music[MusicSelect];//音调
MusicSelect++;//此时Music[MusicSelect]是时长
Delay(SPEED/4*Music[MusicSelect]);
MusicSelect++;
//断奏
TR0 = 0;//关掉定时器
Delay(5);
TR0 = 1;//打开定时器
}
else
{
TR0 = 0;
while(1);
}
}
}