资讯动态

STM32无源蜂鸣器播放旋律:从电路到PWM实现详解

发布时间:2026/8/30 11:57:11 来源:尧图企业网站定制
第一次用STM32驱动蜂鸣器的人十有八九会遇到这几种情况要么接上线一点声音都没有要么响了但像防空警报一样尖得吓人要么“一闪一闪亮晶晶”播出来像在蹦迪。原因往往不是代码写错了而是对蜂鸣器的类型和频率映射理解得不够。这篇围绕“How can I play a simple melody using a piezo buzzer on STM32”展开把从硬件选型、电路搭建到音符频率表、定时器PWM输出的完整链路拆开讲清楚。它适合手里有STM32开发板、想用无源压电蜂鸣器piezo buzzer做点声音反馈的新手也适合已经会点灯但没碰过音频输出的朋友。你不一定要有音乐基础只要懂一点定时器原理就能让蜂鸣器唱出一段完整的旋律。1. 先分清蜂鸣器类型为什么播放旋律必须用无源蜂鸣器1.1 有源蜂鸣器和无源蜂鸣器的本质区别很多开发板配套的蜂鸣器模块看起来都差不多圆柱形黑色外壳两个引脚但内部结构完全不同。有源蜂鸣器内部自带一个振荡电路你只需要给它通上直流电它就会以某个固定频率发声。这个“有源”指的是内部有振荡源不是指需要供电才能工作。有源蜂鸣器适合做报警、提示音因为它一上电就响软件上写一句GPIO_SetBits就能控制不需要任何频率配置。但它几乎只能发出一个固定音高想让它演奏旋律基本不可能。无源蜂鸣器就不一样了它内部没有振荡源需要外部输入一个交流方波信号才能发声。方波的频率决定了音高方波持续的时间决定了节拍所以理论上只要你能精确控制引脚输出的方波频率和持续时间就能演奏任意旋律。这就是为什么本文标题问的是“How can I play a simple melody using a piezo buzzer”答案的前提就是你手上必须是一颗无源蜂鸣器。一个简单的判断方法把蜂鸣器直接接到3.3V电源正负极上如果通电就响说明是有源蜂鸣器如果一点声音都没有极大概率是无源蜂鸣器。也可以用万用表测电阻有源蜂鸣器因为内部有振荡电路直流电阻通常比较小无源蜂鸣器的直流电阻往往在几十欧到几百欧之间。更稳妥的办法是看数据手册或商家标注很多模块上会直接写“active buzzer”或“passive buzzer”。1.2 压电蜂鸣器是怎么“唱歌”的无源蜂鸣器里最常见的是压电陶瓷蜂鸣器也就是标题里说的piezo buzzer。它的发声核心是一块压电陶瓷片当你在它两端施加一个变化的电压时压电陶瓷会发生机械形变电压变化频率越高形变变化越快就会推动空气产生声波。这里运用的是逆压电效应电场变化引起机械振动。所以本质上我们不是在“播放音频文件”而是在“控制方波频率”。比如要让蜂鸣器发出国际标准音A4440Hz就需要让GPIO输出一个频率为440Hz的方波也就是每秒钟翻转440次。通常用50%占空比的方波正负电压交替让压电陶瓷片有最大幅度的形变。很多人第一次做这个项目会直接写一个HAL_GPIO_TogglePin放在死循环里加个延时结果发现声音忽高忽低。原因很简单软件延时翻转的频率受CPU负载、中断、编译器优化影响很大频率不稳定音高自然就不准。后面会讲用定时器PWM来彻底解决这个问题。2. 最小电路搭建引脚、三极管驱动以及供电上的坑2.1 引脚选择不简单JTAG/SWD占用的那几个IO要绕开STM32的引脚不是随便挑一个都行。我见过不少人在F103C8T6上选PB3、PB4或PA15接蜂鸣器配置完定时器后发现引脚没有输出查了半天才发现这几个引脚默认复用为JTAG功能必须先禁用JTAG才能当普通GPIO用。虽然可以通过__HAL_AFIO_REMAP_SWJ_NOJTAG()之类的接口解开但对新手来说完全没必要踩这个坑直接避开即可。建议选一个既不是JTAG、又不是SWD的普通IO比如PA0、PA1、PB0、PB1、PB10等。同时注意这个引脚的定时器复用关系因为我们要用定时器PWM输出方波必须确认你的定时器通道和引脚能不能对应上。以STM32F103系列为例TIM2_CH1对应PA0TIM2_CH2对应PA1TIM3_CH1对应PA6TIM3_CH2对应PA7。选一个和定时器通道匹配的引脚能省掉后面一堆映射配置的麻烦。本文所有代码以PA0和TIM2_CH1为例。2.2 直接驱动还是三极管驱动一个实测对比最偷懒的接法是把蜂鸣器正极接PA0负极接GND或者反过来接。这种直接驱动方式在3.3V下对部分小功率无源蜂鸣器也能响但声音普遍很小而且STM32 GPIO的驱动能力有限长期大电流灌入引脚有风险。我的建议是加一个NPN三极管做开关放大电路非常简单但声音响度和稳定性都好很多。以下是我常用的接法PA0串一个1kΩ电阻接到S8050三极管的基极三极管发射极接GND三极管集电极接蜂鸣器负极蜂鸣器正极接3.3V或5V电源这个电路的本质是用GPIO的微小电流控制三极管的导通让蜂鸣器从电源侧获得更大的驱动电流。基极串1kΩ电阻是为了限制基极电流防止GPIO输出电流过大。S8050这类小功率三极管集电极电流能到几百毫安驱动一颗几十毫安的蜂鸣器绰绰有余。注意NPN三极管的C和E不要接反否则放大能力大打折扣蜂鸣器可能只发出很弱的嘶嘶声。如果用的是电磁式蜂鸣器因为内部有线圈关断瞬间会产生反向电动势建议在蜂鸣器两端反向并联一个1N4148或1N5819二极管做续流保护。压电蜂鸣器是容性负载一般不需要续流二极管但加上也无妨。驱动方式响度对GPIO的压力电路复杂度推荐度GPIO直接驱动偏小高最低不推荐长期使用NPN三极管驱动大低低推荐MOS管驱动大低低也可以但价格略高2.3 供电与共地声音忽大忽小的隐形元凶蜂鸣器的电源可以接3.3V也可以接5V。如果你的蜂鸣器标称工作电压是5V而开发板只有3.3V也能响但音量会明显下降。最好按蜂鸣器规格选择对应的供电电压。这里最容易被忽略的是“共地”问题蜂鸣器电源的地必须和STM32的GND连在一起否则三极管无法形成完整的电流回路蜂鸣器要么不响要么声音极其微弱。还有一个容易踩的坑是开发板本身的稳压芯片最大输出电流有限如果你又接OLED、又接传感器再从5V引脚拉蜂鸣器总电流可能超过稳压器能力导致电压被拉低蜂鸣器声音变小OLED闪烁甚至STM32频繁复位。解决办法是蜂鸣器尽量使用独立供电或者至少确认一下板子供电余量。我在调试时习惯用万用表测一下蜂鸣器两端电压如果带载后电压掉得厉害就需要换更大功率的电源适配器。3. 音符与频率的映射先建一张音高表再谈演奏3.1 十二平均律所有旋律的数学基础音乐里一个八度被分成12个半音相邻半音之间的频率比值是固定的等于2的1/12次方约等于1.05946。这个体系叫十二平均律。国际标准音A4钢琴上小字一组的la频率是440Hz。有了这两个基准理论上任意音符的频率都能算出来频率 440 × 2^((音符编号 - 69) / 12)其中69是A4在MIDI音符编号体系里的序号。但在STM32上一般没人用浮点pow函数去现算频率因为计算量大且没必要。更常见的做法是直接把常用音符的频率查表存进一个数组编写一个频率表。下面是C大调一个八度里最常用的几个音符频率音符频率(Hz)1MHz计数下的ARR值C42623816D42943401E43303030F43492865G43922551A44402272B44942024C55231912这里的ARR值是按“定时器计数频率为1MHz”算出来的后面写代码会直接用。你可能会疑惑为什么C4用262而不是261.63因为压电蜂鸣器本身就不是高保真音频设备整数频率完全够用人耳基本分辨不出几赫兹的偏差。3.2 时值、BPM和音符间隔节奏是旋律的另一半频率决定音高时值决定节奏。一首歌的节奏通常用BPMBeats Per Minute描述表示每分钟有多少拍。如果以四分音符为一拍BPM120表示一拍持续500msBPM240表示一拍持续250ms。八分音符是一拍的一半二分音符是一拍的两倍。写旋律编码时我会把每个音符的时长单独存在一个数组里单位用毫秒。下面以《小星星》第一句为例C4四分音符250msC4四分音符250msG4四分音符250msG4四分音符250msA4四分音符250msA4四分音符250msG4二分音符500ms这里BPM大约240也就是每分钟240拍整体速度比较轻快。如果觉得太快可以把所有时值乘以2BPM就变成120会更舒缓。还有一个很关键、但新手经常忽略的点音符和音符之间一定要加一个短小的静音间隔通常10ms到20ms。否则连续变化频率时方波相位不连续蜂鸣器会因为机械惯性产生“啪嗒”的杂音听感上就像每个音都粘在一起。这个间隔在乐谱编码里不需要体现只要在播放函数里统一加一个gap_ms参数就行。3.3 乐谱编码结构两个数组描述一首歌最简单的乐谱编码方案是一个频率数组存音高一个时值数组存每个音持续的时间两个数组一一对应。休止符用频率0表示播放时遇到0就静音指定时长。这样写起来直观也方便调整。uint16_t melody_freq[] { 262, 262, 392, 392, 440, 440, 392, 0, 349, 349, 330, 330, 294, 294, 262, 0 }; uint16_t melody_dur[] { 250, 250, 250, 250, 250, 250, 500, 100, 250, 250, 250, 250, 250, 250, 500, 100 };数组最后那两个0用来表示一小节的休止让整句唱完后有短暂停顿更接近真实听感。这种编码方案的好处是想换歌只需要换数组里的频率和时值完全不需要改代码逻辑。缺点是数组比较直白无法表达附点音符、三连音等复杂节奏但应付“simple melody”已经足够。4. 让定时器输出音频方波PWM配置与整段旋律代码4.1 为什么是定时器PWM而不是延时翻转GPIO有人可能会问用HAL_GPIO_TogglePin加延时翻转GPIO不也能产生方波吗理论上可以但实际上有两个问题。第一频率不稳定延时函数执行期间如果来一个中断哪怕只打断几十微秒方波周期就会被拉长人耳虽然无法察觉个别周期抖动但积少成多音高就会漂移。第二CPU被完全占死你没法在响的同时扫描按键、刷新屏幕、处理通信数据。定时器PWM就没有这些问题。硬件定时器按照你设定的ARR和CCR自动翻转输出电平一旦启动CPU完全不参与频率精准稳定。只需要在切换音符时更新一次ARR和CCR蜂鸣器就会持续输出对应频率的方波CPU可以腾出来做其他事情。这就是为什么这个项目最适合用定时器PWM实现。它不需要DAC不需要音频编解码芯片一颗定时器加三极管电路就能完成从数字方波到可辨识旋律的转换。4.2 初始化代码一条条拆开看下面是用STM32 HAL库初始化TIM2_CH1输出PWM的代码目标平台是STM32F103C8T6运行时钟72MHz。如果你用其他系列或标准库原理完全一样只是外设库API不同。TIM_HandleTypeDef htim2; void Buzzer_Init(void) { // 开启TIM2和GPIOA时钟 __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // PA0配置为复用推挽输出 GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; gpio.Alternate GPIO_AF1_TIM2; HAL_GPIO_Init(GPIOA, gpio); // 定时器基础配置 htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; // 72MHz / 72 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 1000 - 1; // 初始值稍后动态修改 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim2); // PWM通道配置 TIM_OC_InitTypeDef oc {0}; oc.OCMode TIM_OCMODE_PWM1; oc.Pulse 500; oc.OCPolarity TIM_OCPOLARITY_HIGH; oc.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim2, oc, TIM_CHANNEL_1); }这段代码里最关键的是Prescaler 72 - 1。72MHz主频除以72得到1MHz的定时器计数频率也就是说计数器每计数1次正好1微秒。为什么不直接把预分频设为0因为72MHz下ARR每减1对应约13.9纳秒计算频率时会出现很多小数不方便。调整到1MHz后频率和定时器ARR的换算关系就非常简单ARR 1000000 / 目标频率 - 1。Period 1000 - 1给了一个初始占位值实际播放时会被Buzzer_SetFreq动态修改。如果你打算用TIM3或TIM4要注意它们是16位定时器ARR最大值是65535在1MHz计数频率下最低只能输出约15Hz方波对常见音符来说完全够用。TIM2是32位定时器不需要担心ARR溢出。4.3 发声和静音一个可靠的控制函数播放旋律的核心是“要能随时改变频率”。我封装了一个Buzzer_SetFreq函数传入目标频率自动计算ARR和CCR更新定时器配置。static uint8_t pwm_started 0; void Buzzer_SetFreq(uint16_t freq) { if (freq 0) { // 频率为0表示静音将占空比设为0即可 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 0); return; } uint32_t arr 1000000 / freq; // 计数频率1MHz得到ARR __HAL_TIM_SET_AUTORELOAD(htim2, arr - 1); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, arr / 2); // 50%占空比 if (!pwm_started) { HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); pwm_started 1; } }这里有两个细节值得展开。第一__HAL_TIM_SET_AUTORELOAD和__HAL_TIM_SET_COMPARE是直接操作寄存器更新ARR和CCR不会停止PWM输出。如果先停止PWM再改配置会产生一瞬间的静音或咔哒声反而影响听感。第二静音时不调用HAL_TIM_PWM_Stop而是把比较值CCR设置为0。这样PWM保持运行但输出恒为低电平蜂鸣器不响。这种方法避免了“停止PWM后引脚电平不受控”的麻烦。接下来写播放一个音符的函数。它接收频率、时长和音符间静音间隔三个参数void PlayNote(uint16_t freq, uint16_t duration_ms, uint16_t gap_ms) { if (freq 0) { Buzzer_SetFreq(0); HAL_Delay(duration_ms); return; } Buzzer_SetFreq(freq); HAL_Delay(duration_ms); Buzzer_SetFreq(0); HAL_Delay(gap_ms); }这个函数是阻塞式的执行期间CPU在HAL_Delay里空转。对简单项目来说这不是问题因为播放旋律时本来也没有其他任务要处理。但如果你的系统里还要同时响应按键或通信那就需要对播放逻辑做非阻塞改造后面会说。4.4 播放旋律两个数组描述一首歌有了PlayNote播放整段旋律就非常简单了。直接遍历频率数组和时值数组一个个音符播放void PlayMelody(void) { uint16_t len sizeof(melody_freq) / sizeof(melody_freq[0]); for (int i 0; i len; i) { PlayNote(melody_freq[i], melody_dur[i], 10); } } int main(void) { HAL_Init(); SystemClock_Config(); // 配置到72MHz很重要 Buzzer_Init(); while (1) { PlayMelody(); HAL_Delay(1000); } }SystemClock_Config这一步不能省而且必须确认最终系统主频是72MHz。如果你的开发板没有外部晶振或者HSE启动失败代码可能会回退到内部8MHz RC时钟此时预分频72-1得到的是111kHz计数频率而不是1MHz同样的ARR值输出的频率会低很多整个旋律听起来像慢放了几倍。关于初始化的AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE它启用了ARR预装载功能。这意味着修改ARR后新值不会立即生效而是等计数器溢出进入更新事件后才生效。这样做的目的是避免在更新ARR的瞬间产生一个残缺的方波周期保证频率切换干净利落。实际听感上这个细节对音质有微小的改善。如果你不希望阻塞式播放可以在一个1ms定时器中断里维护“当前音符剩余时间”主循环只负责根据剩余时间决定是否切换频率。逻辑不复杂但代码量会翻倍。对“simple melody”这个目标来说先用阻塞式跑通再用状态机优化是比较合理的路径。5. 实测排错蜂鸣器不响、音高不对、节奏不稳的排查链路5.1 排查“完全没声音”如果程序下载进去之后蜂鸣器一声不吭先不要怀疑代码按下面的顺序排查。第一步确认蜂鸣器是不是无源的。把它直接接到3.3V电源两端如果通电就响说明是有源蜂鸣器你用PWM驱动它大概率只能得到微弱的杂音或不响。第二步检查硬件接线。如果是三极管驱动确认C、E没接反基极串了电阻蜂鸣器正负极没搞错。第三步确认GPIO配置是复用推挽GPIO_MODE_AF_PP而不是普通推挽输出GPIO_MODE_OUTPUT_PP。普通输出模式下PWM信号根本不会从外设映射到引脚。第四步检查PWM通道和引脚是否匹配。PA0对应TIM2_CH1如果你初始化的是TIM2_CH2引脚上不会有信号。第五步确认初始化里调用了HAL_TIM_PWM_Start。很多人配置了PWM但忘记启动结果引脚一直是低电平。调试时用LED代替蜂鸣器也可以验证PWM是否输出但LED对高频方波不敏感最好还是用示波器量引脚。没有示波器的话可以先用万用表测引脚直流电压如果PWM占空比50%且平均电压约1.65V说明有信号在输出。5.2 排查“能响但音高不对”能响说明硬件链路基本通了问题多半出在频率计算或时钟配置上。最常见的错误是把ARR计算反了比如直接拿频率值填入寄存器导致输出频率变成几万赫兹听起来非常刺耳。另一个常见问题是实际系统主频不是72MHz。建议在SystemClock_Config之后读一下HAL_RCC_GetSysClockFreq()确认返回值是72000000。如果用了内部RC时钟且没开PLL主频可能是8MHz预分频72-1后计数频率变成111kHz最终输出频率只有目标值的1/9左右音高会低到听不出旋律。还有一种不太明显的情况代码里使用了浮点数计算ARR但未包含头文件或启用FPU导致计算结果被截断成整数。对于F103这种没有硬件FPU的芯片浮点计算不仅慢还可能因精度问题产生误差。所以我在前面的示例中直接使用整数频率表建议你照做。如果你用手机下载一个频谱分析App把蜂鸣器贴近麦克风能看到基频峰值。对照你的目标频率表偏差在一个整数倍以内说明代码计算正确偏差很大说明定时器时钟或ARR值有问题。这个方法比纯靠耳朵判断可靠得多。5.3 排查“节奏乱、卡死、程序跑飞”节奏乱通常是时值数组和频率数组不对齐。比如频率数组有8个元素时值数组只有7个播放到最后一组时读取了越界数据。建议用sizeof(melody_freq) / sizeof(melody_freq[0])自动计算长度不要让循环下标硬编码。卡死的情况多半和HAL_Delay有关。注意不要在中断回调里调用HAL_Delay。SysTick中断的优先级如果低于当前正在执行的中断HAL_Delay等待uwTick递增时永远不会等来中断程序就卡死了。正确的做法是中断里只置标志位把播放逻辑放到主循环。如果你开的看门狗没有及时喂播放长旋律时也容易复位解决办法是在每个音符播放间隙喂狗或者把播放改成非阻塞。程序跑飞还有一种可能是数组下标越界后进入了硬件错误中断在

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

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

免费获取报价