Skip to content
On this page

基于 51 单片机的音乐播放器:蜂鸣器 + 按键控制实现多曲目切换

一、引言:

  • 项目初衷:笔者对单片机发声原理感兴趣,想通过蜂鸣器实现简单音乐播放,同时学习定时器、中断、IO口控制的综合应用。
  • 项目功能概述:通过按键选择播放《欢乐颂》《虫儿飞》等曲目,数码管显示当前状态,支持重置播放
  • 适用人群:单片机初学者、电子爱好者,帮助理解 “如何将音乐理论转化为代码”。

二、硬件设计:

1.核心元件:

  • 单片机:如STC89C52RC。
  • 蜂鸣器:无源蜂鸣器,通过定时器生成方波驱动,频率可调
  • 按键:4个独立按键(K1-K4),用于选择曲目、重置。
  • 数码管:8位数码管(代码中用P0输出段码,P2控制位选)。
  • 辅助元件:电阻、电容、晶振(11.0592MHz)。

2.硬件连接图(蜂鸣器):

蜂鸣器:连接到P2^5(代码中sbit Buzzer = P2^5)。

蜂鸣器部分原理图

原理图中蜂鸣器连接如上图所示,但是stc89c52rc实验板实测引脚连接有误,实际上蜂鸣器的引脚连接为P2^5。

ULN2003D是高耐压、大电流达林顿管阵列,内部集成7路达林顿管,每路可驱动最大500mA电流,常用于驱动蜂鸣器、继电器、步进电机等大负载,实现单片机弱信号到强功率的转换。其中输入引脚(IN1~IN7) 连接单片机 IO 口,用于控制达林顿管的导通 / 关断。IN7(引脚7) 标有 “×”,表示未使用 / 预留输出引脚(OUT1~OUT7) 内部为 集电极开路结构(达林顿管的集电极),需外接负载和电源(通过COM端),OUT6(引脚11)接蜂鸣器驱动电路。GND(引脚8):芯片接地端,内部达林顿管的发射极统一接地。COM(引脚9):公共电源端,连接负载的电源正极(图中为VCC),为集电极开路输出提供上拉电源。

工作原理为当单片机向IN6输出高电平时,ULN2003内部达林顿管导通,OUT6被拉低至地电位;此时电流路径为:VCC,COM,J8-1,R19,BZ+,BZ-,OUT6(地),蜂鸣器通电发声;当IN6为低电平时,达林顿管关断,OUT6悬浮,蜂鸣器断电静音。

三、代码架构

1.整体架构

  • 文件结构:main.c(主逻辑)、Buzzer.c/h(蜂鸣器驱动)、Timer0.c/h(定时器控制)、Key.c/h(按键检测)、nixie.c/h(数码管显示)、delay.c/h(延时函数)。
  • 模块关系图:
    null

2.核心模块讲解——蜂鸣器驱动模块(buzzer.c/h):

在讲解具体的代码实现过程之前,我们先来讲解一下无源蜂鸣器能够不同频率声音的原理。蜂鸣器(压电式)的核心组件是压电陶瓷片,其特性是:在交变电压作用下会产生机械振动,振动频率与电压变化频率一致,当振动频率在 20Hz~20kHz(人耳可听范围)时,就会发出声音。若施加恒定电压(如持续高电平或低电平),压电片仅会瞬间形变后静止,不会产生持续振动,因此不发声;若施加周期性交变电压(即 “方波”,高低电平交替变化),压电片会随电压变化反复形变,产生持续振动,从而发出声音。

进一步地,我们由乐理知识结合可知,声音的音调高低由方波频率决定:频率越高,音调越高(如 440Hz 对应标准音 A4,880Hz 对应高八度的 A5);音量大小则与电压幅值相关(代码中通过单片机 IO 口高低电平驱动,幅值固定,故音量基本不变)。

那么,要如何生成所需频率的方波呢?笔者在一开始想到了使用delay100us()延时函数,通过多次调用此函数来实现对方波频率的调整。但是由于此函数精度不高,且对于部分频率需要取整后才能延时,累计误差过大,导致最终结果出现了明显的“跑调”。因而,笔者弃用了此方法,并在随之联想到了另一种计时方法,定时器中断系统。

通过定时器 0 的中断功能实现方波生成,核心逻辑是 “定时翻转蜂鸣器引脚电平”,具体流程如下:

首先,将方波频率与定时器中断关联起来:由上文我们已知蜂鸣器的发声频率 = 方波的频率(即每秒内高低电平交替的次数)。为生成频率为f的方波,需让定时器每隔$\mathrm{T} = \frac{1}{2f}$的时间触发一次中断(因为一次完整的方波周期包含 “高电平到低电平” 和 “低电平到高电平” 两次翻转,每次翻转间隔为半个周期)。

例如:要生成 440Hz(A4 标准音)的方波,周期为1/440 ≈ 2.27ms,则定时器需每隔1.135ms(半个周期)触发一次中断,每次中断翻转一次蜂鸣器引脚电平。

其次,我们需要对定时器初值进行计算。定时器的中断频率由初值决定(16 位定时器最大计数为 65535,初值越小,计数到溢出的时间越短,中断频率越高)

代码中CalculateTimerReload()函数专门计算对应频率的初值,公式推导如下(基于 $11.0592MHz$ 晶振):晶振频率为 $11.0592MHz$,单片机默认12分频后,定时器的计数时钟频率为:$11059200Hz / 12 = 921600Hz$(即每秒计数 $921600$次)。若目标方波频率为$f$,则半个周期的计数次数为:$921600 / (2f) = 460800 / f$(因为半个周期需触发一次中断)。定时器初值 = 65536 - 半个周期的计数次数(因为计数从初值开始,到 65535 溢出触发中断),即: $$初值 = 65536 - (460800 / f)$$ 这与代码中CalculateTimerReload()函数的实现原理上完全一致:

c
// 计算定时器初值
static unsigned int CalculateTimerReload(float frequency)
{
    if (frequency <= 0) return 0;  // 休止符
    // 11.0592MHz晶振,12分频,计算16位定时器初值
    return 65536 - (unsigned int)(11059200UL / (24UL * frequency));
}

最后,我们通过定时器中断服务来实现电平的翻转,定时器0的中断函数(Timer0_Routine,位于Timer0.c)是方波生成的执行环节:

c
//定时器0中断函数(用于蜂鸣器方波生成)
void Timer0_Routine() interrupt 1
{
	if (Buzzer_Playing && Buzzer_Freq != 0)	//如果正在播放且频率有效
	{
		//加载当前频率对应的定时器初值
		TL0 = Buzzer_Freq % 256;		
		TH0 = Buzzer_Freq / 256;
		Buzzer = !Buzzer;	//翻转蜂鸣器引脚,产生方波
	}
	else	//停止播放时
	{
		Buzzer = 0;		//确保蜂鸣器关闭
	}
}

