STM32 零基础可移植教程 03:蜂鸣器响一声,LED 跟着翻转一次
上一篇我们已经用 GPIO 点亮了 LED。LED 能闪,说明你已经把 CubeMX 引脚配置、Keil 编译下载、main.c 调用位置这条链路跑通了。
这一篇继续用 GPIO,不过换一个更容易踩坑的小外设:蜂鸣器。
蜂鸣器最容易让新手迷糊的地方,不是 HAL_GPIO_WritePin() 这个函数,而是硬件类型和有效电平:
-
有源蜂鸣器:给一个固定电平就能响;
-
无源蜂鸣器:需要 PWM 方波才能稳定发声;
-
有些蜂鸣器高电平响;
-
有些蜂鸣器低电平响;
-
有些板子中间还加了三极管或 MOS 管,GPIO 控制的其实是"开关管"。
所以这一篇不急着做歌曲,也不急着做不同音调。我们先做一个非常明确的目标:
bash
蜂鸣器短响一次,同时 LED 翻转一次,然后等待 2 秒,循环重复
这样做有两个好处:
-
蜂鸣器响一下,说明蜂鸣器 GPIO 控制通了;
-
LED 翻转一下,说明主循环确实在跑,也顺手复习上一章的
app_led封装。
本篇目标
最终现象:
bash
蜂鸣器短响一声
LED 状态翻转一次
等待 2 秒
然后重复
本篇用到的外设:
bash
GPIO Output
本篇跑通标准:
-
Keil 编译通过;
-
程序能下载到开发板;
-
蜂鸣器能按周期短响;
-
LED 能随着每次蜂鸣器动作翻转亮灭;
-
能说清楚自己的蜂鸣器是高电平有效还是低电平有效;
-
能说清楚自己的蜂鸣器是有源还是无源。
如果你的蜂鸣器是无源蜂鸣器,这一篇用 GPIO 固定电平可能只能听到很短的"咔哒"声,甚至可能不响。那不是代码一定错了,而是无源蜂鸣器本来就需要 PWM。PWM 发声我们后面单独讲。
准备工作
你需要准备:
|
项目
|
说明
|
| --- | --- |
|
STM32 开发板
|
任意 STM32 开发板都可以
|
|
下载器
|
ST-LINK/V2 或板载 ST-LINK
|
|
LED
|
沿用上一篇 LED 工程
|
|
蜂鸣器
|
推荐先用有源蜂鸣器模块
|
|
原理图
|
如果是板载蜂鸣器,一定要看原理图
|
|
杜邦线
|
外接蜂鸣器模块时使用
|
这里先把概念说清楚:
bash
有源蜂鸣器:内部带振荡电路,给固定电平就能响
无源蜂鸣器:内部没有振荡电路,需要 MCU 输出 PWM 方波
如果你手里的模块有 VCC、GND、SIG/I/O 三个引脚,而且卖家说明"高电平触发"或"低电平触发",大概率是有源蜂鸣器模块。
如果你手里只是一个两脚蜂鸣器本体,或者原理图里明确写的是无源蜂鸣器,那固定电平不一定能让它稳定发声。

硬件连接
如果你的开发板自带蜂鸣器,先看原理图,确认蜂鸣器控制脚接到哪个 MCU 引脚。
如果你外接一个常见有源蜂鸣器模块,一般这样接:
|
蜂鸣器模块
|
STM32 开发板
|
| --- | --- |
|
VCC
|
3.3V 或 5V,按模块说明来
|
|
GND
|
GND
|
|
SIG / I/O
|
一个普通 GPIO 引脚
|
注意三件事:
-
GND必须和 STM32 共地。 -
模块电压要看说明,有些 5V 蜂鸣器用 3.3V 也能响,有些声音会很小。
-
如果是直接驱动蜂鸣器本体,最好通过三极管或 MOS 管,不建议让 GPIO 直接带大电流负载。
常见驱动电路有两类:
bash
GPIO 输出高电平 -> 蜂鸣器响
或者:
bash
GPIO 输出低电平 -> 蜂鸣器响
所以这一篇和 LED 一样,也要先确认有效电平。

