很多人拿到 STM32F103 最小系统板和一颗蜂鸣器,第一反应是“给引脚输出高低电平,蜂鸣器就应该会响”。等接好线、烧完程序,发现蜂鸣器要么一直是一种刺耳的固定声音,要么只会“咔哒咔哒”响,完全没有旋律可言。问题出在两个地方:一是蜂鸣器类型选错,二是频率控制没有做到位。
这篇文章就来讲清楚 STM32F103 怎么用蜂鸣器播放歌曲。它会解释有源蜂鸣器和无源蜂鸣器的本质区别,说明为什么播放旋律必须用无源蜂鸣器,再给出完整的定时器 PWM 驱动方案。按照文章里的代码,你可以让蜂鸣器播放《小星星》这类简单歌曲,也能套用同样的数据结构换成任何你会哼的旋律。
先给一个明确判断:蜂鸣器播放歌曲这个需求,技术难度不高,但它是一个典型的“原理清楚、接线简单、代码细节决定成败”的小项目。真正值得花时间理解的不是“怎么让蜂鸣器响”,而是“怎么把一个音符表达成频率,把一段乐谱表达成数据,再把频率交给定时器输出”。
1. 这篇文章真正要解决的问题
很多初学者会在蜂鸣器播放音乐时遇到下面几种情况:
第一种,买的蜂鸣器是有源蜂鸣器,上电就响,给高电平也响,给低电平也响,完全无法改变音调。第二种,用的是无源蜂鸣器,但程序只做 GPIO 翻转,CPU 写HAL_GPIO_TogglePin翻转频率不稳定,声音偶尔能听出音高,但一旦加其他逻辑就变调。第三种,蜂鸣器接在 PA6 上直接驱动,结果声音很轻,或者蜂鸣器工作电流太大,导致单片机复位。
这些问题的源头可以总结成两句话:
- 播放旋律需要“可控频率”的方波,普通 GPIO 延时翻转做不到稳定、精确的频率。
- 蜂鸣器是感性或容性负载,需要有正确的驱动方式和足够的电流,不能简单把它当 LED 灯来点亮。
所以本文会按下面这条线展开:
- 先分辨蜂鸣器类型,确定项目是否选对硬件。
- 说明声音频率和音符时长如何表达成程序数据。
- 使用 STM32F103 的定时器 PWM 生成稳定方波。
- 用一段可复制的 HAL 库代码完成蜂鸣器单音、歌曲播放。
- 最后总结调试方法、常见问题和工程化注意事项。
读完这篇文章,你能完成从“硬件接线”到“跑通一首歌”的完整闭环。即便你后续把蜂鸣器换成其他发声器件,换用 FreeRTOS 做任务调度,核心的“频率-时长”模型仍然可以直接迁移。
2. 蜂鸣器类型与核心原理
2.1 有源蜂鸣器和无源蜂鸣器不是一个东西
很多采购页面上写的“有源”“无源”会把人绕晕。这里的源,指的是“振荡源”,不是“电源”。
有源蜂鸣器内部自带振荡电路,只要给它一个额定直流电压,它自己就能产生固定频率的声音。它对单片机的控制要求很低,给高电平就响,给低电平就停。但问题也很明显:内部振荡频率是固定的,通常只有 2.3kHz 到 3kHz 左右,你没有办法通过改变 GPIO 输出频率来改变它的音调。
无源蜂鸣器内部没有振荡源,它本质上是一个电磁线圈,或者一片压电陶瓷片。你给它一个方波信号,它才会按照这个方波的频率振动发声。方波频率高,音调就高;方波频率低,音调就低。想播放旋律,就必须用无源蜂鸣器。
使用前的区分方法很简单:
| 类型 | 固定声音 | 可变音调 | 需要 PWM/方波 | 上电是否自响 | 适合场景 |
|---|---|---|---|---|---|
| 有源蜂鸣器 | 是 | 否 | 不需要 | 会 | 报警、提示音 |
| 无源蜂鸣器 | 否 | 是 | 需要 | 不会 | 播放音乐、和弦场景 |
这里有一个容易被忽视的坑:如果你项目中已经有现成的三脚蜂鸣器模块,不要默认它就是无源蜂鸣器。三脚封装里也可能是内部带三极管驱动的有源蜂鸣器。以实物上的丝印和数据手册为准,最稳妥的办法是看商家标注“自激式”还是“他激式”,或者直接测试:给蜂鸣器供电,如果上电就一直响,就是有源。
2.2 音高本质是方波频率
人耳能听到的频率范围一般是 20Hz 到 20kHz,蜂鸣器播放音乐时,我们关心的不是 20kHz 以上的超声,而是中频段的几百赫兹到几千赫兹。
在单片机中,无源蜂鸣器播放一个音,最简单的表达方式是:
- GPIO 输出高低电平,形成方波。
- 方波频率为 500Hz 时,声音接近 B4 附近的音高。
- 方波频率为 1000Hz 时,音高比 500Hz 高一倍。
频率和音高不是线性关系,而是指数关系。以十二平均律为例,A4 的标准频率是 440Hz,每升高一个半音,频率乘以 2 的 1/12 次方。所以音阶 C4、D4、E4、F4、G4、A4、B4 的频率大约分别是 262、294、330、349、392、440、494Hz。
实际代码里不需要每次都去算指数,最常用的做法是把需要的音符频率做成一个宏表,或者在乐谱数组里直接填写频率数字。单片机世界里的“乐谱”,本质上就是一组“频率 + 时长”的序列。
2.3 定时器 PWM 是更好的方波来源
有人会用 GPIO 翻转加延时来产生方波,比如:
while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_6); HAL_Delay(1); }这段代码能跑,但有两个问题。第一,HAL_Delay的精度受系统时钟和中断影响,无法稳定产生 262Hz、294Hz 这样的精确频率。第二,如果程序里同时要处理按键、屏幕刷新、传感器读取,整个方波周期会变得不可控,音调就会跑偏。
使用定时器 PWM 之后,方波的频率完全由硬件定时器决定,CPU 不需要一直翻转引脚,只需要在切换音符时修改定时器的自动重载值和比较值。
以 TIM3_CH1 为例,它默认会映射到 PA6。PWM 模式下,定时器计数器从 0 向上计数到自动重载值,再回到 0。比较寄存器 CCR 决定了引脚电平何时翻转。如果把比较值设为自动重载值的一半,就得到占空比约 50% 的方波,这也是驱动蜂鸣器最常用的占空比。
3. 乐谱数据怎么设计
3.1 用频率和节拍表达一首歌曲
写代码前,先把“歌曲”这个抽象概念拆开。
一首歌由一串音符组成,每个音符有两个属性:音高和时值。音高对应 PWM 方波的频率,时值对应这个频率持续多长时间。
举个例子,《小星星》的开头是:
C C G G A A G在 C 大调里,这些音对应的频率是:
| 音名 | 频率(Hz) |
|---|---|
| C4 | 262 |
| D4 | 294 |
| E4 | 330 |
| F4 | 349 |
| G4 | 392 |
| A4 | 440 |
| B4 | 494 |
时值方面,可以用“十六分音符”作为最小单位。在 4/4 拍中,一个全音符等于 16 个十六分音符,二分音符等于 8 个,四分音符等于 4 个,八分音符等于 2 个。这样定义之后,一个音符的时长就可以用整数表达,避免