蜂鸣器与声音:给作品加上"滴"一声
- 分清有源蜂鸣器和无源蜂鸣器,知道各自怎么接、怎么用
- 用 gpio_set_level 让有源蜂鸣器"滴"一声,用 LEDC 变频让无源蜂鸣器奏出不同音调和简单旋律
- 理解 PWM 频率和音调的关系(占空比管亮度、频率管音调),以及什么时候要加三极管驱动
你的作品按下按钮、传感器触发、任务完成——这些时刻有没有声音,体验差一大截。一个安静的设备你得盯着屏幕才知道它在干嘛;加一声"滴",闭着眼都知道"收到了"。报警就更需要声音了,灯亮在角落你可能看不见,但一阵蜂鸣绝对躲不开。
给项目加声音,最便宜、最省事的就是蜂鸣器,几毛钱一个。它不像喇叭能放音乐,但发个提示音、拉个警报、哼一小段旋律绰绰有余。这一节先把"蜂鸣器有两种、别买错"讲清楚,再给你两版能直接烧进去就响的代码。
动手前你得先搭好 ESP-IDF 环境、做过点亮第一个 LED——控制 GPIO 高低电平、app_main + while(1) 的骨架这里要复用;无源蜂鸣器靠的是 LEDC 发方波,和 PWM 调光同一套硬件,做过更好。
第一步:先分清你手上是哪种蜂鸣器
蜂鸣器分两种,买错了代码就对不上,这是新手第一个坑。先认清楚:
有源蜂鸣器(active)——内部自带振荡电路。你只要给它通电,它就响,而且永远是那一个固定的音(通常 2~4kHz 的"滴")。你能控制的只有"响 / 不响",控制不了音调。接法和 LED 一模一样:正极接 GPIO,负极接 GND,gpio_set_level(pin, 1) 就响,0 就停。简单,适合只要个提示音的场景。
无源蜂鸣器(passive)——内部没有振荡电路,自己不会响。你得给它一个方波信号去"敲"它,方波的频率决定它发什么音。给 523Hz 就发中音 do,给 880Hz 就发更高的音。正因为频率可调,它能发不同的音、能奏旋律。更灵活,适合要做音阶、报警声扫频、放小曲子的场景。
怎么分辨手上这颗是哪种?
- 看封口:有源的底部通常用塑料或贴纸封死,看不到内部电路板;无源的底部常常裸露,能看到一块绿色的小电路板。
- 看价格:有源一般略贵一点点(内置了振荡电路)。
- 拿万用表 / 电池量:直接拿 3V 纽扣电池正负极碰一下两个脚,一通电就持续响的是有源;只听到"咔哒"一声、不持续响的是无源。
分不清就拿 3V 电池碰一下脚——持续响=有源,只"咔"一声=无源。这一招比看外观靠谱,30 秒搞定。
第二步:有源蜂鸣器——通电就响
有源蜂鸣器的代码和点灯几乎一样,因为你只需要控制"响 / 停"——一根 GPIO 拉高一小段再拉低,就是一声"滴"。蜂鸣器正极接 GPIO5、负极接 GND,把下面这段放进工程的 main/main.c:
// 有源蜂鸣器:每秒"滴"一声
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define BUZZER GPIO_NUM_5 // 蜂鸣器正极接 GPIO5;负极接 GND
static const char *TAG = "buzzer";
void app_main(void)
{
gpio_set_direction(BUZZER, GPIO_MODE_OUTPUT); // 和点灯一样,报备这根脚要输出
ESP_LOGI(TAG, "active buzzer ready on GPIO%d", BUZZER);
while (1) {
gpio_set_level(BUZZER, 1); // 通电——有源蜂鸣器自己就响了
vTaskDelay(pdMS_TO_TICKS(200)); // 响 200 毫秒
gpio_set_level(BUZZER, 0); // 断电——停
vTaskDelay(pdMS_TO_TICKS(800)); // 静音 800 毫秒
}
}
在工程目录下编译、烧录、看日志(第一次先 idf.py set-target esp32s3):
idf.py build flash monitor
你应该听到(看到)什么
- 每隔 1 秒"滴"一声,每声响 0.2 秒。
- 串口会打出一行
active buzzer ready on GPIO5,确认程序跑起来了。
改 vTaskDelay 的毫秒数就能改节奏:把两个值都改小,变成急促的"滴滴滴";把响的时间拉到 500、静音改 100,就接近报警的"哔——哔——"。
注意:有源蜂鸣器的音调你改不了,gpio_set_level 只有"高 / 低"两档、没有"频率"这个概念,给的就是固定高电平把它的内部振荡闸门打开。想要不同音调,得用下面的无源蜂鸣器。
第三步:无源蜂鸣器——用 LEDC 变频发音、奏旋律
无源蜂鸣器要你"喂"它方波,方波多少赫兹,它就发多高的音。ESP-IDF 里发方波用的是 LEDC 外设——和 PWM 调光那节点呼吸灯用的是同一块硬件,只是这次我们盯的参数从"占空比"换成了"频率"。
思路就三步:配一个 LEDC 定时器(频率就是音调)+ 一个通道绑到蜂鸣器引脚 → 占空比设到 50% 让它输出对称方波(驱动膜片最有劲)→ 换音就改定时器的 freq_hz,停声就把占空比设回 0。
蜂鸣器正极接 GPIO5、负极接 GND,先跑个"滴滴"两声提示音:
// 无源蜂鸣器:两声"滴滴"提示音
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define BUZZER GPIO_NUM_5
static const char *TAG = "buzzer";
// 占空比用 10 位分辨率(0~1023),50% 就是 512
#define DUTY_RES LEDC_TIMER_10_BIT
#define DUTY_HALF 512
static void buzzer_init(void)
{
ledc_timer_config_t timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = DUTY_RES,
.timer_num = LEDC_TIMER_0,
.freq_hz = 2000, // 初始频率,后面会动态改
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&timer);
ledc_channel_config_t ch = {
.gpio_num = BUZZER,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.duty = 0, // 先静音
.hpoint = 0,
};
ledc_channel_config(&ch);
}
// 发出 freq 赫兹的音,持续 ms 毫秒,然后停
static void beep(int freq, int ms)
{
ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, freq); // 改频率=换音调
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, DUTY_HALF); // 占空比 50% → 出声
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
vTaskDelay(pdMS_TO_TICKS(ms));
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); // 占空比归零 → 停声
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
void app_main(void)
{
buzzer_init();
ESP_LOGI(TAG, "passive buzzer ready on GPIO%d", BUZZER);
while (1) {
beep(2000, 100); // 2000Hz 响 100ms
vTaskDelay(pdMS_TO_TICKS(80)); // 两声之间留个短停顿
beep(2000, 100); // 再来一声,凑成"滴滴"
vTaskDelay(pdMS_TO_TICKS(2000)); // 隔 2 秒再循环
}
}
这里的关键新动词是 ledc_set_freq(...):第三个参数就是频率,单位赫兹,数字越大音越高。500Hz 是低沉的嗡声,2000Hz 是清脆的"滴",4000Hz 已经很尖。占空比给 50%(DUTY_HALF)让方波对称、驱动力最足;把占空比设回 0 就等于松开闸门、声音停掉。"响多久"靠 vTaskDelay 控制,停声靠占空比归零——这跟有源那版"高低电平"的玩法是两码事,别搞混。
为什么不直接用 Arduino 里那个 tone()?因为它本就是 Arduino 框架在 LEDC 上包的一层壳,换框架就没了。ESP-IDF 直接给你 LEDC 原语,多写几行换来的是对硬件的完全掌控、以及做产品时能精确控制频率/占空比/通道分配。这就是"产品级框架"和"玩具封装"的区别。
奏一小段旋律
既然 ledc_set_freq 改一下就换音,把不同频率按顺序排出来就是旋律。用 #define 把音符频率定成常量,再配一张时长表,循环奏出来——这里放一段最简单的"哆来咪"上行:
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define BUZZER GPIO_NUM_5
#define DUTY_HALF 512
// 音符频率常量(C4=中音 do 起)
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_G4 392
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
// 旋律:do re mi fa sol la si do(高),配一张同长的时长表(ms)
static const int melody[] = {NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4,
NOTE_G4, NOTE_A4, NOTE_B4, NOTE_C5};
static const int duration[] = {300, 300, 300, 300, 300, 300, 300, 500};
static const int note_count = sizeof(melody) / sizeof(melody[0]);
static void buzzer_init(void)
{
ledc_timer_config_t timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_10_BIT,
.timer_num = LEDC_TIMER_0,
.freq_hz = NOTE_C4,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&timer);
ledc_channel_config_t ch = {
.gpio_num = BUZZER,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.duty = 0,
.hpoint = 0,
};
ledc_channel_config(&ch);
}
void app_main(void)
{
buzzer_init();
for (int i = 0; i < note_count; i++) { // 依次播放每个音
ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, melody[i]); // 第 i 个音的频率
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, DUTY_HALF);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
vTaskDelay(pdMS_TO_TICKS(duration[i])); // 持续这个音的时长
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); // 停声
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
vTaskDelay(pdMS_TO_TICKS(50)); // 音和音之间留 50ms 间隙,否则糊成一片
}
while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } // 只在开机时奏一遍,之后空转
}
你应该听到什么
第一段是清脆的"滴-滴"两声,每 2 秒一轮;第二段是开机时一条由低到高的"哆来咪发嗦拉西哆"音阶。听不出音高变化的话,多半是买成了有源蜂鸣器——有源的不认频率,给什么都只发它那个固定音,这就是第一步要分清两种的原因。
驱动能力:小蜂鸣器能直插,大的要加三极管
常见的小型贴片/直插蜂鸣器电流很小(几毫安到十几毫安),ESP32-S3 单个 GPIO 撑得住,直接接就行。这一节的代码都按"直接接 GPIO"写的。
但如果你用的是大功率蜂鸣器、有源报警器,或电流超过 GPIO 安全值,直接驱动会拉垮引脚电压、甚至损坏芯片。ESP32-S3 单个 GPIO 的灌/拉电流能力本就有限(建议长期工作别超过 12mA 左右,绝对上限更高但不该长期逼近),无论 Arduino 还是 ESP-IDF,这都是芯片的物理上限,软件层面改不了。
这时候要用一个三极管(如 S8050、2N2222)或 MOS 管做开关:GPIO 接三极管的基极、中间串一个限流电阻(常用 1kΩ 量级,限制流进基极的电流),蜂鸣器的工作电流走三极管和独立电源,GPIO 只负责"指挥"不负责"出力"。代码层面完全不用变——你照样 gpio_set_level(pin, 1),只不过这根脚现在控的是三极管的开关,而不是直接喂蜂鸣器。这套"小电流控大电流"的思路,是所有大功率负载(电机、继电器、灯带)的通用打法,原理见 GPIO 引脚详解。
别拿 GPIO 硬扛大负载。看着能响不代表安全——长期超流会让引脚老化、电压下垂导致芯片复位,甚至烧掉这个脚。拿不准电流多大,查蜂鸣器的额定电流;超过 10mA 就老老实实加三极管 + 基极限流电阻。
PWM 和音调到底什么关系
为什么"频率决定音调"?因为声音的本质是空气的振动,振动多快,音就多高。无源蜂鸣器里有一片金属膜,方波每翻转一次就推动膜片振动一下——523Hz 的方波每秒翻转 523 次,膜片每秒振 523 下,你耳朵就听到中音 do。
而方波正是 PWM 的产物:PWM 把时间切成一个个周期,高低电平交替。调光看的是占空比(高电平占多大比例,决定平均能量→亮度),发声看的是频率(一秒翻转多少次→音调)——同一块 LEDC 硬件,盯的参数不同,干的活就不同。所以呼吸灯里你拧的是 ledc_set_duty,这里发声你拧的是 ledc_set_freq,占空比反而固定在 50% 不动。想把"快速开关如何变出连续效果"这套机制彻底吃透,读 PWM 是怎么调出来的。
理解了这层,你就明白为什么有源蜂鸣器调不了音:它内部振荡电路的频率是写死的,你给的高电平只是"开闸放它响",频率轮不到你管。
故障排查:响得不对就按这张表查
| 现象 | 最可能的原因 | 怎么办 |
|---|---|---|
| 完全不响 | 正负极接反 / GPIO 号写错 / 接触不良 | 蜂鸣器正极(标 + 或长脚)朝 GPIO,负极朝 GND;确认 #define BUZZER 和实际接的脚一致 |
| 一直响停不下来 | 无源漏了把占空比归零;或有源漏了 gpio_set_level(pin, 0) |
无源确认 beep 末尾有 ledc_set_duty(...,0)+ledc_update_duty(...);有源确认拉低那行执行到了 |
| 音调对不上、给什么频率都一个音 | 把有源当无源用了 | 换无源蜂鸣器(底部能看到绿色电路板那种);有源只能"响/停" |
改了 ledc_set_freq 音却不变 |
占空比一直是 0,根本没出声 | 确认 ledc_set_duty 给了非零值(如 512)且调了 ledc_update_duty |
| 声音很小、很闷 | 蜂鸣器贴在桌面闷住了 / 供电电流不足 | 让出声孔朝外别捂住;大蜂鸣器加三极管+独立供电 |
| 旋律糊成一团分不清音 | 音符之间没留间隙 | 每个音后把占空比归零 + 短 vTaskDelay(pdMS_TO_TICKS(50)) 再发下一个 |
同一招的两个变体
把"响 / 频率 / 时长"这三个旋钮换着拧,就是不同的声音效果:
- 按键音:配合按钮,每次按下"滴"一声给确认反馈。在按钮按下的那一刻调一次
beep(2000, 30)(短而脆),手感立刻"高级"起来。按钮怎么读见按键输入。 - 报警声(频率扫动):警笛那种"呜——呜——"上下扫的声音,本质是让
ledc_set_freq的频率在一个范围内来回变。先把占空比拉到 50% 出声,再用一个for循环让频率从 400 爬到 1200 再退回来,每档停几毫秒:
// 调用前 buzzer 已 init,且占空比已设到 DUTY_HALF
static void siren(void)
{
for (int f = 400; f <= 1200; f += 10) { // 升调
ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, f);
vTaskDelay(pdMS_TO_TICKS(5));
}
for (int f = 1200; f >= 400; f -= 10) { // 降调
ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, f);
vTaskDelay(pdMS_TO_TICKS(5));
}
}
频率连续变化,听起来就是急促的警笛——比固定单音的报警紧张感强得多。注意 ledc_set_freq 改的是定时器频率,所以扫频期间占空比要一直保持非零(出声状态),扫完想停再把占空比归零。
动手挑战
光跑通不够,做一个真正"有用"的声音设备,你才算把蜂鸣器吃透:
做一个温度超阈值就报警的蜂鸣器。 用 DHT11 温湿度传感器读温度,超过你设的阈值(比如 30°C)就触发上面的 siren() 报警,低于阈值就把占空比归零安静下来。这就是一个最小可用的"高温报警器"。骨架:
- 读温度——传感器接法和读数代码见 DHT11 温湿度传感器。
- 在
while(1)里反复判断:if (温度 > 30) siren(); else { ledc_set_duty(...,0); ledc_update_duty(...); }。 - 进阶:温度越高响得越急(把
siren里的vTaskDelay随温度缩短)。
卡住了就把传感器读数代码和报警代码一起发给 AI,让它帮你拼起来——这种"读传感器 + 触发反馈"的组合,是后面所有智能硬件项目的基本骨架。
小结 · 你现在掌握了什么
- 你能分清有源蜂鸣器(通电就响、音固定、
gpio_set_level控制)和无源蜂鸣器(要喂方波、频率定音调、能奏旋律),不会再买错用错。 - 你会用 LEDC 的
ledc_set_freq+ 50% 占空比让无源蜂鸣器发不同的音、奏简单旋律,也会用频率扫动做报警声。 - 你理解了频率决定音调背后是空气振动的快慢,以及它和 PWM 占空比的分工(占空比管亮度、频率管音调,同一块 LEDC 硬件)。
- 你知道小蜂鸣器能直插 GPIO,大功率的要加三极管 + 基极限流电阻,不会硬拿引脚扛大负载。
下一步:声音是"反馈",接下来给作品加上"动作"——学舵机控制,让你的项目第一次真正动起来。整条进阶路线见学习路线图。
代码为参考实现,请以你所用 ESP-IDF 版本的官方 LEDC API 文档 为准,烧录前自行核对引脚与参数。