当播放音符时(Buzzer_Playing=1),每次中断都会用Buzzer_Freq(即CalculateTimerReload()计算的初值)重新加载定时器,确保中断间隔稳定,同时翻转Buzzer引脚(P2^5)的电平,形成连续方波;停止播放时,引脚固定为低电平,蜂鸣器停止振动。

函数buzzer_PlayNote()的讲解

整合以上,我们可以得到一个音符播放的完整流程(函数Buzzer_PlayNote())该函数将 “频率计算,时长控制,启动 / 停止定时器” 串联,实现单个音符的播放:

c
// Buzzer.c
// 播放指定音符
// baseFrequency: 基础频率
// noteName: 音名(用于调号处理)
// noteType: 音符类型(使用头文件中定义的宏)
void Buzzer_PlayNote(float baseFrequency, NoteName noteName, unsigned int noteType)
{
    float actualFrequency;
    unsigned int duration;
    unsigned int reload;
    
    // 参数校验
    if (baseFrequency <= 0 || noteType == 0 || noteName < NOTE_C || noteName > NOTE_B)
    {
        Buzzer_Freq = 0;
        Buzzer_Playing = 0;
        return;
    }
    
    // 计算实际频率和时长
    actualFrequency = GetActualFrequency(baseFrequency, noteName);
    duration = CalculateNoteDuration(noteType);
    reload = CalculateTimerReload(actualFrequency);
    
    // 设置定时器并启动
    Buzzer_Freq = reload;
    Buzzer_Playing = 1;
    TR0 = 1;  // 启动定时器0
    
    // 等待音符播放完成
    Delay1ms(duration);
    
    // 停止播放
    Buzzer_Playing = 0;
    TR0 = 0;  // 停止定时器0
    Buzzer = 0;  // 关闭蜂鸣器
    
    // 音符间停顿
    Delay1ms(5);
}
  • 参数校验:过滤无效参数(如频率≤0、非法音名),避免错误发声。
  • 计算实际频率:结合调号(keySignatureMap)对基础频率进行偏移调整(升 / 降半音),得到actualFrequency(如 F 大调中 B 音需降半音)。
  • 计算播放时长:根据BPM(每分钟节拍数)和音符类型(全音符、四分音符等),通过CalculateNoteDuration()得到该音符的持续时间(毫秒)。
  • 启动播放:将频率转换为定时器初值(reload),设置Buzzer_FreqBuzzer_Playing,启动定时器(TR0=1),此时定时器中断开始生成方波,蜂鸣器发声。
  • 维持时长:通过Delay1ms(duration)等待音符播放完成(期间方波持续生成)。
  • 停止播放:关闭定时器(TR0=0),复位Buzzer_Playing和蜂鸣器引脚,最后加5ms短延时避免音符连音。

除此之外,(为了偷懒) 笔者还引入了一些较为创新的想法,即通过参数化设计实现动态调整音乐,能够灵活调整对应的调号和BPM。

函数Buzzer_SetKeySignature():调号修改的实现原理

音乐原理: 调号(Key Signature)是音乐中决定 “哪些音需要固定升降” 的规则(如C大调无升降音,G大调需升F音)。该函数通过动态调整音符频率的半音偏移,实现不同调式的切换,其基于 “十二平均律” 的频率计算规则。

  • 十二平均律:将一个八度(如C4C5)平均分为12个半音,相邻半音的频率比为固定值$2^(1/12) ≈ 1.059463094$(代码中定义为SEMITONE_RATIO)。
  • 调号的本质:每个调号规定了 7 个基本音级(如C大调的C-D-E-F-G-A-B)中哪些音需要升高或降低半音。例如:G大调的调号为 “升F”(F音需升高半音);F大调的调号为 “降B”(B音需降低半音)。

函数实现核心: 函数通过修改currentKeySignature变量,让后续音符的频率计算引用新调号的偏移规则,具体流程如下:

(1)调号映射表(keySignatureMap)的设计

代码中预定义了 15 种调号(如 C 大调、G 大调、F 大调等)对应的 “半音偏移表”

c
// 调号映射表:每个调号对应的半音偏移(-1=降半音,0=不变,1=升半音)
// 顺序:C, C#, D, D#, E, F, F#, G, G#, A, A#, B
static const signed char code keySignatureMap[15][12] = {
    {0,0,0,0,0,0,0,0,0,0,0,0},   // C_MAJOR
    {0,0,0,0,0,1,0,0,0,0,0,0},   // G_MAJOR (F#)
    {1,0,0,0,0,1,0,0,0,0,0,0},   // D_MAJOR (F#, C#)
    {1,0,0,0,0,1,0,1,0,0,0,0},   // A_MAJOR (F#, C#, G#)
    {1,0,0,0,0,1,0,1,0,1,0,0},   // E_MAJOR (F#, C#, G#, D#)
    {1,0,0,0,0,1,0,1,0,1,0,1},   // B_MAJOR (F#, C#, G#, D#, A#)
    {1,0,1,0,0,1,0,1,0,1,0,1},   // F_SHARP_MAJOR (F#, C#, G#, D#, A#, E#)
    {1,0,1,0,1,1,0,1,0,1,0,1},   // C_SHARP_MAJOR (所有音升半音)
    {0,0,0,0,0,0,1,0,0,0,0,0},   // F_MAJOR (Bb)
    {0,0,0,0,0,0,1,0,0,1,0,0},   // B_FLAT_MAJOR (Bb, Eb)
    {0,0,0,0,0,0,1,0,0,1,0,1},   // E_FLAT_MAJOR (Bb, Eb, Ab)
    {0,0,0,0,0,0,1,0,1,1,0,1},   // A_FLAT_MAJOR (Bb, Eb, Ab, Db)
    {0,0,0,0,0,0,1,0,1,1,0,1},   // D_FLAT_MAJOR (Bb, Eb, Ab, Db, Gb)
    {0,0,1,0,0,0,1,0,1,1,0,1},   // G_FLAT_MAJOR (Bb, Eb, Ab, Db, Gb, Cb)
    {0,0,1,0,1,0,1,0,1,1,0,1}    // C_FLAT_MAJOR (所有音降半音)
};
(2)Buzzer_SetKeySignature()函数的作用

currentKeySignature(当前调号)更新为用户指定的值,后续音符的频率计算将基于新调号的偏移规则。

c
// 修改调号
void Buzzer_SetKeySignature(unsigned char keySignature)
{
    if (keySignature <= C_FLAT_MAJOR)
    {
        currentKeySignature = keySignature;
    }
}
(3)GetActualFrequency()函数的联动

当播放音符时,GetActualFrequency函数会根据currentKeySignature从映射表中获取偏移量,动态调整基础频率:

