STM32定时器PWM驱动无源蜂鸣器播放《菊次郎的夏天》完整工程解析

发布时间:2026/10/13 20:57:39
STM32定时器PWM驱动无源蜂鸣器播放《菊次郎的夏天》完整工程解析 简介这是一套基于STM32F103ZET6的完整Keil工程源码面向STM32入门与进阶学习者演示如何用定时器输出PWM驱动无源蜂鸣器播放《菊次郎的夏天》噪音版旋律。压缩包共132个文件包含34个C源文件、33个头文件以及Keil工程文件uvprojx/uvoptx、hex固件和axf/map等编译产物体积约3.09MB便于直接打开工程查看或烧录验证。核心代码中STM32外设库与初始化函数如Init_Buzzer分置于标准目录BuzzerMusic等相关文件实现了音符到PWM序列的转换播放逻辑适合学习频率/占空比控制、定时器PWM配置与音乐播放应用。资源已吸引2400人学习对想通过蜂鸣器实战理解定时器、GPIO与PWM原理的开发者来说是一份可直接运行验证的参考工程。1. 蜂鸣器放《菊次郎的夏天》一个小工程里藏着的定时器、PWM和一堆坑如果你在找“stm32 蜂鸣器 菊次郎的夏天”相关资源说明你多半已经受够了蜂鸣器只会“滴——”一声的玩具级玩法想让它完整哼出一段旋律。这件事的难点不在“响”而在“怎么让它按谱子响、响得不像噪音”。《菊次郎的夏天》主旋律音符密集、跨度大蜂鸣器用方波驱动时谐波刺耳加上节拍稍快就糊成一团所以你听到的往往就是“噪音版”。这个资源解决的就是这件事从曲谱提取、频率表映射、定时器PWM输出到主循环节拍调度全部串成一套可编译可复现的裸机工程。适合刚学完GPIO和定时器、想用一个小项目把这两块知识真正焊死的开发者也适合用它应付课设或快速验证PWM操作。全文没有玄学只有能跟到烧录的步骤和参数。2. 把旋律转成STM32能吃的频率表简谱、音符索引与转换脚本2.1 先确认硬件用无源蜂鸣器别用有源蜂鸣器网上买蜂鸣器时常盯着“5V”“贴片”“带驱动板”这些字眼但真正决定你能不能放歌的是“有源”还是“无源”。有源蜂鸣器内部自带振荡电路只要通电就会以固定频率常见约2kHz发声。你给它PWM方波它反而会乱叫或者声音严重变调根本没法播放旋律。无源蜂鸣器内部没有振荡源你必须主动给它一个频率可变的方波它才会出对应音调的声音——这正是放歌的前提条件。区分方法很简单看外观和铭牌。外壳上印着“H”的通常是有源印着“L”或者是纯阻抗标识的多为无源用万用表测直流电阻有源蜂鸣器一般是十几欧到几十欧无源蜂鸣器通常接近8Ω到16Ω。另外拿3V电池碰一下两端持续响的是有源有“嗒嗒”声的是无源。驱动电路也别省。STM32的GPIO直接驱动蜂鸣器电流能力不够声音会发干发闷甚至拉低单片机电压导致复位。常见做法是用一个NPN三极管做开关PA0PWM输出→ 1kΩ限流电阻 → S8050基极S8050发射极接地集电极接蜂鸣器负极蜂鸣器正极接3.3V或5V电源蜂鸣器两端并联一个1N4148二极管阴极朝电源正极保护三极管关断时的反向电动势这套电路是电磁式无源蜂鸣器最通用的驱动方式。如果用的是压电蜂鸣器可以不接续流二极管但驱动原理一样。2.2 从简谱到音符序号先确定调号和音区拿到《菊次郎的夏天》的简谱后别急着敲代码。先做三件事确认调号1C、1D还是1G、圈出所有高音点和低音点、标记每小节的拍子。因为蜂鸣器播放质量本来就有限调号不必过分纠结找到一个自己唱着顺的版本即可。我习惯把音符映射成一张“频率表 索引”的结构。C调自然音阶的基准频率如下// note_freq.h // 索引0保留给休止符频率为0 // 1C4(do), 2D4(re), 3E4(mi) ... 按半音递推 static const float note_freq[] { 0.0f, // 0: rest 261.6256f, // 1: C4 293.6648f, // 2: D4 329.6276f, // 3: E4 349.2282f, // 4: F4 391.9954f, // 5: G4 440.0000f, // 6: A4 493.8833f, // 7: B4 523.2511f, // 8: C5 587.3295f, // 9: D5 659.2551f, // 10: E5 698.4565f, // 11: F5 783.9909f, // 12: G5 880.0000f, // 13: A5 987.7666f, // 14: B5 1046.502f, // 15: C6 };这段代码的逻辑很直接用数组下标当音符索引用float存频率方便后面换算定时器重装值。之所以用float而不是uint16_t是因为分频系数计算时会遇到小数先保精度后取整音准误差更小。休止符对应0.0f在播放层做特殊处理。如果你手里是固定调性的简谱比如1G那“1”对应的是G4频率392Hz而不是261Hz。别偷懒每个调都有自己的频率表。2.3 用脚本把简谱转成C数组别手抄几百个音符手抄音符数组容易抄错尤其《菊次郎的夏天》这种十六分音符密集的旋律二十个小节就有上百个音。我一般先用Python写一个小脚本把简谱转成C语言数组。假设你按“音符名时值”的文本格式记录旋律比如C4:500, E4:250, G4:250, REST:125# build_melody.py # 输入: 音符序列文本行, 如 C4:500 E4:250 G4:250 # 输出: melody.h 里的 Note_t 数组初始化代码 notes [C4:500, E4:250, G4:250, REST:125] note_to_idx { C4: 1, D4: 2, E4: 3, F4: 4, G4: 5, A4: 6, B4: 7, C5: 8, D5: 9, E5: 10, F5: 11, G5: 12, A5: 13, B5: 14, C6: 15, REST: 0 } lines [] for n in notes: name, dur n.split(:) idx note_to_idx[name] lines.append(f {{{idx}, {dur}}}) header typedef struct {\n uint8_t freq_idx;\n uint16_t duration_ms;\n} Note_t;\n\n array_def static const Note_t melody[] {\n ,\n.join(lines) \n}; print(header array_def)运行后会输出一个可以直接贴进源文件的melody[]数组。脚本里name.split(:)把“C4:500”拆成音符名和时长note_to_idx字典是频率表下标的映射关系生成的每个元素{1, 500}表示“播放频率表第1个音符持续500ms”。这个脚本的价值在于你换一首歌时不需要改播放代码只要重新生成melody[]数组就行。那首曲子的完整简谱转好的数组在资源包的源码目录里我不建议大家再手工录入一遍容易把高音点和低音点弄混最后放出来的旋律整段跑调。3. 驱动层选型与实现从GPIO翻转讲到定时器PWM3.1 为什么不用GPIO翻转来“凑”方波有一种极简做法确实能发出声音把GPIO拉高延时半周期拉低再延时半周期。你要C4的262Hz就让周期约3818μs每半周期1909μs翻转一次。// 伪代码示意, 不推荐用在正式工程里 while (1) { HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET); delay_us(1909); HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET); delay_us(1909); }这代码能响但有两个硬伤。第一CPU被完全占死延时期间干不了任何别的活音乐播放中途想响应按键暂停难。第二延时函数本身受中断影响如果系统里开着SysTick或其他中断实际翻转周期会漂移音准就会忽高忽低。这就是很多“扔上去能响、仔细听总跑调”工程的根源。定时器PWM方案的思路完全不同方波由硬件生成CPU只负责在音符切换时改一次参数剩下的时间可以巡检按键、跑状态机。F103主频72MHzPWM输出频率精度轻松做到音符误差小于0.1%人耳听不出偏差。所以这个资源选择PWM方案。通道选TIM2_CH1对应PA0引脚原因是PA0在绝大多数开发板上都引出来了而且TIM2挂在APB1总线上只要时钟树配置合理走线最短、最不容易受其他外设干扰。3.2 定时器参数计算ARR、PSC和PWM频率的关系STM32定时器PWM输出的核心公式是f_PWM TIM_CLK / ((PSC 1) × (ARR 1))其中TIM_CLK是定时器输入时钟PSC是预分频系数ARR是自动重装载值。有一个细节特别容易翻车F103的APB1预分频如果是2那么TIM2/TIM3的时钟是APB1的两倍即72MHz。不少人在CubeMX里看到APB136MHz就直接拿36MHz算ARR出来的音调整体低一个八度。以C4261.63Hz为例如果PSC0TIM_CLK72MHzARR 72000000 / (261.63) - 1 ≈ 275157但ARR是16位寄存器最大只能存65535这明显超了。所以低音区不能直接硬算要把PSC提上去。我一般这样处理——把频率分为两个区间f 500HzPSC设为1实际分频是2让计数时钟降到36MHzf ≥ 500HzPSC保持0用72MHz直接计数对应的ARR计算函数// timer_utils.c // 输入音符频率, 输出PSC和ARR, 保证ARR不溢出 void calc_pwm_params(float freq_hz, uint16_t *psc, uint16_t *arr) { uint32_t tim_clk 72000000UL; if (freq_hz 500.0f) { *psc 1; // 实际分频系数 2, 计数时钟36MHz tim_clk 36000000UL; } else { *psc 0; // 计数时钟72MHz } uint32_t arr_val (uint32_t)((float)tim_clk / freq_hz - 1.0f); *arr (uint16_t)arr_val; }代码先判断频率区间再选PSC最后用上述公式倒推ARR。注意*psc 1时实际分频是2寄存器值加1才是真实分频系数注释里特意写清了避免下次看代码时迷糊。tim_clk变量在低音分支被覆盖为36MHz因为低音音符频率低需要的计数值大降低计数时钟能让ARR回落到65535以内。__HAL_TIM_SET_AUTORELOAD是HAL库提供的宏专门用来在运行中修改ARR。切换音符前先停一下PWM改完ARR和比较值CCR再重新启动能避免极少数情况下占空比跳变产生的爆音。3.3 占空比选多少50%是响度最大但最“噪音”的档位方波驱动蜂鸣器占空比直接决定响度和音色。占空比50%时输出能量最大蜂鸣器膜片振动幅度最大声音最响但谐波成分也最丰富。而《菊次郎的夏天》旋律里高音区音符密集蜂鸣器本身的机械响应有延迟谐波一多听感就是沙沙的“噪音”。这个项目既然叫“噪音版”说明原版资源里的占空比参数是按“能听清旋律”调过的而不是盲目追求响度。我一般会在播放代码里给不同音区配不同占空比// pwm_play.c // 根据音符索引查表设置占空比, 高音区降低脉宽削弱谐波 void set_duty_by_note(uint8_t freq_idx) { uint16_t arr __HAL_TIM_GET_AUTORELOAD(htim2); uint16_t ccr; if (freq_idx 8) { ccr (uint16_t)(arr * 0.25f); // 高音区(C5及以上)占空比25% } else if (freq_idx 1) { ccr (uint16_t)(arr * 0.35f); // 中音区占空比35% } else { ccr 0; // 休止符, 关闭输出 } __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, ccr); }这段代码的关键是先用已有的AI开发环境配置好定时器PWM通道。占空比不是固定50%而是按音区动态调整——中音区35%高音区25%休止符直接占空比置0。高音音符能量本身大再给它大占空比整段旋律的高频区就会盖过中低频听起来“滋哇乱叫”。把高音区压到25%听感上毛刺会明显减少代价是声音变轻一点。休止符占空比必须为0如果只靠切换音符时延时不关PWM休止段会继续响节奏就“拖尾”了。如果你是自己搭工程这一步最容易遗漏。很多人把旋律数组配置好、PWM也启动了一听声音发现“能响但又糙又乱”第一反应是选曲问题其实就是占空比没有按音区分开设置。4. 节拍控制与主循环调度非阻塞状态机替代延时死循环4.1 音符结构体频率索引、时长和音符间隔频率问题解决后下一个坑在节拍。HAL_Delay阻塞延时是最直觉的写法——“开PWM延时500ms关PWM再延时”。但这会让单片机在整首歌播放期间完全无法响应其他输入而且多个延时函数嵌套时如果注释和实际时长不一致调起参来非常痛苦。标准做法是定义音符结构体把每首歌的旋律组织成一张表// melody.h typedef struct { uint8_t freq_idx; // 频率表索引0休止 uint16_t duration_ms; // 音符持续时长(ms) uint8_t gap_ms; // 音符结束后到下一个音开始的静音间隔(ms) } Note_t; // 示例C4唱500ms, 结束后停3ms; E4唱250ms, 结束后停2ms static const Note_t melody_test[] { {1, 500, 3}, {3, 250, 2}, {5, 250, 2}, };结构体里freq_idx对应上一章的频率表播放时通过它查频率、算ARR。duration_ms是PWM持续输出时间gap_ms是我后来加上的字段——音符切换时不马上开始下一个音而是静音几毫秒。这个静音很关键蜂鸣器膜片有机械惯性音符结束后不会立刻停振如果你紧接下一个音符前一个音的余振会和后一个音叠加造成糊音。快速段落gap设3ms慢速段落设2ms基本够用。4.2 从BPM算出每个音符的毫秒数乐谱上标的是BPM每分钟节拍数你要换算成毫秒。公式四分音符 60000 / BPM 毫秒八分音符 30000 / BPM 毫秒十六分音符 15000 / BPM 毫秒附点八分音符 45000 / BPM 毫秒《菊次郎的夏天》原曲演奏速度约在120到130之间十六分音符密集。但蜂鸣器不是钢琴它的发声响应本来就有迟滞速度一快音符一个接一个挤在一起人耳听到的就不是旋律而是一串“哔哔哔”的噪声节点。实践下来BPM取110比较舒服既保住了原曲轻快的感觉蜂鸣器又能把每个音分得开。如果你手头工程里旋律的时值已经是毫秒数那么BPM值就不用改了。但如果是从简谱手敲的时值换算时注意附点音符的算法——附点是延长原时值的一半附点八分音符实际时长是八分音符的1.5倍。4.3 非阻塞播放状态机主循环里巡检时间状态机的三个状态很简单IDLE空闲、PLAYING正在发音、GAP音符间静音。核心逻辑是用SysTick或者任意一个1ms基准时钟记录状态切换的时间点然后在主循环里判断“到点了没”没到就继续巡检其他任务。// app_music.c // 非阻塞播放状态机, 每次只处理一个状态, 不占用CPU空转 static volatile uint32_t s_tick_ms; static uint8_t s_state 0; // 0IDLE, 1PLAYING, 2GAP static uint16_t s_note_idx 0; static uint32_t s_state_start_ms; void music_tick_1ms(void) { s_tick_ms; // 由SysTick中断每1ms调用一次 } void music_play_start(void) { s_note_idx 0; s_state 1; s_state_start_ms s_tick_ms; start_note(melody[s_note_idx]); } void music_task(void) { uint32_t now s_tick_ms; switch (s_state) { case 1: // PLAYING if (now - s_state_start_ms melody[s_note_idx].duration_ms) { stop_note(); s_state 2; s_state_start_ms now; } break; case 2: // GAP if (now - s_state_start_ms melody[s_note_idx].gap_ms) { s_note_idx; if (melody[s_note_idx].freq_idx 0xFF) { // 0xFF作为结束标志 s_state 0; // 播放完毕回到IDLE } else { start_note(melody[s_note_idx]); s_state 1; s_state_start_ms now; } } break; default: break; } }music_task是主循环里调用的非阻塞函数。每次进来先读当前时间再判断当前状态是否超时。now - s_state_start_ms这种写法比直接比较绝对时间更安全因为tick计数器回绕时也不容易出错。start_note内部完成“查频率表→算ARR→设置占空比→启动PWM”整套动作stop_note把占空比置0但保留定时器配置方便下一个音快速启动。这里有个细节GAP状态结束后duration_ms和gap_ms用的是melody[s_note_idx]因为那时s_note_idx已经指向当前这个音符等GAP结束、s_note_idx之后才轮到下一个音。结束标志用了freq_idx 0xFF不会和合法的音符索引0休止符冲突。主循环里只要一行music_task();按键扫描、屏幕刷新都能并行往下走这就是非阻塞的意义。5. 避坑现场蜂鸣器不响、旋律走调、程序卡死的五条排查记录5.1 蜂鸣器毫无反应现象代码烧进去了定时器PWM也在运行逻辑分析仪量PA0能看到方波万用表量蜂鸣器两端也有电压但蜂鸣器就是不出声。原因最常见的是用了有源蜂鸣器。有源蜂鸣器内部自带振荡器给它的电平均匀方波反而会让内部电路进入不确定状态要么不响要么乱响。另一个可能是蜂鸣器额定电压比较高比如12V5V甚至3.3V根本推不动。解决先用3V电池直接碰蜂鸣器两极测试确认是无源蜂鸣器。如果是无源再检查驱动三极管是否工作在饱和区——基极电流不够时三极管半开蜂鸣器得到的电压不够。确保基极限流电阻在470Ω到1kΩ之间且GPIO推挽模式输出。5.2 有声音但是跑调整体音高低了一个八度现象旋律轮廓能听出来但所有音符都偏低整首歌像被“慢放”了。原因定时器时钟树配置出错。F103的APB1预分频设置为2时挂在APB1上的TIM2/TIM3时钟频率自动翻倍到72MHz。如果你在CubeMX里看到APB1显示36MHz就直接用36MHz做计算基准ARR偏大频率偏低。解决确认htim2.Init.Prescaler和你自己代码里期望的时钟一致。最稳妥的办法是写一个自检函数把TIM2的ARR设成一个已知值用另一个定时器测量PWM输出频率打印到串口。我的排查习惯是首测262Hz的C4如果串口打印出来是131Hz左右那就是时钟树问题。5.3 旋律节奏越来越拖沓程序像卡死了现象刚开始几个小节正常后面越来越慢偶尔按键反应失灵甚至直接跑飞。原因播放逻辑里用了阻塞延时整首歌播放期间MCU确实是在空转如果同时开了优先级合适的中断比如按键外部中断延时时间会被拉长节奏越积越乱。另外蜂鸣器由GPIO直接供电时声音大但瞬间电流大可能触发板上欠压复位表现就是“死机”。解决替换成第4章的非阻塞状态机。如果临时要赶时间可以把蜂鸣器供电改成单独的三极管开关方案让蜂鸣器电流不走主电源轨太深。重点检查播放循环里有没有HAL_Delay、delay_ms这类阻塞调用它们都是拖慢节奏的元凶。5.4 高音区一片嘈杂根本听不出波形现象中低音区还凑合一到旋律的高潮部分声音又尖又刺耳密集音段直接糊成一团。原因蜂鸣器的方波谐波成分在高频段会被壳体共振放大。具体来说50%占空比的方波含有大量3次、5次谐波而高音音符本身频率已经在2kHz到4kHz区间谐波叠加后能量集中在更尖的频段。再加上蜂鸣器膜片响应速度快高频段粒子噪声尤其明显。解决按音区动态调整占空比第3.3节的做法高音区占空比压到25%。如果还不够硬件上可以在蜂鸣器两端并联一个104100nF电容把高频毛刺稍微滤掉一点。再不满意就换压电蜂鸣器音色比电磁蜂鸣器干净不少。5.5 音符间隔gap设0整首歌像“机关枪”现象节奏很快每个音符之间没有停顿听起来像连续急促的“哔哔哔”完全没有旋律呼吸感。原因音符切换时PWM立刻关掉又立刻打开下一个音蜂鸣器膜片还没从上一个音的振动中恢复就被强行推到下一个频率机械余振和新的激励叠加产生大量瞬态杂音。解决每个音符结束后至少保留2到3ms的静音gap。在你自己的工程里如果你看到的Note_t结构体里没有gap字段说明那是简化版可以在start_note前加一个最小边沿延时。注意gap不是越长越好超过5ms会让旋律断断续续听感上像在“打嗝”。6. 从噪音版到可听版两个改动立竿见影的进阶方向如果你已经把这个工程跑通了接下来最值得花时间的是改善音色。蜂鸣器本质上是个“一分钱一分货”的发声器件方波驱动下注定音质粗糙但有两个改动能在不改硬件的前提下明显提升听感。第一个改动是加入音符力度控制。旋律中的强拍和弱拍不应该用同一个音量播放这不仅是听感问题也关系到旋律的辨识度。实现方式很简单——在Note_t结构体里再加一个uint8_t velocity字段播放时根据velocity调整占空比。比如强拍音符占空比35%弱拍音符25%。《菊次郎的夏天》的旋律起伏其实很大强弱的对比出来的话“噪音感”会大幅下降更像有人在弹而不是机器在叫。第二个改动是把单音输出升级成“双声部”。F103有多个定时器你可以用TIM2输出主旋律TIM3输出低音伴奏接两个蜂鸣器并排放置。主旋律用之前的单音逻辑伴奏声部只需要额外准备一个低音频率表和对应的melody_bass[]数组。两个PWM通道的占空比都要调低主旋律35%、伴奏20%否则两个蜂鸣器同时响会互相遮盖。这个做法的难度不在播放逻辑而在编曲——你需要把原曲的和声拆成低音线条这个资料包里的说明文档有给一份简单的伴奏参考和弦走向但真正的编配还是要靠自己耳朵。我个人的血泪教训是改动音色时只动一个变量然后完整听一遍整首曲子再动下一个。比如先把占空比全部降到20%听感居然变轻快了许多再单独把高音区调回25%整体平衡感反而比一开始的50%默认值舒服太多。从那以后我每次调这个工程都会先拿最低音、最高音和中间音三个基准音符实测频率确认PWM输出和表里的频率误差在1%以内再开心地调曲子细节。频率表错了的话后面做再多的音色优化都白搭。希望这个资源能帮你在“让蜂鸣器唱歌”这件事上少走点弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询