引言

无源蜂鸣器(Buzzer)内部不含振荡源,需外部提供方波信号才能发声。通过调节方波频率,即可产生不同音调。STM32 的定时器 PWM 输出能精确产生所需频率,配合简单的音符映射,就能实现类似 MIDI 的音乐播放。本文以 STM32F103 为例,讲解如何用 PWM 驱动无源蜂鸣器,并给出可复用的代码框架。

原理讲解

1. PWM 与蜂鸣器

PWM(脉冲宽度调制)输出固定频率的方波,其频率由定时器的预分频器(PSC)和自动重载值(ARR)决定:

PWM频率 = 定时器时钟 / ((PSC+1) * (ARR+1))

无源蜂鸣器的音调由方波频率决定,占空比影响音量(通常 50% 左右)。因此,要播放不同音符,只需改变 ARR 值(或 PSC)来调整频率。

2. 音符频率计算

音乐中,标准音 A4(中央 C 上方的 A)频率为 440Hz。其他音符频率可通过公式计算:

f = 440 * 2^((n-69)/12),其中 n 为 MIDI 音符编号(A4=69)。

实际应用中,可预定义常用音符频率表,或动态计算。为简化,本文使用静态数组存储从 C4 到 B5 的频率值。

3. 简易 MIDI 播放

MIDI 文件本质是音符事件序列(如 note-on/off)。我们可定义一个结构体数组,包含音符编号和持续时间,模拟 MIDI 播放。通过定时器中断或延时控制每个音符的时长。

硬件连接

  • 无源蜂鸣器正极接 STM32 的 PWM 输出引脚(如 PA0,对应 TIM2_CH1)
  • 蜂鸣器负极接 GND(若蜂鸣器工作电压为 5V,需通过三极管或驱动芯片,但本例假设 3.3V 可直接驱动)

配置步骤

1. 使能定时器时钟

使用 TIM2,其时钟来自 APB1,频率为 72MHz(若系统时钟 72MHz)。

2. 配置 GPIO 为复用推挽输出

PA0 映射到 TIM2_CH1,需配置为 AF_PP。

3. 配置定时器 PWM 模式

  • 设置 PSC 和 ARR 以产生目标频率
  • 设置 PWM 模式 1,占空比 50%(CCR = ARR/2)
  • 使能输出比较通道和定时器

4. 编写音符播放函数

根据音符频率计算 ARR 值,并更新定时器。

代码实现

以下代码基于 STM32 标准外设库,使用 TIM2 PWM 输出。

#include "stm32f10x.h"

// 音符频率表(C4~B5,MIDI编号60~71)
const uint16_t note_freq[] = {
    262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494
};

// 简易MIDI事件:音符编号(0~11),时长(ms)
typedef struct {
    uint8_t note;   // 0~11,对应频率表索引
    uint16_t dur;   // 时长ms
} midi_event;

// 示例旋律:小星星
const midi_event melody[] = {
    {0, 400}, {0, 400}, {4, 400}, {4, 400}, {5, 400}, {5, 400}, {4, 800},
    {2, 400}, {2, 400}, {3, 400}, {3, 400}, {4, 400}, {4, 400}, {2, 800},
    {0, 400}, {0, 400}, {4, 400}, {4, 400}, {5, 400}, {5, 400}, {4, 800},
    {2, 400}, {2, 400}, {3, 400}, {3, 400}, {4, 400}, {4, 400}, {2, 800}
};

void TIM2_PWM_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 配置 PA0 为复用推挽
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 定时器时基:72MHz/(PSC+1) = 1MHz,即1us计数一次
    TIM_TimeBaseStructure.TIM_Period = 1000; // ARR,初始值,后续更新
    TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC = 71
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // PWM 模式1,占空比50%
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 500; // CCR = ARR/2
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM2, ENABLE);
    TIM_Cmd(TIM2, ENABLE);
}

// 设置音符频率:根据音符索引更新ARR
void Set_Note(uint8_t note_index) {
    uint16_t freq = note_freq[note_index];
    // 定时器时钟1MHz,ARR = 1MHz / freq - 1
    uint16_t arr = 1000000 / freq - 1;
    TIM_SetAutoreload(TIM2, arr);
    // 更新占空比(50%)
    TIM_SetCompare1(TIM2, arr / 2);
}

// 播放旋律
void Play_Melody(void) {
    uint16_t i;
    for (i = 0; i < sizeof(melody)/sizeof(melody[0]); i++) {
        Set_Note(melody[i].note);
        Delay_ms(melody[i].dur);
        // 停止发声:设置ARR为0或关闭输出
        TIM_SetCompare1(TIM2, 0);
        Delay_ms(20); // 音符间隔
    }
}

// 简单延时函数(需实现)
void Delay_ms(uint32_t ms) {
    // 使用SysTick或循环实现
}

int main(void) {
    TIM2_PWM_Init();
    while (1) {
        Play_Melody();
        Delay_ms(1000);
    }
}

注意事项

  • 频率精度:ARR 计算时取整,可能产生微小误差,但人耳可接受。若需更精确,可调整 PSC 或使用更高分辨率定时器。
  • 占空比:50% 占空比方波音色较纯,但实际蜂鸣器可能对占空比敏感,可适当调整。
  • 停止发声:将 CCR 设为 0 可关闭输出,但定时器仍在运行。也可禁用 TIM 输出,但简单置零更高效。
  • 驱动能力:STM32 GPIO 输出电流有限,若蜂鸣器功率较大,需外接三极管或驱动芯片。
  • 扩展 MIDI:可解析标准 MIDI 文件,提取音符事件,替换 melody 数组。注意时基转换。
  • 实时性:使用延时播放会阻塞 CPU,若需多任务,可改用定时器中断或 DMA。

总结

本文通过 STM32 定时器 PWM 驱动无源蜂鸣器,实现了基于音符频率表的简易 MIDI 播放。核心在于理解 PWM 频率与音符的关系,以及灵活配置定时器。此方法可轻松扩展至更复杂的音乐播放,甚至结合按键交互。希望本文能为你开发蜂鸣器应用提供参考。