c
// 根据调号计算实际频率
static float GetActualFrequency(float baseFrequency, NoteName noteName)
{
		char i = 0;
    signed char offset = keySignatureMap[currentKeySignature][noteName];
    float actualFreq = baseFrequency;
    
    // 根据偏移计算实际频率(半音比例)
    if (offset > 0)
    {
        for (i = 0; i < offset; i++)
        {
            actualFreq *= SEMITONE_RATIO;
        }
    }
    else if (offset < 0)
    {
        for (i = 0; i > offset; i--)
        {
            actualFreq /= SEMITONE_RATIO;
        }
    }
    
    return actualFreq;
}

笔者在此举一个例子,例如:在G大调(currentKeySignature=1)中播放 F 音(noteName=NOTE_F),偏移量为1,因此实际频率 = 基础 F 音频率 × SEMITONE_RATIO(即升半音)。

综上,通过调号映射表 + 半音比例计算,我们就可以将复杂的音乐调式规则转化为可参数化的代码逻辑,实现了同一旋律在不同调号下的无缝切换,无需修改音符序列本身,仅通过修改调号参数即可适配不同音域需求 (狠狠偷懒了说是)

函数Buzzer_SetBPM():播放速度修改的实现原理

BPM(Beats Per Minute,每分钟节拍数)是衡量音乐速度的参数(如 $\mathrm{BPM}=60$表示每分钟60拍,每拍1秒;$\mathrm{BPM}=120$表示每拍0.5秒)。该函数通过动态调整音符的播放时长,实现速度的实时改变,核心原理是节拍时长与BPM成倒数关系。而BPM与音符时长的关系为:音符的实际播放时间由 “节拍数” 决定(如二分音符 = 4拍,四分音符 = 1拍,附点四分音符 = 1.5拍),1拍的持续时间(单位:毫秒)= 60000 / BPM(1分钟 = 60000毫秒)

(1)函数Buzzer_SetBPM的作用
c
// 修改BPM
void Buzzer_SetBPM(unsigned int bpm)
{
    if (bpm > 0)  // 确保BPM为正数(避免除零错误)
    {
        currentBPM = bpm;  // 更新当前BPM值
    }
}

此函数将currentBPM(当前节拍数)更新为用户指定值,后续音符的时长计算将基于新的BPM。

(2)通过函数CalculateNoteDuration()计算BPM对音符时长的影响:

音符的实际播放时长由CalculateNoteDuration函数计算,而该函数的输入依赖currentBPM

c
// 计算音符时长(毫秒)
static unsigned int CalculateNoteDuration(unsigned int noteType)
{
    unsigned int beatDuration = 60000 / currentBPM;  // 一拍的时长(毫秒)
    unsigned int duration = (4 * beatDuration) / (noteType & ~DOT);  // 基础时长
    
    if (noteType & DOT)  // 附点音符增加一半时长
    {
        duration += duration / 2;
    }
    
    return duration;
}

例如:当currentBPM=120时,1拍时长 = 60000/120=500ms,四分音符(noteType=4)的时长 = (4×500)/4=500ms;

(3)对播放流程的影响

Buzzer_PlayNote函数会使用CalculateNoteDuration()的结果作为延时时间(Delay1ms(duration)),因此 BPM 的变化会直接导致音符播放时长的变化:BPM值的大小与1拍时长成反比,与音符播放速度成正比

综上,我们通过将 BPM 与音符时长绑定,实现了同一旋律在不同速度下的灵活播放。即将音乐速度的抽象概念(BPM)转化为可量化的时间计算(毫秒级延时),并通过函数接口实现动态调整,无需重新编写音符序列 (又偷懒了耶)

四、项目代码

1.main.c(内附欢乐颂和虫儿飞片段音符序列)

c
#include <REGX52.H>
#include "Key.h"
#include "nixie.h"
#include "Buzzer.h"
#include "delay.h"
#include "Timer0.h"


unsigned char KeyNum, i = 0;

// 定义结构体,格式:{基础频率, 音名, 音符类型}
typedef struct {
    float freq;
    NoteName name;
    unsigned int type;
} Note;

// 欢乐颂音符序列(C大调)
const Note code odeToJoy[] = {
	//(1)
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
    {f2,  NOTE_F,  QUARTER_NOTE},    // 4
    {g2,  NOTE_G,  QUARTER_NOTE},    // 5
		
    {g2,  NOTE_G,  QUARTER_NOTE},    // 5
    {f2,  NOTE_F,  QUARTER_NOTE},    // 4
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
    {d2,  NOTE_D,  QUARTER_NOTE},    // 2
		
    {c2,  NOTE_C,  QUARTER_NOTE},    // 1
    {c2,  NOTE_C,  QUARTER_NOTE},    // 1
    {d2,  NOTE_D,  QUARTER_NOTE},    // 2
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
		
    {e2,  NOTE_E,  DOT_QUARTER_NOTE}, // 3(附点四分音符)
    {d2,  NOTE_D,  EIGHTH_NOTE},     // 2(八分音符)
    {d2,  NOTE_D,  HALF_NOTE},      // 2(二分音符)
		
		{e2,  NOTE_E,  QUARTER_NOTE},    // 3
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
    {f2,  NOTE_F,  QUARTER_NOTE},    // 4
    {g2,  NOTE_G,  QUARTER_NOTE},    // 5
		
	//(6)
    {g2,  NOTE_G,  QUARTER_NOTE},    // 5
    {f2,  NOTE_F,  QUARTER_NOTE},    // 4
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
    {d2,  NOTE_D,  QUARTER_NOTE},    // 2
		
		{c2,  NOTE_C,  QUARTER_NOTE},    // 1
    {c2,  NOTE_C,  QUARTER_NOTE},    // 1
    {d2,  NOTE_D,  QUARTER_NOTE},    // 2
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
		
		{d2,  NOTE_D,  DOT_QUARTER_NOTE}, // 2(附点四分音符)
    {c2,  NOTE_C,  EIGHTH_NOTE},     // 1(八分音符)
    {c2,  NOTE_C,  HALF_NOTE},      // 1(二分音符)
		
		{d2,	NOTE_D,  QUARTER_NOTE},		 // 2
		{d2,	NOTE_D,	 QUARTER_NOTE},		 // 2
		{e2,	NOTE_E,  QUARTER_NOTE},		 // 3
		{c2, 	NOTE_C,  QUARTER_NOTE},		 // 1
		
		{d2, 	NOTE_D,	 QUARTER_NOTE},		 // 2
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(八分音符)
		{f2,	NOTE_F,  EIGHTH_NOTE},		 // 4(八分音符)
		{e2,	NOTE_E,	 QUARTER_NOTE},		 // 3
		{c2,	NOTE_C,  QUARTER_NOTE},		 // 1
		
	//(11)
		{d2, 	NOTE_D,	 QUARTER_NOTE},		 // 2
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(八分音符)
		{f2,	NOTE_F,  EIGHTH_NOTE},		 // 4(八分音符)
		{e2,	NOTE_E,	 QUARTER_NOTE},		 // 3
		{d2,	NOTE_D,  QUARTER_NOTE},		 // 2
		
		{c2,  NOTE_C,  QUARTER_NOTE},    // 1
    {d2,  NOTE_D,  QUARTER_NOTE},    // 2
		{g1,  NOTE_G,  QUARTER_NOTE},    // 5(LOW1)
		{e2,	NOTE_E,  HALF_NOTE},			 // 3
		{e2,	NOTE_E,  QUARTER_NOTE},		 // 3
		{f2,  NOTE_F,  QUARTER_NOTE},    // 4
    {g2,  NOTE_G,  QUARTER_NOTE},    // 5
		
		{g2,  NOTE_G,  QUARTER_NOTE},    // 5
    {f2,  NOTE_F,  QUARTER_NOTE},    // 4
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
    {d2,  NOTE_D,  QUARTER_NOTE},    // 2
		
		{c2,  NOTE_C,  QUARTER_NOTE},    // 1
    {c2,  NOTE_C,  QUARTER_NOTE},    // 1
    {d2,  NOTE_D,  QUARTER_NOTE},    // 2
    {e2,  NOTE_E,  QUARTER_NOTE},    // 3
		
	//(16)
		{d2,  NOTE_D,  DOT_QUARTER_NOTE}, // 2(附点四分音符)
    {c2,  NOTE_C,  EIGHTH_NOTE},     // 1(八分音符)
    {c2,  NOTE_C,  HALF_NOTE},      // 1(二分音符)
		

    {0,   NOTE_C,  0}                // 结束标志
};