这张图展示了蜂鸣器驱动电路,核心功能是通过PC0引脚输出的PWM方波信号控制蜂鸣器发声。
电路中,PC0连接至NPN三极管Q3的基极,R17(1kΩ)用于基极限流,R16(10kΩ)作为下拉电阻,防止IO口悬空时三极管误触发。
BEEP1为蜂鸣器,其一端接3.3V电源,另一端接Q3的集电极,Q3的发射极接地,因此Q3起到电子开关的作用。
当PC0输出高电平时,Q3导通,蜂鸣器得电发声;当PC0输出低电平时,Q3截止,蜂鸣器断电停止。
由于蜂鸣器为感性负载,断电瞬间会产生反向感应电动势,二极管D6(1N4148)作为续流二极管,为该反向电压提供泄放回路,从而保护Q3不被击穿。
需要注意的是,该电路适配无源蜂鸣器,需要单片机提供方波信号才能发声,若想发出不同音调,可通过改变PWM频率实现。
如果你的原理图像下面这种结构:
bash
MCU GPIO -> 基极电阻 -> NPN 三极管 -> 蜂鸣器 -> 3.3V
那 MCU 的 GPIO 通常不是直接驱动蜂鸣器,而是在控制三极管这个"电子开关"。
以常见 NPN 三极管低端开关为例:
-
GPIO 输出高电平:三极管导通,蜂鸣器得到电流;
-
GPIO 输出低电平:三极管截止,蜂鸣器断电;
-
基极串联电阻用于限流;
-
下拉电阻用于避免 MCU 引脚悬空时误响;
-
如果蜂鸣器或负载有感性特征,旁边可能会加续流二极管保护三极管。
如果图纸或资料明确写的是"无源蜂鸣器",那就记住:这一篇只能先验证控制脚和开关电路,真正稳定发声、改变音调,需要等 PWM 篇。
CubeMX 配置步骤
1. 复制上一篇 LED 工程
建议从上一篇 02_led_gpio 复制一份,改名为:
bash
03_buzzer_gpio
这样你可以保留上一篇 LED 工程,后面遇到问题时方便对比。
如果你重新新建工程,也可以按第一篇的流程走:
-
选择芯片型号;
-
SYS -> Debug设置为Serial Wire; -
时钟先保持默认 HSI;
-
Project Manager 选择
MDK-ARM; -
生成 Keil 工程。

2. 保留 LED 输出配置
这一篇会继续使用 LED,所以第 02 篇里的 LED 配置要保留:
|
配置项
|
推荐值
|
| --- | --- |
|
GPIO mode
|
GPIO_Output
|
|
User Label
|
LED
|
|
初始电平
|
按 LED 有效电平设置为默认灭
|
如果你的 CubeMX 里 LED 标签叫 LED_Red,那代码里就要适配 LED_Red_Pin 和 LED_Red_GPIO_Port;如果想直接复用教程里的 app_led.c,建议把 User Label 统一改成 LED。
你现在如果已经用 LED_Red 跑通了,也可以保留这个名字,只要你的 app_led.c 里已经做了对应适配即可。

3. 找到蜂鸣器对应的 GPIO 引脚
假设你的蜂鸣器控制脚接在 PB8。
在 CubeMX 的 Pinout 页面找到 PB8,点击后选择:
bash
GPIO_Output
如果你的蜂鸣器接在 PA8、PB5、PC0,就配置对应引脚。
原则还是那句话:原理图接到哪个 MCU 引脚,CubeMX 就配置哪个引脚。
4. 给蜂鸣器引脚起名字
进入:
bash
System Core -> GPIO
找到刚才配置的蜂鸣器引脚,把 User Label 改成:
bash
BUZZER
这样 CubeMX 生成代码后,会在 main.h 里生成类似:
bash
#define BUZZER_Pin GPIO_PIN_8
#define BUZZER_GPIO_Port GPIOB
后面我们的 app_buzzer.c 就直接用这两个宏。
为什么要这样做?
因为换板子时,只要 CubeMX 里仍然把蜂鸣器标签叫 BUZZER,应用层代码就不用到处改。

5. 设置蜂鸣器 GPIO 参数
蜂鸣器控制脚作为普通输出,可以这样配置:
|
配置项
|
推荐值
|
说明
|
| --- | --- | --- |
|
GPIO output level
|
按有效电平选择默认"关闭"状态
|
上电时不要让蜂鸣器乱响
|
|
GPIO mode
|
Output Push Pull
|
普通推挽输出
|
|
GPIO Pull-up/Pull-down
|
No pull-up and no pull-down
|
输出模式一般不需要内部上下拉
|
|
Maximum output speed
|
Low
|
本篇只是开关控制,不需要高速
|
|
User Label
|
BUZZER
|
生成 BUZZER_Pin 和 BUZZER_GPIO_Port
|
如果你的蜂鸣器是高电平响,初始电平建议设为 Low。
如果你的蜂鸣器是低电平响,初始电平建议设为 High。
这样上电后蜂鸣器默认不响,只有代码调用 App_Buzzer_On() 时才响。
6. 生成 Keil 工程
配置完成后点击:
bash
GENERATE CODE
然后打开 Keil 工程,先编译一次,确认 CubeMX 生成的工程没有问题。

