系列文章目录
前言
之前有做过一个生日礼物:
基于51单片机的心形流水灯+无源蜂鸣器播放音乐
能播放流水灯,同时播放生日快乐歌,现在做一个只有音乐播放的。
用的是普中A2开发板,单片机是STC89C52RC,用到了板上的无源蜂鸣器。
【频率】12T@11.0592MHz
效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。
一、效果展示
二、原理分析
1、声音的产生
声音的产生是由于物体的振动,例如,人说话是靠声带振动。无源蜂鸣器也是一样,通电的时候,电流流过蜂鸣器内部的线圈,线圈产生磁场,把内部的永磁体吸过来,断电的时候,线圈无磁场,永磁体回到原来位置,永磁铁带动振动薄膜来回运动。如果通电0.5ms,断电0.5ms,再通电0.5ms,如此反复,则振动的周期是1ms,就会发出1KHz的声音,改变通电或断电的时间,就可以改变频率了。
2、音符、时值
不同的音符对应不同的频率,不同的频率对应不同的定时器初值(通过定时器中断函数让无源蜂鸣器的引脚的电平翻转),这个需要提前计算好数据。不懂的建议看一下江协科技的51单片机入门教程,讲得很详细。
时值就是每个音符持续的时间,以《阳光彩虹小白马》为例,♩=96,表示1分钟有96个四分音符,即每个四分音符的时长为60/96s=0.625s=625ms,即16分音符的时长为625ms/4=156.25ms,取整为156ms(由于音符之间有短暂的停顿和代码运行误差,节奏偏慢,本代码中改成了145ms),以此为最小单位(因为简谱中有16分音符)。
定时器0控制时值,定时器1控制音符,并且要设置定时器1的优先级较高,否则无源蜂鸣器的发声会收到影响。
用这两个定时器就可以控制无源蜂鸣器发出音乐了,主函数的主循环中可以执行其他代码。
文末有《阳光彩虹小白马》的简谱。
3、注意事项
相邻两个音符之间需要有一定的间隔,否则如果两个相邻的音符是一样的话,就分不出是两个音符了。代码中设置了5ms的时间间隔,这个也是上面所说的节奏偏慢的原因之一。
三、各模块代码
1、音乐数据头文件
h文件
#ifndef __MUSIC_H__
#define __MUSIC_H__
//十六分音符的时长(ms)
//一分钟96个四分音符,根据计算,十六分音符的时长约为156ms(156.25ms)
//实测播放偏慢,所以将时长改小一点,改成145ms
# define Duration 145
//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音,下划线:升半音符号#
#define P 0
#define L1 1
#define L1_ 2
#define L2 3
#define L2_ 4
#define L3 5
#define L4 6
#define L4_ 7
#define L5 8
#define L5_ 9
#define L6 10
#define L6_ 11
#define L7 12
#define M1 13
#define M1_ 14
#define M2 15
#define M2_ 16
#define M3 17
#define M4 18
#define M4_ 19
#define M5 20
#define M5_ 21
#define M6 22
#define M6_ 23
#define M7 24
#define H1 25
#define H1_ 26
#define H2 27
#define H2_ 28
#define H3 29
#define H4 30
#define H4_ 31
#define H5 32
#define H5_ 33
#define H6 34
#define H6_ 35
#define H7 36
//索引与频率(定时器重装值)对照表
unsigned int code FreqTable[]={
0, //休止符,即进入定时器1中断程序后之后,蜂鸣器电平不翻转
63628,63731,63835,63928,64021,64103,64185,64260,64331,64400,64463,64524,
64580,64633,64684,64732,64777,64820,64860,64898,64934,64968,65000,65030,
65058,65085,65110,65134,65157,65178,65198,65217,65235,65252,65268,65283,
};
//乐谱
unsigned char code Music[]={
//两个一组,分别是音符和时值
//音符(频率对应的定时器重装值)
//时值(十六分音符时长的倍数)
/*《阳光彩虹小白马》*/
//原来是G调,这里按C调播放
0, //第0个数据为0,目的是主函数中开启中断后,能立刻进入定时器0的中断函数进行频率和时值的赋值
//怎么能
P,4,P,4,P,2,L5,2,L6,2,M1,2,
//哭呢,一切会
M5,2,M3,6,P,2,M5,2,M2,2,M1,2,
//好哒,一切都
M3,2,M2,6,P,2,M5,2,M2,2,M1,2,
//去吧,你就得
M2,2,M2,6,P,2,M3,2,M1,2,L6,2,
//想着。既然没
M2,2,M1,6,P,2,L5,2,L6,2,M1,2,
//办法,还很他
M5,2,M3,6,P,2,M5,2,M2,2,M1,2,
//干嘛,还管它
M3,2,M2,6,P,2,M5,2,M2,2,M1,2,
//干嘛,心里要
M3,2,M2,6,P,2,M3,2,L6,2,M1,2,
//记得。你是
M1,2,M1,6,P,4,P,2,M3,1,M4,1,
//内内个内内内个内个内内
M5,2,M5,1,M3,1,M5,2,M5,2,M5,1,M3,1,M5,1,M6,1,M5,2,M5,2,
//内内个内内内个内个内内
M5,2,M5,1,M3,1,M5,2,M5,2,M5,1,M3,1,M5,1,M6,1,M5,2,M5,2,
//阳光彩虹小白马,
M5,2,M3,2,M2,2,M1,2,L5,2,M1,2,M1,4,
//滴滴哒滴滴哒。
M3,2,M3,1,M2,1,M3,2,M2,2,M1,6,P,2,
0xFF, //终止标记
};
#endif
2、定时器0
h文件
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0_Init(void);
#endif
c文件
#include <REGX52.H>
/**
* 函 数:定时器0初始化
* 参 数:无
* 返 回 值:无
*/
void Timer0_Init(void)
{
// AUXR&=0x7F; //定时器时钟12T模式(STC89C52RC是12T单片机,无需设置)
TMOD&=0xF0; //设置定时器模式(高四位不变,低四位清零)
TMOD|=0x01; //设置定时器模式(通过低四位设为16位不自动重装)
TL0=0x18; //设置定时初值,定时1ms,12T@11.0592MHz
TH0=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
TF0=0; //清除TF0标志
TR0=1; //定时器0开始计时
ET0=1; //打开定时器0中断允许
EA=1; //打开总中断
PT0=0; //当PT0=0时,定时器0为低优先级,当PT0=1时,定时器0为高优先级
}
/*定时器中断函数模板
void Timer0_Routine() interrupt 1 //定时器0中断函数
{
static unsigned int T0Count; //定义静态变量
TL0=0x18; //设置定时初值,定时1ms,12T@11.0592MHz
TH0=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
3、定时器1
h文件
#ifndef __TIMER1_H__
#define __TIMER1_H__
void Timer1_Init(void);
#endif
c文件
#include <REGX52.H>
/**
* 函 数:定时器1初始化
* 参 数:无
* 返 回 值:无
*/
void Timer1_Init(void)
{
// AUXR&=0xBF; //定时器时钟12T模式(STC89C52RC是12T单片机,无需设置)
TMOD&=0x0F; //设置定时器模式(低四位不变,高四位清零)
TMOD|=0x10; //设置定时器模式(通过高四位设为16位不自动重装的模式)
TL1=0x66; //设置定时初值,定时1ms,12T@11.0592MHz
TH1=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
TF1=0; //清除TF1标志
TR1=1; //定时器1开始计时
ET1=1; //打开定时器1中断允许
EA=1; //打开总中断
PT1=1; //当PT1=0时,定时器1为低优先级,当PT1=1时,定时器1为高优先级
}
/*定时器中断函数模板
void Timer1_Routine() interrupt 3 //定时器1中断函数
{
static unsigned int T1Count; //定义静态变量
TL1=0x66; //设置定时初值,定时1ms,12T@11.0592MHz
TH1=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
T1Count++;
if(T1Count>=1000)
{
T1Count=0;
}
}
*/
四、主函数
main.c
/*by甘腾胜@20250330
【效果查看/操作演示】B站搜索“甘腾胜”或“gantengsheng”查看
【单片机】STC89C52RC
【频率】12T@11.0592MHz
【外设】无源蜂鸣器
【简单的原理分析】https://blog.csdn.net/gantengsheng/article/details/143581157
*/
#include <REGX52.H>
#include "Music.h"
#include "Timer0.h"
#include "Timer1.h"
sbit Buzzer=P2^5;
unsigned char FreqSelect; //频率选择变量
unsigned int MusicSelect; //音乐选择变量,用来选择音符的频率和时长
bit TimeIntervalFlag=1; //控制音符之间短暂间隔(5ms)的标志
/**
* 函 数:主函数(有且仅有一个)
* 参 数:无
* 返 回 值:无
* 说 明:主函数是程序执行的起点,负责执行整个程序的主要逻辑
*/
void main()
{
P0=0x00; //防止开发板的数码管亮
Timer0_Init(); //定时器0初始化
Timer1_Init(); //定时器1初始化
while(1)
{
}
}
/**
* 函 数:定时器0中断函数
* 参 数:无
* 返 回 值:无
*/
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count1,T0Count2; //定义静态变量
TL0=0x18; //设置定时初值,定时1ms,12T@11.0592MHz
TH0=0xFC; //设置定时初值,定时1ms,12T@11.0592MHz
T0Count1++;
T0Count2++;
if(T0Count1>=Music[MusicSelect]*Duration) //控制每个音符持续的时长
{ //如果上一个音符的时长到了(即如果时间=倍数*十六分音符的时长),则进入此函数,重新赋值
T0Count1=0;
//如果是终止标志0xFF,则音乐选择变量清零,重新开始播放
if(Music[MusicSelect+1]==0xFF){MusicSelect=0;}
MusicSelect++; //音乐选择变量自增,此时对应Music数组里音符频率的索引
FreqSelect=Music[MusicSelect]; //将音符的频率的索引赋值给频率选择变量FreqSelect
//音乐选择变量自增,此时对应Music数组里↑上面频率的时长数据(十六分音符时长的倍数)
//用来控制从现在到下次进入此函数的时间长度
MusicSelect++;
TimeIntervalFlag=1; //控制音符之间短暂间隔(5ms)的标志置1
TR1=0; //定时器1暂停计时,蜂鸣器不发声
T0Count2=0; //T0Count2清零
}
if(T0Count2>=5 && TimeIntervalFlag==1) //5ms
{
T0Count2=0;
TimeIntervalFlag=0; //控制音符之间短暂间隔(5ms)的标志置0,在置1前不会进入此函数
TR1=1; //定时器1继续计时,蜂鸣器继续发声
}
}
/**
* 函 数:定时器1中断函数
* 参 数:无
* 返 回 值:无
*/
void Timer1_Routine() interrupt 3
{
if(FreqTable[FreqSelect]) //如果不是休止符
{
/*取对应频率值的重装载值到定时器*/
TL1 = FreqTable[FreqSelect]%256; //设置定时初值
TH1 = FreqTable[FreqSelect]/256; //设置定时初值
Buzzer=!Buzzer; //翻转蜂鸣器IO口
}
}
附录
《阳光彩虹小白马》简谱