//虫儿飞音符序列(F大调)
const Note code bugsFly[] = {
	//(1)
		{e2,	NOTE_E,  QUARTER_NOTE},		 // 3
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(八分音符)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(八分音符)
		{f2,	NOTE_F,  QUARTER_NOTE},		 // 4
		{g2,	NOTE_G,  QUARTER_NOTE},		 // 5
		
		{e2,	NOTE_E,  DOT_QUARTER_NOTE},		 // 3(附点四分音符)
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(八分音符)
		{d2,	NOTE_D,  HALF_NOTE},		 // 2(二分音符)
		
		{c2,	NOTE_C,  QUARTER_NOTE},		 // 1
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(八分音符)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(八分音符)
		{d2,	NOTE_D,  QUARTER_NOTE},		 // 2
		{e2,	NOTE_E,  QUARTER_NOTE},		 // 3
		
		{e2,	NOTE_E,  DOT_QUARTER_NOTE},		 // 3(附点四分音符)
		{b1,	NOTE_B,  EIGHTH_NOTE},		 // 7(LOW1, 八分音符)
		{b1,	NOTE_B,  HALF_NOTE},		 // 7(LOW1, 二分音符)
		
	//(5)
		{a1,	NOTE_A,  QUARTER_NOTE},		 // 6(LOW1)
		{e2,	NOTE_E,  QUARTER_NOTE},		 // 3
		{d2,	NOTE_D,  HALF_NOTE},		 // 2(二分音符)
		
		{a1,	NOTE_A,  QUARTER_NOTE},		 // 6(LOW1)
		{e2,	NOTE_E,  QUARTER_NOTE},		 // 3
		{d2,	NOTE_D,  HALF_NOTE},		 // 2(二分音符)
		
		{a1,	NOTE_A,  QUARTER_NOTE},		 // 6(LOW1)
		{e2,	NOTE_E,  QUARTER_NOTE},		 // 3
		{d2,	NOTE_D,  DOT_QUARTER_NOTE},		 // 2(附点四分音符)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(八分音符)
		
		{c2,	NOTE_C,  WHOLE_NOTE},		 // 1(全音符)
			
		{0,		NOTE_C,	 0}					 // 结束标志
};


void main(){
	
	//蜂鸣器初始化,设置调号为C大调,指定bpm = 120
	Buzzer_Init(120, C_MAJOR);
	//定时器初始化
	Timer0Init();
	
	NixieDisplay(1,0);
	
	while(1){
		
		KeyNum = Key();
		if(KeyNum == 1){
				NixieDisplay(1,1);
				// 播放旋律
				Buzzer_SetBPM(96); 	//设定BPM为92
				Buzzer_SetKeySignature(C_MAJOR);	 //修改调号为C大调
				if(TR0 == 0){TR0 = 1;}	//启动定时器
        while(odeToJoy[i].freq != 0) {
            Buzzer_PlayNote(
                odeToJoy[i].freq, 
                odeToJoy[i].name, 
                odeToJoy[i].type
            );
            // 音符间短暂停顿(避免连音)
            // Delay1ms(50);
            i++;
        }
		}
		if(KeyNum == 2){
				NixieDisplay(1,2);
				// 播放旋律
				Buzzer_SetBPM(103); 	//设定BPM为103
				Buzzer_SetKeySignature(F_MAJOR);	 //修改调号为F大调
				if(TR0 == 0){TR0 = 1;}	//启动定时器
        while(bugsFly[i].freq != 0) {
            Buzzer_PlayNote(
                bugsFly[i].freq, 
                bugsFly[i].name, 
                bugsFly[i].type
            );
            // 音符间短暂停顿(避免连音)
            // Delay1ms(50);
            i++;
        }
		}
		if(KeyNum == 4){
				NixieDisplay(1,4);
				//重新开始
				if(TR0 == 1){TR0 = 0;}	//关闭定时器
        i = 0;
		}
		
	}
	
}

2.Buzzer.hBuzzer.c蜂鸣器模块

Buzzer.h

c
#ifndef __BUZZER__H_
#define __BUZZER__H_

#include <REGX52.H>
#include "Timer0.h"  // 引入定时器头文件

// 调号定义
#define C_MAJOR         0   // C大调/ a小调
#define G_MAJOR         1   // G大调/ e小调
#define D_MAJOR         2   // D大调/ b小调
#define A_MAJOR         3   // A大调/ f#小调
#define E_MAJOR         4   // E大调/ c#小调
#define B_MAJOR         5   // B大调/ g#小调
#define F_SHARP_MAJOR   6   // F#大调/ d#小调
#define C_SHARP_MAJOR   7   // C#大调/ a#小调
#define F_MAJOR         8   // F大调/ d小调
#define B_FLAT_MAJOR    9   // 降B大调/ g小调
#define E_FLAT_MAJOR    10  // 降E大调/ c小调
#define A_FLAT_MAJOR    11  // 降A大调/ f小调
#define D_FLAT_MAJOR    12  // 降D大调/ b_flat小调
#define G_FLAT_MAJOR    13  // 降G大调/ e_flat小调
#define C_FLAT_MAJOR    14  // 降C大调/ a_flat小调

// 音符类型定义
#define DOT             0x80    // 附点标记(用于按位或)
#define WHOLE_NOTE      1       // 全音符(4拍)
#define HALF_NOTE       2       // 二分音符(2拍)
#define QUARTER_NOTE    4       // 四分音符(1拍)
#define EIGHTH_NOTE     8       // 八分音符(1/2拍)
#define SIXTEENTH_NOTE  16      // 十六分音符(1/4拍)