Keil 工程生成和编译
打开 Keil 后,先编译:
bash
Build / F7
确认输出里没有错误:
bash
0 Error(s)

如果这一步报错,先别怀疑蜂鸣器代码,因为我们还没写。优先检查工程路径、芯片 Pack、CubeMX 是否生成成功。
完整代码
这一篇会用到两个应用模块:
bash
Core/Inc/app_led.h
Core/Src/app_led.c
Core/Inc/app_buzzer.h
Core/Src/app_buzzer.c
其中 app_led.h/.c 沿用上一篇,app_buzzer.h/.c 是本篇新增。
main.c 只负责初始化和调用,不把蜂鸣器控制逻辑直接写散。
1. 新建 Core/Inc/app_buzzer.h
在 Core/Inc 目录下新建:
bash
app_buzzer.h
写入下面代码:
bash
#ifndef APP_BUZZER_H
#define APP_BUZZER_H
#include "main.h"
#include <stdint.h>
void App_Buzzer_Init(void);
void App_Buzzer_On(void);
void App_Buzzer_Off(void);
void App_Buzzer_Beep(uint32_t ms);
#endif
2. 新建 Core/Src/app_buzzer.c
在 Core/Src 目录下新建:
bash
app_buzzer.c
写入下面代码:
bash
#include "app_buzzer.h"
/*
* BUZZER_GPIO_Port and BUZZER_Pin are generated by CubeMX in main.h.
* Set the buzzer pin User Label to BUZZER in CubeMX.
*/
#ifndef BUZZER_GPIO_Port
#error "BUZZER_GPIO_Port is not defined. Set the buzzer pin User Label to BUZZER in CubeMX."
#endif
#ifndef BUZZER_Pin
#error "BUZZER_Pin is not defined. Set the buzzer pin User Label to BUZZER in CubeMX."
#endif
/*
* Many external active buzzer modules are active-high.
* If your board uses active-low buzzer drive, swap these two macros.
*/
#ifndef APP_BUZZER_ON_LEVEL
#define APP_BUZZER_ON_LEVEL GPIO_PIN_SET
#endif
#ifndef APP_BUZZER_OFF_LEVEL
#define APP_BUZZER_OFF_LEVEL GPIO_PIN_RESET
#endif
void App_Buzzer_Init(void)
{
App_Buzzer_Off();
}
void App_Buzzer_On(void)
{
HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, APP_BUZZER_ON_LEVEL);
}
void App_Buzzer_Off(void)
{
HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, APP_BUZZER_OFF_LEVEL);
}
void App_Buzzer_Beep(uint32_t ms)
{
App_Buzzer_On();
HAL_Delay(ms);
App_Buzzer_Off();
}
这段代码里最关键的是这两个宏:
bash
#define APP_BUZZER_ON_LEVEL GPIO_PIN_SET
#define APP_BUZZER_OFF_LEVEL GPIO_PIN_RESET
它默认认为蜂鸣器是高电平响。
如果你的蜂鸣器是低电平响,改成:
bash
#define APP_BUZZER_ON_LEVEL GPIO_PIN_RESET
#define APP_BUZZER_OFF_LEVEL GPIO_PIN_SET
不要觉得这一步多余。不同开发板的蜂鸣器驱动电路不一样,有些是高电平打开三极管,有些是低电平打开。把有效电平集中放在宏里,后面移植会省很多事。
3. 把 app_buzzer.c 加入 Keil 工程
如果你是手动新建 .c 文件,Keil 不一定会自动编译它。
在 Keil 工程树里右键:
bash
Application/User/Core
选择:
bash
Add Existing Files to Group 'Application/User/Core'
然后添加:
bash
Core/Src/app_buzzer.c
如果你复用了上一篇 LED 代码,也要确认:
bash
Core/Src/app_led.c
已经在 Keil 工程里。

