资讯动态

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

发布时间:2026/9/10 0:59:33 来源:尧图企业网站定制
简介面向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_CH1PA0本身也是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.6HzARR 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到7const 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来驱动音符切换那会是个很不错的多任务入门练习。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价