STM32 无源蜂鸣器音乐播放:基于 PWM 的简易 MIDI 实现
👁 6 阅读 · 2026-08-18 · STM32 PWM
无源蜂鸣器通过 PWM 驱动可精确控制频率,从而播放旋律。本文介绍如何利用 STM32 定时器的 PWM 输出,结合音符频率表,实现简易 MIDI 音乐播放。内容涵盖 PWM 原理、音符频率计算、定时器配置、代码实现及注意事项,帮助开发者快速上手并扩展。
## 引言
无源蜂鸣器(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 输出。
```c
#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 频率与音符的关系,以及灵活配置定时器。此方法可轻松扩展至更复杂的音乐播放,甚至结合按键交互。希望本文能为你开发蜂鸣器应用提供参考。