// 附点音符(原时长 + 1/2原时长)
#define DOT_WHOLE_NOTE      (WHOLE_NOTE | DOT)
#define DOT_HALF_NOTE       (HALF_NOTE | DOT)
#define DOT_QUARTER_NOTE    (QUARTER_NOTE | DOT)
#define DOT_EIGHTH_NOTE     (EIGHTH_NOTE | DOT)
#define DOT_SIXTEENTH_NOTE  (SIXTEENTH_NOTE | DOT)

// 音名枚举(用于调号映射)
typedef enum {
    NOTE_C, NOTE_C_SHARP, NOTE_D, NOTE_D_SHARP, NOTE_E, NOTE_F,
    NOTE_F_SHARP, NOTE_G, NOTE_G_SHARP, NOTE_A, NOTE_A_SHARP, NOTE_B
} NoteName;

// 音高宏定义(根据音高与频率对照表)
#define A2 27.5
#define Asharp2_Bflat2 29.14
#define B2 30.87
#define C1 32.7
#define Csharp1_Dflat1 34.65
#define D1 36.71
#define Dsharp1_Eflat1 38.89
#define E1 41.2
#define F1 43.65
#define Fsharp1_Gflat1 46.25
#define G1 49.0
#define Gsharp1_Aflat1 51.91
#define A1 55.0
#define Asharp1_Bflat1 58.27
#define B1 61.74

#define C 65.41
#define Csharp_Dflat 69.3
#define D 73.42
#define Dsharp_Eflat 77.78
#define E 82.41
#define F 87.31
#define Fsharp_Gflat 92.5
#define G 98.0
#define Gsharp_Aflat 103.83
#define A 110.0
#define Asharp_Bflat 116.54
#define B 123.47

#define c 130.81
#define csharp_db 138.59
#define d 146.83
#define dsharp_eb 155.56
#define e 164.81
#define f 174.61
#define fsharp_gb 185.0
#define g 196.0
#define gsharp_ab 207.65
#define a 220.0
#define asharp_bb 233.08
#define b 246.94

#define c1 261.63    // 中央C
#define csharp1_db1 277.18
#define d1 293.66
#define dsharp1_eb1 311.13
#define e1 329.63
#define f1 349.23
#define fsharp1_gb1 369.99
#define g1 392.0
#define gsharp1_ab1 415.3
#define a1 440.0          // A4标准音
#define asharp1_bb1 466.16
#define b1 493.88

#define c2 523.25
#define csharp2_db2 554.37
#define d2 587.33
#define dsharp2_eb2 622.25
#define e2 659.25
#define f2 698.46
#define fsharp2_gb2 739.99
#define g2 783.99
#define gsharp2_ab2 830.61
#define a2 880.0
#define asharp2_bb2 932.33
#define b2 987.77

#define c3 1046.5
#define csharp3_db3 1108.73
#define d3 1174.66
#define dsharp3_eb3 1244.51
#define e3 1318.51
#define f3 1396.91
#define fsharp3_gb3 1479.98
#define g3 1567.98
#define gsharp3_ab3 1661.22
#define a3 1760.0
#define asharp3_bb3 1864.66
#define b3 1975.53

#define c4 2093.0
#define csharp4_db4 2217.46
#define d4 2349.32
#define dsharp4_eb4 2489.02
#define e4 2637.02
#define f4 2793.83
#define fsharp4_gb4 2959.96
#define g4 3135.96
#define gsharp4_ab4 3322.44
#define a4 3520.0
#define asharp4_bb4 3729.31
#define b4 3951.07

#define c5 4186.01

// 蜂鸣器端口
sbit Buzzer = P2^5;

// 定时器相关全局变量
extern unsigned int Buzzer_Freq;  // 存储定时器初值
extern bit Buzzer_Playing;        // 播放状态标志

// 函数声明
void Buzzer_Init(unsigned int bpm, unsigned char keySignature);
void Buzzer_SetKeySignature(unsigned char keySignature);
void Buzzer_SetBPM(unsigned int bpm);
void Buzzer_PlayNote(float baseFrequency, NoteName noteName, unsigned int noteType);

#endif

/*
// 定义结构体,格式:{基础频率, 音名, 音符类型}
typedef struct {
    float freq;
    NoteName name;
    unsigned int type;
} Note;

// 音符序列
const Note code odeToJoy[] = {
    {c2,  NOTE_C,  QUARTER_NOTE},    // 1
}
*/

Buzzer.c

c
#include <REGX52.H>
#include <INTRINS.H>
#include "Buzzer.h"
#include "Delay.h"

// 私有变量
static unsigned int currentBPM = 120;               // 当前BPM
static unsigned char currentKeySignature = C_MAJOR; // 当前调号
static const float code SEMITONE_RATIO = 1.059463094f;     // 半音频率比 (2^(1/12))

// 调号映射表:每个调号对应的半音偏移(-1=降半音,0=不变,1=升半音)
// 顺序:C, C#, D, D#, E, F, F#, G, G#, A, A#, B
static const signed char code keySignatureMap[15][12] = {
    {0,0,0,0,0,0,0,0,0,0,0,0},   // C_MAJOR
    {0,0,0,0,0,1,0,0,0,0,0,0},   // G_MAJOR (F#)
    {1,0,0,0,0,1,0,0,0,0,0,0},   // D_MAJOR (F#, C#)
    {1,0,0,0,0,1,0,1,0,0,0,0},   // A_MAJOR (F#, C#, G#)
    {1,0,0,0,0,1,0,1,0,1,0,0},   // E_MAJOR (F#, C#, G#, D#)
    {1,0,0,0,0,1,0,1,0,1,0,1},   // B_MAJOR (F#, C#, G#, D#, A#)
    {1,0,1,0,0,1,0,1,0,1,0,1},   // F_SHARP_MAJOR (F#, C#, G#, D#, A#, E#)
    {1,0,1,0,1,1,0,1,0,1,0,1},   // C_SHARP_MAJOR (所有音升半音)
    {0,0,0,0,0,0,1,0,0,0,0,0},   // F_MAJOR (Bb)
    {0,0,0,0,0,0,1,0,0,1,0,0},   // B_FLAT_MAJOR (Bb, Eb)
    {0,0,0,0,0,0,1,0,0,1,0,1},   // E_FLAT_MAJOR (Bb, Eb, Ab)
    {0,0,0,0,0,0,1,0,1,1,0,1},   // A_FLAT_MAJOR (Bb, Eb, Ab, Db)
    {0,0,0,0,0,0,1,0,1,1,0,1},   // D_FLAT_MAJOR (Bb, Eb, Ab, Db, Gb)
    {0,0,1,0,0,0,1,0,1,1,0,1},   // G_FLAT_MAJOR (Bb, Eb, Ab, Db, Gb, Cb)
    {0,0,1,0,1,0,1,0,1,1,0,1}    // C_FLAT_MAJOR (所有音降半音)
};

