STM32F103蜂鸣器驱动:有源无源识别与PWM定时器配置
2026/9/14 20:47:24 网站建设 项目流程

简介:面向STM32F103的蜂鸣器驱动程序,定位嵌入式入门驱动开发场景,帮助开发者快速掌握GPIO操作、定时器配置与PWM发声等基础技能。资源覆盖从GPIO推挽输出模式设置、上拉/下拉与速度选择,到定时器预分频和重载值调节音调,再到中断/DMA输出及音频序列控制的完整流程,并附带功耗与测试建议,适合初学者对照学习。压缩包共163个文件,其中h文件承载接口声明,c文件实现功能逻辑,uvproj工程可直接用Keil打开,hex/axf/map等编译产物便于烧录与调试,整体约2.94MB,结构清晰方便查阅。已有491人学习下载。通过该资源可获取可直接改写的驱动框架,减少底层寄存器配置摸索时间,对理解STM32标准外设库的蜂鸣器控制方式有实际帮助,无论是报警提示还是交互反馈均可在此基础上扩展应用。

1. 写 STM32F103 蜂鸣器驱动前,先把有源和无源分清楚

在 STM32F103 上写蜂鸣器驱动,第一关往往不是代码,而是板子上那枚蜂鸣器究竟有源还是无源。有源蜂鸣器内部带振荡电路,给高电平就持续发声;无源蜂鸣器没有振荡源,必须靠方波或 PWM 在音圈里制造交变磁场才能发声。两种器件外形几乎一样,但驱动代码的写法和排查思路完全不同。

标题里“1-2-6-蜂鸣器驱动程序”看起来像实验编号或工程目录名,落到 STM32F103 上,本质是用有限的引脚和定时器资源,把提示音、按键音、报警音按需放出来,同时不拖垮主循环。对刚接触定时器外设的开发者来说,蜂鸣器是理解 PWM 输出最直观的低成本载体。

下面按硬件连接、定时器 PWM、驱动逻辑、调试验证四个阶段展开。看完后能分辨哪些场景用 GPIO 高低电平即可,哪些必须开定时器通道,以及驱动无声时第一步该查哪里。

2. 蜂鸣器硬件连接与三极管驱动电路参数

2.1 有源蜂鸣器和无源蜂鸣器的判断与选型

STM32F103 的驱动程序和蜂鸣器类型强绑定:有源蜂鸣器上电即响,无源蜂鸣器需要外部频率信号。两者外观接近,最可靠的区分方法是看丝印和做通电测试。元件面写着 H、Active 或带振荡电路标识的,基本是有源;标注 Passive 或只给出额定电压电流的,多半是无源。手头没有资料时,把蜂鸣器接到 3.3V 电源:发出连续尖锐声的是有源,只出“嗒”一声或者完全不动的是无源。

选型时有个容易犯的错,为了省定时器资源选有源蜂鸣器,程序里只做 GPIO 翻转,结果声音频率固定,按键音和旋律音都放不出来。无源蜂鸣器虽然需要一个 PWM 通道,但换来的是频率可控、能表达不同音符。对需要多种提示音的项目,我更愿意用一个定时器通道换这个灵活性。

对比项有源蜂鸣器无源蜂鸣器
内部振荡电路
驱动信号高低电平方波/PWM
发声频率固定,约 2.3kHz由 PWM 频率决定
最小 STM32F103 资源1 个 GPIO1 个定时器通道
可表达内容响 / 不响音调、旋律
软件复杂度

选型定下来,驱动代码的结构也就定下来:有源蜂鸣器只解决“什么时候响、响多久”,无源蜂鸣器还要解决“以什么频率响、占空比取多少”。第 3 章的定时器配置全部针对无源蜂鸣器;如果项目用的是有源蜂鸣器,第 4 章的时序逻辑依然适用。

2.2 NPN 三极管开关与 GPIO 电流边界

