
用 UNO 板和蜂鸣器演奏《千本樱》——文艺复兴那天我在翻旧物箱翻出一块积灰的 Arduino UNO 板和几颗蜂鸣器突然想起早年看过一段用单片机播放《千本樱》的视频。当时只觉得神奇现在再看其实原理远比想象中简单蜂鸣器发声本质就是让引脚输出特定频率的方波。频率对了节奏对了剩下的就是把乐谱翻译成代码。用 UNO 和蜂鸣器演奏《千本樱》这个项目听起来像是“文艺复兴”但它不是靠什么高级音频库也不是靠外部功放模块。它真正考验的是你对定时器、频率换算、音符时长和代码结构的理解。尤其当你从“能响”走向“能听出是《千本樱》”时会遇到一个很多人没意识到的坎无源蜂鸣器和有源蜂鸣器完全是两回事。这篇文章我会从硬件选型、发声原理、乐谱转换、代码实现到常见坑点把整个流程拆开讲清楚。目标是让你不只跑通一次还能理解背后的逻辑以后换任何曲子都能自己搞定。1. 先搞清楚蜂鸣器类别有源和无源是两条完全不同的路很多人第一次做蜂鸣器项目时会卡在一个非常基础的问题上为什么我下载的例程不响或者响是响了但只有一个单调的“嘀——”声根本演奏不了旋律。十有八九问题出在蜂鸣器类型选错了。蜂鸣器按内部结构划分可以分成有源蜂鸣器和无源蜂鸣器两大类。这里说的“源”指的是振荡源而不是电源。1.1 有源蜂鸣器适合报警不适合演奏有源蜂鸣器内部自带振荡电路只要给它通上直流电它就会自己发出固定频率的声音。声音通常比较尖锐频率固定无法改变。它主要用在报警器、电子门铃、提示音场景。你如果用有源蜂鸣器去接 Arduino调用tone()函数或者写digitalWrite控制通常会有两种结果一直响音调无法变化只发出刺耳的固定频率声音完全听不出旋律。这是因为有源蜂鸣器的频率是由内部振荡器决定的外部输入高低电平只能控制它响或者不响不能控制它发出不同音调。1.2 无源蜂鸣器没有振荡源才是演奏旋律的关键无源蜂鸣器内部没有振荡源需要外部输入一定频率的方波信号才能发声。这听起来像是劣势其实是最大的优势因为频率可以由你控制你就能控制音调高低。频率越高音调越高频率越低音调越低。要演奏《千本樱》你只需要按照乐谱把每个音符转换成对应的频率再控制每个频率持续的时间就能还原旋律。无源蜂鸣器通常有 3 个引脚也有 2 个引脚的版本。3 脚的一般是 VCC、GND 和信号脚I/O2 脚则直接一个接信号引脚一个接 GND。选购时你可以看封装和标识实在不确定就用万用表测一下有源蜂鸣器在直流电阻测试时通常能测到较低阻值且不会随按压变化太多无源蜂鸣器会表现出一定的线圈特性。还有一个更直观的判断方式给引脚通上 5V 直流电如果响了是有源如果不响但你把手指轻轻按住发声孔它能感觉到震动或者有轻微气流那多半是无源。无源蜂鸣器在某些状态下对直流电不发声因为它需要交变信号才震动。1.3 驱动方式直接用引脚还是用三极管/驱动芯片UNO 的引脚输出电流有限单个引脚最大输出电流一般在 20mA 左右。小功率无源蜂鸣器可以直接接在数字引脚上这也是很多入门教程的做法能响音量偏小。如果想要音量更大、声音更稳定可以用一个三极管做开关放大或者用 ULN2003 这类达林顿驱动芯片。ULN2003 常见于步进电机驱动板但里面的达林顿管也可以用来驱动蜂鸣器、继电器等负载。输入信号接 ULN2003 的输入端输出端接蜂鸣器负极蜂鸣器正极接 VCC这种接法比较稳妥。注意驱动蜂鸣器时注意共地。Arduino 的 GND、蜂鸣器的 GND、以及外部电源的 GND 必须连在一起否则信号参考地不一致可能造成不发声或乱响。2. 从乐谱到频率这不是音乐课是数学换算如果你已经准备好了无源蜂鸣器下一步就是让按键响起来然后按频率变化演奏旋律。你需要一张音符频率对照表。2.1 十二平均律与音符频率音乐里常用的标准音 A4 频率是 440Hz。相邻半音之间的频率比是 2 的 1/12 次方约等于 1.059463。也就是说从 A4 往上推一个半音A#4频率就是 440 × 1.059463 ≈ 466.16Hz。我们可以通过这个公式计算所有音符的频率f 440 × 2^((n - 69) / 12)其中 69 是 MIDI 音符编号体系中 A4 对应的编号。这个公式可以写成一个函数在代码里直接调用比手动查表更灵活——尤其是面对《千本樱》这样速度很快、音域跨度很大的曲子。你可能不是音乐专业出身没关系只要你有一份简谱或者 MIDI 音符编号就能套公式算出来。2.2 《千本樱》的音域和速度《千本樱》原曲速度非常快BPM 常在 180 左右甚至更高。演奏时每个音符的持续时间很短如果使用 Arduino 的tone()函数结合delay()控制时长看起来很简单但实际跑起来会发现两个问题如果音符密集且 delay 太长整体速度会明显拖慢。tone()函数在持续输出方波时会占用定时器而delay()又会阻塞程序导致无法同时处理其他逻辑比如按键输入、LED 闪烁。对于纯播放旋律的场景阻塞问题不大。但如果你想做成一个“按下按键开始演奏、再按停止”的小装置你就需要一种不阻塞的方式或者接受 delay 期间无法响应按键的现实。先看跑通版这是最核心的部分。2.3 音符频率对照表常用范围下面是从低音到高音区间的一些常用频率值单位 Hz。实际编码时你可以通过公式生成也可以用数组直接查表。音符频率C4261.63D4293.66E4329.63F4349.23G4392.00A4440.00B4493.88C5523.25D5587.33E5659.25F5698.46G5783.99A5880.00《千本樱》里常用音符基本集中在中音区到高音区如果主旋律需要低音或更高音可以按公式扩展。2.4 简谱转代码把音符变成数组拿到一个曲子的简谱后你可以把它转成两个数组音符数组记录每个音符的频率可以用NOTE_C4这样的宏定义或直接用浮点数值。节拍数组记录每个音符的时长用相对值表示比如 1 表示四分音符0.5 表示八分音符。播放时循环读取两个数组根据节拍值乘以基础时长就得到每个音符的持续毫秒数。一个简单的结构如下struct Note { float freq; int duration; // 相对时长单位可以用节拍单位如 1 表示四分之一音符 };把整首曲子的主旋律放在数组里loop 里依次播放。3. 最小可运行实现先让 UNO 出声在搞懂原理之前我建议不要直接抄一整首《千本樱》的代码。先写一个最小程序让无源蜂鸣器发出 C 大调音阶。确认硬件没问题再逐步扩展。3.1 硬件连接最简单的接法UNO 数字引脚 8 接无源蜂鸣器信号脚。蜂鸣器另一个脚接 GND。如果使用 3 脚蜂鸣器一般 VCC 接 5VGND 接 GNDI/O 接数字引脚。如果你用 ULN2003 驱动典型接法UNO 数字引脚 8 → ULN2003 输入端例如 IN1。ULN2003 输出端例如 OUT1→ 蜂鸣器负极。蜂鸣器正极 → 5V。ULN2003 GND → UNO GND。如果使用外部电源需要共地。3.2 最小代码示例int buzzerPin 8; void setup() { pinMode(buzzerPin, OUTPUT); } void loop() { // 播放 C 大调音阶 tone(buzzerPin, 261.63, 300); delay(400); tone(buzzerPin, 293.66, 300); delay(400); tone(buzzerPin, 329.63, 300); delay(400); tone(buzzerPin, 349.23, 300); delay(400); tone(buzzerPin, 392.00, 300); delay(400); tone(buzzerPin, 440.00, 300); delay(400); tone(buzzerPin, 493.88, 300); delay(400); tone(buzzerPin, 523.25, 500); delay(600); }这段代码的目的不是演奏完整曲子而是确认三个问题蜂鸣器类型是否正确无源才能变调。引脚连接是否正确。扬声器音量是否在可听范围内。如果这段代码能按顺序发出 do re mi fa sol la si do说明硬件通路没问题接下来才进入真正的“文艺复兴”阶段。3.3 为什么 tone 里除了频率还要传时长tone(pin, frequency, duration)的第三个参数 duration 表示发声持续时间单位毫秒。它不阻塞主程序。然后又delay(duration 间隔)作用是让程序停住保证每个音之间有明确间隙。注意tone()即使不传第三个参数也会持续发声音直到调用noTone()或再次调用tone()改变频率。如果两个音符之间没有noTone()蜂鸣器会一直响声音很糊。一个更常见的做法是tone(buzzerPin, freq); delay(duration); noTone(buzzerPin); delay(gap);这能避免音符之间粘连。4. 把《千本樱》主旋律写进代码现在进入正题。4.1 找一个你能看懂的主旋律乐谱《千本樱》的完整版本很长包含前奏、A 段、B 段、间奏、副歌和结尾。不建议一开始就写完整曲建议只截取主旋律的一段比如副歌段。副歌辨识度高用来练习最合适。如果你手上只有简谱可以手动标注音名和节奏。例如6 6 1 3 4 4 3 1 3 3 5 1 7先把音名和频率对应起来再决定每拍的时长。下面是一个简化示例不是完整曲谱只演示数据结构#include pitches.h int buzzerPin 8; int melody[] { NOTE_E5, NOTE_E5, NOTE_G5, NOTE_E5, NOTE_D5, NOTE_C5, NOTE_D5, NOTE_G4 }; int noteDurations[] { 4, 4, 4, 4, 4, 4, 4, 4 }; void setup() { for (int thisNote 0; thisNote 8; thisNote) { int noteDuration 1000 / noteDurations[thisNote]; tone(buzzerPin, melody[thisNote], noteDuration * 0.9); delay(noteDuration * 1.1); noTone(buzzerPin); } } void loop() { // 只播放一遍 }你需要pitches.h头文件里面定义了全音阶音符对应的频率常量。Arduino 官方示例的pitches.h可以直接参考。4.2 速度处理BPM 与毫秒换算《千本樱》的速度极快如果你用1000 / 4 250ms来表示四分音符那么每分钟只有 60/0.25 240 拍这可能超过原曲速度。实际要按 BPM 计算。换算公式四分音符时长(ms) 60000 / BPM 八分音符时长(ms) 30000 / BPM 十六分音符时长(ms) 15000 / BPM例如 BPM 180四分音符就是 60000 / 180 ≈ 333ms。但《千本樱》很多段落是十六分音符甚至在快速段落里听起来像三十二分音符所以实际每个音可能只有 80ms 左右。4.3 为什么直接塞满整个代码可能效果不好Arduino 的tone()是软件模拟方波通过定时器中断翻转引脚电平。在快速连续调用时频率切换本身会有极小延迟。对于 180 BPM 以上的快速旋律这个延迟会造成节奏不稳尤其当你中间还夹了noTone()和delay()整体听感会被拉慢。优化方向有几种去掉音符之间的noTone()让相邻音符连续切换减少中断。代价是音符可能会有一点粘连但高速旋律下听感影响不大。把 duration 调成理论时长的 0.9 倍留出一点间隔避免糊成一片。不要用太长的delay()用一个delay(noteDuration)配合tone(pin, freq, duration)的非阻塞特性播放。从工程经验看建议先用低速调试BPM 120跑通后再逐步提高。注意tone()同一时间只能在一个引脚上输出。如果你用两个引脚分别接两只蜂鸣器想同时播放两个声部tone()做不到。那是多声道音频合成的问题UNO 的定时器资源有限不要往那个方向硬塞。4.4 完整演奏与按键控制如果只想让它通电就演奏一遍写完 setup 里的循环loop 留空即可。如果想用按键控制开始/停止可以加一个按键并在循环里检测按键状态。但这时的难点在于delay()会阻塞按键检测播放期间按停止无效。解决办法是用非阻塞播放。核心思路记录每个音符的开始时间。当前时间减去开始时间 音符时长时切换到下一个音符。期间每轮 loop 都检测按键。这是一个“状态机”思维unsigned long previousMillis 0; int currentNote 0; int totalNotes 10; bool playing false; void playNextNote() { int noteDuration 60000 / BPM / noteDurations[currentNote]; tone(buzzerPin, melody[currentNote], noteDuration * 0.9); previousMillis millis(); currentNote; }在loop()中if (playing millis() - previousMillis noteDuration) { if (currentNote totalNotes) { playNextNote(); } else { noTone(buzzerPin); playing false; currentNote 0; } } if (buttonPressed) { playing !playing; if (playing) { currentNote 0; playNextNote(); } else { noTone(buzzerPin); } }这种结构才是“工程化”的雏形。虽然它仍然是在单线程上模拟多任务但它不再被delay()绑架可以同步响应按键。5. 常见问题排查为什么我跑起来声音怪怪的很多人在这个项目上卡住不是代码问题而是硬件、选型或参数设置问题。这里给你一个排查链路按顺序走。5.1 按“输入 → 环境 → 参数 → 工具边界”逐层排查不响先确认是蜂鸣器类型。有源蜂鸣器可能一直响或者不产生旋律。再确认引脚是否正确正负极是否接反。再确认代码里pinMode是否设置成 OUTPUT。只响一声就不响了检查是不是tone()只执行了一次循环结构有没有问题。声音特别小可以直接用引脚驱动试试也可以用三极管放大还可以换一个更大尺寸的无源蜂鸣器。用 ULN2003 驱动时检查输入输出接线是否对应蜂鸣器正极是否接对电源。音调不对检查音符频率表是否正确尤其是标准音 A4 是否为 440Hz。另一个常见问题是把有源和无源混在一起直接用tone()控制有源蜂鸣器频率变化会被内部振荡器盖住。节奏太慢检查 BPM 换算是否正确音符时长是否被放大了。例如把所有音符都按四分音符处理那《千本樱》听起来会像慢速练习曲。声音发糊、拖尾音符之间缺少间隔。可以在tone()的第三个参数里把 duration 调短一点让每个音留出尾音衰减时间。嗡嗡声或杂音检查电源是否稳定接线是否过长蜂鸣器是否靠近其他干扰源。也可以尝试把 Arduino 用 USB 供电不用外部电源。5.2 ULN2003 连接蜂鸣器时的特殊坑ULN2003 是一款达林顿晶体管阵列芯片输入端是高电平触发输出端是集电极开路结构。驱动蜂鸣器时通常把蜂鸣器接在 VCC 和 OUT 引脚之间。接线示例UNO D8 → ULN2003 IN1ULN2003 OUT1 → 无源蜂鸣器负极无源蜂鸣器正极 → 5VULN2003 GND → UNO GND如果不响先检查 VCC 是否给蜂鸣器正极供电再检查 ULN2003 的 GND 和 UNO 是否共地最后检查代码输出的是高电平还是低电平。ULN2003 是高电平有效输入高电平时输出导通蜂鸣器才会发声。5.3 乐谱来源与版权意识《千本樱》的原曲版权归作者所有。个人学习、实验、练手使用一般属于合理使用范畴。如果要把作品发布到视频平台或商业用途建议使用获得授权的乐谱或自行编曲。不要使用来路不明的盗版乐谱文件也不要直接搬运他人收费乐谱。自己动手听歌扒谱或使用公开授权的 MIDI 文件转简谱会更稳妥。6. 从播放旋律到做成一个小作品一个可复用的框架到这里你已经可以播放一段旋律了。但“能播放”和“能做成作品”之间还差几个工程化步骤。6.1 三步走先跑通、再优化、最后工程化先跑通。用最简电路、最简代码只播放一小段旋律确认硬件和声音正常。再优化。调整音符间隔调整播放速度把单段旋律扩展到完整主旋律整理代码结构和数据表。这里可以加入按键控制、LED 闪烁、音量调节等功能。最后工程化。把代码从setup()里的一坨循环改成用结构体数组、非阻塞播放、状态机控制。把引脚定义统一放一个头文件把音符频率表独立出来方便以后换歌。6.2 一个可复用的代码骨架你可以把下面的结构当作一个模板struct Song { const int* notes; const int* durations; int length; int bpm; };歌曲数据用PROGMEM存到 Flash避免占用太多 RAM。UNO 只有 2KB RAM如果乐谱很长放到 SRAM 里可能不够。#include avr/pgmspace.h const int melody[] PROGMEM { NOTE_E5, NOTE_E5, NOTE_G5, ... }; const int durations[] PROGMEM { 8, 8, 4, ... };读取时用pgm_read_word_near()获取音符值和时长值。6.3 换曲子时只需要换数据当你把播放逻辑写成通用状态机后换一首曲子只需要把音符数组和节拍数组替换成新曲子的数据BPM 改一下即可。播放逻辑不用动。这就是“把一次临时操作沉淀成一套可复用流程”的典型例子。刚开始你会觉得直接写死代码更快但当你换到第三首曲子时就会发现通用框架的价值。6.4 这类项目的边界UNO 播放蜂鸣器旋律适合学习、创客教育、DIY 小项目但它不是音频播放解决方案。它有几个明显的边界音色单一只能发出方波像早期的 8-bit 游戏音效。无法同时播放多个音轨tone()同一时间只支持一个引脚。音量有限适合小房间环境不适合户外或嘈杂环境。不能播放采样音频比如 MP3、WAV。如果你想播放完整音频需要 DFPlayer、VS1053 这类音频模块而不是蜂鸣器。分清边界才能知道哪种方案适合什么场景。7. 我的建议如果你也想复刻这个项目如果你也想用 UNO 和蜂鸣器演奏《千本樱》我建议按这个顺序来7.1 第一步确认蜂鸣器类型买几个无源蜂鸣器别买成了有源。用万用表或通直流电测试类型。别偷懒这一步决定了后面所有调试方向。7.2 第二步先播放 C 大调音阶跑通最小代码听一听音阶是否清晰。如果音阶都听不出差别那问题还没到《千本樱》这一步。7.3 第三步找一小段主旋律不用完整曲谱先选副歌前两小节。把音符和节拍按数组形式编码用低速播放确认旋律识别度。7.4 第四步逐步提升速度从 BPM 100 开始每次增加 20直到接近原曲速度。每次提速后都要听一下是否有音符粘连或漏音。7.5 第五步加交互控制用按键控制播放/停止用 LED 显示播放状态。这样一个小项目就有了“作品感”而不是只能通电从头响到尾的演示板。7.6 第六步整理代码与文档把音符表单独放一个头文件把播放逻辑封装成函数写下注释记录接线图。以后再看或者给朋友参考都能快速上手。8. 真正值得长期关注的点用 UNO 和蜂鸣器演奏《千本樱》看起来是一个偏向入门、偏向娱乐的项目但它背后其实包含了几项非常重要的底层能力理解频率与音调的关系。理解定时器与方波输出的局限。理解阻塞式编程与非阻塞式编程的区别。理解数据驱动的设计思路把逻辑和曲谱数据分离。这些能力不只适用于蜂鸣器项目。当你以后去控制舵机、读取传感器、驱动 LED 灯带甚至做一个完整的智能小车时都会用到相同的思想。所以当你成功让 UNO 响出一段《千本樱》时不要只觉得“好玩”。你可以再往前想一步这段代码能不能扩展成播放任意曲子能不能通过按键切换歌曲能不能播放的同时控制 LED能不能用 OLED 显示当前音符这些扩展才是一个项目从“能跑”走向“好用”的真正动力。先把最小系统跑通再一步步加需求。这不仅是蜂鸣器项目的做法也是嵌入式开发中最朴素、最有效的成长路径。