开发流程:
搞清原理,知道用哪个引脚
到cubemx配置引脚并更新keil
在对应层(common interface等文件夹)中创建.c .h文件
在keil中添加层文件夹路径并将文件纳入编译范围内
在vscode中开发
先在原理图中搜索数据手册弄清驱动原理
从高层项底层写

I 搞清原理,知道用哪个引脚


一眼盯真,用PB4。
原理就是通过PB4发出不同频率方波控制蜂鸣器发声。
对于有源无源的蜂鸣器的驱动方式是不同的,因此我们需要查看数据手册:

1. 关键参数:Resonant Frequency = 2700Hz
- 这是 无源蜂鸣器的核心特征。
- "谐振频率"(Resonant Frequency)意味着它需要外部提供 2700Hz 的方波信号 才能有效发声。
- 如果是有源蜂鸣器,这个参数通常不会出现,或者会写成"内置振荡电路,输出频率 X Hz"。
💡 类比:就像一个喇叭,本身不发声,必须输入音频信号才能响。
2. Rated Current 条件中明确提到"Square Wave"
1Max.95mA, at 2700Hz 50% duty Square Wave 5Vo-p
- 它的额定电流是在 2700Hz 方波驱动 下测得的。
- 这说明它不是简单地接直流电就响,而是需要 特定频率的交流信号 驱动。
⚠️ 如果是有源蜂鸣器,这里应该写成"DC 5V"或类似描述。
因此必须使用无源蜂鸣器的驱动方式,使用定时器(GPIO 无法直接生成精确频率)生成PWM方波进行驱动。

我们使用内部时钟,这意味,定时器共享芯片内部的时钟,也就是主频72MHz。


在通道一选择PWM波

由于do大概是261Hz,72MHz太高了,我们需要做分频处理。

这里第一个是分频器,范围是0-65535,代表除数,除以0是不可能的,因此代表除以1,因此除以x则需要填除以x-1,我们没法填72000-1,只能填7200-1
现在的分频后频率是10kHz,还是很大,因此我们考虑用重装载继续分频。
重装载就是指走n个步长触发一次事件,比如步长20,则走20个分频周期触发一次,这就相当于给分频后的频率再做了一次分频。
我们选择20-1为重装载值(其范围也从0开始,因此要减一),此时的频率是10k/20=500hHz。
已经是人耳的听觉范围了。
一、PWM 工作原理回顾
在 STM32 定时器中,PWM 输出由两个关键寄存器决定:
| 寄存器 | 含义 |
|---|---|
| ARR(Auto-Reload Register) | 决定 PWM 周期(最大计数值) |
| CCR(Capture/Compare Register) | 决定 PWM 高电平持续时间(即"脉冲宽度") |
在 CubeMX 中,
Pulse就是 CCR 的值(即 Compare Value)
为了使得输出更稳定,最好的实践是将CCR占到50%,也就是高电平占用一半的时间,因此Pulse填重装载系数20的一半:

然后generatecode。
II 在对应层(common interface等文件夹)中创建.c .h文件
这里改为Int_buzzer.h .c即可

这一步之前直接把整个interface层加入了,因此不需要修改

III 在keil中添加层文件夹路径并将文件纳入编译范围内
然后是添加到编译文件中:

IV 在vscode中开发
总体思路:
先在原理图中搜索数据手册弄清驱动原理 从高层项底层写
开发流程:
在.c文件include自己的头文件,然后在.h文件先#ifndef def endif
然后在.h定义暴露给外部的函数,并/** */写函数声明和参数声明
复制下来到.c文件中写实现函数,如果需要嵌套函数,则需要static声明
在main.c中的USERINCLUDE加入头文件,并在USERCODE2加入测试代码
这里找到Int_buzzer说明成功添加了

一般来说,生成code的时候会自动加入定时器的初始化函数,因此可以直接写代码。

那么,根据流程,Int_buzzer.h
cpp
#ifndef __INT_BUZZER_H__
#define __INT_BUZZER_H__
#include "tim.h"
/**
* @brief 打开蜂鸣器
*
*/
void Int_buzzer_on(void);
/**
* @brief 关闭蜂鸣器
*
*/
void Int_buzzer_off(void);
#endif /* __INT_BUZZER_H__ */
Int_buzzer.c
由于我们已经在cubemx写好了TIM的配置,因此之间开启就有声音,也就是说,控制TIM即可。
cpp
#include "Int_buzzer.h"
/**
* @brief 打开蜂鸣器
*
*/
void Int_buzzer_on(void){
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
/**
* @brief 关闭蜂鸣器
*
*/
void Int_buzzer_off(void){
HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1);
}
在main.c


正常编译

蜂鸣器编曲