STM32F103 的 GPIO 在推挽输出模式下,持续输出电流建议按 20mA 设计,绝对最大 25mA。蜂鸣器稳态工作电流普遍在 30mA 到 80mA 之间,直接把蜂鸣器挂在引脚上,输出电压会被拉低,长时间运行还会损伤 IO。常见做法是用一个 NPN 三极管(S8050 或 2N2222)做低边开关:GPIO 输出 3.3V,经基极电阻驱动三极管导通,蜂鸣器接在电源正极和集电极之间。

在确认驱动代码之前,要先排除硬件通路的问题。我一般会写一个最基础的 GPIO 控制代码做自检:

/* 硬件自检:PB0 作为蜂鸣器开关引脚,确认三极管和蜂鸣器通路 */ #include "stm32f10x.h" void Buzzer_GPIO_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); } void Buzzer_Set(uint8_t on) { if (on) GPIO_SetBits(GPIOB, GPIO_Pin_0); // 基极高电平,三极管导通 else GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 基极低电平,三极管截止 }

这段代码把 PB0 配置成推挽输出,Buzzer_Set(1)时基极电压约 3.3V,经过 1kΩ 基极电阻后基极电流约 2.3mA,足以让 S8050 进入饱和区。关键参数是基极电阻:取 10kΩ 以上,三极管可能工作在线性放大区,蜂鸣器两端压降大、声音发闷;取 100Ω 又会把 GPIO 输出电流推到极限。常见范围是 1kΩ 到 4.7kΩ。

如果Buzzer_Set(1)后有源蜂鸣器长响、无源蜂鸣器只有短促的“哒”声,说明开关电路正常;如果完全无声,先量三极管集电极电压,判断是蜂鸣器开路、三极管没导通还是电源没供上。这里要注意,GPIO 和三极管驱动的电源必须共地,否则基极和发射极之间形不成电流回路,三极管永远不会导通。

2.3 下拉电阻、续流二极管和去耦电容的实际作用

蜂鸣器电路里还有三个元器件,位置不起眼,但对系统可靠性影响很大。第一个是基极下拉电阻,STM32F103 复位期间引脚默认是浮空输入,PB0 外接三极管基极会处于不确定状态,可能被板上噪声抬高,造成上电瞬间误鸣。基极对地并一个 10kΩ 电阻,复位期间三极管就能可靠截止。

第二个是续流二极管,反向并联在蜂鸣器两端,阴极接电源正极,用来吸收 PWM 关闭瞬间感性负载产生的反向电动势,保护三极管集电极不被尖峰击穿。无源蜂鸣器是典型的感性负载,这个二极管不能省。第三个是电源去耦电容,在蜂鸣器电源两端放一个 100nF 陶瓷电容,能明显减少蜂鸣器启停对同一电源轨上 ADC、串口的干扰。

这三个元件不改变驱动代码逻辑,但缺了下拉电阻,可能会在调试时看到“无缘无故响一声”的怪现象;缺了续流二极管,运行几十分钟后三极管可能过热失效。STM32F103 最小系统板上如果已经集成蜂鸣器电路,通常不需要改硬件,直接看原理图确认三极管基极接到哪个引脚即可,省掉自己搭电路这一步。

3. 用 STM32F103 定时器 PWM 驱动无源蜂鸣器

3.1 定时器和通道选择:避开串口与输入捕获冲突

无源蜂鸣器需要一个能连续输出方波的通道,STM32F103 的 TIM1、TIM2、TIM3、TIM4 都能输出 PWM,但引脚映射不同。常见映射:TIM2_CH3 默认在 PA2,TIM3_CH4 默认在 PB1,TIM4_CH2 默认在 PB7。选通道前要打开原理图,确认蜂鸣器控制脚接到哪个引脚,再对照数据手册查这个引脚还复用了哪些外设。