// 全局变量定义
unsigned int Buzzer_Freq = 0;
bit Buzzer_Playing = 0;

/*
// 蜂鸣器私有延时函数 (100us)
void Buzzer_Delay100us(unsigned int us)	//@11.0592MHz
{
	unsigned char i;
	while(us--)
	{
		_nop_();
		i = 43;
		while (--i);
	}
}
*/

// 初始化蜂鸣器,设置BPM和调号
void Buzzer_Init(unsigned int bpm, unsigned char keySignature)
{
    if (bpm > 0) currentBPM = bpm;
    if (keySignature <= C_FLAT_MAJOR) currentKeySignature = keySignature;
		Buzzer = 0;
}

// 修改调号
void Buzzer_SetKeySignature(unsigned char keySignature)
{
    if (keySignature <= C_FLAT_MAJOR)
    {
        currentKeySignature = keySignature;
    }
}

// 修改BPM
void Buzzer_SetBPM(unsigned int bpm)
{
    if (bpm > 0)
    {
        currentBPM = bpm;
    }
}

// 计算音符时长(毫秒)
static unsigned int CalculateNoteDuration(unsigned int noteType)
{
    unsigned int beatDuration = 60000 / currentBPM;  // 一拍的时长(毫秒)
    unsigned int duration = (4 * beatDuration) / (noteType & ~DOT);  // 基础时长
    
    if (noteType & DOT)  // 附点音符增加一半时长
    {
        duration += duration / 2;
    }
    
    return duration;
}

// 根据调号计算实际频率
static float GetActualFrequency(float baseFrequency, NoteName noteName)
{
		char i = 0;
    signed char offset = keySignatureMap[currentKeySignature][noteName];
    float actualFreq = baseFrequency;
    
    // 根据偏移计算实际频率(半音比例)
    if (offset > 0)
    {
        for (i = 0; i < offset; i++)
        {
            actualFreq *= SEMITONE_RATIO;
        }
    }
    else if (offset < 0)
    {
        for (i = 0; i > offset; i--)
        {
            actualFreq /= SEMITONE_RATIO;
        }
    }
    
    return actualFreq;
}

// 计算定时器初值
static unsigned int CalculateTimerReload(float frequency)
{
    if (frequency <= 0) return 0;  // 休止符
    // 11.0592MHz晶振,12分频,计算16位定时器初值
    return 65536 - (unsigned int)(11059200UL / (24UL * frequency));
}

// 播放指定音符
// baseFrequency: 基础频率
// noteName: 音名(用于调号处理)
// noteType: 音符类型(使用头文件中定义的宏)
void Buzzer_PlayNote(float baseFrequency, NoteName noteName, unsigned int noteType)
{
    float actualFrequency;
    unsigned int duration;
    unsigned int reload;
    
    // 参数校验
    if (baseFrequency <= 0 || noteType == 0 || noteName < NOTE_C || noteName > NOTE_B)
    {
        Buzzer_Freq = 0;
        Buzzer_Playing = 0;
        return;
    }
    
    // 计算实际频率和时长
    actualFrequency = GetActualFrequency(baseFrequency, noteName);
    duration = CalculateNoteDuration(noteType);
    reload = CalculateTimerReload(actualFrequency);
    
    // 设置定时器并启动
    Buzzer_Freq = reload;
    Buzzer_Playing = 1;
    TR0 = 1;  // 启动定时器0
    
    // 等待音符播放完成
    Delay1ms(duration);
    
    // 停止播放
    Buzzer_Playing = 0;
    TR0 = 0;  // 停止定时器0
    Buzzer = 0;  // 关闭蜂鸣器
    
    // 音符间停顿
    Delay1ms(5);
}

3.Timer0.hTimer0.c,定时器模块

Timer0.h

c
#ifndef __TIMER0_H__
#define __TIMER0_H__

/*
函数:Timer0Init
功能:定时器0初始化
输入:无
输出:无
*/
void Timer0Init(void);//1毫秒@11.0592MHz

#endif

Timer0.c

c
#include <REGX52.H>
#include "Buzzer.h"

/*
函数:Timer0Init
功能:定时器0初始化
输入:无
输出:无
*/
void Timer0Init(void)//1毫秒@11.0592MHz
{
	
	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	TL0 = 0x66;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值
	TF0 = 0;				//清除TF0标志
	TR0 = 1;				//定时器0开始计时
	ET0 = 1;
	EA = 1;
	PT0 = 0;
}

//定时器0中断函数(用于蜂鸣器方波生成)
void Timer0_Routine() interrupt 1
{
	if (Buzzer_Playing && Buzzer_Freq != 0)	//如果正在播放且频率有效
	{
		//加载当前频率对应的定时器初值
		TL0 = Buzzer_Freq % 256;		
		TH0 = Buzzer_Freq / 256;
		Buzzer = !Buzzer;	//翻转蜂鸣器引脚,产生方波
	}
	else	//停止播放时
	{
		Buzzer = 0;		//确保蜂鸣器关闭
	}
}

4.Key.hKey.c独立按键模块

Key.h

c
#ifndef __KEY_H__
#define __KEY_H__

/**
 * 功能:获取独立按键键值
 * 返回:1-4(对应K1-K4),无按键按下返回0
 */
unsigned char Key();

#endif

Key.c

c
#include <REGX52.H>
#include "delay.h"

// 定义按键引脚
#define KEY1 P3_1
#define KEY2 P3_0
#define KEY3 P3_2
#define KEY4 P3_3

/**
 * 功能:扫描单个按键状态(通过函数参数区分不同按键)
 * 参数:
 *   keyIndex:按键编号(1-4,对应K1-K4)
 * 返回:按键按下时返回对应编号,否则返回0
 */
static unsigned char scanSingleKey(unsigned char keyIndex) {
    // 根据按键编号判断对应引脚状态
    bit pinState = 1;  // 默认为高电平(未按下)
    switch(keyIndex) {
        case 1: pinState = KEY1; break;  // 读取K1引脚
        case 2: pinState = KEY2; break;  // 读取K2引脚
        case 3: pinState = KEY3; break;  // 读取K3引脚
        case 4: pinState = KEY4; break;  // 读取K4引脚
    }

    // 按键按下检测(引脚为低电平)
    if (pinState == 0) {
        Delay1ms(20);  // 消抖
        // 再次检测确认按下
        switch(keyIndex) {
            case 1: pinState = KEY1; break;
            case 2: pinState = KEY2; break;
            case 3: pinState = KEY3; break;
            case 4: pinState = KEY4; break;
        }
        if (pinState == 0) {
            // 等待按键释放
            while(pinState == 0) {
                switch(keyIndex) {
                    case 1: pinState = KEY1; break;
                    case 2: pinState = KEY2; break;
                    case 3: pinState = KEY3; break;
                    case 4: pinState = KEY4; break;
                }
            }
            Delay1ms(20);  // 释放后消抖
            return keyIndex;  // 返回按键编号
        }
    }
    return 0;  // 未按下返回0
}

