C语言单片机音乐盒如何实现旋律播放与控制?

99ANYc3cd6
预计阅读时长 25 分钟
位置: 首页 音乐 正文

设计原理

声音是如何产生的?

声音的本质是振动,不同频率的振动产生不同音高(Do, Re, Mi...)的声音,在单片机中,我们无法直接产生复杂的模拟音频信号,但我们可以通过数字信号来模拟。

c语言单片机音乐盒设计
(图片来源网络,侵删)

最简单的数字音频信号就是方波,一个方波在高电平和低电平之间快速切换,其切换的频率就是声音的频率,要演奏中央 Do (C4),频率约为262Hz,意味着我们每秒钟要让I/O口在高低电平之间切换262次,也就是每个高电平或低电平持续的时间约为 1 / (2 * 262) 秒。

如何用单片机产生方波?

单片机产生精确时序的最佳工具就是定时器

  • 基本思路
    1. 设置一个定时器,让它每隔一段时间就产生一次中断
    2. 定时器中断服务程序中,我们翻转一个I/O口的状态(如果原来是高电平,就变为低电平;反之亦然)。
    3. 这个“每隔一段时间”的长度,决定了输出方波的频率,要产生262Hz的方波,我们需要每 1 / (262 * 2) 秒(约1.91毫秒)翻转一次I/O口。

如何演奏一首曲子?

一首曲子由不同音高的音符(对应不同频率)和不同时长的节拍(对应延时)组成。

  • 基本思路
    1. 定义音符表:我们需要一个数据结构来存储每个音符的信息,最简单的方法是使用一个数组,每个元素代表一个音符,包含其频率和节拍时长。
    2. 播放循环:在主程序中,我们循环遍历这个音符数组,对于每个音符:
      • 根据音符的频率,计算出定时器需要设置的初值。
      • 启动定时器,让蜂鸣器发出这个音符的声音。
      • 根据音符的节拍时长,延时一段时间。
      • 关闭定时器,停止发声,形成音符间的间隙。
    • 休止符:用一个特殊的频率(比如0)来表示休止符,当遇到休止符时,我们只需要关闭定时器并延时即可。

硬件准备

  1. 单片机开发板:任何一款51系列、STM32或AVR的开发板都可以,这里我们以经典的STC89C52(51内核)为例。
  2. 蜂鸣器
    • 有源蜂鸣器:内部有振荡源,只要给高电平就会响,频率固定。不适合本项目,因为我们无法改变其音高。
    • 无源蜂鸣器:内部没有振荡源,需要单片机提供特定频率的方波才能发声。必须使用无源蜂鸣器。
  3. 杜邦线、下载器等。

硬件连接

将无源蜂鸣器的正极通过一个限流电阻(如1kΩ)连接到单片机的任意一个I/O口(P2.7),蜂鸣器的负极接地。

c语言单片机音乐盒设计
(图片来源网络,侵删)

软件设计 (以STC89C52为例)

我们将分步实现代码:

步骤 1:建立工程和配置头文件

#include <reg52.h> // 包含STC89C52的头文件
// sbit 是C51扩展的关键字,用于定义位寻址的I/O口
sbit BEEP = P2^7; // 定义蜂鸣器连接的I/O口

步骤 2:定义音符频率表

我们需要一个将音符映射到频率的查找表,这里我们使用一个简单的数组。

// 音符频率表 (单位: Hz)
// C调, 4MHz晶振 (频率可能需要微调)
// 音符:  Do, Re, Mi, Fa, Sol, La, Si, Do(H), Re(H), Mi(H), Fa(H), Sol(H), La(H), Si(H)
// 索引:   1,   2,   3,   4,    5,   6,   7,    8,    9,    10,    11,     12,     13,     14
// 索引0用于休止符
unsigned int freq[] = {
    0,      // 0: 休止符
    262,    // 1: Do (C4)
    294,    // 2: Re (D4)
    330,    // 3: Mi (E4)
    349,    // 4: Fa (F4)
    392,    // 5: Sol (G4)
    440,    // 6: La (A4)
    494,    // 7: Si (B4)
    523,    // 8: Do (C5)
    587,    // 9: Re (D5)
    659,    // 10: Mi (E5)
    698,    // 11: Fa (F5)
    784,    // 12: Sol (G5)
    880     // 13: La (A5)
};

