news 2026/9/10 0:59:29

STM32无源蜂鸣器播放音乐:从驱动电路到PWM定时器配置全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32无源蜂鸣器播放音乐:从驱动电路到PWM定时器配置全解析

简介:面向STM32F103入门学习者,这份资源演示如何用无源蜂鸣器演奏音乐,内置《红海情歌》与《生日快乐》两首曲目,通过修改音调与时间参数即可换成任意旋律,适合用作单片机定时器/PWM输出或音频驱动的练手项目。压缩包共157个文件,大小仅552KB,以C语言源文件、H头文件为主,同时包含Keil工程配置、编译中间文件以及可直接烧录的HEX文件,便于对照工程与代码理解蜂鸣器控制逻辑。已有12573人学习下载,作者结合参考博文在描述中给出了修改思路,虽然是固定分值下载,但内容完整度较高。对想快速实现蜂鸣器播放音乐、研究音调编码或构建类似项目的开发者,都可以直接借鉴工程结构和程序框架。 前阵子做一个带提示音的小产品,需要在板子上加一个提示音,顺便把无源蜂鸣器播放音乐的玩法也认真研究了一遍。网上其实有很多例程,但大多数只给代码,不讲为什么这么接、为什么这么算。这篇文章基于STM32F103标准库,从硬件驱动电路到定时器PWM配置,完整走一遍无源蜂鸣器播放音乐的过程。如果你正想给产品加提示音,或者想用单片机弹首曲子,又或者只是想弄明白蜂鸣器频率和定时器ARR之间的关系,这篇文章应该能给你一个可以直接抄作业的方案,还能避开我踩过的坑。

1. 项目整体思路:先搞清楚无源蜂鸣器是怎么发声的

1.1 有源蜂鸣器和无源蜂鸣器的本质区别

这是新手最容易踩的第一个坑。有源蜂鸣器内部带了一个振荡电路,你只要给它通上直流电,它自己就能发出固定频率的声音,通常是2.7kHz左右。这种蜂鸣器做报警提示很方便,但想让它播放音乐基本不可能,因为频率是固定的,你改变不了音调。

无源蜂鸣器内部没有振荡源,你给它通直流电它根本不会响,必须给它一个一定频率的交流信号或者说方波,它才会发声。正是因为声音频率完全由外部输入方波的频率决定,我们只要让单片机输出不同频率的方波,就能让无源蜂鸣器发出不同的音调,进而组合成音乐。

所以这个项目的核心任务就一句话:用STM32F103输出频率可调的方波信号驱动无源蜂鸣器。

1.2 为什么选择定时器PWM而不是IO口翻转

想输出方波,最简单粗暴的写法是GPIO翻转加延时:

GPIO_SetBits(GPIOA, GPIO_Pin_0); Delay_us(500); GPIO_ResetBits(GPIOA, GPIO_Pin_0); Delay_us(500);

但仔细想一下就会发现这个方案有两个硬伤。第一,延时函数本身依赖主频和编译器优化等级,换一个优化等级、换一个晶振,音准就飘了。第二,播放音乐期间CPU完全被占死,你没法同时做按键扫描、OLED刷新这些事,稍微复杂一点的应用直接卡壳。

换成定时器PWM就舒服很多。定时器一旦启动,它会自动在引脚上输出指定频率和占空比的方波,全程不需要CPU干预。你要做的只是按乐谱节奏,每隔一段时间把定时器的自动重装载值(ARR)改成目标频率对应的数值。这才是工程上真正可用的方案。

2. 硬件准备与最小系统细节

2.1 所需器件清单一览

这个项目硬件很简单,我列一下我实际用到的:

器件型号/规格数量用途
主控板STM32F103C8T6最小系统板1核心控制
无源蜂鸣器5V无源蜂鸣器,直径9mm或12mm1发声器件
三极管S8050 NPN1电流放大驱动
电阻1kΩ1限流
二极管1N41481反向电动势保护
面包板/杜邦线若干-电路搭建

有一点要注意,STM32F103的GPIO引脚驱动能力虽然标称能输出20mA左右,但蜂鸣器这东西是感性负载,启动瞬间的电流尖峰比较大,直接用引脚驱动很容易导致电压跌落、单片机复位。而且用引脚直接驱动时声音很小,因为引脚拉电流能力有限。我实测下来,同一个蜂鸣器,用三极管驱动和直接驱动,音量差距非常明显。

2.2 驱动电路设计:三极管加保护二极管

驱动电路我用了最常见的NPN三极管低边驱动方案:

  • 蜂鸣器正极接电源VCC(我用的5V)
  • 蜂鸣器负极接三极管集电极
  • 三极管发射极接地
  • 单片机GPIO通过1kΩ电阻接三极管基极
  • 蜂鸣器两端反向并联一个1N4148二极管

