2026/10/5 15:50:44

STM32电子琴音乐播放器设计:从定时器PWM到状态机实战解析

STM32电子琴音乐播放器设计:从定时器PWM到状态机实战解析 第一次见这个题目的人大概率会觉得它是个普通的单片机课设。但真正上手做过就知道这个看起来“没什么技术含量”的项目其实是把STM32的定时器、中断、GPIO、PWM、DAC、状态机编程全部串起来的一个综合体。尤其是加上了“播放器”这个功能之后整个系统的复杂度直接上了一个台阶不光是按键发声还要考虑乐曲数据的存储与解析、节拍的控制、播放和弹奏模式的切换。把这一套东西理清楚STM32的大部分基础外设你基本就能融会贯通了。如果你正在准备单片机类毕业设计或者想找一个能把知识体系串起来的实践项目这个题目是非常值得做的。今天这篇就把整个设计过程完整拆开从方案选型、硬件设计、软件架构到具体的按键扫描、音符频率生成、乐曲播放逻辑再到我实际调试中踩过的坑一次性讲明白。1. 项目整体设计与方案选型1.1 为什么选STM32而不是51单片机先说一个很多人纠结的问题做电子琴51单片机它不香吗网上确实有一堆基于51的简易电子琴教程矩阵键盘加蜂鸣器几行代码就能跑起来。但51的方案有个致命短板——定时器资源少频率精度有限做8个音符还能凑合一旦扩展到两个八度、甚至加上多首乐曲播放定时器的分配立刻捉襟见肘。STM32的F1系列虽然也是入门级芯片但72MHz的主频、多个16位定时器、内置12位ADC和DAC这些硬件资源给了设计很大的余量。关键一点是STM32的定时器可以输出PWMPWM频率就是音符的频率而PWM的占空比又能控制音量大小。这就意味着你只需要一个定时器通道就能同时完成“发声”和“音量控制”两件事这在51上实现起来要费不少劲。另外做这个项目能用上的不只是PWM还有定时器中断做节拍、外部中断按键响应、DMA如果需要播放WAV音频、甚至I2C或SDIO如果扩展SD卡存储乐曲。把这些外设全部用起来项目的技术含金量就不是51能比的了。1.2 系统整体架构与工作模式划分设计之前我先把整个系统按照功能拆成了五个模块主控核心、输入模块、音频输出模块、人机交互模块和电源模块。主控就是STM32F103C8T6输入模块是4x4矩阵键盘音频输出模块是PWM加滤波电路再加小功放人机交互就用一个OLED显示屏加两个LED指示灯电源直接采用USB的5V供电再经AMS1117降压到3.3V。系统运行起来分两个工作模式。第一种是演奏模式用户按下矩阵键盘上的琴键系统立即发声这个是实时的对延迟要求很高。第二种是播放模式系统按照预先存储在Flash里的乐谱数据自动播放音乐相当于一个简易的MIDI播放器。两种模式通过一个功能按键切换OLED屏幕会显示当前模式。这个设计逻辑基本涵盖了一个“电子琴音乐播放器”该有的所有功能。1.3 为什么音符频率要用查表法而不是实时计算这里要讲一个关键的工程取舍。音符频率的理论计算非常简单就是十二平均律的公式f 440 × 2^((n-69)/12)其中n是MIDI音符编号。听起来很科学但在STM32里实际用的时候问题不少——C语言里的pow和log库函数在单片机上运算很慢而且在嵌入式环境里用浮点数做指数运算容易引入误差实时计算的话还会占用CPU时间影响按键响应的实时性。所以最常规的做法是直接把音符频率做成一张常量表。以A4中央C往上数第9个白键的440Hz为基准把C大调音阶从C4到B5这些常用音符的频率全部预先算好存放在Flash里。程序运行时只需要根据按键的索引直接查表拿到频率值再去更新定时器的自动重装载值整个过程没有浮点运算速度极快。1.4 音量和音色的实现思路电子琴如果只会嗡嗡响那和门铃没什么两样。想让声音有点“琴味”有两个技术点要处理音量和包络。音量靠PWM的占空比来控制。占空比越大输出的方波平均电压越高喇叭就越响。我把TIM2的通道1用来做音频输出PWM占空比固定在一个合适的值比如50%但如果你想做琴键力度感应也可以通过检测按键按下的速度去动态调整占空比这就是进阶玩法了。音色方面纯方波听起来会很生硬。我用的办法是给方波加一个简单的衰减包络按下按键的瞬间音量从最大值开始在几百毫秒内逐步衰减到初始音量的80%左右。这个效果用定时器中断里的一个递减变量就能实现缺点是音色离真实钢琴还很远但从听感上说比一直嗡嗡响的纯方波要自然得多。2. 硬件设计核心电路与音频输出方案2.1 矩阵键盘电路省IO口的关键设计矩阵键盘的原理不复杂就是把按键排成行和列的交叉点通过扫描方式检测哪个按键被按下。我的设计用4x4矩阵总共16个按键但只需要8个IO口。如果全部用独立按键16个按键就要16个IO口F103C8T6总共才37个IO口太浪费了。在电子琴这个项目里矩阵键盘的电路有个细节值得注意按键之间的行线和列线需要加上拉电阻或者使能STM32内部的上下拉电阻。如果在扫描时把没有驱动的引脚悬空检测到的电平就会不稳定轻则误触发重则一个按键按下时多个音同时响。我用的是内部上拉方式也就是GPIO配置成上拉输入模式省掉了4个外部上拉电阻。实际接线时我把矩阵键盘的4根行线接到STM32的PB0~PB34根列线接到PB4~PB7。这里有个坑PB4和PB3在F103上默认是JTAG引脚JTAG的NJTRST和JNTRST。如果直接用标准库的GPIO初始化这两个脚是不工作的必须先把AFIO的调试端口配置为复用功能或者禁用JTAG不然矩阵键盘的这两列会一直读不到正确的电平。具体配置方法是调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)。2.2 音频输出PWM直接驱动还是加滤波音频输出是整个硬件设计里最需要花心思的部分。很多人一开始图省事直接把PWM引脚接到无源蜂鸣器上发声。无源蜂鸣器的阻抗比较高声音普遍偏小而且蜂鸣器本身有谐振特性频率响应不均匀放出来的音高虽然对但音色很闷。我做了两级升级。第一级是PWM信号先通过一个简单的RC低通滤波器把方波平滑成接近正弦波的形状。滤波器的截止频率我取10kHz左右这样既能滤掉PWM的高频分量72MHz主频下PWM载波通常是几kHz到几十kHz又不会把音频信号本身衰减太多。用1k电阻加10nF电容就能实现成本几分钱。第二级是加一个小功放电路。我用的是常见的LM386小功率音频放大器增益可以通过1脚和8脚之间的电容调整。供电5V时LM386可以输出的功率足够驱动一个8欧姆0.5W的小喇叭音量在室内听完全够用。实际测试中PWM的频率分辨率足够保证音准而滤波电路之后的声音明显比直接驱动蜂鸣器柔和很多不会有那种刺耳的“吱吱”声。2.3 电源和复位电路的设计细节电源部分我用了两路降压USB输入的5V直接给LM386功放供电再通过AMS1117-3.3降到3.3V给STM32供电。这样做的原因很简单——功放在输出音频信号时电流波动很大如果让MCU和功放共用一路电源音频电流的波动会通过电源线串扰进MCU的供电导致ADC采样不稳定甚至在播放大音量时MCU直接复位。每个芯片的电源引脚旁边都要放一个100nF的去耦电容这是基本功但很多新手会漏。STM32F103的VDDA引脚要单独接一个10uF电容加一个1uF电容并联到地这个在数据手册里有明确要求如果省了芯片内部模拟部分就会出现电源噪声最明显的表现是ADC采集到的数据跳动非常大。复位电路用最经典的10k电阻上拉加100nF电容下拉NRST引脚再通过一个按键接地实现手动复位。BOOT0引脚通过10k电阻下拉到地保证从Flash启动。晶振用8MHz无源晶振两个20pF负载电容。3. 软件实现音符、按键与状态机3.1 音符频率表的生成与精度问题音符频率是音乐的基础所以这个表一定要精确。在代码里我直接写了一组宏定义以A4440Hz为标准用十二平均律算出从C4到B5的每一个半音频率。这里需要注意整数运算的精度。TIM2的时钟是72MHz预分频设置为1-1即72MHz不分频那么PWM输出频率就是f 72000000 / (ARR 1)所以ARR 72000000 / f - 1。以C4261.63Hz为例ARR 72000000 / 261.63 - 1 ≈ 275192 - 1 275191。这个值超过了16位定时器能表示的最大值65535。所以直接用定时器干不过去需要设置预分频系数。我把预分频设为2-172MHz/236MHz这样ARR的最大值可以到262143足以表示C4及更低音。再低的话还可以继续调整预分频。实际做表的时候我直接把计算好的ARR值存进数组而不是存频率这样在中断服务函数里就不用做除法运算了响应速度更快。3.2 矩阵键盘扫描防抖、连按与长按识别矩阵键盘的扫描逻辑是先把四根行线设置为输出逐行拉低然后读取列线的状态。如果读到的电平为低说明该行该列的按键被按下了。这个过程要在主循环里不断循环每扫描完一轮大约耗时1ms左右。按键消抖我用的是“软件延时消抖加连续确认”的方式。检测到某个按键变为按下状态后先延时10ms再次确认如果是低电平则认为有效。同时我在代码里记录了一个按键状态表用来判断按键是“首次按下”还是“持续按下”。对于电子琴来说持续按下要持续发声首次按下要触发一次音符启动和包络重置这个区分很重要。长按识别在电子琴上的用途不大但如果你想给每个琴键分配第二种音色长按切换这个功能就有用了。我实现的长按逻辑是检测到按键按下后如果持续400ms没有松开就触发长按事件。3.3 状态机设计演奏模式与播放模式的切换整个系统的核心是一个简单的有限状态机。我定义了三个状态MODE_PLAY演奏模式、MODE_PLAYING正在播放音乐、MODE_PAUSE播放暂停。主循环先扫描功能按键如果检测到模式切换键按下就在演奏模式和播放模式之间切换。演奏模式下矩阵键盘的每个按键直接对应一个音符播放模式下矩阵键盘变成播放控制面板比如4个按键分别控制播放上一首、播放下一首、暂停/继续、停止。状态机的好处在于代码逻辑清晰不会出现“按下模式切换键后声音还在响”这种状态错乱的问题。每个状态都有明确的进入动作和退出动作比如从演奏模式切到播放模式时要立即关闭所有正在发声的定时器通道并且初始化播放索引。3.4 乐曲数据的存储格式与解析“播放器”功能的关键在于乐曲数据怎么存。如果直接把WAV或者MP3存在STM32内部Flash里C8T6芯片只有64KB Flash一首歌都放不下。所以最实际的方案是把乐谱编码成音符序列类似MIDI的简化版用极少的空间存完整首曲子。我定义了一个很精简的乐曲数据格式。每个音符用两个字节表示第一个字节是音符编号0表示休止符1~8对应C大调音阶的各音第二个字节是节拍数单位是1/4拍或者1/8拍。曲子以0x00 0x00作为结束标志。比如《小星星》的前两句编码出来就是5 1 5 1 6 1 5 1 …… 这样的序列。这里的数字是简谱的唱名后面跟的是该音符的时值以几分音符为单位。这样一首几十小节的儿歌大概只需要几百个字节完全放得下。播放逻辑是定时器以固定的节拍速度产生中断比如每分钟120拍的4分音符中断周期就是500ms每次中断取出一个音符数据查找音符频率表设置定时器的输出频率同时启动音符时值计数到时自动停止发声。整个播放过程完全由中断驱动主循环只需要处理按键控制即可。3.5 核心代码实现PWM输出与节拍控制直接上具体的代码结构。这里用的是标准库STM32CubeMX生成工程的话逻辑一样只是API不同。// 音符ARR值表按照预分频1即36MHz计数时钟计算 const uint16_t NoteARR[] { 0, // 休止符 68767, // C4 261.63Hz 61276, // D4 293.66Hz 54623, // E4 329.63Hz 51590, // F4 349.23Hz 45986, // G4 392.00Hz 40996, // A4 440.00Hz 36550, // B4 493.88Hz 34462, // C5 523.25Hz }; void TIM2_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; // PA0作为PWM输出 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 1000; TIM_TimeBaseStructure.TIM_Prescaler 1; // 72MHz / (11) 36MHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // 初始占空比50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); } // 设置音符频率note是NoteARR的索引 void SetNote(uint8_t note) { if (note 0) { TIM_SetCompare1(TIM2, 0); // 休止符关闭输出 TIM_SetAutoreload(TIM2, 1000); return; } TIM_SetAutoreload(TIM2, NoteARR[note]); TIM_SetCompare1(TIM2, NoteARR[note] / 2); // 50%占空比 }节拍控制用的是TIM3定时器中断。以120BPM为例四分音符的时长是60/1200.5s如果预分频后定时器每1ms中断一次那么500次中断正好是0.5秒。在中断服务函数里累加计数到了预设的值就切换下一个音符。volatile uint16_t BeatCounter 0; volatile uint16_t BeatDuration 500; // 500ms即四分音符120BPM volatile uint8_t CurrentNote 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); BeatCounter; if (BeatCounter BeatDuration) { BeatCounter 0; CurrentNote GetNextNote(); // 从乐曲数据中取下一个音符 SetNote(CurrentNote); } // 音量包络衰减逻辑 if (EnvelopeCounter 0) { EnvelopeCounter--; uint16_t duty (uint16_t)((uint32_t)CurrentARR * EnvelopeCounter / EnvelopeMax); TIM_SetCompare1(TIM2, duty); } } }这套代码跑起来演奏模式下按键按下立即发声播放模式下自动按节拍播放乐曲整体逻辑比较完整。4. 播放器功能扩展让STM32“会唱歌”4.1 从单音到复音的扩展思路上面说的方案是单音播放同一时间只能发出一个音作为电子琴来说有点单调因为同时按下两个键时只有最后按下的那个键会响。想实现双音甚至和弦效果就要用到多个定时器的多个通道。STM32F103C8T6有TIM1、TIM2、TIM3、TIM4四个定时器其中TIM1是高级定时器有4个通道TIM2、TIM3、TIM4各4个通道。理论上最多能同时输出12路PWM。但实际做的时候不用那么贪心做双音和三和弦用TIM2的CH1和CH2输出两个不同频率的PWM再通过一个加法电路把两路信号混合后送功放就能实现双音。实现双音的关键在于频率不同的两路PWM信号在同一个电源上混合需要做隔离避免两路信号互相干扰。我用的办法是每一路输出各串一个1k电阻然后接在一个公共节点上再进滤波电路。这样两路信号相当于通过电阻做电流求和互相影响会小很多。4.2 加入OLED显示从“能用”到“好用”做课程设计或者毕业设计功能实现了只是及格加一个有显示功能的屏幕能让你的项目提升一个档次。我用的是0.96寸的I2C接口OLED128x64分辨率只需要接VCC、GND、SCL、SDA四根线软件上用软件模拟I2C就能驱动不需要占用硬件I2C外设。OLED上我显示的内容包括当前模式演奏/播放、当前音符名称用C4、D4这种标记、正在播放的曲目编号和播放进度用方块表示进度条、音量大小。用OLED的好处是调试信息也能直接显示在屏幕上比如实时查看TIM2的ARR寄存器值、按键扫描到的行列坐标这些在调试阶段帮了大忙。4.3 从内部Flash扩展到外部存储内部Flash只能存几首简单的儿歌想做真正的“音乐播放器”让用户自己上传歌曲就需要外部存储。可选方案有在SD卡上存WAV文件然后用DAC播放或者加一个SPI Flash存更多的MIDI风格乐曲数据。使用DAC播放WAV是另一个深水区了。STM32F103的DAC是12位的采样率如果设置成8kHz采样率一首10秒的歌曲WAV数据量就有160KB需要从SD卡读数据再往DAC灌。这个过程需要用DMA加双缓冲的办法否则CPU会被数据搬运完全占死。这个扩展方向特别适合作为项目的加分点因为涉及的知识点跨越了文件系统FATFS、外设DMA、DAC、中断嵌套等多个层面技术深度完全不是一个量级的。5. 开发环境与调试工具从零到能跑5.1 CubeMX生成工程还是标准库手动配置现在新学STM32的人大多用STM32CubeMX配合HAL库图形化配置引脚和时钟自动生成工程骨架确实省事。但做这个电子琴项目我更推荐新手先弄懂标准库的寄存器级配置或者至少对照着CubeMX生成的代码去看标准库版本。原因很简单PWM、定时器中断这些外设如果用CubeMX配置注册几个回调函数就能用了确实快。但对于电子琴这种要求精确时序的项目你会遇到很多“为什么我的音符频率不对”“为什么按键响应有延迟”的问题而这些问题几乎都出在时钟树配置和定时器预分频上。如果你只会用图形化界面配置出了问题根本不知道从哪里排查。反过来如果你能手写TIM_TimeBaseInitStructure和TIM_OCInitStructure你对定时器工作原理的理解就完全不一样了。当然我不是反对用CubeMX。我的建议是不管用不用CubeMX生成框架你至少要能看懂配置产生的代码知道自己配置的预分频系数和自动重装载值到底在寄存器里是怎么工作的。5.2 KEIL5环境搭建的几个关键坑开发环境用KEIL5 MDK是最常见的。环境搭建这个话题可以专项写一篇避坑指南这里只说几个和我这个项目相关的重点。第一KEIL5本身不带STM32芯片的支持包需要单独下载安装对应的device pack。装完之后才能在器件选择里找到STM32F103C8。第二很多人同时学51和STM32电脑上装了C51和MDK两个版本。正常情况下两个版本可以共存但如果先安装MDK再安装C51启动时可能默认打开C51的IDE导致找不到ARM编译器。解决的办法是把两个的安装目录分开严格区分然后在Project菜单里检查一下是不是用了ARMCC。第三调试器选择ST-Link的话需要在Debug设置里把Port选成SW如果选了JTAG会识别不到芯片因为F103C8T6的JTAG引脚占用了。第四网上很多教程用的是ST-LINK Utility烧录工具主流的ST-LINK Utility在Windows上使用比较稳定烧录速度也快比直接用IDE下载要快很多。如果你的板子已经焊好了先用ST-LINK Utility尝试连接一下目标芯片能连上再继续做代码调试能省很多排查时间。5.3 使用逻辑分析仪抓取PWM波形进行音准验证软件写完了怎么确认音准对不对人的耳朵对音准的辨别有时候不太靠得住特别是当输出经过滤波和功放之后声音可能会有些失真。我的做法是用逻辑分析仪直接抓TIM2_PWM引脚输出的波形测量频率是否和预期一致。2块钱的逻辑分析仪加DSView或者PulseView软件就能干这个活。把探针夹在PA0引脚模式设置为PWM解码软件会自动计算出信号的频率。实测C4输出261.7Hz、A4输出440.1Hz和理论值只差不到0.1Hz说明预分频和ARR的计算没有问题。如果你发现频率有偏差优先检查时钟配置——如果RCC里把系统时钟配成了8MHz没开PLL那所有频率都会比预期低好几倍。6. 常见问题排查与调试笔记6.1 矩阵键盘乱触发或按键不响应这个问题90%出在引脚配置上。首先是PB3、PB4这两个引脚刚才说过它们默认是JTAG功能不配置AFIO重映射就没法当普通GPIO用。其次是内部上下拉方向搞反了。我的扫描逻辑是把行线设置为推挽输出并拉低列线上拉输入读取状态所以列线必须配置为GPIO_Mode_IPU上拉输入。如果配成了下拉输入列线电平一直为低那扫描结果就永远是“所有按键都按下”的状态乱响一气。6.2 延时函数卡死或者进入HardFault项目里用到了软件延时消抖比如Delay_ms(10)如果这个延时函数卡住了多半是SysTick的中断优先级或者系统时钟配置出了问题。最常见的两个原因一是在定时器中断里调用了Delay_ms函数导致中断嵌套死锁二是SysTick没有配置好中断标志一直不置位。排查方法很简单在延时函数的第一行和最后一行各设置一个GPIO反转用示波器或者逻辑分析仪看这个引脚的波形如果只有一次反转就说明延时函数一直没退出问题出在SysTick配置上。如果在中断里调用了延时卡住那就是中断优先级的问题把SysTick的优先级设成最高并且在延时函数里不要关总中断一般就能解决。6.3 CFSR寄存器报0x00008200是什么错误做这个项目时我还遇到过一个大坑程序运行一段时间后突然进HardFault从调试器中看到的CFSR可配置错误状态寄存器值为0x00008200。这个值的含义是0x8000表示总线错误地址寄存器有效0x0200表示发生了NOCP无协处理器用法错误。用大白话解释就是CPU尝试执行一条需要协处理器支持的指令比如浮点运算指令但这个协处理器并不存在或者没有使能。我在代码里用了浮点数计算音量包络的衰减比例编译器生成了ARM的浮点指令而F103C8T6没有浮点单元FPUCPU一执行到这条浮点指令就立即触发UsageFault。解决办法有两个一个是在编译选项里把浮点模型改为软件模拟Use softfp这样编译器就不会生成硬件浮点指令另一个是编程时尽量避免浮点运算把浮点乘法改成整数运算比如用分子分母的整数比例递减。6.4 常见问题速查表现象可能原因排查思路喇叭无声PWM通道未使能、占空比为0用逻辑分析仪查PA0引脚有没有波形声音音量很小滤波电容过大、功放增益不足检查RC滤波截止频率检查LM386增益配置按键按下声音不停按键扫描没有识别到松开状态检查状态表逻辑确认松手状态被正确记录自动播放节奏不对定时器节拍计时误差累积用示波器抓TIM3更新中断的周期OLED不显示I2C地址错误、SDA/SCL接反确认OLED地址是0x3C还是0x3D播放时主程序卡顿主循环里做了耗时操作把按键扫描和显示刷新改为中断驱动或分时处理切换模式后仍有声音没有执行发声通道清零退出动作在状态机切换函数里调用SetNote(0)7. 项目可扩展的方向与个人经验小结这个项目做完之后如果你还想继续深挖可以往这几个方向扩展把单音改成双音甚至多音实现简单的和弦用DAC加DMA播放WAV音频配合SD卡做一个真正的立体声播放器用ADC采样麦克风信号做一个简易调音器或者加上ESP8266模块通过手机蓝牙或者Wi-Fi无线控制选曲和弹奏这就是物联网音乐播放器的雏形了。根据我个人的体会这个题目最大的价值不在于“电子琴”本身而在于它把STM32的定时器、中断、GPIO、波形输出、状态机设计这些最基础但最核心的知识点串联成了一个整体。很多人学单片机外设是逐个外设学的每个外设单独用都没问题但一综合起来就不知道怎么设计了。这个项目恰恰给了你一个综合演练的机会——所有的外设都在为一个共同目标协作这种“系统思维”才是嵌入式开发真正需要的东西。最后分享一个小经验调试这种多媒体项目一定要先把声音通路打通再调试功能和逻辑。意思是先让定时器PWM的波形稳定输出确认物理电路没问题然后再去写按键扫描和状态机。不要一上来就写一大堆代码到时候声音响不响都不知道是软件还是硬件的问题排查起来特别痛苦。先把基础打牢后面就是一路畅通了。