步骤 3:编写定时器中断服务程序

这是项目的核心,我们使用定时器0工作在模式1(16位定时器)。

  • 计算定时器初值
    • 假设晶振频率为 Fosc = 12MHz,则机器周期为 T = 12 / Fosc = 1μs
    • 定时器需要每 T_period = 1 / (2 * freq) 秒触发一次中断。
    • 定时器需要计数的个数 N = T_period / T = (1 / (2 * freq)) / (1 / 12,000,000) = 6,000,000 / freq
    • 定时器初值 TH0 = TL0 = 65536 - N
// 定时器0中断服务程序
void Timer0_ISR() interrupt 1
{
    static bit toggle_bit = 0; // 静态局部变量,用于翻转I/O口
    // 重装初值 (这里以262Hz为例,实际应在播放时动态计算)
    // TH0 = (65536 - 6000000 / 262) / 256;
    // TL0 = (65536 - 6000000 / 262) % 256;
    // 翻转蜂鸣器I/O口状态
    toggle_bit = ~toggle_bit;
    BEEP = toggle_bit;
    // 注意:在真正的代码中,初值会根据当前播放的音符频率动态计算。
    // 为了简化,这里先不动态计算,后面会整合。
}

步骤 4:编写主程序

主程序负责初始化、定义曲谱、并控制播放。

c语言单片机音乐盒设计
(图片来源网络,侵删)
// 定义曲谱 (使用上面音符表的索引)
// 节拍: 4代表1/4音符, 8代表1/8音符, 0代表休止符
// 这里以《小星星》为例
unsigned char song[] = {
    1, 1, 5, 5, 6, 6, 5, 0, // 一闪一闪亮晶晶
    4, 4, 3, 3, 2, 2, 1, 0, // 满天都是小星星
    1, 1, 5, 5, 6, 6, 5, 0, // 一闪一闪亮晶晶
    4, 4, 3, 3, 2, 2, 1, 0  // 满天都是小星星
};
// 播放单个音符
void PlayNote(unsigned char note_index, unsigned int beat)
{
    unsigned int frequency;
    unsigned int timer_reload_value;
    if (note_index == 0) {
        // 休止符
        BEEP = 0; // 确保蜂鸣器不响
        TR0 = 0;  // 关闭定时器
    } else {
        // 计算当前音符的频率
        frequency = freq[note_index];
        // 计算定时器重载值
        timer_reload_value = 65536 - 6000000 / frequency;
        // 设置定时器初值
        TH0 = timer_reload_value / 256;
        TL0 = timer_reload_value % 256;
        // 启动定时器0
        TR0 = 1;
    }
    // 根据节拍延时 (这里用简单的软件延时,不精确)
    // 假设一个4分音符的延时为250ms
    // 8分音符就是125ms
    Delay_ms(250 / beat); // 注意:这是一个简化的延时函数
    // 停止发声,形成音符间的间隙
    TR0 = 0;
    BEEP = 0;
    Delay_ms(50); // 音符间隙
}
// 软件延时函数 (粗略延时)
void Delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = ms; i > 0; i--)
        for (j = 114; j > 0; j--);
}
// 主函数
void main()
{
    unsigned char i = 0;
    // 初始化定时器0
    TMOD = 0x01;    // 设置定时器0为模式1 (16位定时器)
    TH0 = 0x00;     // 设置初值 (初始值不重要,因为PlayNote会重置)
    TL0 = 0x00;
    ET0 = 1;        // 使能定时器0中断
    EA = 1;         // 开启总中断
    // 初始化蜂鸣器
    BEEP = 0;
    while(1)
    {
        // 循环播放曲谱
        for (i = 0; i < sizeof(song) / sizeof(song[0]); i++)
        {
            // 假设所有音符都是4分音符 (beat=4)
            // 如果需要不同节拍,可以再建一个节拍数组
            PlayNote(song[i], 4); 
        }
        // 曲子播放完后,暂停一会儿
        Delay_ms(1000);
    }
}

代码整合与优化

上面的代码结构清晰,但有一个问题:PlayNote函数中的延时会阻塞CPU,导致无法同时处理其他任务(如果有的话),一个更好的方法是使用定时器来驱动节拍,而不是软件延时。

