目录
[1. 全局变量定义](#1. 全局变量定义)
[2. 主函数 main()](#2. 主函数 main())
[3. 定时器0中断服务程序 Timer0_Routine()](#3. 定时器0中断服务程序 Timer0_Routine())
[3. 音乐数据组织](#3. 音乐数据组织)
[1.频率表 FreqTable[]:](#1.频率表 FreqTable[]:)
[2.音乐数据 Music[]:](#2.音乐数据 Music[]:)
[LCD1602 基本操作函数解析](#LCD1602 基本操作函数解析)
[1. 写命令函数 (lcd1602_write_cdm)](#1. 写命令函数 (lcd1602_write_cdm))
[2. 写数据函数 (lcd1602_write_date)](#2. 写数据函数 (lcd1602_write_date))
[3. 初始化函数 (lcd1602_init)](#3. 初始化函数 (lcd1602_init))
[4. 清屏函数 (lcd1602_clear)](#4. 清屏函数 (lcd1602_clear))
[5. 字符串显示函数 (lcd1602_show_string)](#5. 字符串显示函数 (lcd1602_show_string))
1.设计背景
基于51单片机的电子音乐盒是一种嵌入式系统,旨在利用单片机的数字信号处理能力和可编程性,实现音乐的合成、存储和播放,并且可以通过按键等方式进行控制。该设计可以应用于玩具、礼品、装饰品等领域,为用户提供精美、便捷的音乐体验。
设计功能:
1)设计基于单片机的伴奏乐曲
2)设计基于单片机的彩屏图片
3)通过矩阵键盘按键显示不同彩屏图片,并伴有不同乐曲
创新部分(自主设计)
设计清单:
|----|-------------|----|
| 序号 | 器件名称 | 数量 |
| 1 | STC89C52开发板 | 1 |
| 2 | 蜂鸣器模块 | 1 |
| 3 | LCD显示屏 | 1 |
| 4 | 矩阵键盘模块 | 1 |
2. 模块简介
蜂鸣器是一种一体化结构的电子讯响器,采用直流电压供电,广泛应用于计算机、 打印机、 复印机、 报警器、 电子玩具、 汽车电子设备、 电话机、 定时器等电子产品中作发声器件。 蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。压电式蜂鸣器主要由多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱、外壳等组成。多谐振荡器由晶体管或集成电路构成,当接通电源后(1.5~15V 直流工作电压),多谐振荡器起振,输出 1.5~5kHZ 的音频信号,阻抗匹配器推动压电蜂鸣片发声。

3.硬件设计
实现蜂鸣器的控制,我们能否 直接使用单片机的 IO 口驱动呢?答案是否定的,因为51单片机IO口的驱动能 力较弱(即使外接上拉电阻),而蜂鸣器驱动需要约30mA,所以非常困难,即 使可以驱动,那对于整个芯片的其它IO剩下驱动能力就更加弱甚至无法工作。 所以我们不会直接使用 IO 口驱动蜂鸣器,而是通过三极管把电流放大后再驱动 所以我们也经常说到51单片机是用来做控制的,而不是驱动。 蜂鸣器,这样51单片机的 IO 口只需要提供不到1mA 的电流就可控制蜂鸣器。 我的开发板上的蜂鸣器模块电路如下图所示:

从图中可以看出,蜂鸣器控制管脚直接连接到51单片机的P2.5管脚上。图 中并没有使用三极管进行电流放大,而是使用ULN2003芯片来驱动,大家暂时只需知道当P25输出高电平,BEEP则输出低电平;当P25输出低电平,BEEP则输出高电平,类似一个非门。
开发板上使用的是无源蜂鸣器,它需要一定频率的脉冲(高低电平)才会发 声,因此需要让P25脚以一定频率不断输出高低电平信号才能控制蜂鸣器发出声音。
4.识谱
既然要通过蜂鸣器来发出音乐,那必然需要认识一定的基本乐理识谱入门知识,因此我个人推荐这两个网址去学习
知乎:乐理识谱入门知识(建议收藏) - 梦心的文章 - 知乎
https://zhuanlan.zhihu.com/p/106632622
B站:【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?p=25\&vd_source=f56e2058dc55b3606129b126739c49ea
5.编曲
我们从网上搜索可以得到音符与频率的对照表(我在此只拿c调举例)

我们知道周期=频率的倒数,而要想使无源蜂鸣器发声就需要一定频率的脉冲(即高低电平),那么由此我们只需要在想要的音符频率周期中使蜂鸣器的电平翻转两次(以实现高低电平的需求)即可使蜂鸣器发出相应音符的声音。
例如低音1的频率是262Hz,那么他的周期T=3816us,它周期的一半T/2
1908,再算出它的重装值为63628
重装值 = 65536 - 定时时间
我们即可通过定时中断来实现对应音符的发声
乐谱存储
cpp
u16 yuepu[]={
0,
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,
};//用以数组将所有要用的乐谱存起来
音符定义
cpp
#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
我们拿《两只老虎》的谱来举例

我们就写"两只老虎,两只老虎,跑得快,跑得快"这一小段来写
cpp
u8 code TwoTiger[]=
{
M1,2,
M2,2,
M3,2,
M1,2,
M1,2,
M2,2,
M3,2,
M1,2,
M3,2,
M4,2,
M5,4,
M3,2,
M4,2,
M5,4,
0xff //结束标志符
};
音符后面是时值,时值决定音符持续时间(如4表示全音符,2表示二分音符)
6.编程
这段代码是一个基于51单片机驱动蜂鸣器播放音乐的程序。它通过定时器中断生成特定频率的方波,并通过主循环控制音符的切换与节拍时长。
1.代码工作原理
此代码的核心是利用定时器中断 来产生特定频率的波形,并通过主循环 控制每个音符的持续时间(节拍)。蜂鸣器通过IO口(Buzzer
)的不断翻转来发声,其音高由定时器中断的频率决定,而每个音符的演奏时长则由主循环中的延时控制。
2.代码详细解释
1. 全局变量定义
unsigned char FreqSelect, MusicSelect;
-
FreqSelect
:当前音符索引 。用于查询FreqTable
,获取对应频率的定时器重载值。 -
MusicSelect
:音乐数据索引 。作为指针,遍历Music
数组。
2. 主函数 main()
void main()
{
Timer0Init(); // 初始化定时器0
while(1)
{
if(Music[MusicSelect] != 0xFF) // 检测当前数据是否为停止标志
{
FreqSelect = Music[MusicSelect]; // 获取音符对应的频率索引
MusicSelect++;
Delay(SPEED/4 * Music[MusicSelect]); // 获取节拍时长并延时
MusicSelect++;
TR0 = 0; // 关闭定时器,停止发声
Delay(5); // 音符间短暂静音,产生停顿感
TR0 = 1; // 打开定时器,准备下一个音符
}
else // 如果遇到停止标志
{
TR0 = 0; // 关闭定时器
while(1); // 程序进入死循环,停止播放
}
}
}
主循环的工作流程如下:

3. 定时器0中断服务程序 Timer0_Routine()
void Timer0_Routine() interrupt 1
{
if(FreqTable[FreqSelect]) // 检查当前频率值是否非0(非休止符)
{
/*取对应频率值的重装载值到定时器*/
TL0 = FreqTable[FreqSelect] % 256; // 设置定时初值低8位
TH0 = FreqTable[FreqSelect] / 256; // 设置定时初值高8位
Buzzer = !Buzzer; // 翻转蜂鸣器IO口电平,产生方波
}
}
-
此函数由定时器0溢出中断自动调用。
-
它重新装载定时器初值(以维持所需频率)并翻转蜂鸣器引脚电平,从而产生特定频率的方波信号驱动蜂鸣器。
-
if(FreqTable[FreqSelect])
判断避免了在休止符时翻转引脚,此时蜂鸣器不响。
3. 音乐数据组织
程序的演奏依赖于两个核心数组:
1.**频率表 FreqTable[]
**:
一个存储了不同音符对应定时器重载值的数组。这些值是根据单片机晶振频率和音符频率计算得出的。(前面已举例过了)例如:
// 假设晶振为12MHz, 定时器1us计数一次
const unsigned int FreqTable[] = {
0, // 0 代表休止符
63628, // 1: Do (261.6Hz)
63835, // 2: Re (293.7Hz)
64021, // 3: Mi (329.6Hz)
64103, // 4: Fa (349.2Hz)
64260, // 5: So (392.0Hz)
64400, // 6: La (440.0Hz)
64524, // 7: Si (493.9Hz)
// ... 其他音符或高频
};
**2.音乐数据 Music[]
**:
一个按特定顺序存储音符指令的数组。通常每两个数字一组(前面举例过了):
// 示例:《小星星》第一句 "一闪一闪亮晶晶"
const unsigned char Music[] = {
1,4, 1,4, 5,4, 5,4, 6,4, 6,4, 5,2,
// ... 后续音符
0xFF // 结束标志
};
4.矩阵按键设计
cpp
//矩阵按键检测函数
u8 key_matrix_ranks_scan(void)
{
u8 key_value=0;
KEY_MATRIX_PORT=0xf7;//给第一列赋值0,其余全为1
if(KEY_MATRIX_PORT!=0xf7)//判断第一列按键是否按下
{
delay_10us(1000);//消抖
switch(KEY_MATRIX_PORT)//保存第一列按键按下后的键值
{
case 0x77: key_value=1;break;
case 0xb7: key_value=5;break;
case 0xd7: key_value=9;break;
case 0xe7: key_value=13;break;
}
}
while(KEY_MATRIX_PORT!=0xf7);//等待按键松开
KEY_MATRIX_PORT=0xfb;//给第二列赋值0,其余全为1
if(KEY_MATRIX_PORT!=0xfb)//判断第二列按键是否按下
{
delay_10us(1000);//消抖
switch(KEY_MATRIX_PORT)//保存第二列按键按下后的键值
{
case 0x7b: key_value=2;break;
case 0xbb: key_value=6;break;
case 0xdb: key_value=10;break;
case 0xeb: key_value=14;break;
}
}
while(KEY_MATRIX_PORT!=0xfb);//等待按键松开
KEY_MATRIX_PORT=0xfd;//给第三列赋值0,其余全为1
if(KEY_MATRIX_PORT!=0xfd)//判断第三列按键是否按下
{
delay_10us(1000);//消抖
switch(KEY_MATRIX_PORT)//保存第三列按键按下后的键值
{
case 0x7d: key_value=3;break;
case 0xbd: key_value=7;break;
case 0xdd: key_value=11;break;
case 0xed: key_value=15;break;
}
}
while(KEY_MATRIX_PORT!=0xfd);//等待按键松开
KEY_MATRIX_PORT=0xfe;//给第四列赋值0,其余全为1
if(KEY_MATRIX_PORT!=0xfe)//判断第四列按键是否按下
{
delay_10us(1000);//消抖
switch(KEY_MATRIX_PORT)//保存第四列按键按下后的键值
{
case 0x7e: key_value=4;break;
case 0xbe: key_value=8;break;
case 0xde: key_value=12;break;
case 0xee: key_value=16;break;
}
}
while(KEY_MATRIX_PORT!=0xfe);//等待按键松开
return key_value;
}
5.LCD1602设计
LCD1602 基本操作函数解析
1. 写命令函数 (lcd1602_write_cdm
)
void lcd1602_write_cdm(u8 cdm) {
LCD1602_RS = 0; // 选择命令寄存器
LCD1602_WR = 0; // 设置为写入模式
LCD1602_E = 0;
LCD1602_DATEPORT = cdm; // 发送命令值
delay_ms(1);
LCD1602_E = 1; // 产生使能信号上升沿
delay_ms(1);
LCD1602_E = 0; // 下降沿执行命令
}
功能:向LCD1602发送控制命令。
-
RS=0:选择指令寄存器
-
WR=0:设置为写操作模式
-
E信号:使能信号,通过高电平脉冲(1→0)锁存数据
-
延时:确保LCD有足够时间处理指令(典型需要40μs)
2. 写数据函数 (lcd1602_write_date
)
void lcd1602_write_date(u8 dat) {
LCD1602_RS = 1; // 选择数据寄存器
LCD1602_WR = 0; // 写入模式
LCD1602_E = 0;
LCD1602_DATEPORT = dat; // 发送数据值
delay_ms(1);
LCD1602_E = 1;
delay_ms(1);
LCD1602_E = 0;
}
功能:向LCD1602发送显示数据。
-
RS=1:选择数据寄存器,准备接收字符数据
-
其他信号与时序同写命令函数
3. 初始化函数 (lcd1602_init
)
void lcd1602_init() {
lcd1602_write_cdm(0x38); // 8位总线,2行显示,5×8点阵
lcd1602_write_cdm(0x0c); // 开显示,关光标
lcd1602_write_cdm(0x06); // 地址递增,显示不移动
lcd1602_write_cdm(0x01); // 清屏
}
初始化序列含义:
-
0x38:设置16×2显示、5×8点阵、8位数据接口
-
0x0c:开启显示,关闭光标
-
0x06:写入新数据后地址指针自动加1
-
0x01:清屏并将光标复位到地址0x00位置
4. 清屏函数 (lcd1602_clear
)
void lcd1602_clear() {
lcd1602_write_cdm(0x01); // 发送清屏指令
}
注意 :清屏指令需要额外延时(约1.52ms),但此函数中未体现,建议添加delay_ms(2)
确保指令完成。
5. 字符串显示函数 (lcd1602_show_string
)
void lcd1602_show_string(u8 x, u8 y, u8 *str) {
u8 i = 0;
if(x > 15 || y > 1) return; // 检查坐标有效性
if(y < 1) { // 第一行显示
while(*str != '\0') {
if(i < 16 - x) {
lcd1602_write_cdm(0x80 + i + x); // 第一行地址设置
} else {
lcd1602_write_cdm(0x80 + 0x40 + i + x - 16); // 自动换至第二行
}
lcd1602_write_date(*str);
str++;
i++;
}
} else { // 第二行显示
// 类似逻辑,地址基值为0xC0
}
}
地址计算原理:
-
第一行:起始地址0x80
-
第二行:起始地址0xC0
-
自动换行:当字符串超出本行时,自动切换到另一行继续显示
硬件连接参考
// 典型51单片机连接方式
sbit LCD1602_RS = P2^6; // 寄存器选择
sbit LCD1602_WR = P2^5; // 读写控制
sbit LCD1602_E = P2^7; // 使能信号
#define LCD1602_DATEPORT P0 // 8位数据总线
使用示例
// 示例代码
lcd1602_init(); // 初始化LCD
lcd1602_show_string(0, 0, "Hello"); // 第一行显示"Hello"
lcd1602_show_string(0, 1, "World!"); // 第二行显示"World!"
优化建议
-
添加忙检测:当前代码使用延时等待,可优化为检测BF忙标志位(但需连接RW引脚)
-
清屏函数完善:清屏指令后应添加足够延时(≥1.52ms)
-
字符显示优化:显示长字符串时,可考虑添加滚动功能
-
自定义字符:可利用CGRAM生成自定义字符(如特殊符号)
6.主函数的总设计
cpp
u8 fselect,mselect;
u8 key=0;
void main()
{
time0_init();
lcd1602_init();//LCD1602初始化
lcd1602_show_string(0,0,"Which song?");//第一行显示
lcd1602_show_string(6,1,"O_o");//第二行显示
while(1)
{
key=key_matrix_ranks_scan();
if(key!=0)
{
if(key==1)
{
lcd1602_init();//LCD1602初始化
lcd1602_show_string(0,0,"TwoTiger");//第一行显示
lcd1602_show_string(0,1,"1/2");//第二行显示
while(1){
if(TwoTiger[mselect]!=0xff)
{
fselect=TwoTiger[mselect];
mselect++;
delay_ms(speed/4*TwoTiger[mselect]);
mselect++;
TR0=0;
delay_ms(5);
TR0=1;
}else
{
TR0=0;
fselect=0;
mselect=0;
lcd1602_clear();
lcd1602_show_string(0,0,"Which song?");//第一行显示
lcd1602_show_string(6,1,"O_o");//第二行显示
break;
}
}
}
else if(key==2)
{
lcd1602_init();//LCD1602初始化
lcd1602_show_string(0,0,"SkyBuild");//第一行显示
lcd1602_show_string(0,1,"2/2");//第二行显示
while(1){
if(SkyBuild[mselect]!=0xff)
{
fselect=SkyBuild[mselect];
mselect++;
delay_ms(speed/4*SkyBuild[mselect]);
mselect++;
TR0=0;
delay_ms(5);
TR0=1;
}else
{
TR0=0;
fselect=0;
mselect=0;
lcd1602_clear();
lcd1602_show_string(0,0,"Which song?");//第一行显示
lcd1602_show_string(6,1,"O_o");//第二行显示
break;
}
}
}
}
key=0;
}
}
void TIME0() interrupt 1
{
if(yuepu[fselect])
{
TH0=yuepu[fselect]/256;
TL0=yuepu[fselect]%256;
BUZZ=!BUZZ;
}
}
1.音乐播放原理:
通过定时器T0中断产生不同频率的方波信号来驱动蜂鸣器(或扬声器)发出不同音调
。代码中的 yuepu
数组(谱表)存储了各个音符对应的定时器重装载值,用于产生特定频率的方波(计算原理是:定时器初值 = 65536 - Fosc / (12 * 2 * 频率))。TwoTiger
和 SkyBuild
这两个数组则存储了乐曲的编码序列, likely 是 {音符索引1, 时值1, 音符索引2, 时值2, ..., 0xFF}
的形式,其中 0xFF
表示乐曲结束。
2.显示与交互:
使用LCD1602显示屏显示当前状态和歌曲信息
。用户通过矩阵键盘选择要播放的歌曲(按键1或按键2)。播放完成后,系统会自动回到等待选择歌曲的初始状态。
3.程序结构特点:
主循环持续扫描键盘。一旦有有效按键,进入相应的歌曲播放循环。在歌曲播放循环中,根据乐曲数据数组中的值,依次取出音符索引和持续时间,设置定时器并延迟相应时间,从而奏响一个个音符直至乐曲结束。fselect
和 mselect
作为全局索引变量,分别用于定位频率表中的具体频率值和乐曲数据数组中的当前位置。
总结
该系统有一点点复杂性对于我们初学者来说,但仔细理清原理后还是能弄懂得,还有许多基础的函数和定义我在此没有写上(如模块脚的定义和delay函数)如有需要的同志也可以评论,对于乐谱编曲方面必然会有专业性的错误的(欢迎指教),毕竟想一下子学会音乐方面的知识是不现实的,但发出音乐是肯定没问题的。
请大家点点关注和点赞,后面我一定会分享更多实用的项目的