这是1-6的发声频率,可以根据这个让蜂鸣器唱歌。
那么,依然是先在buzzer.h添加暴露函数,并宏定义各个音高的频率
cpp
#define BUZZER_FREQ_1 261
#define BUZZER_FREQ_2 294
#define BUZZER_FREQ_3 329
#define BUZZER_FREQ_4 349
#define BUZZER_FREQ_5 392
#define BUZZER_FREQ_6 440
/**
* @brief 设置当前蜂鸣器的频率和占空比
*
* @param freq 频率
*/
void Int_buzzer_set(uint16_t freq);
/**
* @brief 播放音乐
*
*/
void Int_buzzer_music(void);
那么如何设置音高呢?
我们考虑重装载,第二次分频的值,来调整。
经过第一次分频,频率是10kHz,假设频率是freq,那么分频数就是10k/freq。
那么重装载值修改了,对应的pulse占空比也要修改(对应重装载值一半),否则就会发不出声音。
最后还需要重置重装载计数值。
这个函数就是:
cpp
/**
* @brief 设置当前蜂鸣器的频率和占空比
*
* @param freq 频率
*/
void Int_buzzer_set(uint16_t freq){
__HAL_TIM_SetAutoreload(&htim3, 10000/freq);
__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, 5000/freq);
__HAL_TIM_SetCounter(&htim3, 0);
}
剩余的就是编写音乐了:
cpp
#include "Int_buzzer.h"
/**
* @brief 打开蜂鸣器
*
*/
void Int_buzzer_on(void){
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}
/**
* @brief 关闭蜂鸣器
*
*/
void Int_buzzer_off(void){
HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1);
}
/**
* @brief 设置当前蜂鸣器的频率和占空比
*
* @param freq 频率
*/
void Int_buzzer_set(uint16_t freq){
__HAL_TIM_SetAutoreload(&htim3, 10000/freq);
__HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, 5000/freq);
__HAL_TIM_SetCounter(&htim3, 0);
}
static void Int_buzzer_byte(uint16_t freq){
Int_buzzer_set(freq);
Int_buzzer_on();
HAL_Delay(800);
Int_buzzer_off();
HAL_Delay(200);
}
static void Int_buzzer_byte_long(uint16_t freq){
Int_buzzer_set(freq);
Int_buzzer_on();
HAL_Delay(1600);
Int_buzzer_off();
HAL_Delay(400);
}
/**
* @brief 播放音乐
*
*/
void Int_buzzer_music(void){
//每个音节响0.8 间隔0.2 - 响1.6 乐句间间隔0.4
//1155665 4433221
Int_buzzer_byte(BUZZER_FREQ_1);
Int_buzzer_byte(BUZZER_FREQ_1);
Int_buzzer_byte(BUZZER_FREQ_5);
Int_buzzer_byte(BUZZER_FREQ_5);
Int_buzzer_byte(BUZZER_FREQ_6);
Int_buzzer_byte(BUZZER_FREQ_6);
Int_buzzer_byte_long(BUZZER_FREQ_5);
Int_buzzer_byte(BUZZER_FREQ_4);
Int_buzzer_byte(BUZZER_FREQ_4);
Int_buzzer_byte(BUZZER_FREQ_3);
Int_buzzer_byte(BUZZER_FREQ_3);
Int_buzzer_byte(BUZZER_FREQ_2);
Int_buzzer_byte(BUZZER_FREQ_2);
Int_buzzer_byte_long(BUZZER_FREQ_1);
}
成功编译:

补充资料
🔔 一、两种蜂鸣器的本质区别
| 类型 | 工作原理 | 控制方式 | 是否需要定时器 |
|---|---|---|---|
| 有源蜂鸣器(Active Buzzer) | 内部自带振荡电路 | 只需给 直流电压(高/低电平) | ❌ 不需要 TIM,用 GPIO 即可 |
| 无源蜂鸣器(Passive Buzzer) | 本质是电磁式扬声器,类似喇叭 | 需要外部提供 一定频率的方波信号 才能发声 | ✅ 必须用 TIM(或 PWM)生成频率 |
✅ 所以:"蜂鸣器往往使用 TIM 定时器"这个说法,其实特指「无源蜂鸣器」。
🎯 二、为什么无源蜂鸣器必须用 TIM 定时器?
1. 发声原理决定
- 无源蜂鸣器没有内部振荡源,必须靠外部交变信号驱动膜片振动。
- 要让它发出 特定音调(如 1kHz 的"嘀"声) ,就必须输入 1kHz 的方波。
- 方波的 频率 = 音调高低 ,占空比 ≈ 响度。
2. GPIO 无法直接生成精确频率
-
如果只用 GPIO + 软件延时(如
for循环或HAL_Delay)翻转电平:1while(1) { 2 HAL_GPIO_TogglePin(BEEP_GPIO, BEEP_PIN); 3 HAL_Delay(0.5); // 想生成 1kHz,但 HAL_Delay 最小单位是 1ms! 4}- 精度差 :
HAL_Delay最小是 1ms,无法生成 1kHz(周期 1ms,半周期 0.5ms); - 占用 CPU:主程序被阻塞,无法做其他事;
- 频率不稳定:受中断、编译优化等影响,音调不准甚至失真。
- 精度差 :
3. TIM 定时器的优势
- 硬件自动翻转 :配置好后,TIM 自动在精确时间点翻转 GPIO(通过输出比较或 PWM 模式),无需 CPU 干预;
- 高精度:基于系统时钟(如 72MHz),可生成微秒级甚至纳秒级精度的方波;
- 支持任意频率:通过调整 PSC(预分频)和 ARR(自动重载值),轻松生成 100Hz ~ 20kHz 的音频信号;
- 可播放音乐:动态修改 TIM 参数,就能切换不同音符(如《小星星》旋律)。
💡 示例:用 TIM3 输出 1kHz 方波驱动无源蜂鸣器
1// PSC=71, ARR=999 → 计数频率 = 72MHz/(71+1)=1MHz → 周期=1000 → 频率=1kHz 2htim3.Instance = TIM3; 3htim3.Init.Prescaler = 71; 4htim3.Init.Period = 999; 5HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 硬件自动输出 PWM
⚙️ 三、有源蜂鸣器:为什么不用 TIM?
-
有源蜂鸣器内部已集成振荡电路(通常固定频率,如 2.7kHz 或 4kHz);
-
只需通电就响,断电就停;
-
控制逻辑极简单:
1HAL_GPIO_WritePin(BEEP_GPIO, BEEP_PIN, GPIO_PIN_RESET); // 响(低电平有效) 2HAL_Delay(100); 3HAL_GPIO_WritePin(BEEP_GPIO, BEEP_PIN, GPIO_PIN_SET); // 停 -
用 GPIO 足够,用 TIM 反而浪费资源。