一、文档概述
1.1 项目简介
本程序基于标准 51 内核单片机(11.0592MHz 晶振),通过 IO 口无源蜂鸣器输出方波实现《小星星》乐曲循环播放,全程无定时器中断,采用纯软件延时翻转 IO 生成不同频率音符,结构简单、易移植,适合单片机入门音乐播放实验。
1.2 硬件环境
- 主控:STC89C52/AT89C52 51 单片机
- 晶振:11.0592MHz
- 发声器件:无源蜂鸣器(高电平关闭,低电平导通发声)
- IO 分配:
P1^0驱动蜂鸣器 - 编译环境:Keil C51
1.3 功能特性
- 支持低 / 中 / 高三个八度音阶,包含休止符静音功能
- 乐谱采用二维数组存储,音符 + 节拍分离,修改曲目便捷
- 纯软件方波生成,不依赖定时器中断,无中断冲突问题
- 乐曲播放完成自动循环,间隔 1 秒重复播放
- 分层函数设计:延时、单音符播放、整曲播放、硬件初始化解耦
二、头文件与宏、类型定义说明
c
运行
#include <reg52.h>
typedef unsigned char uchar;
typedef unsigned int uint;
sbit BEEP = P1^0;
#define REST 0
reg52.h:51 单片机寄存器标准头文件,定义 P0~P3、TCON、TMOD 等特殊功能寄存器- 类型别名:
uchar8 位无符号字符、uint16 位无符号整型,简化代码书写 sbit BEEP = P1^0:位定义,将 P1.0 引脚绑定蜂鸣器控制位;无源蜂鸣器默认高电平静音REST:休止符标记,索引 0,代表无音符、静音延时
三、音符频率对照表 NOTE_TABLE
3.1 数组说明
NOTE_TABLE 为常量数组,存储各音阶定时器初值,用于换算方波半周期延时,索引对应音符:
表格
| 索引 | 音符 | 音区 | 索引 | 音符 | 音区 | 索引 | 音符 | 音区 |
|---|---|---|---|---|---|---|---|---|
| 0 | REST | 休止 | 8 | 1 | 中音 | 15 | 1 | 高音 |
| 1 | 1 | 低音 | 9 | 2 | 中音 | 16 | 2 | 高音 |
| 2 | 2 | 低音 | 10 | 3 | 中音 | 17 | 3 | 高音 |
| 3 | 3 | 低音 | 11 | 4 | 中音 | 18 | 4 | 高音 |
| 4 | 4 | 低音 | 12 | 5 | 中音 | 19 | 5 | 高音 |
| 5 | 5 | 低音 | 13 | 6 | 中音 | 20 | 6 | 高音 |
| 6 | 6 | 低音 | 14 | 7 | 中音 | 21 | 7 | 高音 |
| 7 | 7 | 低音 | ------ | ------ | ------ | ------ | ------ | ------ |
3.2 原理说明
数组存储定时器 1 计数初值,程序仅提取低 8 位计算半周期延时 half_period = 256 - (timer_val & 0xFF);数值越小,延时越短,输出频率越高,音调越高。
四、乐谱数组 music_star
4.1 存储格式
c
运行
uint code music_star[][2] = {{音符索引, 节拍时长ms}, ... , {REST,0}};
- 二维数组第 1 列:音符索引,取值
0~21,0 为休止符 - 二维数组第 2 列:当前音符持续时间(单位 ms)
- 结束标记:
{REST,0},循环读取乐谱时以此判定乐曲结束 code关键字:数组存储在程序 ROM 区,不占用 RAM 内存
4.2 乐曲逻辑
完整《小星星》简谱转译,短音 200ms,长音 400ms,播放完毕延时 1s 后循环重放。
五、全局变量说明
表格
| 变量名 | 类型 | 作用 |
|---|---|---|
| beat_delay | uint | 存储当前音符节拍时长,预留扩展参数 |
| timer_val | uint | 临时存储当前音符对应的定时器初值 |
六、底层延时函数
6.1 void DelayMs(uint ms)
- 功能:毫秒级软件延时,适配 11.0592MHz 晶振
- 参数:
ms延时毫秒数 - 实现:双层 for 循环空跑,固定循环参数
j=110保证 1ms 基准延时
6.2 void DelayUs(uint us)
- 功能:微秒级短延时,用于生成方波高低电平保持时间
- 参数:
us微秒延时值 - 使用场景:蜂鸣器 IO 翻转后延时,控制方波频率,决定音调高低
七、硬件初始化函数 Timer1_Init()
c
运行
void Timer1_Init(void)
{
TMOD |= 0x20;
ET1 = 0;
TR1 = 0;
}
TMOD |= 0x20:配置定时器 1 为 8 位自动重装模式(仅预留,本程序未使用定时器中断)ET1 = 0:关闭定时器 1 中断允许位,彻底屏蔽定时器中断TR1 = 0:关闭定时器 1 计数,定时器全程不启动- 设计目的:统一定时器寄存器初始化,避免寄存器乱序导致的潜在干扰
八、核心播放函数
8.1 单音符播放 PlayNote(uint note, uint beat)
入参
note:音符索引(0~21),0 为休止符beat:音符持续节拍(ms)
执行逻辑
- 休止符判断:音符为 REST 时,蜂鸣器置高静音,直接延时对应节拍,函数返回
- 音调计算:读取音符对应初值,计算方波半周期微秒延时
- 方波循环输出 :在总节拍时长内循环翻转蜂鸣器 IO:
- BEEP=0 发声 → 微秒延时
- BEEP=1 静音 → 微秒延时
- 总时长自减,直到节拍耗尽
- 音符播放完成后蜂鸣器置高,停止发声
8.2 整曲播放 PlayMusic(uint music[][2])
- 遍历乐谱二维数组,循环读取每组
音符+节拍 - 判断结束标记
music[i][0] == REST && music[i][1] == 0,满足则退出循环 - 调用
PlayNote()逐音符播放,播放完成索引自增读取下一个音符
九、主函数 main()
- 调用
Timer1_Init()完成定时器寄存器初始化 BEEP = 1:上电默认关闭蜂鸣器,防止上电杂音- 无限大循环:
- 完整播放《小星星》乐曲
- 延时 1000ms,乐曲间隔静音 1 秒
- 重复播放
十、程序工作原理
- 无源蜂鸣器发声原理:无源蜂鸣器必须输入固定频率方波才能发声,高低电平交替翻转产生振动;翻转频率决定音调。
- 音调控制:不同音符对应不同半周期延时,延时越短,翻转频率越高,音调越高。
- 节拍控制:外层总时间循环控制音符持续时长,实现长短音区分。
- 静音处理:休止符直接拉高蜂鸣器引脚,停止方波输出,仅做延时。
- 无中断设计优势:代码逻辑直观,不会与按键、串口等外设中断产生冲突,适合初学者调试。
十一、修改扩展指南
11.1 修改乐曲
- 新增二维
code乐谱数组,格式{``{音符索引,节拍},...,{REST,0}} main函数循环内调用PlayMusic(新数组名)即可切换曲目
11.2 调整播放速度
- 统一修改乐谱内所有节拍数值:数值变大播放变慢,数值变小播放变快
- 或修改主函数循环间隔
DelayMs(1000)调整歌曲间隔
11.3 更换蜂鸣器 IO
修改 sbit BEEP = P1^0; 引脚定义,如P2^0、P0^1等,硬件同步改接线
11.4 适配其他晶振
需重新校准DelayMs内层循环值、DelayUs延时基准、NOTE_TABLE初值表。
十二、注意事项
- 硬件必须使用无源蜂鸣器;有源蜂鸣器无法通过方波调节音调,本程序不兼容。
- 蜂鸣器驱动建议增加三极管放大,51 单片机 IO 驱动电流不足会导致声音微弱。
- 程序全程阻塞延时,播放乐曲时无法响应按键、串口等其他任务,如需同步交互需改用定时器中断播放。
- 乐谱数组加
code关键字,存放在程序存储器,减少 RAM 占用,去掉后大容量乐谱会导致内存溢出。 - 休止符判断条件严格匹配
{REST,0},乐谱末尾必须添加结束标记,否则程序数组越界死机。
十三、程序优缺点
优点
- 逻辑简单,无中断、无复杂寄存器操作,新手易理解
- 乐谱与程序分离,换曲仅需修改数组,维护方便
- 资源占用低,不占用定时器运行资源
- 移植简单,仅需修改 IO 定义与延时参数
缺点
-
阻塞式延时,播放期间 CPU 无法处理其他任务
-
软件延时精度受晶振、编译器优化影响,音准存在微小误差
-
长节拍播放时 CPU 空转,资源利用率低
cpp#include <reg52.h> typedef unsigned char uchar; typedef unsigned int uint; // 蜂鸣器IO定义 sbit BEEP = P1^0; // 休止符常量 #define REST 0 // 定时器1初值表(11.0592MHz,仅用来换算半周期延时) uint code NOTE_TABLE[] = { 0, // 0:休止符REST 0xF895, // 低音1 0xF960, // 低音2 0xFA09, // 低音3 0xFA90, // 低音4 0xFB00, // 低音5 0xFB68, // 低音6 0xFBC7, // 低音7 0xFC0B, // 中音1 0xFC58, // 中音2 0xFC9C, // 中音3 0xFCCF, // 中音4 0xFCFF, // 中音5 0xFD2E, // 中音6 0xFD59, // 中音7 0xFD7F, // 高音1 0xFDA3, // 高音2 0xFDC4, // 高音3 0xFDDD, // 高音4 0xFDF4, // 高音5 0xFE08, // 高音6 0xFE1A // 高音7 }; // 小星星乐谱 格式{音符索引, 节拍ms} uint code music_star[][2] = { {8,200}, {8,200}, {15,200}, {15,200}, {17,200}, {17,200}, {15,400}, {13,200}, {13,200}, {12,200}, {12,200}, {10,200}, {10,200}, {8,400}, {15,200}, {15,200}, {13,200}, {13,200}, {12,200}, {12,200}, {10,400}, {15,200}, {15,200}, {13,200}, {13,200}, {12,200}, {12,200}, {10,400}, {8,200}, {8,200}, {15,200}, {15,200}, {17,200}, {17,200}, {15,400}, {13,200}, {13,200}, {12,200}, {12,200}, {10,200}, {10,200}, {8,400}, {REST,0} // 结束标志 }; uint beat_delay; uint timer_val; // 毫秒延时 11.0592MHz void DelayMs(uint ms) { uint i,j; for(i=ms; i>0; i--) for(j=110; j>0; j--); } // 微秒级短延时,用于生成音符方波 void DelayUs(uint us) { uint i; for(i=us; i>0; i--); } // 定时器初始化(仅配置,关闭中断,不使用溢出中断) void Timer1_Init(void) { TMOD |= 0x20; ET1 = 0; // 彻底关闭定时器1中断 TR1 = 0; } // 播放单音符 - 纯软件翻转,无中断 void PlayNote(uint note, uint beat) { uint total_time = beat; uint half_period; beat_delay = beat; // 休止符直接静音延时 if(note == REST) { BEEP = 1; DelayMs(total_time); return; } // 根据定时初值换算半周期延时 timer_val = NOTE_TABLE[note]; half_period = 256 - (timer_val & 0xFF); // 在节拍总时长内循环翻转蜂鸣器输出方波 while(total_time > 0) { BEEP = 0; DelayUs(half_period); BEEP = 1; DelayUs(half_period); total_time--; } BEEP = 1; } // 播放整首乐曲 void PlayMusic(uint music[][2]) { uchar i = 0; while(music[i][0] != REST || music[i][1] != 0) { PlayNote(music[i][0], music[i][1]); i++; } } void main(void) { Timer1_Init(); BEEP = 1; // 蜂鸣器默认关闭 while(1) { PlayMusic(music_star); DelayMs(1000); // 歌曲循环间隔1秒 } }