如果忘了这一步,可能会报:
bash
undefined symbol App_Buzzer_Init
undefined symbol App_Buzzer_Beep
这不是函数写错了,而是 app_buzzer.c 没有参与编译。
main.c 调用方式
接下来修改 main.c。
这里有一个非常重要的习惯:自己加的代码尽量写在 USER CODE 区域里。你现在如果把 [#include](javascript:;) "app_buzzer.h" 写在 CubeMX 自动生成区域,或者把 while 里的逻辑写在 USER CODE BEGIN 3 外面,代码能编译,但下次 CubeMX 重新生成时可能会被覆盖。
1. 在 Includes 区域添加头文件
找到:
bash
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
改成:
bash
/* USER CODE BEGIN Includes */
#include "app_led.h"
#include "app_buzzer.h"
/* USER CODE END Includes */
如果你现在的代码是这样:
bash
#include "main.h"
#include "app_buzzer.h"
#include "app_led.h"
也能编译,但不建议这样放。更稳的做法是把自己加的头文件放进 USER CODE BEGIN Includes 里面。
2. 在初始化区域调用初始化函数
找到 main() 里的初始化部分,确保 MX_GPIO_Init() 已经在前面执行:
bash
MX_GPIO_Init();
然后在 USER CODE BEGIN 2 里添加:
bash
/* USER CODE BEGIN 2 */
App_LED_Init();
App_Buzzer_Init();
/* USER CODE END 2 */
为什么要放在 MX_GPIO_Init() 后面?
因为 LED 和蜂鸣器引脚必须先被 CubeMX 初始化成输出模式,我们自己的初始化函数才能正确控制它们。
3. 在 while 循环里蜂鸣器响一下,LED 翻转一次
找到:
bash
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
/* USER CODE END 3 */
}
改成:
bash
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
App_Buzzer_Beep(100);
App_LED_Toggle();
HAL_Delay(2000);
/* USER CODE END 3 */
}
这段逻辑很直白:
bash
蜂鸣器响 100 ms
LED 翻转一次
等待 2 秒
再重复
如果你想让蜂鸣器声音更短,可以改成:
bash
App_Buzzer_Beep(10);
不过 10 ms 对很多蜂鸣器来说太短,可能只是很轻的一声。教程里建议先用 100 ms 或 200 ms,确认能听清楚以后再改短。
注意:不要把这几行放在 /* USER CODE END WHILE */ 和 /* USER CODE BEGIN 3 */ 中间。那个位置不适合手写逻辑,CubeMX 重新生成代码时更容易出问题。手写循环逻辑应该放在 USER CODE BEGIN 3 和 USER CODE END 3 之间。
编译、下载和验证
代码加完后,先编译:
bash
Build / F7
没有错误后再下载:
bash
Download
下载后观察现象:
bash
蜂鸣器短响一声
LED 从亮变灭,或者从灭变亮
等待 2 秒
然后重复

