简介:面向意法半导体STM32F103系列微控制器的蜂鸣器驱动程序,主要用于报警提示、人机交互反馈等需要声音输出的嵌入式场景,特别适合正在学习通用输入输出接口与定时器等基础外设驱动的入门工程师。资源包共一百六十三个文件,以C语言源文件、头文件为主,同时包含编译中间文件、调试信息、启动汇编文件以及Keil工程配置文件,压缩包约二点九四兆字节。目前已有四百九十一人学习浏览,可作为项目移植或学习参考。驱动设计覆盖通用输入输出接口推挽输出初始化、上下拉与速度配置、定时器预分频与自动重载、脉宽调制波形生成、蜂鸣器开关控制、音频序列播放以及低功耗管理等多个关键环节。工程结构清晰,代码注释详细,示例可直接编译运行,便于单步跟踪初始化流程,快速理解寄存器配置与实际信号产生的关系;并可根据需要修改端口定义或频率参数,适配不同蜂鸣器模块,缩短项目开发周期。
1. 蜂鸣器驱动看似是点灯,实际上在练 GPIO 和定时器的基本功
“1-2-6”这种编号一出来,基本能猜到是某个课程设计或者实训手册里的模块序号,后面的“蜂鸣器驱动程序_STM32F103_”才是真正的技术内容。这个任务在嵌入式入门里非常常见,但它和点灯有一个本质区别:LED 只需要关心电平是高还是低,蜂鸣器却还需要关心电平持续多久、翻转多快、驱动电流够不够。换句话说,蜂鸣器驱动是 GPIO 输出、定时器、中断、还有一点状态机设计思想的综合体,把这些都过一遍,后面再做按键扫描、数码管显示、PWM 调光都是同一个套路。
这篇文章按“先判断硬件类型、再写最小驱动、再上 PWM 和状态管理、最后讲验证”的顺序走。新手能照着把蜂鸣器弄响,老手也能看到驱动层怎么拆分才不至于在后续项目里改一处崩一片。
2. 先分清有源和无源蜂鸣器,再写 STM32F103 的驱动代码
2.1 有源和无源的判断方法:看引脚、看封装、看数据手册
写驱动之前必须确认蜂鸣器是有源还是无源。所谓的“源”指的是有没有内部振荡电路。有源蜂鸣器内部自带振荡源,只要给它一个直流电平就会发声;无源蜂鸣器没有振荡电路,必须外部给它一个方波信号,靠线圈在交变磁场下振动发声。
判断方法有三种,按可靠性从高到低排列:
| 判断方式 | 有源蜂鸣器 | 无源蜂鸣器 |
|---|---|---|
| 引脚高度 | 引脚通常较高,外壳上贴有 vinyl 膜 | 引脚较矮,外壳可能裸露线圈 |
| 万用表测电阻 | 阻值约几百欧,且固定不变 | 阻值约 16Ω 或 32Ω,测量时可能轻微摆动 |
| 通直流电 | 直接长鸣 | 只有“嗒”的一声,不会持续响 |
注意第三种方法虽然最直观,但通直流电时无源蜂鸣器线圈相当于短路,长时间通电可能烧坏线圈,测试时点触一下即可,不要一直按着。
2.2 最小驱动代码:GPIO 输出翻转控制蜂鸣器
假设确认是有源蜂鸣器,驱动就非常简单:初始化一个 GPIO 推挽输出,拉高响,拉低停。这是“1-2-6-蜂鸣器驱动程序_STM32F103_”里最常见的最小实现。用标准外设库写的话如下:
void BEEP_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能 GPIOB 时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; // PB8 接蜂鸣器 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 翻转速度足够 GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_ResetBits(GPIOB, GPIO_Pin_8); // 默认不响 } void BEEP_ON(void) { GPIO_SetBits(GPIOB, GPIO_Pin_8); // 输出高电平,蜂鸣器响 } void BEEP_OFF(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_8); // 输出低电平,蜂鸣器停 }这段代码里有两个参数需要解释。GPIO_Mode_Out_PP 选择推挽输出,因为蜂鸣器是单极性负载,推挽模式可以提供更大的灌电流和拉电流能力。GPIO_Speed 设置为 50MHz 并不是让 GPIO 真的跑那么快,而是让输出驱动器的翻转速率上限更高;对蜂鸣器这种低频应用来说 2MHz 也够,但统一配置成 50MHz 可以减少后续复用为其他功能时的麻烦。
有源蜂鸣器的驱动电流一般在 20mA 到 30mA 之间,STM32F103 的 GPIO 灌电流和拉电流能力通常够用,但如果你选的是额定电流偏大的蜂鸣器,推荐在外面加一个 NPN 三极管或 MOSFET 做开关,GPIO 只控制基极或栅极。这是新手最容易踩的坑:直接接 GPIO 导致电流不足,蜂鸣器声音发闷或者干脆不响。
2.3 驱动层必须把“怎么响”和“响多久”分开
有源蜂鸣器最容易写崩的代码是“响 500ms 然后停”。很多初学者会这么写:
BEEP_ON(); delay_ms(500); BEEP_OFF();这在裸机顺序执行的程序里没问题,但一旦你的系统里同时要处理按键扫描、串口接收、OLED 刷新,这 500ms 的阻塞延时就会让整个主循环卡死。串口数据在延时期望�后继续跑,是典型的“蜂鸣器会响了,但其他功能全乱了”。
合理的设计是把“控制蜂鸣器状态”和“决定何时切换状态”拆开。裸机方案可以借助 SysTick 做一个非阻塞延时标记:
volatile uint32_t beep_timeout = 0; void BEEP_Start(uint32_t duration_ms) { BEEP_ON(); beep_timeout = GetTick(); // 记录开始时间 } void BEEP_Process(void) { if (beep_timeout && (GetTick() - beep_timeout >= 500)) { BEEP_OFF(); beep_timeout = 0; // 防止重复进入 } }BEEP_Process 放在主循环里,每圈检查一次时间是否到达。这样蜂鸣器响 500ms 的过程中,主循环依然可以处理其他事情。GetTick 通常由 SysTick 中断里的计数器提供,实现方式是每 1ms 对全局变量加一,返回它的当前值。
3. 用定时器 PWM 驱动无源蜂鸣器,让 STM32F103 发出不同音调
3.1 为什么无源蜂鸣器不能用 GPIO 的高低电平驱动
无源蜂鸣器的驱动核心是“交变电流”。GPIO 输出固定高电平,线圈两端没有电压差变化,振膜不会振动,自然没有声音。而 PWM 输出天然就是高低电平交替的方波,频率决定音调,占空比影响响度,这正是无源蜂鸣器需要的驱动信号。
STM32F103 的定时器可以输出 PWM,且频率可调,这让无源蜂鸣器不只是“响”和“不响”,还能发出不同音调,比如报警器的急促音、按键确认的短促音、电量不足的断续音。相比之下,如果只是做门铃提示,有源蜂鸣器加 GPIO 就够了,没必要上 PWM。
3.2 CubeMX 里配置定时器 PWM 输出的关键参数
现在主流做法是用 STM32CubeMX 生成初始化代码,再在 HAL 库基础上写应用层。假设用 TIM2 的通道 1,引脚 PA0,配置步骤如下:
在 CubeMX 的 Timers 菜单里选中 TIM2,Channel1 选择 PWM Generation CH1。然后在 Parameter Settings 里填写下面的关键参数,这也是整个配置里最容易出错的部分。
| 参数 | 值 | 说明 |
|---|---|---|
| Prescaler | 71 | 72MHz 时钟分频后得到 1MHz 计数频率 |
| Counter Mode | Up | 向上计数,最常用 |
| Counter Period | 999 | 溢出周期,配合预分频得到 1kHz PWM |
| Auto-Reload Preload | Enable | 避免在运行中直接改写 ARR 导致波形跳变 |
| Pulse | 500 | 占空比 50%,蜂鸣器响度通常在此附近最优 |
这三个关键参数决定 PWM 频率,公式是:PWM 频率 = 定时器时钟 / (Prescaler+1) / (Counter Period+1)。以 72MHz 主频、Prescaler=71、Period=999 为例,频率就是 72MHz / 72 / 1000 = 1kHz,对应人耳听起来比较响亮的频段。
Pulse 参数的意义需要单独强调。很多人把 Pulse 理解成“占空比百分比”,以为填 50 就是 50%。实际上它设置的是捕获比较寄存器的值,占空比 = Pulse / (Counter Period+1)。Period=999 时,Pulse=500 才是 50% 占空比。填 50 就只有约 5% 的占空比,蜂鸣器声音会明显偏小。
3.3 PWM 频率和占空比的设定逻辑
无源蜂鸣器的发声频率由 PWM 波形的基频决定。不同频率对应的音调不同:3kHz 左右声音尖锐,像老式闹钟;1kHz 左右声音温和,适合做提示音。常用音符频率可以参考这个表:
| 音符 | 频率 (Hz) | 适合场景 |
|---|---|---|
| C6 | 1047 | 提示音,清脆 |
| E6 | 1319 | 按键反馈,短促 |
| G6 | 1568 | 报警音,易辨识 |
| C7 | 2093 | 紧急报警,刺耳 |
实际使用中不建议让蜂鸣器长时间工作在 4kHz 以上的频率,一是人耳对这个频段敏感度下降,二是无源蜂鸣器的机械振膜有响应上限,超出规格书标称频率后声压级会明显下降。
占空比方面,50% 通常是最响的配置。占空比太高或太低都会让音质变差,极端情况下占空比接近 0% 或 100%,振膜几乎不动,声音反而消失。如果觉得声音刺耳,可以先把占空比降到 30% 到 40% 试试,不要一开始就动频率,频率改变影响的是音调而不是响度。
3.4 用 PWM 输出实现多音调驱动
在 HAL 库下,启动 PWM 输出只需要一行代码:
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);之后想改变音调,有两种方式。第一种是修改自动重载寄存器的值来改变频率,同时要同步修改比较寄存器以维持占空比;第二种是保持 ARR 不变,只改预分频值。工程上用第一种更方便,因为频率和占空比分开控制比较直观。具体实现:
void BEEP_Set_PWM(uint16_t freq_hz, uint8_t duty_percent) { uint16_t period_value, compare_value; uint32_t timer_clock = 72000000; // 假设 APB1 定时器时钟 72MHz period_value = timer_clock / (htim2.Init.Prescaler + 1) / freq_hz - 1; compare_value = (period_value + 1) * duty_percent / 100; __HAL_TIM_SET_AUTORELOAD(&htim2, period_value); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, compare_value); }注意这段代码假设预分频值保持不变,只调整 ARR。__HAL_TIM_SET_AUTORELOAD 写入的是 ARR,__HAL_TIM_SET_COMPARE 写入的是 CCR。如果 PWM 已经启动,ARR 的修改会在下一个溢出周期生效,不会让波形瞬间乱掉,但前提是 CubeMX 里开启了 Auto-Reload Preload。
还有一种常见的误用是直接用 HAL_TIM_PWM_Stop 停 PWM,然后 GPIO 输出低电平。确实可以,但会多一次函数的上下文切换开销。更干净的方式是只把占空比设成 0,波形继续输出但振膜不动:
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 0);这样再次输出声音时只需把比较值恢复,不需要重新启动定时器,也不会在启停瞬间产生电压尖峰。
4. 从裸机到带状态管理:给蜂鸣器驱动加上开关和频率控制
4.1 驱动函数分层:硬件层、控制层、应用层
蜂鸣器驱动写到 PWM 阶段,已经涉及 GPIO、定时器、中断三类硬件资源。如果所有代码堆在 main.c 里,后续别人接手时根本分不清哪部分是蜂鸣器的,哪部分是定时器的。常见的做法是把驱动拆成两层:硬件层负责和寄存器、HAL 库打交道,控制层负责提供给应用层的 API。
硬件层典型接口长这样:
void BEEP_Hardware_Init(void); // 初始化 GPIO 和定时器 void BEEP_Hardware_Set(uint16_t freq_hz, uint8_t duty); // 设置频率和占空比 void BEEP_Hardware_Enable(void); // 启动输出 void BEEP_Hardware_Disable(void);// 停止输出控制层在上面加语义:
void BEEP_Tone(uint16_t freq_hz, uint16_t duration_ms); // 单次鸣叫 void BEEP_Alarm_Start(void); // 开始报警,持续直到调用停止 void BEEP_Alarm_Stop(void); void BEEP_Key_Click(void); // 短促按键音控制层才是应用层真正调用的接口。应用代码不需要知道蜂鸣器是有源还是无源,不需要知道用的是 TIM2 还是 TIM3,只需要说“我要一个按键音”或者“我要报警”。如果后面从标准库换成 HAL 库,硬件层的函数内部全部重写,控制层和应用层一行不用动。
这就是为什么标题里的“驱动程序”而不是“控制代码”很重要。驱动程序强调的是可复用、可裁剪,而不是在 main 函数里写一堆 GPIO_SetBits。
4.2 中断里关蜂鸣器,避免阻塞主循环
非阻塞鸣叫的一个可靠方案是用定时器中断。控制层记录蜂鸣器应该持续响多久,中断里递减计数器,减到 0 就自动关断。这样做的好处是主循环完全感知不到蜂鸣器的存在,即使在串口中断里长时间处理数据,也不影响蜂鸣器按时停止。
用 SysTick 中断做这个功能最省资源,因为 STM32F103 的 HAL 库原本就有一个 1ms 的 SysTick 时基。在 SysTick 中断回调里检查蜂鸣器状态变量:
volatile uint16_t beep_remaining_ticks = 0; void HAL_SYSTICK_Callback(void) { if (beep_remaining_ticks > 0) { beep_remaining_ticks--; if (beep_remaining_ticks == 0) { BEEP_Hardware_Disable(); } } } void BEEP_Tone(uint16_t freq_hz, uint16_t duration_ms) { BEEP_Hardware_Set(freq_hz, 50); BEEP_Hardware_Enable(); beep_remaining_ticks = duration_ms; }这种实现的精妙之处在于 BEEP_Tone 函数本身不做任何等待,调用后立即返回,蜂鸣器在后台中断里自动停止。多次调用 BEEP_Tone 时,后一次调用直接覆盖 beep_remaining_ticks,前一次的时长自动失效,这在交互界面上可以做出“按键音打断报警音”的效果。
需要注意一个细节:中断回调里只改变输出状态,不要调用 HAL_Delay 一类的阻塞函数,也不要写日志或串口输出。SysTick 中断优先级在多中断系统里通常设成最低,如果在里面做耗时操作,会拖累整个系统的中断响应。
4.3 配合状态机做短鸣、长鸣、连响
很多报警器的实际需求比“响一次”复杂,比如烟雾报警需要连续鸣叫三声、停一秒、再鸣叫三声。简单地调 BEEP_Tone 三次加 delay 会阻塞主循环,用状态机则可以优雅地解决。
以“三短一长”报警音为例,设计一个只有三个状态的状态机:
typedef enum { BEEP_STATE_IDLE, // 空闲不响 BEEP_STATE_LONG, // 长鸣 BEEP_STATE_PAUSE, // 间歇 } BEEP_State_t;状态机调度的内容放在一个周期函数里,由主循环每 10ms 调用一次。如果需要更精细的时序,也可以把 10ms 改成 1ms,但要注意整除关系,否则计时会累积误差。
static BEEP_State_t beep_state; static uint8_t beep_step; static uint16_t beep_timer; void BEEP_Alarm_Task(void) { if (beep_state == BEEP_STATE_IDLE) { return; } if (--beep_timer > 0) { return; } switch (beep_state) { case BEEP_STATE_LONG: BEEP_Hardware_Set(2093, 50); // 高频报警音 BEEP_Hardware_Enable(); beep_timer = 500; // 响 500ms beep_state = BEEP_STATE_PAUSE; break; case BEEP_STATE_PAUSE: BEEP_Hardware_Disable(); beep_timer = 200; // 停 200ms if (++beep_step >= 3) { beep_state = BEEP_STATE_IDLE; beep_step = 0; } else { beep_state = BEEP_STATE_LONG; } break; default: break; } }这个状态机的核心思想是:每个状态只负责两件事,执行本状态该做的动作,然后指定下一个状态和等待时间。状态之间没有阻塞,主循环可以腾出手去处理按键、刷新显示、处理串口数据。STM32F103 的主频只有 72MHz,资源不算充裕,用这种分时调度的思路比上 FreeRTOS 轻量得多。
状态机的时序精度由调用周期决定。如果希望长鸣和停歇的时间误差控制在几毫秒以内,BEEP_Alarm_Task 需要在主循环里高频调用,或者在定时器中断里调用。
5. 验证 STM32F103 蜂鸣器驱动正确性的几个办法
蜂鸣器不是屏幕,坏了不会花屏,它可能只是不响、声音小、音调不对,需要靠工具和经验判断问题出在哪里。
先用万用表测量蜂鸣器两个引脚的电压。有源蜂鸣器在 GPIO 输出高电平时,引脚电压应该接近 3.3V;如果只有 1V 左右,说明驱动能力不足,蜂鸣器虽然可能振了一下但不会持续响,这时去检查 GPIO 是否配置成复用功能而不是推挽输出。无源蜂鸣器在 PWM 输出时,引脚电压应该在 0V 和 3.3V 之间方波跳动,用万用表的直流档量到的可能是有效值,不代表有问题。
没有示波器但有逻辑分析仪的话,可以把通道夹在蜂鸣器正极,看波形频率对不对。逻辑分析仪一般都有频率测量功能,比听音调更客观。配置 1kHz 的 PWM,逻辑分析仪读出来应该是 1.00kHz 附近;如果差很多,先查 CubeMX 里的预分频值和 ARR 有没有被代码重新覆盖。
最后提一个和蜂鸣器驱动没有直接关系但影响排查的问题:STM32F103 的 BOOT0 引脚电平决定启动模式。BOOT0 拉高时芯片从系统存储器启动,程序下载后根本不会运行你的蜂鸣器代码,表现得像“驱动完全没生效”。如果下载正常但程序不跑,先查 BOOT0 是不是被外部电路意外拉高了。类似的,DAP 下载失败时优先检查 BOOT0 和 BOOT1 引脚的电平状态,不要一上来就怀疑蜂鸣器电路焊错。
本文还有配套的精品资源,点击获取