这个电路的工作逻辑很简单:GPIO输出高电平让三极管导通,蜂鸣器通电;GPIO输出低电平三极管截止,蜂鸣器断电。PWM方波到达引脚后,蜂鸣器就以PWM的频率振动发声。

那1N4148二极管是干嘛的?蜂鸣器线圈在断电瞬间会产生一个反向电动势,方向是试图维持原来的电流,如果不加这个续流二极管,这个高压尖峰很可能损坏三极管,极端情况下还会顺着电源线干扰单片机。并联二极管后,反向电动势会被二极管钳位并泄放掉。这个元件成本不到一毛钱,但能避免很多奇怪的故障,建议别省。

S8050的基极电流大概是(3.3V-0.7V)/1kΩ ≈ 2.6mA,足以让三极管进入饱和导通状态,集电极电流可以到几百毫安,驱动蜂鸣器绰绰有余。基极电阻不建议再用更小的,太大会增加单片机引脚负担,太小会导致三极管基极电流过大。

2.3 电源和下载方面的坑

做这个实验最容易被坑的地方在电源。无源蜂鸣器工作电流通常在20mA到80mA之间,看起来不大,但如果你的开发板是用USB供电,同时USB线又比较细长,线阻就会比较大。蜂鸣器一响,线上压降增加,VCC瞬间跌落,MCU就复位了。现象就是程序跑起来蜂鸣器响两声然后设备重启,非常头疼。

解决办法有几个:用带屏蔽的粗USB线;给蜂鸣器单独用外部5V电源,共地即可;或者在蜂鸣器电源脚加一个100μF左右的电解电容做储能缓冲。我习惯是加电容,效果最直接。

另外引脚选择也有一点讲究。PA9、PA10是串口1的下载调试引脚,如果代码里要用串口打印调试信息,就别把蜂鸣器接在这两个脚上。PA0、PA1、PB5这类都是比较干净的引脚。我用的是PA0,它复用为TIM2_CH1,PA0本身也是WKUP引脚,但正常做PWM输出没影响。

3. 软件实现:基于定时器PWM播放音乐

3.1 定时器频率计算和工程配置

STM32F103的系统时钟是72MHz,挂在APB1总线上的定时器(TIM2、TIM3、TIM4等)时钟是72MHz。我们通过设置预分频器PSC把计数时钟降下来,然后用ARR控制计数上限,从而控制PWM周期。

我的做法是先把定时器计数时钟分频到1MHz,也就是PSC设置为71:

计数频率 = 72MHz / (PSC + 1) = 72MHz / 72 = 1MHz

这样计数器每计一个数就是1微秒,ARR的值就直接对应PWM周期:

PWM频率 = 1MHz / (ARR + 1)

反过来,想让蜂鸣器发出某个频率f的声音:

ARR = 1000000 / f - 1

比如中音do是261.6Hz,ARR = 1000000 / 261.6 - 1 ≈ 3822,实际频率约261.4Hz,误差不到0.1%,人耳完全分辨不出来。

占空比我设为50%,也就是比较值CCR = ARR / 2。50%占空比对方波驱动蜂鸣器来说声音最饱满,再高容易导致蜂鸣器磁芯饱和,声音反而发闷。

3.2 PWM初始化代码详解

我这里以标准库为例,用TIM2的通道1输出PWM,引脚是PA0。先初始化GPIO和定时器:

void BEEP_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); } void BEEP_TIM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 72MHz / 72 = 1MHz,计数周期1us TIM_TimeBaseInitStructure.TIM_Period = 999; // 默认1kHz,后续播放时再改 TIM_TimeBaseInitStructure.TIM_Prescaler = 71; // 分频系数 TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 500; // 默认50%占空比 TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); }

这段代码有几个细节值得强调:

GPIO必须配置为GPIO_Mode_AF_PP,也就是复用推挽输出。如果用普通推挽输出,PWM信号根本不会映射到引脚上。这是新手经常忽略的问题。

TIM_ARRPreloadConfig也非常关键。如果不使能ARR预装载,修改ARR后不会立刻生效,而是在下一轮更新事件才生效,但如果配置了预装载,并且打开了定时器更新中断,就能保证修改频率时PWM波形连续不抖动。

如果你用的是TIM1(比如PA8引脚),记住一句口诀:高级定时器必须额外调用TIM_CtrlPWMOutputs(TIM1, ENABLE),否则没有波形输出。这个函数是给高级定时器的刹车和死区电路用的主输出使能,很多人在标准库里折腾半天没波形,就是少了这一句。