如果你用 Keil 下载后程序没有自动跑,但按复位键或点 Run 能跑,这不是蜂鸣器代码问题。前面调试时已经遇到过类似情况,本质是 Keil/ST-LINK 下载后自动复位运行没有触发。先以"按复位后能正常运行"为准继续验证外设。
移植到其他板子的修改点
这篇的移植点主要有 7 个。
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
|
蜂鸣器引脚
|
不同板子的蜂鸣器接到不同 GPIO
|
CubeMX Pinout 页面
|
|
蜂鸣器 User Label
|
代码依赖 BUZZER_Pin 和 BUZZER_GPIO_Port
|
CubeMX GPIO 页面,标签设为 BUZZER
|
|
蜂鸣器有效电平
|
有些蜂鸣器高电平响,有些低电平响
| app_buzzer.c
里的 APP_BUZZER_ON_LEVEL 和 APP_BUZZER_OFF_LEVEL
|
|
蜂鸣器初始电平
|
上电后不要让蜂鸣器乱响
|
CubeMX 的 GPIO output level
|
|
LED 引脚和标签
|
本篇用 LED 显示主循环节奏
|
沿用第 02 篇,建议标签为 LED
|
|
驱动电路
|
GPIO 不能直接带大电流负载
|
看原理图是否有三极管/MOS 管
|
|
蜂鸣器类型
|
无源蜂鸣器不能只靠固定电平稳定发声
|
确认是有源还是无源
|
换板子的推荐顺序:
-
看原理图,确认蜂鸣器接到哪个 MCU 引脚;
-
确认蜂鸣器是有源还是无源;
-
确认蜂鸣器是高电平响还是低电平响;
-
在 CubeMX 里把对应引脚设置成
GPIO_Output; -
User Label 填
BUZZER; -
保留或配置 LED 引脚,确保
App_LED_Toggle()能正常工作; -
根据有效电平修改
APP_BUZZER_ON_LEVEL和APP_BUZZER_OFF_LEVEL; -
重新生成代码、编译、下载。
如果你手上是无源蜂鸣器,本篇代码可能只会让它"咔嗒"一下,或者完全不响。这不是代码错,而是驱动方式不对。无源蜂鸣器要等 PWM 篇再处理。
常见问题排查
1. 蜂鸣器完全不响,但 LED 会翻转
这种情况说明主循环在跑,LED 也正常,问题基本集中在蜂鸣器侧。
优先检查:
|
优先检查
|
具体方法
|
| --- | --- |
|
是不是有源蜂鸣器
|
看模块说明;无源蜂鸣器需要 PWM
|
|
蜂鸣器引脚是否选错
|
回原理图确认蜂鸣器控制脚接到哪个 GPIO
|
|
有效电平是否反了
|
交换 APP_BUZZER_ON_LEVEL 和 APP_BUZZER_OFF_LEVEL
|
|
模块是否供电
|
量 VCC 和 GND,确认电压正常
|
|
GND 是否共地
|
外接模块必须和 STM32 共地
|
|
响声时间是否太短
|
先把 App_Buzzer_Beep(10) 改成 App_Buzzer_Beep(200)
|
2. 蜂鸣器会响,但 LED 不翻转
优先检查:
-
app_led.c是否加入 Keil 工程; -
LED 的 User Label 是否和
app_led.c使用的宏一致; -
LED 是高电平亮还是低电平亮;
-
App_LED_Toggle()是否写在while (1)里; -
App_LED_Init()是否放在MX_GPIO_Init()后面。
如果你用的是 LED_Red_Pin、LED_Red_GPIO_Port,而教程代码使用 LED_Pin、LED_GPIO_Port,就要么把 CubeMX 里的 User Label 改成 LED,要么在 app_led.c 里做一次适配。
3. 蜂鸣器一直响
常见原因有三个:
-
CubeMX 里 GPIO 初始电平设成了"打开蜂鸣器"的电平;
-
APP_BUZZER_ON_LEVEL和APP_BUZZER_OFF_LEVEL写反; -
驱动电路是低电平有效,但你按高电平有效写了。
先做一个简单测试:
bash
App_Buzzer_Off();
while (1)
{
}
如果这样蜂鸣器还一直响,优先看有效电平和原理图。
4. 编译报 BUZZER_GPIO_Port is not defined
说明 CubeMX 没有生成:
bash
BUZZER_GPIO_Port
BUZZER_Pin
去 Core/Inc/main.h 里看一下,如果只有类似:
bash
#define BEEP_Pin ...
#define BEEP_GPIO_Port ...
那说明你的 User Label 不是 BUZZER。
解决方法:
-
回 CubeMX;
-
找到蜂鸣器 GPIO;
-
把 User Label 改成
BUZZER; -
重新 Generate Code。
5. 编译报 undefined symbol App_Buzzer_Beep
通常是 app_buzzer.c 没有加入 Keil 工程。
解决方法:
-
右键
Application/User/Core; -
选择
Add Existing Files to Group; -
添加
Core/Src/app_buzzer.c; -
重新编译。
6. 声音很小或者断断续续
优先检查:
-
蜂鸣器模块是否需要 5V;
-
STM32 GPIO 是否只是接了信号脚,模块 VCC 是否供电正常;
-
App_Buzzer_Beep()的时间是否太短; -
如果是直接驱动蜂鸣器本体,GPIO 驱动能力可能不够,需要三极管/MOS 管;
-
如果是无源蜂鸣器,用固定电平不会得到稳定声音,需要 PWM。
7. 下载后不响,按复位键才响
这说明程序本身没问题,只是 Keil 下载后没有自动复位运行。
可以先这样验证:
bash
下载完成 -> 按开发板复位键 -> 观察蜂鸣器和 LED
后续再慢慢处理 Keil 的 Reset and Run、ST-LINK 复位线、CubeProgrammer 下载等问题。不要把这个现象误判成蜂鸣器驱动失败。
本篇小结
这一篇我们完成了一个更完整的 GPIO 输出小实验:蜂鸣器短响一次,LED 翻转一次。
你现在至少应该知道:
-
蜂鸣器分有源和无源,不能混着写驱动;
-
有源蜂鸣器可以用 GPIO 固定电平控制;
-
无源蜂鸣器需要 PWM,这篇先不处理;
-
CubeMX 里蜂鸣器引脚要配置成
GPIO_Output; -
User Label 填
BUZZER后,代码里可以使用BUZZER_Pin和BUZZER_GPIO_Port; -
有效电平要根据原理图或模块说明来改;
-
app_buzzer.c和app_led.c都要加入 Keil 工程; -
main.c里自己的 include 和 while 逻辑要放在 USER CODE 区域里。
下一篇我们开始处理输入:
STM32 按键输入:为什么按下去读到的是 0 或 1。
按键这一篇会把 GPIO 输入、上拉/下拉、按键有效电平讲清楚。等按键跑通后,我们再继续讲按键消抖和外部中断。