优化版思路(使用定时器驱动节拍)

  1. 使用两个定时器
    • 定时器0:负责产生特定频率的方波(音高)。
    • 定时器1:负责产生节拍延时(时长)。
  2. 状态机:在主循环中,我们不再阻塞延时,而是检查节拍定时器是否到了,如果到了就播放下一个音符。

优化版代码框架

// ... (头文件和频率表同上) ...
// 定义曲谱
unsigned char song[] = { ... };
unsigned char beat[] = { 4, 4, 4, 4, 4, 4, 4, 8, ... }; // 对应每个音符的节拍
unsigned char current_note = 0; // 当前播放到第几个音符
bit is_playing = 0;            // 是否正在播放一个音符
// 定时器0中断 (产生音高)
void Timer0_ISR() interrupt 1
{
    // 翻转I/O口
    BEEP = ~BEEP;
    // 重装初值 (这个初值在开始播放音符时就已经设置好了)
    // TH0 = ...;
    // TL0 = ...;
}
// 定时器1中断 (产生节拍)
void Timer1_ISR() interrupt 3
{
    static unsigned int beat_count = 0;
    // 重装初值 (每5ms触发一次)
    // TH1 = ...;
    // TL1 = ...;
    beat_count++;
    if (beat_count >= 100) { // 假设100 * 5ms = 500ms为一个基准单位
        beat_count = 0;
        is_playing = 0; // 标记当前音符播放结束
        TR0 = 0;        // 停止发声
        BEEP = 0;
    }
}
void PlayNextNote()
{
    if (current_note >= sizeof(song) / sizeof(song[0])) {
        current_note = 0; // 重新开始
        return;
    }
    if (song[current_note] == 0) {
        // 休止符
        TR0 = 0;
        BEEP = 0;
    } else {
        // 根据音符频率设置定时器0
        unsigned int timer_reload_value = 65536 - 6000000 / freq[song[current_note]];
        TH0 = timer_reload_value / 256;
        TL0 = timer_reload_value % 256;
        TR0 = 1; // 启动定时器0开始发声
    }
    // 根据节拍设置定时器1的延时
    // 假设4分音符 = 500ms, 8分音符 = 250ms
    unsigned int beat_duration = 500 / beat[current_note];
    // 需要根据beat_duration调整定时器1的溢出次数或重载值
    // ... (这里省略具体计算) ...
    is_playing = 1;
    current_note++;
}
void main()
{
    // 初始化定时器0 (模式1)
    TMOD = 0x11; // 定时器0和1都为模式1
    // 初始化定时器1 (节拍)
    // TH1 = ...;
    // TL1 = ...;
    ET1 = 1;
    // 开启中断
    ET0 = 1;
    EA = 1;
    TR1 = 1; // 启动节拍定时器
    while(1)
    {
        if (!is_playing) {
            PlayNextNote();
        }
        // 在这里可以添加其他非阻塞的任务
    }
}

总结与扩展

  1. 设计一个C语言单片机音乐盒,核心是利用定时器中断产生特定频率的方波来模拟音符,并通过遍历音符数组来控制曲子的播放。
  2. 扩展
    • 更精确的节拍:使用定时器驱动节拍,而不是软件延时,使系统更高效。
    • 多任务处理:在主循环中,除了播放音乐,还可以同时点亮LED、读取按键等。
    • 按键控制:添加按键,实现“播放/暂停”、“上一曲/下一曲”等功能。
    • LCD显示:结合LCD屏幕,显示正在播放的歌曲名或歌词。
    • 更复杂的曲谱:可以设计一个更灵活的曲谱数据结构,支持升降调、附点音符等。
    • PWM输出:对于一些高级单片机(如STM32),可以使用PWM(脉冲宽度调制)输出,音质会比简单的方波更好。

这个项目从简单到复杂,有很多可以深入探索的地方,是学习单片机外设和中断编程的绝佳实践,祝你成功!

-- 展开阅读全文 --
头像
广东星海音乐学院招生网怎么查招生信息?
« 上一篇 2025-12-07
中央音乐学院附中声乐,如何培养顶尖歌唱人才?
下一篇 » 2025-12-07

相关文章

取消
微信二维码
支付宝二维码

最近发表

标签列表

目录[+]