3.3 音符频率表与乐谱数据结构

关于音调,我们约定用C调来演奏。我定义了一个三行八列的频率表,行分别是低音、中音、高音,列从左到右是休止符、简谱1到7:

const u16 freq_table[3][8] = { // 休止 1 2 3 4 5 6 7 { 0, 131, 147, 165, 175, 196, 220, 247 }, // 低音 { 0, 262, 294, 330, 349, 392, 440, 494 }, // 中音 { 0, 523, 587, 659, 698, 784, 880, 988 } // 高音 };

乐谱里怎么表示一个音符呢?我用的方式是用一个u16的高字节表示音区,低字节表示音名:

#define L1 0x11 // 低音do #define L2 0x12 #define L3 0x13 #define L5 0x15 #define L6 0x16 #define L7 0x17 #define M1 0x21 // 中音do #define M2 0x22 #define M3 0x23 #define M4 0x24 #define M5 0x25 #define M6 0x26 #define M7 0x27 #define H1 0x31 // 高音do #define H2 0x32 #define H3 0x33 #define H4 0x34 #define H5 0x35 #define H6 0x36 #define H7 0x37

解码的时候,高字节右移一位就是音区,低字节是简谱数字:

u8 level = (note >> 4) - 1; // 0低音,1中音,2高音 u8 index = note & 0x0F; // 简谱数字 u16 freq = freq_table[level][index];

为什么要搞得这么麻烦?因为最后写乐谱的时候可读性非常高。比如《小星星》的第一句:

const u16 music_star[] = { M1, 400, M1, 400, M5, 400, M5, 400, M6, 400, M6, 400, M5, 800, M4, 400, M4, 400, M3, 400, M3, 400, M2, 400, M2, 400, M1, 800, 0, 0 // 结束标志 };

一眼就能看出这是简谱,想换曲子直接按简谱填数字,非常方便。这里每个音符后面的数字是时长,单位毫秒。400ms是四分音符、800ms是二分音符,200ms是八分音符,具体节奏可以根据曲子速度和风格自己调,我用的是小星星80拍/分钟的速度。

3.4 播放函数:把乐谱变成声音

核心播放函数就两个:一个是单音符发声函数,一个是遍历乐谱的播放函数。

void BEEP_PlayNote(u8 level, u8 index, u16 duration_ms) { u16 freq = freq_table[level][index]; u16 arr; if (freq == 0) // 休止符,关闭输出 { TIM_Cmd(TIM2, DISABLE); DelayMs(duration_ms); return; } arr = 1000000 / freq - 1; TIM_SetAutoreload(TIM2, arr); TIM_SetCompare1(TIM2, arr / 2); // 50%占空比 TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); DelayMs(duration_ms); } void BEEP_PlayMusic(const u16 *music) { u16 note, dur; while (1) { note = *music++; dur = *music++; if (note == 0 && dur == 0) { break; // 结束标志 } u8 level = (note >> 4) - 1; u8 index = note & 0x0F; BEEP_PlayNote(level, index, dur); // 音符之间加一个极短的间隙,避免音符粘连 TIM_Cmd(TIM2, DISABLE); DelayMs(10); } }

注意我在每个音符之间都关闭了PWM输出10ms。这个间隙很重要,如果不加,相邻音符的频率切换虽然很快,但蜂鸣器本身有机械惯性,会出现类似“滑音”的效果,听起来黏糊糊的。间隙10ms比较合适,太长了会显得曲子断断续续。

这个方案是阻塞式的,播放期间CPU被延时函数占住。如果是做产品,提示音一般只有一两秒,完全够用。如果要在播放音乐的同时响应按键、跑显示逻辑,建议把播放逻辑放进定时器中断:定时器中断里每1ms减一次计数器,计数器到0就切下一个音符。这样CPU占用率几乎为零,代码结构也清晰。

4. 常见问题与调试实录

4.1 无声、沙哑、断续等问题排查

我在调试这个项目时遇到过不少问题,把最典型的整理成了一张速查表,方便你遇到问题直接对号入座。

现象可能原因解决思路
完全无声,引脚没有波形GPIO没配置为复用推挽模式;定时器没有使能;频率参数配错检查GPIO_Mode_AF_PP;检查是否调用TIM_Cmd使能;用示波器看引脚
只有“哒哒”声,不是音调PWM频率太低,或ARR计算错误,导致实际频率过低检查预分频PSC和ARR的值,用公式freq = 1MHz / (ARR + 1)反算
声音沙哑、发闷占空比太高;驱动管没有完全饱和;电源波动占空比设为50%;检查基极电阻和三极管接法;电源加电容
播放时单片机重启蜂鸣器启动瞬间拉低电源电压加大电源电容;用外部5V供电;换粗USB线
引脚有波形但蜂鸣器不响蜂鸣器接反了;蜂鸣器是有源蜂鸣器无源蜂鸣器正负极接反不响,反过来试;确认外壳有没有“+”号标记
TIM1/PA8输出不了PWM高级定时器主输出使能没开TIM_CtrlPWMOutputs(TIM1, ENABLE)
音符间隔有杂音音符切换时PWM波形不连续使能TIM_ARRPreloadConfig;在音符间加短暂静音

4.2 用示波器和逻辑分析仪验证波形

如果蜂鸣器不响,第一步不是怀疑硬件,而是先确认软件有没有输出正确的PWM波形。把示波器探头夹在PA0和GND之间,应该看到一个频率随音符变化、占空比50%左右的方波。

没有示波器的话,可以用一个非常土但有效的办法:把蜂鸣器引脚断开,接一个LED串联1kΩ电阻到GND,运行播放程序,如果LED亮度有节奏变化,说明PWM在正常工作,问题大概率出在蜂鸣器驱动电路。LED的亮度变化频率很低,肉眼看不大出来,但至少能确认引脚有输出。

如果手头有逻辑分析仪,注意无源蜂鸣器的PWM频率在100Hz到5kHz之间,采样率设1MHz以上就足够了,可以看到波形周期是否符合预期。中音do是261.6Hz,周期约3.8ms,如果周期差太多,查PSC和ARR的计算。

4.3 蜂鸣器驱动电路的几个细节经验

最后说几个硬件上的经验,都是实际试出来的:

NPN三极管的集电极接蜂鸣器负极、发射极接地,这个接法叫低边驱动。很多人会把蜂鸣器接到集电极和GND之间,这也有电流流过,但蜂鸣器是悬浮在电源和集电极之间更好,因为三极管导通时压降小,蜂鸣器能拿到接近满幅的电源电压。

蜂鸣器正负极怎么判断?一般外壳上有标注,有“+”号那端为正极,没有的话看引脚长度,长脚是正极。如果接反了,无源蜂鸣器接上PWM信号也不会响,换一下引脚就行,不会烧东西,这点比有源的好,有源接反可能直接损坏内部振荡电路。

如果你感觉声音音量不够,除了检查驱动电路,还可以试试把占空比从50%调到60%~70%。有源蜂鸣器因为内部有振荡电路,对占空比不敏感;无源蜂鸣器对占空比变化反应比较明显,一般50%最均衡,但个别型号的蜂鸣器在60%左右声音更大,你可以微调一下看看效果。

5. 项目扩展:从提示音到完整的音乐播放器

做完基础播放功能之后,这个项目还有很多可以扩展的空间。我在实际产品里就把这套逻辑改造成了多音调报警器,通过组合不同频率和时长的声音,用几种比较短的提示音区分不同报警等级,用户一听就能分辨。如果需要播放完整的歌曲,可以考虑用DAC加音频解码芯片的方案,但STM32F103的内存和Flash都比较小,播放WAV或者MP3太吃力,无源蜂鸣器播放音乐更多是玩票性质或者做短提示音。

按键切换歌曲、调整播放速度都是很直接的玩法,只需要在BEEP_PlayMusic函数里加一个外部变量控制节拍缩放系数。我自己实际使用中的体会是,无源蜂鸣器播放音乐这个项目虽然代码量不大,但它把GPIO复用、定时器PWM、频率计算、中断延时这几个嵌入式基础知识点全都串起来了,做完一遍对STM32F103的定时器系统理解会深很多。如果你觉得用延时阻塞的方式太占CPU,下一步值得试试用定时器中断或者DMA来驱动音符切换,那会是个很不错的多任务入门练习。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 0:58:02

电赛E题运动目标追踪系统:OpenMV+STM32云台视觉伺服全程实录

简介:面向2023年全国大学生电子设计竞赛E题备赛者,这份压缩包围绕“基于STM32F1的自动追光云台”提供了完整工程与源码参考。包内包含STM32F10x系列外设驱动(如ADC、I2C、USART、定时器)的C语言源文件及头文件,附带Kei…

作者头像 李华
网站建设 2026/9/10 0:48:30

51单片机AD/DA转换实验:AD0809与DA0832汇编代码及调试要点

简介:针对51单片机汇编语言中AD 0809模数转换与DA0832数模转换实验,这份附件压缩包提供了可运行的Keil5工程及参考代码注释,适合电子、自动化、计算机等专业正在学习单片机接口编程的初学者和开发者。包内共10个文件,以uvproj工程…

作者头像 李华