/**
 * 功能:获取独立按键键值
 * 返回:1-4(对应K1-K4),无按键按下返回0
 */
unsigned char Key() {
    unsigned char keyValue = 0;

    // 依次扫描4个按键(避免同时按下时冲突,优先识别先扫描的按键)
    if (keyValue == 0) {
        keyValue = scanSingleKey(1);  // 扫描K1
    }
    if (keyValue == 0) {
        keyValue = scanSingleKey(2);  // 扫描K2
    }
    if (keyValue == 0) {
        keyValue = scanSingleKey(3);  // 扫描K3
    }
    if (keyValue == 0) {
        keyValue = scanSingleKey(4);  // 扫描K4
    }

    return keyValue;
}

5.nixie.hnixie.c数码管模块

nixie.h

c
#ifndef __NIXIE_H__
#define __NIXIE_H__

/**
 * @brief 数码管显示函数
 * @param location 数码管位置(1-8)
 * @param number 要显示的数字(0-9),超过9将显示横杠
 */
void NixieDisplay(unsigned char location, unsigned char number);

#endif

nixie.c

c
#include "nixie.h"
#include "delay.h"
#include <REGX52.H>

// 数码管段码表
const unsigned char NixieTable[] = {
    0x3F, // 0
    0x06, // 1
    0x5B, // 2
    0x4F, // 3
    0x66, // 4
    0x6D, // 5
    0x7D, // 6
    0x07, // 7
    0x7F, // 8
    0x6F  // 9
};

// 宏定义简化位操作
#define DIGIT_SEL_PORT P2
#define DIGIT_DATA_PORT P0

/**
 * @brief 设置数码管位置(内部函数)
 * @param location 位置参数(1-8)
 */
static void SetDigitPosition(unsigned char location)
{
    // 仅保留低3位,自动限制在0-7范围
    unsigned char pos_code = (location - 1) & 0x07;
    
    // 先关闭所有位选
    DIGIT_SEL_PORT &= ~(0x1C);  // 清除P2_4-P2_2位
    
    // 设置新的位选信号
    if(pos_code & 0x04) 
        DIGIT_SEL_PORT &= ~(1<<4); // 第2位
    else 
        DIGIT_SEL_PORT |= (1<<4);
    
    if(pos_code & 0x02) 
        DIGIT_SEL_PORT &= ~(1<<3); // 第1位
    else 
        DIGIT_SEL_PORT |= (1<<3);
    
    if(pos_code & 0x01) 
        DIGIT_SEL_PORT &= ~(1<<2); // 第0位
    else 
        DIGIT_SEL_PORT |= (1<<2);
}

/**
 * @brief 数码管显示函数
 * @param location 数码管位置(1-8)
 * @param number 要显示的数字(0-9),超过9将显示横杠
 * @note 如果需要静态显示,可以关闭消隐处理
 */
void NixieDisplay(unsigned char location, unsigned char number)
{
	// 边界检查,非法位置不显示
	if(location < 1 || location > 8)
		return;
	
	// 数字越界处理,显示特殊符号(此处显示横杠)
	if(number > 9)
		number = 0x40; // 横杠的段码
	else
		number = NixieTable[number];
	
	// 关闭显示,防止位选切换时的串扰
	DIGIT_DATA_PORT = 0x00;
	
	// 设置位选
	SetDigitPosition(location);
	
	// 输出段码
	DIGIT_DATA_PORT = number;
	
	// 短暂延时,保证显示亮度
	Delay1ms(1);
	
	// 消影处理
	//DIGIT_DATA_PORT = 0x00;
}
    

6.delay.hdelay.c延时函数模块

delay.h

c
#ifndef __DELAY_H__
#define __DELAY_H__

/**
 * @brief 延时函数(约1ms)
 * @param xms 延时时长,单位为毫秒
 */
void Delay1ms(unsigned int xms);

#endif

delay.c

c
#include <INTRINS.H>
#include "delay.h"

/**
 * @brief 延时函数(约1ms)
 * @param xms 延时时长,单位为毫秒
 * @note 基于11.0592MHz晶振
 */
void Delay1ms(unsigned int xms)	//@11.0592MHz
{
	unsigned char i, j;
	while(xms--)
	{
		_nop_();
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
	}
}

五、总结与展望

  • 项目收获:掌握定时器中断、IO 口控制、音乐理论与代码的结合,理解模块化编程的优势。
  • 目前不足:代码不够简洁,当音符序列过长时会超出内存限制无法编译。
  • 未来扩展:或许可以考虑移植到矩阵键盘上以支持更多曲目,同时支持歌曲播放的快进和回退,增加暂停播放等功能。添加OLED屏幕显示歌词,歌曲播放时长乃至添加一个歌曲进度条。通过蓝牙控制播放等。

2025年8月8日更新:收到反馈,原代码中无法处理含有休止符0的音符序列,现做修正。

