基于51单片机和无源蜂鸣器的音乐播放

系列文章目录


前言

之前有做过一个生日礼物:
基于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口
 	}
}

附录

《阳光彩虹小白马》简谱
在这里插入图片描述

### 关于普中A2板的仿真教程及相关信息 #### 1. 普中A2板简介 普中A2板是一款基于STM32系列微控制器的开发板,广泛应用于嵌入式系统的教学与开发。其核心处理器通常为STM32F103或其他型号,支持多种外设接口功能扩展[^2]。 #### 2. 开发环境配置 为了在普中A2板上进行开发,需先完成开发环境的搭建。以下是具体说明: - **安装Keil MDK** 需要下载并安装最新本的Keil MDK工具链(如MDK5)。此工具用于编写、编译调试代码[^1]。 - **驱动程序安装** 如果使用USB转串口通信或JTAG/SWD调试接口,则需要安装对应的驱动程序。例如CH340驱动适用于部分USB-TTL模块[^3]。 - **固件库导入** 根据所使用的MCU型号(如STM32F103),从官方资获取标准外设库或HAL库,并将其集成到项目中。这一步骤有助于简化GPIO及其他外设的操作[^4]。 #### 3. 仿真与调试方法 对于普中A2板而言,仿真是验证代码逻辑的重要手段之一。以下是两种常见的仿真方式及其设置流程: - **软件仿真** 利用Keil自带的功能可以模拟目标芯片的行为而无需实际硬件参与。只需确保工程参数正确配置即可启动仿真模式[^1]。 - **硬件仿真** 推荐采用ST-LINK/V2等专用调试探针配合RealView Interface Adapter (RVI) 或其他兼容设备来实现在线调试。通过这种方式能够实时观察寄存器状态以及变量变化情况[^5]。 #### 4. 编程实例——LED流水灯 下面展示了一段简单的C语言码片段,展示了如何利用GPIO端口驱动八个独立LED形成流动显示效果: ```c #include "stm32f1xx_hal.h" void SystemClock_Config(void); static void MX_GPIO_Init(void); int main(void){ HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while(1){ for(int i=0;i<8;i++){ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0 << i , GPIO_PIN_SET); //点亮当前位 HAL_Delay(100); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0 << i , GPIO_PIN_RESET); //熄灭当前位 } } } static void MX_GPIO_Init(void){ __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; /**Configure pins as Output */ GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3| GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA,&GPIO_InitStruct); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值