“蜂鸣器怎么播放歌曲”这个问题,看起来像是嵌入式初学者的玩具题,但它背后藏着 STM32F103 入门阶段最值得掌握的一组能力:理解信号频率与声音的关系、看懂蜂鸣器类型与驱动电路、学会用定时器/PWM 精确产生方波,以及用数据结构把乐谱翻译成机器能执行的指令。
先说一个明确判断:蜂鸣器播放歌曲并不需要复杂的音频解码算法。STM32F103 要做的,只是按照乐谱的时间轴,在蜂鸣器引脚上输出“对应音符频率的方波”。难点不在能不能唱,而在“频率是否足够准确”“每个音持续多久”“换音时有没有杂音”“驱动电路能不能稳定工作”。把这四条搞明白,不止能放《小星星》,以后放什么歌都只是换数组的问题。
这篇文章会用偏实战的方式展开:先带你避开有源蜂鸣器这个最常见的坑,再说清楚驱动电路为什么不能只靠 GPIO 直推,然后给出定时器中断翻转 IO、PWM 输出两种播放方案,最后补充验证步骤、常见问题和工程建议。
1. 蜂鸣器播放歌曲到底难在哪里
很多初学者在第一次拿到蜂鸣器时,会以为“播放歌曲”等于“给单片机上写一段旋律”。等代码写完了,板子却只发出刺耳的“滴——滴——”,完全听不出任何曲调。原因通常不在代码,而在三个地方:
第一,选错了蜂鸣器。有源蜂鸣器内部自带振荡电路,通电就响,但频率固定,无法改变音高。第二,驱动电路不够。蜂鸣器工作电流可能超过 STM32F103 GPIO 的可靠输出能力,直接接引脚容易导致输出电压跌落、响度不足,甚至长期使用可能损坏 IO。第三,没有任何“时间轴”概念。声音由频率决定音高,但只输出频率还不够,每个音符必须持续正确的时长,节拍乱了,旋律也就散了。
换个角度理解:MCU 播放音乐,本质上是一个“频率调度器”加一个“节拍管理器”。STM32F103 主频 72MHz,输出常见音符频率 200Hz 到 4kHz 的方波绰绰有余,甚至不会占用太多 CPU。真正需要训练的是你对定时器分频、自动重装载值、PWM 通道和中断回调的理解。这也是为什么这个项目很适合作为单片机进阶练习:它把硬件、外设和软件模型全部串了起来。
所以,如果你正在学 STM32F103,或者准备做带提示音、报警音、简单音乐效果的物联网小项目,本文的内容可以直接复用。如果你只想听到一段现成旋律,不关心原理,也不用急着收藏,因为下面的分析会比最终那段代码更值钱。
2. 有源蜂鸣器与无源蜂鸣器的本质区别
2.1 两种蜂鸣器的命名很容易误会
“有源”和“无源”并不是指“有没有电源”,而是指“内部有没有自带振荡源”。常见表述为:
- 有源蜂鸣器:内部集成振荡电路,只要在正负极之间加上额定直流电压,它就能自行产生固定频率的声音。你给它高电平,它就响;给低电平,它就停。节奏可以控制,但音高不能控制。
- 无源蜂鸣器:内部没有振荡源,必须由外部提供一定频率的方波或脉冲信号才能发声。外部方波的频率决定了它发出的音高。
| 对比项 | 有源蜂鸣器 | 无源蜂鸣器 |
|---|---|---|
| 内部振荡器 | 有 | 无 |
| 输入直流电平是否发声 | 会发声 | 通常不发声或只发出很小的咔哒声 |
| 音高是否可控 | 不可控,频率固定 | 可控,由外部方波频率决定 |
| 典型应用 | 报警、按键提示音 | 播放旋律、音乐提示、频率输出 |
| 控制复杂度 | 低 | 需要定时器或 PWM |
| 适合播放歌曲 | 不适合 | 适合 |
2.2 为什么播放歌曲必须用无源蜂鸣器
歌曲旋律的本质是不同音高的音符按时间顺序排列。比如《小星星》开头是“1 1 5 5 6 6 5”,每个数字对应的是一组特定频率。要让蜂鸣器“唱”出这些音,就必须随时改变它的发声频率。有源蜂鸣器做不到这一点,它只能从固定频率和静音之间二选一。而无源蜂鸣器不内置振荡源,外部给多少 Hz 的方波,它就发多少 Hz 的声音,这正好符合需求。
实际项目里,无源蜂鸣器还可以再细分为电磁式和压电式。电磁式蜂鸣器一般音量大,适合报警和旋律通知;压电式蜂鸣器更省电,但需要的驱动电压较高,声音偏尖。选择时注意蜂鸣器额定电压,常见有 3.3V、5V、12V 等型号。STM32F103 的 GPIO 输出 3.3V,所以用 5V 蜂鸣器时通常不能直接靠 IO 驱动,这一点在下一节展开。
3. 驱动电路:为什么不能直接把蜂鸣器接到 GPIO
3.1 GPIO 直驱的常见问题
网上很多最小系统板集成的是 3.3V 有源蜂鸣器,这种板子内部已经做了三极管驱动,可以直接由 MCU 引脚控制。但如果你是在面包板上自己搭电路,把无源蜂鸣器两个引脚直接接到 STM32F103 的 GPIO 和 GND,很容易遇到问题。
STM32F103 的 GPIO 输出能力有限,不同封装和供电条件下的驱动能力并不一致。工程师习惯上不会让单个 GPIO 长时间输出超过 20mA 左右的电流。很多蜂鸣器工作电流在 20mA 到 40mA,甚至更高。如果直接驱动,GPIO 电压会被拉低,蜂鸣器响度不足;如果蜂鸣器是感性负载,断电瞬间还会产生反电动势,对 GPIO 存在潜在威胁。
所以更稳妥的方案是让 GPIO 只负责输出控制信号,由三极管、MOS 管或达林顿驱动芯片去驱动蜂鸣器这个大电流负载。这也是“GPIO 控制外设”最标准的工程做法。
3.2 使用 NPN 三极管低边驱动
以 S8050 这类 NPN 三极管为例,电路连接如下:
- 蜂鸣器正极接电源 VCC,常见为 5V。
- 蜂鸣器负极接三极管集电极。
- 三极管发射极接 GND。
- 三极管基极串一个 1kΩ 到 4.7kΩ 的电阻再接 STM32F103 GPIO。
当 GPIO 输出高电平时,三极管导通,蜂鸣器通电;GPIO 输出低电平时,三极管截止,蜂鸣器断电。基极电阻限制基极电流,避免三极管过驱动。
如果是电磁式蜂鸣器,建议在蜂鸣器两端反向并联一个续流二极管,二极管的阴极接电源正极,阳极接三极管集电极。这样在三极管关断瞬间,线圈中的电流可以通过二极管续流,保护三极管不被反向电动势击穿。压电式蜂鸣器电容性更强,通常不像继电器那样强调续流,但加一个反向二极管在很多电磁式蜂鸣器应用里属于标准设计。
3.3 使用 ULN2003APG 达林顿驱动
你在检索“蜂鸣器播放歌曲”时,可能会看到 ULN2003APG 连接蜂鸣器的视频或文章。ULN2003APG 是一类达林顿晶体管阵列,常见型号内部有 7 路达林顿管,每路输出都是集电极开路结构,输出端可以接到地,适合驱动继电器、蜂鸣器、小电机等负载。
使用 ULN2003APG 驱动一路蜂鸣器的接线方式为:
- STM32F103 GPIO 接 ULN2003APG 的输入引脚,比如 1B。
- ULN2003APG 对应输出引脚 1C 接蜂鸣器负极。
- 蜂鸣器正极接电源 VCC。
- STM32F103 和蜂鸣器电源之间必须共地。
ULN2003APG 内部已经为每一路输入集成了基极电阻,因此不需要像三极管电路那样额外串电阻。它的输出电流能力远高于单个三极管方案,驱动蜂鸣器非常从容。不过要注意,ULN2003APG 属于集电极开路输出,导通时输出低电平,逻辑上要注意控制电平的“有效”方向。
3.4 三类驱动方案对比
| 驱动方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| GPIO 直驱 | 电路最简单 | 驱动能力弱,可能烧 IO | 只适合低压微功耗蜂鸣器,不推荐 |
| NPN 三极管 | 成本低,电路常见 | 需要计算基极电阻 | 入门和大多数项目 |
| ULN2003APG | 驱动能力强,内部带电阻 | 占用体积大,芯片成本高于三极管 | 同时驱动多路蜂鸣器/继电器/电机 |
本文后续代码示例中,蜂鸣器控制引脚默认为 PA0,也就是 TIM2_CH1。只要你的硬件驱动电路在前面三级基础上任选一种,软件部分都通用。若使用 ULN2003APG,输入高电平时蜂鸣器导通,GPIO 输出高电平与 PWM 高电平语义一致,软件不需要额外反转。
4. 从音符频率到定时器 PWM
4.1 频率决定音高,时值决定节奏
要让无源蜂鸣器发出某个音,只需让引脚输出一个该频率的方波。例如 A4 的标准频率是 440Hz,那就让 GPIO 以 440Hz 的周期翻转高低电平。占空比保持在 50% 左右时,蜂鸣器声音通常最清晰。
但音乐不是一串独立的频率,每个音符还要持续一定时间。我们把“持续多久”叫时值。BPM(Beats Per Minute)表示每分钟有多少个四分音符。BPM=120 时,一个四分音符持续 60000 / 120 = 500ms。八分音符持续 250ms,二分音符持续 1000ms。因此,一首歌曲的播放程序可以简化成循环处理一个数组:
第 1 步:从数组中取出音符频率。
第 2 步:配置定时器/PWM,让输出频率等于音符频率。
第 3 步:按 BPM 计算该音符的持续时间,并延时。
第 4 步:切换到下一个音符,直到数组结束。
4.2 常用音符频率表
下面是一张 C 调常用音符频率表,频率单位是 Hz。实际使用中可以保留整数,因为蜂鸣器振动单元对频率误差并不敏感,几十赫兹以内的误差人耳很难察觉。
| 音名 | C4 | D4 | E4 | F4 | G4 | A4 | B4 | C5 |
|---|---|---|---|---|---|---|---|---|
| 频率 Hz | 262 | 294 | 330 | 349 | 392 | 440 | 494 | 523 |
这张表是后续歌曲数组的基础。如果用数字简谱“1 2 3 4 5 6 7”,在 C 调下可以分别映射到 C4、D4、E4、F4、G4、A4、B4。
4.3 STM32F103 定时器与 PWM 频率公式
STM32F103 定时器在向上计数模式下,PWM 输出频率的计算公式是:
PWM_Freq = TIM_CLK / ((PSC + 1) * (ARR + 1))- TIM_CLK:定时器输入时钟。
- PSC:预分频值。
- ARR:自动重装载值。
很多网上教程把 PSC 设置为 71,因为如果 TIM_CLK 是 72MHz,那么 (PSC+1)=72,计数时钟变成 1MHz。这时每个计数周期是 1μs,ARR 与输出周期直接对应:
PWM_Freq = 1MHz / (ARR + 1)所以需要输出 freq Hz 时:
ARR + 1 = 1000000 / freq例如 freq=262Hz 时,ARR = 1000000 / 262 - 1 ≈ 3816。这样设置后,定时器以 1MHz 的计数频率数完 3817 个周期,输出一个 262Hz 的方波,实际频率误差远小于 1%。
这里需要注意:如果你的系统主频不是 72MHz,或者定时器所在总线时钟不是 72MHz,PSC 和 ARR 计算必须基于实际时钟。CubeMX 工程中,建议通过 HAL_RCC_GetPCLK1Freq 或 HAL_RCC_GetPCLK2Freq 获取总线时钟,而不是把 72MHz 写死在代码里。
5. 方案一:定时器中断翻转 GPIO 最小验证
在进入 PWM 播放歌曲之前,先介绍一种更直观的思路:让定时器产生固定周期的更新中断,每次中断翻转一次蜂鸣器 IO。这样软件亲手“拼接”出一个方波。
以 1kHz 方波为例,它的周期是 1ms,半周期是 0.5ms。定时器每 0.5ms 产生一次更新中断,中断里翻转 IO,输出就是 1kHz 方波。用 HAL 库实现时,核心代码如下:
// TIM2 更新中断回调 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); } }主程序中启动定时器更新中断:
HAL_TIM_Base_Start_IT(&htim2);配置 TIM2 时,如果计数时钟是 1MHz,则要得到 0.5ms 更新周期,需要设置:
htim2.Init.Prescaler = 72 - 1; // 计数时钟 1MHz htim2.Init.Period = 500 - 1; // 0.5ms这种方案的优点是逻辑简单,很容易理解“方波频率由定时器周期决定”。缺点是每个音符都要靠中断翻转 IO,如果要输出较高频率,中断频率会成倍增加。比如播放 4kHz 音高,中断频率就是 8kHz,CPU 负担不小。而且中断服务函数里每条指令都会影响占空比精度,所以这个方案适合教学验证,不适合追求稳定旋律播放的工程实现。
如果你在实验板上跑通了“每 0.5ms 翻转一次,蜂鸣器发出 1kHz 声音”,说明驱动电路和声音通路已经正常。下一步就可以换成 PWM 方案了。