STM32F103 PWM驱动无源蜂鸣器播放音乐全攻略
简介这份资源面向使用STM32F103系列单片机的嵌入式初学者与进阶开发者解决无源蜂鸣器驱动与音乐播放的实践问题。包内提供完整的Keil工程源码通过定时器PWM控制蜂鸣器频率与节拍实现《红海情歌》和《生日快乐》两首曲目的播放修改音调与时间参数即可替换为其他歌曲。资源共157个文件以36个c源文件、37个h头文件为核心涵盖STM32标准外设库的定时器、RCC、GPIO等驱动模块另有23个d依赖文件、23个o编译目标文件、22个crf交叉引用文件及uvprojx工程文件、hex可执行文件、map映射文件等压缩包约552KB结构完整可直接编译下载。目前已有12580人学习下载适合作为STM32定时器与PWM输出的练手项目帮助读者掌握无源蜂鸣器发声原理、频率与音调的对应关系并快速移植到自己的开发板上验证效果。1. 从“滴一声”到《青花瓷》STM32F103 驱动无源蜂鸣器的完整落地思路很多人第一次在 STM32F103 上点响蜂鸣器都是拿有源蜂鸣器直接接 GPIO给个高电平就“滴”一声完事。但一旦需求变成“播放一段旋律”有源蜂鸣器就彻底歇菜了——它内部自带振荡源频率固定你只能控制它响或不响控制不了音高。而无源蜂鸣器本质上就是一个电磁线圈加振膜没有内置振荡电路必须由 MCU 输出特定频率的方波才能发声频率决定音高占空比影响响度和音色。这意味着只要你能精确控制方波频率和持续时间就能让 STM32F103 演奏出完整的乐曲。这套方案适合已经掌握 GPIO 和定时器基础、想从“点灯”进阶到“发声”的嵌入式开发者也适合做课程设计、毕业设计或小玩具需要加音乐提示音的从业者。核心就三件事用定时器产生方波、用延时或系统滴答控制节拍、把音符频率和时值编码成数组。听起来简单但实际调起来坑比想象中多。2. 无源蜂鸣器的驱动原理与 STM32F103 定时器选型2.1 为什么必须用 PWM 而不是 GPIO 翻转无源蜂鸣器需要外部提供交变信号才能持续振动发声。最朴素的做法是在 GPIO 上写一个循环用delay翻转电平但这种方式有两个致命问题一是 CPU 被完全占用无法同时处理其他任务二是delay的精度受中断和编译器优化影响频率容易漂移听感上就是“跑调”。用定时器输出 PWM 则完全不同——定时器硬件自动翻转引脚CPU 只需在需要换音符时更新寄存器主循环可以空出来做按键扫描、显示刷新等事情。STM32F103 有多个通用定时器TIM2、TIM3、TIM4、TIM5和高级定时器TIM1、TIM8它们都能输出 PWM。对于蜂鸣器这种单通道应用选哪个都行但要注意引脚分布。比如 TIM3_CH2 可以映射到 PB5TIM4_CH1 映射到 PB6TIM2_CH1 映射到 PA0。我一般优先选 TIM3 或 TIM4因为 TIM1 和 TIM8 是高级定时器配置互补输出和死区等寄存器更复杂杀鸡不用牛刀。2.2 频率计算从音符到 ARR 和 PSCSTM32F103 的定时器时钟来自 APB1TIM2~TIM7或 APB2TIM1、TIM8。以常见的 72MHz 系统时钟为例APB1 预分频系数为 2 时定时器时钟为 72MHz。PWM 频率公式为[ f_{PWM} \frac{f_{TIM}}{(PSC1) \times (ARR1)} ]其中 ( f_{TIM} ) 是定时器输入时钟PSC 是预分频器ARR 是自动重装载值。要产生某个音符频率 ( f )可以先固定 PSC再反算 ARR[ ARR \frac{f_{TIM}}{(PSC1) \times f} - 1 ]比如中音 A440Hz取 PSC71则定时器计数频率为 72MHz/721MHzARR 1000000/440 - 1 ≈ 2272。这样占空比通过 CCR 设置一般取 ARR 的一半即可得到 50% 方波蜂鸣器响度最大。2.3 硬件连接与驱动电路无源蜂鸣器不能直接接 GPIO 和 GND因为蜂鸣器线圈在断电瞬间会产生反向电动势可能击穿 IO 口。常见做法是用一个 NPN 三极管如 S8050做开关GPIO 接基极串联 1kΩ 电阻集电极接蜂鸣器负极蜂鸣器正极接 3.3V 或 5V发射极接地。同时蜂鸣器两端反向并联一个续流二极管如 1N4148用来泄放反向电动势。如果蜂鸣器工作电压高于 3.3V必须用三极管隔离不能直接接 GPIO。注意有些无源蜂鸣器标称“3V~5V”但实际在 3.3V 下响度偏低如果对音量有要求建议用 5V 供电并加三极管驱动。3. 从零搭建CubeMX 配置与代码实现3.1 用 CubeMX 生成定时器 PWM 初始化代码打开 CubeMX选择 STM32F103C8T6在 Pinout 视图里找到 TIM3将 Channel2 设为 PWM Generation CH2对应引脚 PB5 会自动高亮。在 Clock Configuration 里确认系统时钟为 72MHzAPB1 Prescaler 为 2这样 TIM3 时钟为 72MHz。在 TIM3 参数设置中Prescaler: 71Counter Period (ARR): 先填 2272对应 440HzPWM Generation CH2: Pulse (CCR): 113650% 占空比CH Polarity: High生成代码后HAL 库会自动初始化 TIM3 并启动 PWM。但要注意CubeMX 默认生成的MX_TIM3_Init()里会调用HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2)吗不会启动 PWM 需要手动在main()里调用。很多人生成代码后直接烧录发现没声音就是因为忘了这一步。/* main.c 中手动启动 PWM */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2);3.2 动态改频率一个函数搞定音符切换要播放音乐核心就是不断改变 PWM 频率。HAL 库提供了__HAL_TIM_SET_PRESCALER和__HAL_TIM_SET_AUTORELOAD宏可以在运行时修改 PSC 和 ARR。但更高效的做法是固定 PSC只改 ARR这样计算量小切换速度快。/** * brief 设置蜂鸣器发声频率 * param freq: 目标频率单位 Hz0 表示停止发声 * note 定时器时钟 72MHzPSC 固定为 71计数频率 1MHz */ void Buzzer_SetFreq(uint32_t freq) { if (freq 0) { /* 停止 PWM 输出蜂鸣器不发声 */ HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_2); return; } /* 确保 PWM 已启动 */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); /* 计算 ARR1MHz / freq - 1 */ uint32_t arr 1000000 / freq - 1; __HAL_TIM_SET_AUTORELOAD(htim3, arr); /* 占空比设为 50%响度最大 */ __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, arr / 2); /* 产生更新事件让新 ARR 立即生效 */ HAL_TIM_GenerateEvent(htim3, TIM_EVENTSOURCE_UPDATE); }这段代码的逻辑说明freq为 0 时直接停止 PWM避免蜂鸣器一直响。非零时先确保 PWM 处于启动状态然后根据公式计算 ARR。__HAL_TIM_SET_AUTORELOAD修改的是影子寄存器需要产生一个更新事件才能把新值加载到实际寄存器。HAL_TIM_GenerateEvent就是干这个的。占空比设为arr/2即 50%这是无源蜂鸣器响度最大的点。参数freq的有效范围建议在 200Hz~5000Hz 之间太低或太高蜂鸣器可能不响应或声音极小。3.3 节拍控制用 SysTick 做非阻塞延时播放音乐时每个音符需要持续一定时间比如四分音符 500ms八分音符 250ms。如果用HAL_Delay()整个程序会卡住按键和显示都没法响应。常见做法是用HAL_GetTick()做非阻塞延时/** * brief 播放一个音符 * param freq: 频率 Hz * param duration_ms: 持续毫秒数 */ void Buzzer_PlayNote(uint32_t freq, uint32_t duration_ms) { uint32_t start HAL_GetTick(); Buzzer_SetFreq(freq); while ((HAL_GetTick() - start) duration_ms) { /* 这里可以插入按键扫描、显示刷新等任务 */ } Buzzer_SetFreq(0); /* 音符间短暂停顿避免连音 */ HAL_Delay(10); /* 10ms 间隔让音符更清晰 */ }HAL_GetTick()返回的是自系统启动以来的毫秒数利用无符号整数减法可以安全处理溢出。duration_ms根据曲谱的 BPM 计算比如 BPM120 时四分音符为 500ms八分音符为 250ms。音符之间加 10ms 停顿是为了区分相邻同频率音符否则听起来会连成一个长音。3.4 把简谱翻译成频率数组中音 C 到高音 C 的频率表如下实际编码时用宏定义或常量数组音符频率(Hz)音符频率(Hz)低音5392中音1523低音6440中音2587低音7494中音3659中音4698中音5784中音6880中音7988高音11047高音21175以《小星星》为例简谱为1 1 5 5 6 6 5 - 4 4 3 3 2 2 1 -编码成数组/* 音符频率数组0 表示休止符 */ const uint16_t star_freq[] { 523, 523, 784, 784, 880, 880, 784, 698, 698, 659, 659, 587, 587, 523 }; /* 对应节拍单位 ms */ const uint16_t star_dur[] { 400, 400, 400, 400, 400, 400, 800, 400, 400, 400, 400, 400, 400, 800 }; void Buzzer_PlaySong(void) { for (uint32_t i 0; i sizeof(star_freq)/sizeof(star_freq[0]); i) { Buzzer_PlayNote(star_freq[i], star_dur[i]); } }这段代码把频率和时长分开成两个数组遍历时依次播放。star_dur里 400ms 对应四分音符800ms 对应二分音符。实际演奏时可以根据听感微调比如把 400 改成 350 会让节奏更轻快。4. 避坑指南无源蜂鸣器调试中的五个血泪教训4.1 蜂鸣器不响但示波器有波形现象用示波器测 GPIO 有方波输出频率也对但蜂鸣器一点声音没有。原因驱动电路电流不足。STM32 的 GPIO 最大输出电流约 20mA而无源蜂鸣器工作电流通常在 30mA 以上直接驱动带不动。解决加三极管驱动或者用达林顿管 ULN2003。检查三极管引脚是否接错基极电阻是否过大建议 1kΩ。4.2 声音断断续续或频率漂移现象播放时声音发抖或者用手机调音器测频率发现偏差很大。原因在Buzzer_PlayNote的 while 循环里调用了HAL_Delay或其他阻塞函数导致 PWM 更新事件被延迟。另外如果中断优先级配置不当SysTick 中断被高优先级中断打断HAL_GetTick()会丢 tick。解决确保播放循环里只做非阻塞操作。检查 NVIC 优先级SysTick 优先级不要设得太低。如果用了 FreeRTOS注意HAL_GetTick的基准可能被任务调度影响。4.3 音符切换时有“咔哒”声现象每个音符开始或结束时听到明显的爆音。原因频率突变时PWM 占空比没有平滑过渡或者停止 PWM 时引脚状态不确定。解决在切换频率前先把 CCR 设为 0再改 ARR最后恢复 CCR。或者在音符之间插入 5~10ms 的静音期。更彻底的做法是用定时器中断做包络控制但代码复杂度会上升。4.4 用 HAL_Delay 导致按键无响应现象播放音乐时按键完全没反应必须等整首曲子播完。原因HAL_Delay是忙等待期间不执行主循环。解决改用状态机。把每个音符的播放拆成“设置频率 → 记录起始 tick → 主循环里检查是否到时 → 到时后切下一个音符”。这样主循环每轮都能扫描按键。4.5 不同蜂鸣器响度差异大现象同一块板子换一个蜂鸣器声音大小天差地别。原因无源蜂鸣器的谐振频率不同有的在 2kHz 附近最响有的在 4kHz。另外占空比不是 50% 时响度也会下降。解决查阅蜂鸣器规格书找到谐振频率播放时尽量让音符靠近这个频率。如果曲子里低频较多可以考虑加一级放大或换用谐振频率更低的蜂鸣器。5. 进阶技巧用定时器中断实现真正的多任务播放前面用HAL_GetTick()做非阻塞延时已经能应付大多数场景但如果你想让蜂鸣器在后台播放同时主循环跑 OLED 刷新、串口通信、传感器采集更优雅的方案是用定时器中断驱动音符切换。思路是用一个基本定时器如 TIM6产生 1ms 中断在中断里递减当前音符的剩余时长减到 0 就切换到下一个音符并更新 PWM 频率。这样主循环完全不用管音乐播放只需要在需要时启动或停止即可。/* 全局变量 */ volatile uint32_t note_index 0; volatile uint32_t note_remain_ms 0; volatile uint8_t is_playing 0; /* TIM6 中断服务函数1ms 周期 */ void TIM6_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim6, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim6, TIM_FLAG_UPDATE); if (!is_playing) return; if (note_remain_ms 0) { note_remain_ms--; } else { /* 切换到下一个音符 */ if (note_index sizeof(star_freq)/sizeof(star_freq[0])) { is_playing 0; Buzzer_SetFreq(0); return; } Buzzer_SetFreq(star_freq[note_index]); note_remain_ms star_dur[note_index]; note_index; } } } /* 启动播放 */ void Buzzer_StartSong(void) { note_index 0; note_remain_ms 0; is_playing 1; HAL_TIM_Base_Start_IT(htim6); }这段代码的关键点note_remain_ms和note_index用volatile修饰因为它们在中断和主循环之间共享。Buzzer_SetFreq在中断里调用要确保它执行时间足够短——实际上它只操作几个寄存器完全没问题。TIM6 的配置很简单PSC71ARR999就是 1ms 中断。启动播放时把is_playing置 1中断里就会自动推进。停止播放时把is_playing清 0 并调用Buzzer_SetFreq(0)。验证方法播放《小星星》的同时在主循环里翻转一个 LED用示波器看 LED 引脚波形是否均匀如果 LED 闪烁节奏稳定说明中断没有阻塞主循环。另外可以用逻辑分析仪抓 PWM 引脚看频率切换是否干净有没有多余的毛刺。从那以后我每次做蜂鸣器驱动都会先把Buzzer_SetFreq单独测一遍用示波器确认频率误差在 1% 以内再往上叠音乐播放逻辑。这个习惯帮我省下了大量“明明代码没错但就是难听”的排查时间。希望帮到你。本文还有配套的精品资源点击获取