TIM1 是高级定时器,带刹车、互补输出和主输出使能位 MOE,配置时少了主输出使能这一步,引脚就可能一直没有波形。蜂鸣器用不到这些特性,我一般直接选通用定时器 TIM2 或 TIM3。另一个要留意的是引脚复用冲突:PA2 同时被复用为 USART2_TX,PA3 是 USART2_RX;项目里常用的串口 1 在 PA9/PA10,串口 3 在 PB10/PB11,和定时器通道打架的概率各不相同。选蜂鸣器通道前,我习惯先把串口、SPI、I2C 占用的引脚列一张表,再挑剩余不冲突的那个,避免功能调通了之后改版。

3.2 CubeMX 下 TIM2 PWM 的参数计算与初始化代码

用 STM32CubeMX 配置 PWM 的步骤是:先开定时器时钟源,再选择 PWM Generation,最后设置 Prescaler、Counter Period 和 Pulse。假设定时器时钟是 72MHz,目标输出 1kHz、占空比 50% 的波形:

  • Prescaler(PSC)= 71,计数频率变为 72MHz / 72 = 1MHz
  • Counter Period(ARR)= 999,输出频率为 1MHz / 1000 = 1kHz
  • Pulse = 500,占空比 = 500 / 1000 = 50%

生成代码后,HAL 库的初始化函数如下:

/* TIM2_CH3 输出 1kHz / 50% PWM,驱动无源蜂鸣器 */ TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC; void MX_TIM2_PWM_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); // 打开 TIM2 时钟 htim2.Instance = TIM2; htim2.Init.Prescaler = 71; // 72MHz/(71+1)=1MHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 999; // 1MHz/(999+1)=1kHz htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); sConfigOC.OCMode = TIM_OCMODE_PWM1; // 向上计数,先高后低 sConfigOC.Pulse = 500; // 50% 占空比 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_3); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); }

代码里三个核心参数是 PSC、ARR 和 Pulse。PSC 决定计数频率,ARR 决定周期,Pulse 决定高电平持续时间。改变频率时需要同步调整 ARR 和 Pulse 才能保持 50% 占空比;只改 ARR 而 Pulse 不变,占空比会偏移。CubeMX 中 TIM2 挂载在 APB1 总线上,当 APB1 预分频系数大于 1 时,定时器时钟是 PCLK1 的两倍,配置界面显示 36MHz 时实际定时器时钟可能是 72MHz,计算 PSC 前要先在 Clock Configuration 里确认这个值。

提示:无源蜂鸣器对波形极性不敏感,PWM1 和 PWM2 两种模式都能驱动,差异只是输出电平逻辑反相,只要占空比合适,发声效果几乎一样。

3.3 音阶频率表与占空比选择

蜂鸣器驱动从“响”升级到“能放旋律”,需要把音符频率换算成 ARR。以 PSC=71、定时器计数频率 1MHz 为例:

ARR = 1000000 / 目标频率 - 1 Pulse = (ARR + 1) / 2

常用音调对应参数如下,占空比统一按 50%:

音名频率(Hz)ARRPulse
C4(do)26238161908
E4(mi)33030301515
G4(sol)39225511276
C5(高音 do)5231912956
1kHz 测试音1000999500
2kHz 测试音2000499250

占空比不是越大越好。无源蜂鸣器的发声本质是振膜的机械振动,50% 占空比能提供对称的交变磁场,声音干净且响度充足。把占空比提到 80% 以上,平均电流增大,声音可能因为磁饱和而失真,三极管也更烫。如果觉得音量过大,优先串联限流电阻或把占空比降到 30% 左右;把 Pulse 调得过小也不行,接近直流电平会让线圈不振动,蜂鸣器反而无声。

3.4 频率切换的代码封装

把改变 ARR 和 Pulse 的过程封装成一个函数,上层代码就不用关心寄存器细节:

