十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于STM32的多功能电子琴:PWM发声、录音回放与调试实践

基于STM32的多功能电子琴:PWM发声、录音回放与调试实践 简介本资源是一套基于STM32单片机开发的多功能电子琴完整工程面向嵌入式初学者与课程设计实践者解决音源生成、人机交互及音乐控制逻辑等典型嵌入式应用问题。项目通过STM32定时器PWM驱动无源蜂鸣器发声配合独立按键实现弹奏、播放、暂停、切歌等核心功能涵盖硬件原理图与Keil MDK工程源码适合作为单片机外设综合实训案例。压缩包共169个文件含47个.h头文件定义外设配置与功能接口、20个.c源文件含HAL库驱动与主控逻辑、1个.schdoc原理图、1个.prjpcb工程及.hex可烧录文件另有调试配置、编译日志与说明文档总大小10.6MB。已有1368人学习下载资源结构规范包含完整工程框架、TIM/PWM底层配置、按键消抖与状态机管理代码并附带实操演示mp4视频便于理解音乐节奏控制与时序协调机制。 其实最开始做这个项目的时候我并没有想过要把电子琴做成什么“高大上”的东西纯粹是因为手头有一块吃灰很久的STM32F103C8T6最小系统板加上想系统地把定时器、GPIO、中断这些外设串在一起练一练。结果做着做着发现这个“基于STM32设计的多功能电子琴”能玩出的花样比想象中多得多——除了按键发声还能加录音回放、自动演奏、OLED显示、八度切换。而且整个项目用到的都是STM32最基础的资源对刚学完单片机、想找一个综合训练项目的朋友来说是一个非常合适的练手选题。这篇文章我就把这个项目的完整思路、原理图设计、源代码关键逻辑以及我在调试中踩过的坑全部整理出来给后面做类似项目的人一个可以直接“抄作业”的参考。1. 项目整体设计与功能拆解1.1 为什么选STM32F103C8T6而不是51单片机我做之前其实纠结过一阵子要不要用STC89C52这种51内核的芯片来做毕竟网上电子琴的51版本一抓一大把。但认真评估之后还是选了STM32F103C8T6核心原因有三个定时器资源、PWM精度、后期扩展空间。先说定时器。电子琴发声的核心是生成不同频率的方波或PWM信号而STM32F103C8T6内部有TIM1高级定时器、TIM2、TIM3、TIM4等多个定时器可以独立输出多路PWM还能通过修改自动重装载值ARR快速调整输出频率。51单片机虽然也能用定时器中断翻转IO口来发声但CPU会被占得比较死一旦你要同时做按键扫描和录音存储非常容易卡顿。再说精度。STM32的定时器是16位的配合72MHz系统时钟分频后可以得到从几赫兹到几十千赫兹的精准频率。像标准A4音是440Hz按十二平均律算C4大约是261.63Hz对这些频率做到个位数误差完全没问题。51的定时器是8位要么溢出率不够要么频率精度做不上去弹高音的时候音准漂移会很明显。最后就是扩展性了。这个项目做完之后如果你想加上OLED显示当前音符、用ADC做音量调节、通过串口和上位机交互STM32F103C8T6的64KB Flash、20KB RAM、丰富的外设接口都能轻松扛住。而51的RAM通常只有128字节到512字节存一小段录音就爆了。所以如果你希望这个项目“做完还能接着升级”STM32是不二之选。1.2 “多功能”具体包含哪些功能模块这个项目的“多功能”不是我拍脑袋吹出来的而是把电子琴常见的功能需求梳理了一遍之后选出的四个最有代表性、又都能在单片机资源范围内实现的方向功能实现方式对应硬件模块实时弹奏检测矩阵按键输出对应频率PWM4×4按键矩阵、定时器PWM八度切换按键对音符频率进行倍频/半频独立功能按键录音回放将按键和时长存入数组回放时重新触发内部SRAM、定时器中断自动演奏预置曲谱数据按节拍依次播放内部Flash存储曲谱信息显示显示当前音符、模式、播放状态OLED屏或数码管选这几项是有讲究的。实时弹奏是电子琴的基本盘八度切换能让你用有限数量的物理按键覆盖更广的音域录音回放考验存储和状态机设计自动演奏则把音符频率表和节拍定时这两个核心逻辑都串起来了。一句话说做完这个项目你对“单片机怎么把一个物理操作变成声音输出”的整个链路会有非常清晰的认识。2. 硬件设计原理图与模块化思路2.1 电源与主控最小系统设计STM32F103C8T6的工作电压是2.0V到3.6V一般用3.3V供电。我的方案是USB的5V输入经过AMS1117-3.3稳压芯片降到3.3V同时在输入和输出端各加一个10uF钽电容和0.1uF陶瓷电容做滤波。很多新手容易忽略这些电容觉得“不就是供电吗”但实际上STM32内部逻辑翻转频率很高瞬间电流需求大如果电源纹波大轻则ADC采样不准重则芯片直接复位。主控最小系统里还有几个要注意的点。首先是8MHz晶振必须并联两个20pF左右的负载电容电容值按晶振datasheet要求来接长了容易不起振。其次是复位电路用10k电阻上拉到3.3V、0.1uF电容下拉到地NRST引脚上电瞬间拉低复位这个是最经典的RC复位电路。然后是BOOT0引脚正常工作必须通过10k电阻下拉到地否则芯片上电会进入Bootloader模式程序跑了没反应。最后是每个VDD引脚旁边都要放一个0.1uF去耦电容并且要尽量靠近芯片引脚放置这是PCB布线的常识很多手焊的板子在这上面吃过亏。如果你是用现成的STM32F103C8T6最小系统板这部分可以省心但如果是自己画PCB一定把最小系统画对因为这是整个项目的地基地基不稳后面全是问题。2.2 按键矩阵电路设计考虑到按键数量我用的是4×4矩阵扫描方式一共16个琴键加上4个独立功能键。矩阵的接法是4根行线接STM32的PB0~PB34根列线接PA0~PA3。行线设置为推挽输出列线设置为上拉输入。扫描的时候按行依次拉低再读取所有列的电平如果某一行被拉低且某一列读到低电平就说明该行和该列交叉位置的按键被按下了。连接方式很简单但原理图设计里有两个细节值得注意。第一个细节是上拉电阻。列线用STM32内部上拉就能工作但如果你发现按键按下时电平跳变不明显或者误触很严重可以在每根列线外部再加一个10k上拉到3.3V增强抗干扰能力。我用的是内部上拉实测足够稳定但外部上拉的余量更大。第二个细节是串联电阻。每个按键两端不直接接GPIO而是串一个100Ω到330Ω的电阻再接入MCU。这个电阻的作用是限制静电放电对GPIO的冲击也能在按键接线的瞬间抑制毛刺电流。虽然不串也能跑但做产品化和可靠性测试的时候这个电阻是省不得的。功能键我用了4个独立按键分别接PB4、PB5、PB8、PB9同样每个按键串一个100Ω电阻另一端接GNDGPIO配置为上拉输入。按下为低电平松开为高电平逻辑非常直观。2.3 音效输出PWMDAC滤波功放方案电子琴的声音输出是整个项目最核心的模拟链路。我不建议直接用有源蜂鸣器因为那玩意儿内部带了振荡源只能发固定频率的声音根本无法随按键改变音调。正确做法是用STM32的定时器输出可调频率的PWM方波经过RC低通滤波把方波变成近似正弦的柔和声音再通过功放芯片驱动扬声器。STM32的PWM输出引脚我选的是PA8这是TIM1_CH1的输出通道。TIM1是高级定时器输出PWM时需要额外配置刹车和死区相关的寄存器用STM32CubeMX初始化比标准库手动写要省事很多。PWM信号经过一个一阶RC低通滤波器R用100Ω、C用100nF可以算出截止频率大约是16kHz人耳听觉范围是20Hz到20kHz音乐信号主要集中在几百赫兹到几千赫兹16kHz的截止能保留有效频段同时滤掉PWM的高频载波。这个值我调过太大则声音发毛太小则高音衰减明显100Ω加100nF是折中后比较满意的一组。滤波后接功放我用的是LM386这是最经典的低功率音频放大器供电5V外围只需要几个电容电阻就能工作。1脚和8脚之间接一个10uF电容可以把增益从默认的20倍提高到200倍但实测200倍增益在5V供电下容易削波失真所以我保留默认20倍音量靠电位器分压调节。功放输出接一个8Ω/1W的小扬声器听感上低频稍微薄一点但做电子琴演示完全够用。3. 核心代码实现从音符频率到发声3.1 音符频率表与十二平均律电子琴发声的本质是让扬声器振动特定频率的空气所以第一步就是建立“按键到频率”的映射关系。国际标准规定A4钢琴中央A的频率是440Hz其余音符按照十二平均律计算相邻半音之间的频率比是2的1/12次方也就是大约1.059463。比如C4中央C距离A4有9个半音频率就是440 / 2^(9/12) 261.63Hz。虽然可以在代码里实时计算但我强烈建议直接查表。MCU做浮点运算的开销不低而且你需要在中断或主循环里频繁调用查表是零延迟的。我做过一个MIDI音符表把C4到B5共24个半音的频率值预先算好用const数组存在Flash里。下面是表格的一部分// 音符频率表索引0对应C4索引11对应B4依此类推 const uint16_t note_freq[] { 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, // C4 ~ B4 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, // C5 ~ B5 };这个表里数值我用的是四舍五入后的整数对听感来说误差远小于1%完全够用。如果你的琴键跨度更大可以按同样的公式扩展到C3到C6甚至更大范围。八度切换的本质就是让当前按键对应的索引加12或减12频率刚好翻倍或减半听感上是同一个音升高或降低了一个八度。3.2 定时器PWM配置与发声函数我用的是TIM1_CH1来输出PWM。TIM1挂在APB2总线上时钟是72MHz。要让PWM输出指定频率需要先设置预分频器PSC和自动重装载值ARR计算公式是PWM频率 定时器时钟 / ((PSC 1) × (ARR 1))我的做法是把PSC固定为71这样定时器计数时钟就是72MHz / 72 1MHz也就是计数器每1微秒加1。那么要得到262Hz的C4ARR 1,000,000 / 262 - 1 ≈ 3815。用代码封装一个函数每次传入频率就重新计算ARR并更新void set_tone(uint16_t freq) { if (freq 0) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); // 静音 return; } uint32_t arr 1000000 / freq - 1; __HAL_TIM_SET_AUTORELOAD(htim1, arr); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, arr / 2); // 50%占空比 }这里占空比设成50%方波音色最饱满谐波分量也比较丰富听着像传统电子琴的音色。如果占空比调小声音会偏“薄”有类似拨弦的效果感兴趣的可以自己试这也是后面做音色切换的入口。需要注意一点ARR是16位寄存器最大值是65535。我们这个公式下最低能输出的频率大约是1,000,000 / 65535 ≈ 15Hz远低于电子琴最低音所以不用担心超出范围。但如果你把PSC调小了或者希望输出更低频就一定要检查ARR是否溢出。3.3 矩阵按键扫描与防抖状态机按键扫描我放在了主循环里轮询没有用外部中断原因是琴键按下时需要快速响应但中断频繁触发会干扰PWM定时器反而影响发声稳定性。主循环每10ms跑一圈扫描完全能满足演奏需求。矩阵扫描的核心逻辑是依次将PB0到PB3中的一行拉低其他行保持高电平然后读取PA0到PA3的电平任一为低说明该列有按键按下。把行索引和列索引组合起来就能唯一确定一个琴键。按键抖动是个绕不开的问题。机械按键在按下和松开的瞬间电平会产生几毫秒到十几毫秒的毛刺如果不做处理按一次可能被识别成好几次弹出来的音就会“抖”。我的做法是20ms软件延时消抖具体实现是检测到电平变化后延时20ms再读一次如果状态一致就认为有效。为了给录音功能打基础我把按键检测改成了带边缘触发的状态机。核心变量是当前按键值和上一次按键值只有当前按下且上一次没按下才认为产生了一次新的“按下事件”。代码如下uint8_t key_scan(void) { uint8_t current read_matrix(); // 读取当前按键无按键返回0xFF uint8_t event 0xFF; if (current last_key) { return 0xFF; // 状态未变化 } HAL_Delay(20); current read_matrix(); // 消抖后重新读取 if (current ! last_key) { if (current ! 0xFF) { event current; // 产生按下事件 } last_key current; } return event; }这种设计的好处是录音时可以只记录这个“按下事件”和它的时间戳回放时按同样的事件和时间间隔去触发发声逻辑会非常清晰。3.4 录音回放与自动演奏的实现录音功能看起来高大上本质上就是一个带时间戳的事件记录器。我用一个结构体数组来存储音符和时长typedef struct { uint8_t note; // 音符索引0代表无音休止符 uint16_t duration; // 持续时间单位ms } MusicNote;开始录音后每次检测到按键事件就把当前音符和该音符持续的时间写入数组按键松开或进入下一个按键时把上一段时长封存。这样弹完一段旋律数组里就存了一串音符和时值。回放时创建一个播放状态机定时器中断每1ms递减一个计数器计数器归零就播放数组中的下一个音符直到数组结束。这个方案实现简单而且录完马上就能听效果成就感很强。自动演奏则完全复用回放逻辑区别只是曲谱数据不用现场录而是预先把几首内置乐曲的“音符时值”数组烧进Flash。曲谱的编写格式用我上面定义的结构体比如《小星星》的一段就是const MusicNote twinkle[] { {0, 250}, {0, 250}, {4, 250}, {4, 250}, {7, 250}, {7, 250}, {9, 500}, // 简谱1 1 5 5 6 6 5 -需要把简谱映射为音符索引 };写曲谱最花时间的就是把简谱一个个映射成音符索引我的建议是先写好映射表再批量填减少出错概率。另外曲谱数组的结束一定要放一个{0xFF, 0}作为结束标记不然回放不知道什么时候停下会把Flash后面垃圾数据也当音符播出来声音会非常诡异。4. 显示模块与交互逻辑设计4.1 OLED显示当前音符与模式想要知道当前按的是哪个音、处于什么模式光靠耳朵听其实不够直观所以我加了一个0.96寸的OLED屏接口是I2CIC是SSD1306这是做STM32小项目最常见也最便宜的显示方案几块钱一片驱动代码网上遍地都是移植成本很低。OLED的I2C我用的是PB6I2C1_SCL和PB7I2C1_SDA。选择这两个引脚是因为I2C1刚好默认映射在这俩脚上不需要配置重映射减少出错概率。注意PB6和PB7是开漏输出必须外部接上拉电阻一般OLED模块板上已经自带了4.7k上拉所以不用额外加。显示内容我规划了三块区域第一行显示当前模式PLAY、REC、AUTO PLAY第二行显示当前音符名比如C4、E5第三行显示音量或者八度数字。刷新频率不需要太高200ms刷一次就够因为人的肉眼对慢速变化的文本已经非常敏感了刷太快反而占用主循环时间影响按键扫描的响应速度。SSD1306驱动里有一个需要注意的地方它内部是1KB的GDDRAM你要把要显示的内容先写入一个缓存数组再通过I2C一次性刷到屏幕。如果逐像素直接写I2C通信会被撕碎成几十次小传输屏幕会闪烁所以务必要用“缓存整屏刷新”的模式。4.2 操作流程与功能按键设计4个功能键的用途我做了明确分工K1切换模式K2开始/停止录音K3播放录音K4八度切换。这里有一个交互上的小设计模式并不是循环切换而是按一下K1进入录音模式再按一下回到弹奏模式中间通过OLED上的状态提示来引导用户。这样比单个按键循环切模式直观得多不会让用户搞不清自己到底在哪个状态。整个操作流程是这样的上电默认弹奏模式直接按16个琴键就能发声。按下K1进入录音待机状态OLED显示REC这时弹奏的每个音符都会被记录。再按K1退出录音按K3开始回放刚才的旋律。在弹奏模式按K4八度在1、0、-1之间循环OLED同步显示当前八度。这个流程我让几个朋友试过基本不需要说明书就能上手对嵌入式项目来说交互做到这个程度就算合格了。5. 调试过程与常见问题排查实录5.1 上电没声音或声音断续这个是我遇到最多的现象排查的思路一定要按信号链从后往前捋。先看功放输入端有没有信号用示波器或者万用表交流档测LM386的3脚如果这里有PWM波形但扬声器不响问题在功放电路如果没有波形问题在主控或滤波前端。我早期犯过一个低级错误——PA8的GPIO复用没配对。STM32的引脚不是你想输出PWM就能输出必须把GPIO模式设成AF_PP复用推挽并选择对应的复用功能。用STM32CubeMX的时候你在Pinout界面把PA8选为TIM1_CH1它会自动配好但如果你手动写寄存器初始化很容易只配置了GPIO而忘了选复用功能结果PA8一直输出高电平扬声器只有上电的“啪”一声。还有一个坑是PWM比较值CCR设成了0。我最初测试发声函数时忘记在静音分支把CCR清零而是在静音时直接关闭定时器结果重新开启定时器后因为ARR已经改过了CCR还是老的输出频率和占空比就乱了。用上面代码里的写法静音就设CCR0发声就设CCRARR/2不要在定时器使能上反复横跳省心很多。5.2 声音发闷、高音缺失如果你感觉声音闷闷的弹高音时音量明显比低音小十有八九是RC低通滤波的截止频率设太低了。方波本身包含大量高频谐波这是它听感“亮”的原因如果把截止频率压到1kHz以下高音的基波都会被削掉自然就闷了。我的调试经历是这样的最开始用R10k、C10uF算出来的截止频率才1.6Hz基本把音频信号全滤光了出来的声音只有微弱的“噗噗”声。后来才意识到这里不是要滤掉方波的高次谐波而是要滤掉PWM载波频率。TIM1的PWM载波频率在ARR几百的时候大约是1MHz/PSC远超20kHz所以RC的截止频率设在15kHz到20kHz之间是合理的最终R100Ω、C100nF就是按这个思路定下来的。调试高频缺失的时候建议直接用示波器看LM386输入端的波形。当频率到2kHz以上时如果你看到正弦波顶部变得圆滑、幅度明显下降那就是滤波把这个频段吃掉了。把截止频率提高或者干脆把RC滤波改成二阶高频衰减会改善很多。5.3 按键误触和“跳音”按键误触有两种情况一种是没有按却自己发声另一种是按一个音结果跳到了别的音。前者大概率是上拉电阻没配置好GPIO悬空导致电平漂移后者则是矩阵扫描逻辑有bug或者行线和列线序搞错了。我的排查方法是写一个自检程序把16个矩阵按键全部轮询一遍串口打印出“行-列”坐标逐个按键测试看坐标是否和预期一致。这套方法帮我抓到一个非常隐蔽的问题——我把行线和列线接反了导致扫描时读到的矩阵是镜像的按左上角的键实际触发的是右下角的音。PCB布线的时候不仔细看丝印很容易犯这个错所以原理图阶段一定把网络标号写清楚焊接完先跑自检再装外壳。按键消抖时间也有讲究。消抖太短比如5ms按键容易重复触发消抖太长比如50ms演奏快速音阶时会感觉卡顿。我最终用的是20ms在稳定性和手感之间比较平衡。注意消抖延时不要用HAL_Delay阻塞实现否则主循环会被卡住建议改成“检测到变化→记录时间戳→时间到了再确认”的非阻塞方式。我的代码为了演示简单用了HAL_Delay但如果你想让演奏手感更好建议改成状态机加定时器计数。6. 源码工程结构与复现指引最后说一下工程的结构方便你拿到手能快速看懂。我用的是STM32CubeMX生成HAL库工程IDE是Keil MDK整个工程目录分为三层Core主函数、中断服务函数、DriversHAL库、App我自己的应用代码。App目录下有几个关键文件key_scan.c负责矩阵按键和功能键扫描music.c负责音符频率表、发声、录音回放oled.c负责屏幕显示main.c里是主循环和状态机流转。如果后面有人想基于这个项目扩展我的建议是先看music.c因为所有跟声音相关的核心逻辑都在这里理解了它整个项目的骨架也就清楚了。工程配置上有一个关键点TIM1的时钟源要选内部时钟通道1要选PWM Generation CH1否则CubeMX不会自动生成PWM配置代码。另外定时器的预分频PSC我是在MX_TIM1_Init里手动改成71的CubeMX默认是0如果你直接编译不修改初始PWM频率会高出几十倍声音非常刺耳。这个细节我在代码注释里写了加粗加亮但很多人还是会漏掉。说句实在话这个电子琴项目我前后折腾了将近一周真正写原理图和代码只用了两天剩下时间全花在调试声音和按键稳定性上。但恰恰是这些调试过程让我把STM32的定时器、GPIO复用、PWM输出这几个知识点嚼碎了。如果你也准备动手做我的建议是先别急着追求功能多把发声链路调通、把按键扫描调稳再去加录音和自动演奏。另外一个小技巧调试阶段用串口把按键状态和频率值打印出来比插着示波器效率高得多我后面几轮调试基本全靠串口定位问题。这个项目后续我还打算加个通过电位器调节音量的ADC通道再扩展成支持双音同时发声不过那是另一个故事了。本文还有配套的精品资源点击获取
返回列表