修正说明

  1. 参数校验调整: 原代码中baseFrequency <= 0会直接返回,导致休止符不处理时长。修改后允许baseFrequency = 0(休止符),仅校验noteTypenoteName的有效性。
  2. 休止符处理逻辑:baseFrequency <= 0时,视为休止符: 关闭蜂鸣器(Buzzer = 0) 停止定时器(TR0 = 0) 等待与音符类型对应的时长(Delay1ms(duration)) 保持音符间的停顿(Delay1ms(5)

具体修正如下

  1. 修改Buzzer.c中的Buzzer_PlayNote函数:
c
// 播放指定音符
// baseFrequency: 基础频率
// noteName: 音名(用于调号处理)
// noteType: 音符类型(使用头文件中定义的宏)
void Buzzer_PlayNote(float baseFrequency, NoteName noteName, unsigned int noteType)
{
    float actualFrequency;
    unsigned int duration;
    unsigned int reload;
    
    // 参数校验(允许baseFrequency为0作为休止符,其他参数需有效)
    if (noteType == 0 || noteName < NOTE_C || noteName > NOTE_B)
    {
        Buzzer_Freq = 0;
        Buzzer_Playing = 0;
        return;
    }
    
    // 计算实际频率和时长
		
		// 处理休止符(基础频率为0)
    if (baseFrequency <= 0)
    {
        // 停止蜂鸣器,保持静音
        Buzzer_Playing = 0;
        TR0 = 0;          // 停止定时器
        Buzzer = 0;       // 确保蜂鸣器关闭
				duration = CalculateNoteDuration(noteType);
        Delay1ms(duration); // 等待休止符时长
        Delay1ms(5);      // 音符间停顿
        return;
    }
		
		 // 处理正常音符(非休止符)
    actualFrequency = GetActualFrequency(baseFrequency, noteName);
    duration = CalculateNoteDuration(noteType);
    reload = CalculateTimerReload(actualFrequency);
    
    // 设置定时器并启动
    Buzzer_Freq = reload;
    Buzzer_Playing = 1;
    TR0 = 1;  // 启动定时器0
    
    // 等待音符播放完成
    Delay1ms(duration);
    
    // 停止播放
    Buzzer_Playing = 0;
    TR0 = 0;  // 停止定时器0
    Buzzer = 0;  // 关闭蜂鸣器
    
    // 音符间停顿
    Delay1ms(5);
}
  1. 修改main.cmain()函数中,判断音符序列是否播放的逻辑:
c
void main(){
	
	......//初始化(保持不变)
	
	while(1){
		
		KeyNum = Key();
		if(KeyNum == 1){
				......
			/*此处做出修改,当音名和音符类型同为0时才会判断播放结束*/	
        while(answer[i].freq != 0 || answer[i].type != 0) {
            Buzzer_PlayNote(
                answer[i].freq, 
                answer[i].name, 
                answer[i].type
            );
            i++;
        }
		}

		if(KeyNum == 4){
				......
		}
		
	}
	
}
  1. 以下附上修正后main.c文件的代码,其中附歌曲《答案》的片段音符序列以供测试。
c
#include <REGX52.H>
#include "Key.h"
#include "nixie.h"
#include "Buzzer.h"
#include "delay.h"
#include "Timer0.h"


unsigned char KeyNum, i = 0;

// 定义结构体,格式:{基础频率, 音名, 音符类型}
typedef struct {
    float freq;
    NoteName name;
    unsigned int type;
} Note;

// 答案音符序列(C大调)
const Note code answer[] = {
	//有个简单的问题,
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{a1,	NOTE_A,  SIXTEENTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  SIXTEENTH_NOTE},		 // 1(HIGH1)
		{g1, 	NOTE_A,  EIGHTH_NOTE},     // 6
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{g1,	NOTE_G,  HALF_NOTE},		 // 5
		
	//什么是爱情?
		{g1,	NOTE_G,  EIGHTH_NOTE},		 // 5
		{g1,	NOTE_G,  EIGHTH_NOTE},		 // 5
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{e1,	NOTE_E,  DOT_EIGHTH_NOTE},		 // 3
		{c1,  NOTE_C,  QUARTER_NOTE},			// 1
		{0,   NOTE_C,  QUARTER_NOTE},			// 0
		
	//它是否是一种味道还是引力?
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{a1,	NOTE_A,  SIXTEENTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  SIXTEENTH_NOTE},		 // 1(HIGH1)
		{a1, 	NOTE_A,  DOT_EIGHTH_NOTE},     // 6
		{g1,	NOTE_G,  EIGHTH_NOTE},		 // 5
		{g1,	NOTE_G,  EIGHTH_NOTE},		 // 5
		
		{a1, 	NOTE_A,  EIGHTH_NOTE},     // 6
		{g1,	NOTE_G,  EIGHTH_NOTE},		 // 5
		{a1, 	NOTE_A,  EIGHTH_NOTE},     // 6
		{e1,	NOTE_E,  DOT_QUARTER_NOTE},     // 3
		{0,   NOTE_C,  EIGHTH_NOTE},			// 0
		
	//从我初恋那天起,
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{a1,	NOTE_A,  SIXTEENTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{c2,	NOTE_C,  SIXTEENTH_NOTE},		 // 1(HIGH1)
		{g1, 	NOTE_A,  EIGHTH_NOTE},     // 6
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{g1,	NOTE_G,  HALF_NOTE},		 // 5
		{0,   NOTE_C,  QUARTER_NOTE},			// 0
		
	//先是甜蜜然后紧接就会有风雨,
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(HIGH1)
		{c2,	NOTE_C,  DOT_QUARTER_NOTE},		 // 1(HIGH1)
		
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{a1,	NOTE_A,  SIXTEENTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  SIXTEENTH_NOTE},		 // 1(HIGH1)
		{g1, 	NOTE_A,  EIGHTH_NOTE},     // 6
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{g1,	NOTE_G,  QUARTER_NOTE},		 // 5
		{g1, 	NOTE_A,  EIGHTH_NOTE},     // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  DOT_HALF_NOTE},		 // 1(HIGH1)
	
	//爱就像蓝天白云晴空万里
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{a1,	NOTE_A,  SIXTEENTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
	//突然暴风雨
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{g2,	NOTE_G,  QUARTER_NOTE},		 // 5(HIGH1)
		{e2,	NOTE_E,  DOT_QUARTER_NOTE},		 // 3(HIGH1)
		
	//无处躲避总是让人始料不及
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(HIGH1)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(HIGH1)
		{c2,	NOTE_C,  DOT_QUARTER_NOTE},		 // 1(HIGH1)
		
	//人就像患重感冒打着喷嚏
		{g1,	NOTE_G,  SIXTEENTH_NOTE},		 // 5
		{a1,	NOTE_A,  SIXTEENTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
	//发烧要休息
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{g2,	NOTE_G,  QUARTER_NOTE},		 // 5(HIGH1)
		{e2,	NOTE_E,  DOT_QUARTER_NOTE},		 // 3(HIGH1)
		
	//冷热交替欢喜犹豫乐此不疲
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  SIXTEENTH_NOTE},		 // 2(HIGH1)
		{e2,	NOTE_E,  EIGHTH_NOTE},		 // 3(HIGH1)
		{a1,	NOTE_A,  DOT_EIGHTH_NOTE},		 // 6
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(HIGH1)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{d2,	NOTE_D,  EIGHTH_NOTE},		 // 2(HIGH1)
		{c2,	NOTE_C,  EIGHTH_NOTE},		 // 1(HIGH1)
		{c2,	NOTE_C,  HALF_NOTE},		 // 1(HIGH1)
			
		{0,		NOTE_C,	 0}								 // 结束标志
};


void main(){
	
	//蜂鸣器初始化,设置调号为C大调,指定bpm = 120
	Buzzer_Init(120, C_MAJOR);
	//定时器初始化
	Timer0Init();
	
	NixieDisplay(1,0);
	
	while(1){
		
		KeyNum = Key();
		if(KeyNum == 1){
				NixieDisplay(1,1);
				// 播放旋律
				Buzzer_SetBPM(74); 	//设定BPM为74
				Buzzer_SetKeySignature(C_MAJOR);	 //修改调号为C大调
				if(TR0 == 0){TR0 = 1;}	//启动定时器
        while(answer[i].freq != 0 || answer[i].type != 0) {
            Buzzer_PlayNote(
                answer[i].freq, 
                answer[i].name, 
                answer[i].type
            );
            // 音符间短暂停顿(避免连音)
            // Delay1ms(50);
            i++;
        }
		}

		if(KeyNum == 4){
				NixieDisplay(1,4);
				//重新开始
				if(TR0 == 1){TR0 = 0;}	//关闭定时器
        i = 0;
		}
		
	}
	
}