static void Buzzer_SetFreq(uint32_t freq_hz) { uint32_t arr, pulse; if (freq_hz == 0) { HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_3); // 休止符 return; } arr = 1000000UL / freq_hz - 1UL; // 计数频率 1MHz,ARR 计算结果 pulse = (arr + 1) / 2; // 保持 50% 占空比 __HAL_TIM_SET_AUTORELOAD(&htim2, arr); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, pulse); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); }

调用Buzzer_SetFreq(1000)就能立即输出 1kHz。__HAL_TIM_SET_AUTORELOAD修改 ARR,__HAL_TIM_SET_COMPARE修改 Pulse,两个宏不需要关闭定时器,切换过程不会产生明显咔嗒声。如果工程里 PSC 不是 71,把1000000UL换成实际计数频率即可,其余逻辑不变。

4. 蜂鸣器驱动时序:从 HAL_Delay 阻塞到状态机

4.1 阻塞延时与按键、串口共处的冲突

最简单的发声驱动是在主循环里这样写:

Buzzer_Set(1); HAL_Delay(200); Buzzer_Set(0); HAL_Delay(100);

这段代码逻辑正确,但它让 CPU 空转了 300ms。这里的 200 是发声时长,100 是静默时长,改成任意毫秒都可以,代价是主循环被占用同样时间。在只有蜂鸣器的实验里没问题,一旦系统里还有按键扫描、OLED 刷新、串口接收,主循环会被拖住:按键按下去要等 300ms 才有反应,串口数据也可能溢出。

更隐蔽的问题是HAL_Delay依赖 SysTick,如果在定时器中断回调里调用,整个中断系统都会被卡住。所以真实的驱动不会把发声过程放在主循环里“死等”,而是把时间拆成可检查的状态,用定时器中断来推进。

4.2 1ms 时基状态机与状态转移表

常见做法是定义一个发声状态机:IDLE 表示空闲,ON 表示正在发声,OFF 表示发声停止后的短暂关断,WAIT 表示两次发声间的静默间隔。每 1ms 进入一次中断,让计时器减一,减到零就切换状态。

状态发生条件输出动作迁移到
BUZZ_IDLE收到 Buzzer_Request启动 PWMBUZZ_ON
BUZZ_ON发声计时归零停止 PWMBUZZ_OFF
BUZZ_OFF关断计时归零保持静默BUZZ_WAIT
BUZZ_WAIT静默计时归零保持静默BUZZ_IDLE
/* 无阻塞蜂鸣器状态机:请求 100ms 发声,结束后自动回 IDLE */ typedef enum { BUZZ_IDLE, BUZZ_ON, BUZZ_OFF, BUZZ_WAIT } BuzzState; static volatile BuzzState buzzState = BUZZ_IDLE; static volatile uint32_t buzzTimer = 0; void Buzzer_Request(uint32_t ms) { buzzTimer = ms; buzzState = BUZZ_ON; HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); // 立即出声 } void Buzzer_Tick_1ms(void) { if (buzzState == BUZZ_IDLE || buzzTimer == 0) { return; } if (--buzzTimer == 0) { switch (buzzState) { case BUZZ_ON: // 发声时间到 HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_3); buzzTimer = 20; // 关断 20ms,防冲击声 buzzState = BUZZ_OFF; break; case BUZZ_OFF: buzzTimer = 200; // 静默 200ms buzzState = BUZZ_WAIT; break; default: buzzState = BUZZ_IDLE; break; } } }

Buzzer_Tick_1ms()放进SysTick_Handler,主循环里调用Buzzer_Request(100),100ms 后声音自动关闭,期间主循环可以继续处理按键和显示。buzzStatebuzzTimervolatile,是为防止编译器把它们优化进寄存器,造成中断内外读写不同步。发声结束后不直接回 IDLE,而是经过一个 20ms 的 OFF 关断,作用是让振膜在完全停止前先释放残余冲击,听起来不会像“啪”的一声。

4.3 旋律播放:音符表和节拍推进

无源蜂鸣器的价值在于能放旋律。旋律是一组“频率 + 时长”的音符序列,中间夹着休止符。把 4.2 的状态机扩展一下:每个节拍结束后,不是回到 IDLE,而是从音符表里取下一个音符重新填充计时器。

typedef struct { uint32_t freq; /* 0 表示休止符 */ uint16_t duration; /* 10ms 为单位 */ } BuzzerNote; static const BuzzerNote s_melody[] = { { 1000, 10 }, { 0, 5 }, /* 响 100ms,停 50ms */ { 1500, 10 }, { 0, 5 }, { 2000, 20 }, { 0, 10 }, { 0, 0 } /* 结束标志 */ }; static uint8_t s_melodyIndex = 0; static uint8_t s_playing = 0; void Buzzer_PlayStep(void) { const BuzzerNote *n; if (!s_playing) return; n = &s_melody[s_melodyIndex]; if (n->freq == 0 && n->duration == 0) { s_playing = 0; HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_3); return; } if (n->freq == 0) { HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_3); /* 休止符静音 */ } else { Buzzer_SetFreq(n->freq); } buzzTimer = (uint32_t)n->duration * 10; /* 换算成毫秒 */ buzzState = BUZZ_ON; s_melodyIndex++; } void Buzzer_Play(void) { s_melodyIndex = 0; s_playing = 1; Buzzer_PlayStep(); }

把这个示例和 4.2 的状态机组合时,需要改一个地方:Buzzer_Tick_1ms()BUZZ_OFF分支在s_playing为真时,不进入BUZZ_WAIT,而是直接调用Buzzer_PlayStep()继续下一个音符。duration以 10ms 为最小单位,足够表达大多数提示音和短旋律;需要更精细节奏时,把音符表里的单位改成 1ms,同时把buzzTimer的换算系数改成 1 即可。

5. 蜂鸣器驱动验证:无声排查与波形检查方法

5.1 从硬件到软件的三步排查

驱动写完不出声,问题不一定在代码。我习惯按三个层次排查。第一步,把蜂鸣器控制引脚设为固定高电平:有源蜂鸣器应该长响,无源蜂鸣器应该不响,用这个现象确认三极管和蜂鸣器通路是否正常。量三极管集电极电压,正常应该在 3.3V 左右。第二步,让 MCU 运行最简单的 GPIO 翻转程序,用万用表测引脚电压是否在 0V 和 3.3V 之间跳变,确认 STM32F103 的输出没被异常钳位。第三步,在Buzzer_Tick_1ms()里翻转一个 LED 引脚,确认 1ms 时基中断真的在跑。

实际项目里最常碰到的无声原因不是频率算错,而是引脚对不上:CubeMX 里配置的是 PA2,硬件接线却接到 PB0,程序执行完全正常,就是听不到声音。所以排查第一步永远是核对原理图上的网络标号和代码里的 GPIO 引脚是否一致。

5.2 用示波器和逻辑分析仪看 PWM 是否真的发出来

如果确认引脚正确,把示波器探头接到 PWM 输出脚,检查两点:频率和占空比。频率和预期相差超过 3%,先查时钟树,确认 HSE 晶振有没有起振、PLL 配置是否正确,而不是急着改 ARR。占空比不足 5%,大概率是 Pulse 和 ARR 的关系写反了,或者__HAL_TIM_SET_COMPARE的通道号写错。

没有示波器时,逻辑分析仪也能验证时序。把通道接到 PWM 输出脚,采样率不要低于 2M S/s,测出波形周期后反算频率。逻辑分析仪只能看到高低电平,看不到电压幅值,所以不能替代万用表去验证电平是否真的到达三极管基极。

5.3 用双频率切换快速定位问题根源

针对“有声音但音调不对”的情况,有个简洁的定位方法:在驱动里保留两个测试函数,一个输出 1kHz,一个输出 2kHz,用按键或串口命令切换。如果两个频率都能发声,只是音调高低不对,问题在 ARR 计算或定时器时钟;如果只有一个频率正常,优先查占空比是否过小,振膜驱动力不足会导致高音明显偏弱;如果两个都不响,回到 5.1 从硬件开始查。

还有一个值得做的收尾优化:把Buzzer_SetFreq里的1000000UL替换成HAL_RCC_GetPCLK1Freq() * 2,再按 APB1 分频系数重新设置 PSC,这样代码能自动适应不同系统时钟配置,以后换板子或改时钟树,不用每次重新计算频率参数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询