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

资讯详情

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

基于STM32的多功能电子琴设计:从原理图到代码实战

基于STM32的多功能电子琴设计:从原理图到代码实战 简介本资源是一套面向嵌入式初学者与课程设计者的STM32多功能电子琴完整开发资料聚焦单片机外设驱动与音频控制实践解决从原理理解到工程落地的典型学习痛点。资源包含169个文件涵盖Keil工程uvprojx、ioc、hex等、硬件原理图schdoc、pdf、核心驱动代码47个.h头文件与20个.c源文件含HAL_TIM/PWM/按键/蜂鸣器模块、编译中间文件o、d、crf及说明文档txt、md、docx包体大小为10.6MB结构清晰便于分模块研读。已有1368人学习下载适合开展单片机实训、电子设计竞赛备赛或毕业设计参考。读者可直接烧录运行深入理解STM32定时器PWM波形生成机制、无源蜂鸣器驱动逻辑、多按键状态机设计以及音乐播放与实时交互功能的软硬件协同实现方法。 一块STM32、一组4x4按键矩阵、一颗无源蜂鸣器或者小喇叭拼起来就是一台能弹的电子琴。听起来很像大学单片机课的大作业但真把它做出“多功能”的味道从硬件原理图到软件架构要处理的问题一点都不少。我这次基于STM32F103C8T6做了一台支持演奏模式、自动播歌、录音回放、八度切换的多功能电子琴原理图和源代码都整理出来了。这篇文章就把它拆开讲透从为什么要用矩阵按键、音符频率怎么算、定时器PWM怎么配到原理图里最容易踩的坑、实际调试时怎么一步步定位问题全部按我真实做这个项目的流程来讲。适合正在做单片机课程设计、电子设计竞赛入门或者纯粹想把手头的蓝丸板子玩出花样的朋友参考。1. 项目全貌为什么选STM32F103以及“多功能”到底指什么1.1 主控选型的现实考量选STM32F103C8T6不是因为它最强而是因为它最“顺手”。这颗芯片算是国内玩嵌入式的人手一块的型号Cortex-M3内核主频最高72MHz64KB Flash20KB SRAM定时器资源丰富价格也便宜。做电子琴这种项目核心需求是“能用定时器输出精确频率的PWM信号”这对F103来说是基本功。有人可能会问用51单片机或者Arduino行不行行但体验会差一截。51的主频和定时器位数在同时处理按键扫描、OLED刷新、多段录音存储时会明显捉襟见肘。Arduino的 tone() 函数虽然能直接发声但同一时刻只能出一个音调而且底层占用的定时器资源让你很难再同时做复杂的状态管理。F103的优势在于定时器多TIM1-TIM4可以单独拿一个出来专职产PWM剩下的处理逻辑引脚足够用HAL库或者标准库的开发资料多到随便搜都有参考。1.2 功能清单与设计目标我在做之前先给自己列了一份需求清单明确这台“多功能电子琴”至少要能干什么演奏模式按下琴键按键矩阵立即发出对应音高的声音松手停止。八度切换一个八度只有12个半音通过上下切换把音域扩展到3个八度。自动播歌内置几首预置歌曲按下一个功能键就开始自动演奏可以随时停止。录音回放把用户弹奏的按键序列和时值记录下来再按一次键回放出来。音量控制通过电位器或者按键分档调节输出音量。这些功能里演奏模式是骨架其它都是围绕“多功能”这个词展开的血肉。设计目标是所有交互只用一块4x4矩阵键盘完成不额外接复杂外设让原理图和代码都保持在“一个人能完整吃透”的复杂度内。用一句话概括用最少的器件把软件层的功能逻辑做到尽量完整。2. 硬件链路拆解按键矩阵、发声单元与扩展电路2.1 按键矩阵的扫描原理与引脚分配4x4矩阵按键16个按键只需要8个IO口相比直连16个IO口省了一半资源。原理很简单4根行线设为推挽输出4根列线设为带上拉输入。扫描时依次让某一行输出低电平其余行输出高电平然后读取所有列的电平。哪一列被拉低了就说明该行和该列交叉处的按键被按下。举个例子第0行输出低读到第2列为低说明坐标行0列2这个按键被按下。这种行列交叉扫描的方式理论上一个IO口都没浪费。实际操作时要注意行线和列线之间会形成交叉网络如果同时按下多个键可能出现“鬼键”现象这个我在后面调试部分会专门讲。我的引脚分配是这样的功能引脚配置矩阵行0~3PB0 ~ PB3推挽输出矩阵列0~3PB4 ~ PB7上拉输入PWM音频输出PA6 (TIM3_CH1)复用推挽输出I2C OLED (可选)PB6 (SCL)、PB7 (SDA)开漏输出录音指示灯PC13推挽输出板载LED这里有个非常关键的坑PB3和PB4在STM32F103上默认是JTAG引脚PB3是JTDOPB4是JNTRST。芯片上电默认开启JTAG功能如果直接把PB3、PB4当普通IO用你会发现引脚电平怎么配置都不对。解决办法是在初始化最前面加一句GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这句话把JTAG关闭、只保留SWD调试PB3和PB4就被释放成普通IO了。我在用标准库做这套设计的时候就被这个坑卡了半个多小时后面会在调试章节详细拆。2.2 发声单元无源蜂鸣器还是小喇叭发声是电子琴的核心输出选型上必须区分有源蜂鸣器和无源蜂鸣器。有源蜂鸣器内部自带振荡电路只要通电就响频率固定没法用来演奏不同音高的音符直接排除。无源蜂鸣器需要外部提供特定频率的方波才会发声频率不同音高就不同这正是我们要的。但无源蜂鸣器有个缺陷音色很尖、音量小而且频率响应范围窄低音区表现差。我实测下来用无源蜂鸣器弹《小星星》还能听弹低音区就基本糊了。所以我的方案是用一只8Ω、0.5W的小扬声器配合一颗S8050三极管做驱动。STM32的IO口驱动能力只有几毫安带不动扬声器三极管的基极通过1kΩ电阻接PA6集电极接扬声器一端扬声器另一端接3.3V发射极接地。电路逻辑是PA6输出高电平时三极管导通扬声器两端产生压差发声输出低电平时截止扬声器两端等电位不发声。PWM方波不断翻转扬声器就被驱动发出对应频率的声音。别忘了在扬声器两端反向并联一个1N4148二极管用来吸收关断瞬间的感应电动势保护三极管。实测下来这个电路声音清晰中音区和高音区都够用房间内听没问题。想让音质更好可以加一片LM386功放但对这个量级的项目来说三极管驱动已经足够了电路还简单。2.3 可选的OLED显示与音量调节为了让“多功能”看得见我加了一个0.96寸I2C接口OLED。它能做的事很实际显示当前模式演奏/播歌/录音、当前八度位置、录音状态。没有它用户只能凭声音判断模式交互体验差很多。OLED接PB6、PB7标准I2C时序F103的硬件I2C虽然口碑一般但用软件模拟I2C也完全够用毕竟只是显示少量文本不追求高刷新率。音量调节我用了两种方案叠加硬件上在PWM输出到三极管基极之间串联一个10kΩ电位器直接手动衰减信号幅度软件上给PWM设置不同的占空比挡位。占空比50%的音量最大把占空比降到20%、10%音量会明显变小音色会稍微变化但能接受。两个方案各有用途电位器是总体音量旋钮软件挡位是给录音回放或自动播歌时做动态音量变化留的口子。3. 音符频率的数学原理与定时器PWM配置3.1 十二平均律频率表的推导过程电子琴要发声第一步是解决“一个音符对应的频率是多少”的问题。现代音乐采用十二平均律把一个八度平均分成12个半音相邻半音之间的频率比是固定的 2^(1/12)约等于1.05946。国际标准音A4的频率是440Hz。以它为基准任意音符的频率可以用公式算f(n) 440 × 2^((n - 69) / 12)这里n是MIDI音符编号A4的编号是69中央C4是60。但更直观的做法是从中央C4261.63Hz往上逐个乘1.05946得到C4、C#4、D4、D#4……一直到B4然后高一个八度频率翻倍低一个八度频率减半。实际写代码时不需要在运行时算浮点直接把三个八度的音符频率表预先算好做成一个const数组存进Flash。用浮点逐次相乘算频率表会有累进误差但人耳对几个赫兹的偏差完全不敏感所以直接用公式在Excel或者Python里算好填进去就行。我用的频率表长这样单位Hz音符八度1八度2八度3C130.81261.63523.25C#138.59277.18554.37D146.83293.66587.33E164.81329.63659.25F174.61349.23698.46G196.00392.00783.99A220.00440.00880.00B246.94493.88987.773.2 定时器PWM生成指定频率的计算过程STM32的定时器PWM输出频率由三个参数决定定时器时钟频率、预分频器PSC、自动重装载值ARR。公式是PWM频率 定时器时钟 / ((PSC 1) × (ARR 1))我的系统时钟配置为72MHz预分频器设为71得到定时器计数频率1MHz。那么要得到某个音符频率fARR的计算就是ARR 1,000,000 / f - 1以中央C4的261.63Hz为例ARR 1000000 / 261.63 - 1 ≈ 3821。这个值远小于16位定时器的65535上限所以TIM3完全能覆盖从低音到高音的全部频段。注意PSC不能设成0否则72MHz直接除ARR会超过6553516位定时器装不下这是新手最容易卡住的地方。占空比通过CCR寄存器控制。我设置了四挡50%最大音量、35%、20%、10%对应数字音量0到3。占空比本质决定的是方波高电平在一个周期内占的比例三极管导通时间随之变化扬声器上的平均功率就变了音量自然不同。3.3 代码实现定时器初始化与单音切换我用标准库写的初始化代码核心思路是TIM3的通道1工作在PWM模式1输出极性为高预分频固定ARR和CCR在运行时动态调整。初始化部分如下void TIM3_PWM_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tim.TIM_Prescaler 71; // 72MHz / 72 1MHz tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_Period 1000; // 默认值运行时改 tim.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tim); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 500; // 占空比50% oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); } void Note_Play(uint16_t freq, uint8_t volume) { uint16_t arr 1000000 / freq - 1; TIM3-ARR arr; TIM3-CCR1 arr * volume / 10; // volume 取1~5 }这里面有一个细节值得说改ARR和CCR的时候我直接操作寄存器而没有用库函数。原因是库函数TIM_SetCompare1每次调用都要重新判断一些状态在按键按下到发声这条路径上我希望延迟尽量低。直接写寄存器是最快的做法代码也最直白。另外注意arr * volume可能超过16位所以先乘再除时要小心我这里是arr最大3821乘5不到两万安全范围但如果把音量档位扩展成百分比就要考虑用uint32_t中间量。4. 软件架构按键状态机、模式切换与自动演奏逻辑4.1 按键矩阵扫描与消抖状态机按键扫描是软件层的地基。我维护了一个10ms周期定时器用SysTick每个周期扫描一遍矩阵把原始按下的坐标记录到当前状态里然后和上次状态比较连续两次状态一致才确认按键有效。这就是最简单的“两次确认消抖”比纯延时消抖更稳因为扫描过程不阻塞主循环。每一行的扫描过程uint8_t Key_Scan(void) { uint8_t row, col; for (row 0; row 4; row) { GPIO_WriteBit(GPIOB, Pin_Row[row], Bit_SET); } for (row 0; row 4; row) { GPIO_ResetBits(GPIOB, Pin_Row[row]); // 当前行拉低 for (col 0; col 4; col) { if (GPIO_ReadInputDataBit(GPIOB, Pin_Col[col]) Bit_RESET) { return row * 4 col; // 返回按键编号0~15 } } GPIO_SetBits(GPIOB, Pin_Row[row]); // 恢复高电平 } return 0xFF; // 无按键 }这个函数每次扫描返回一个值主循环里用它驱动一个简单的状态机状态IDLE没按键。检测到有效键值进入PRESS_DEBOUNCE。状态PRESS_DEBOUNCE如果下一个周期还是同一个键值确认按下触发按键回调进入PRESSED。状态PRESSED等待按键释放释放后回到IDLE。为什么要这样设计而不是检测到低电平就触发因为机械按键的抖动通常持续5到20ms如果不做状态机处理一个按键按下去可能被当成三四次触发。在电子琴里一次按键触发两次音高切换演奏时就全是杂音。4.2 多功能模式的状态机设计电子琴的“多功能”核心在模式管理。我用一个全局枚举变量保存当前模式四个功能键分别承担模式循环切换、八度加、八度减、录音/回放开关。模式0演奏按键矩阵前12个键对应12个半音按下发声松开停声。功能键控制八度和录音。模式1自动播歌预置歌曲自动播放播放过程中按键矩阵被忽略按下“模式切换”键停止并返回演奏模式。模式2录音回放录音时按键矩阵照常演奏同时把键号和按下持续时间存入数组最多存128个音符事件。模式切换的细节是切换时必须先停止当前声音调用Note_Stop()把TIM3的输出关闭否则切换瞬间会有一个刺耳的爆音。八度切换同理八度变了已按下的键对应频率也要跟着变所以我在八度加减的函数里除了改全局变量还会重新计算当前按下键的频率并立刻生效。4.3 预置歌曲的存储与播放实现预置歌曲本质上就是“音符编号 时值”的数组。音符编号我直接用频率表的下标时值用“节拍单位”表示一个全音符是4拍四分音符是1拍。播放时用一个定时中断做节拍器。以《小星星》为例它在C调下的旋律是“1 1 5 5 6 6 5 4 4 3 3 2 2 1”。我把它写成数组typedef struct { uint8_t note; // 频率表下标 uint8_t beat; // 节拍数 } MusicNote; const MusicNote song_twinkle[] { {0, 1}, {0, 1}, {7, 1}, {7, 1}, {9, 1}, {9, 1}, {7, 2}, {5, 1}, {5, 1}, {3, 1}, {3, 1}, {2, 1}, {2, 1}, {0, 2}, {0xFF, 0} // 结束标记 };播放逻辑用了一个节拍基准时间一个四分音符默认400ms。定时器每50ms产生一次心跳播放状态机里累计心跳数达到当前音符的时值要求就切到下一个音符。注意必须保持每个音符之间有空隙或者直接切换频率即可否则连音效果不明显。实测发现纯方波连续播放时音符之间的突变在扬声器上会有“咔哒”声这是正常的忍一忍就习惯了如果想消除可以在音符切换处做几个毫秒的淡出淡入但代码复杂度会上升不少我没在这个版本里做。4.4 录音回放的核心逻辑录音回放的实现比想象中简单。核心是一个结构体数组每个元素记录三件事按下按键的编号、按下到松开的时间长度、按下前的空闲时间长度。typedef struct { uint8_t key; uint16_t press_time; uint16_t gap_time; } RecordEvent;录音开始时清空数组按下键就启动计时松开键就把事件写入数组。回放时先等gap_time然后播放press_time循环处理完所有事件。用128个事件的空间正常弹一段曲子完全够用。这里有个值得注意的取舍我只记录音符和时长不记录力度因为矩阵按键本来就是数字量没有力度检测。想让电子琴支持力度感应得换用带力度检测的键盘模组成本和复杂度是另一回事了。5. 原理图绘制要点最小系统、供电与引脚避坑5.1 最小系统电路与供电设计原理图的核心是STM32最小系统主控芯片、时钟电路、复位电路、供电电路、BOOT配置、去耦电容。这块画错了程序写得再好也白搭。时钟电路用8MHz无源晶振加两个20pF负载电容分别接在OSC_IN和OSC_OUT上电容另一端接地。注意走线要短晶振下方不要铺铜这是保证稳定起振的关键。复位电路用一个10kΩ电阻上拉到3.3V按键一端接地另一端接NRST再并一个100nF电容滤除高频干扰。供电用的是经典方案USB的5V进AMS1117-3.3稳压到3.3V输入端接100uF电解电容和100nF陶瓷电容输出端接10uF和100nF电容。AMS1117的最大输出电流1A对这套电路主控加OLED加三极管驱动扬声器绰绰有余。BOOT0和BOOT1各接一个10kΩ下拉电阻正常运行时从Flash启动。每个电源引脚旁边都要放一个100nF去耦电容这是很多初学者容易忽略的。F103有4个VDD引脚如果去耦电容放不到位PWM高频切换时电源纹波会明显增大严重时会让芯片复位或者ADC采样抖动。我这个设计方案里VDD去耦电容都靠近引脚放置走线先过电容再到引脚。5.2 引脚分配的完整对照与JTAG陷阱前面提过PB3、PB4默认是JTAG功能这里再展开说一下。STM32F103上电默认SWJ串行调试接口全部开启PB3 JTDO、PB4 JNTRST。如果我只用SWD两根线SWDIOPA13、SWCLKPA14调试可以把JTAG关掉释放PB3、PB4。这行代码必须放在GPIO初始化之前GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);我见过有人在这个问题上卡了一整天按键矩阵里有两个键永远无法触发用万用表量引脚电平又是对的代码逻辑也没问题最后才发现是JTAG占用了引脚。这个坑在原理图设计阶段就可以避掉——如果不想处理JTAG重映射就把矩阵列线换到PC0~PC3或者PA8、PA11这些完全空闲的引脚上省得给自己添麻烦。5.3 从原理图到PCB的关键注意事项这版项目我做了PCB虽然用杜邦线飞线也能跑通但高频PWM信号和按键矩阵的交叉走线在面包板上容易引入干扰。画PCB时有几点实际经验晶振电路走线尽量短周围用地包一圈不要有平行长走线经过。按键矩阵的行线和列线是交叉网络如果全部走顶层交叉处必须用过孔跳线。我习惯矩阵走线在PCB的一侧集中减少过孔数量。扬声器驱动部分和主控的地要单点连接避免大电流瞬间拉低参考地。PA6的PWM输出走线远离晶振引脚避免高频方波耦合到时钟线上。很多人画完原理图直接自动布局、自动布线出来的板子噪声很大。实际上对这类项目手动布局半小时把电源、晶振、音频输出这三个区域隔开板子的稳定性会好很多。6. 实测调试从板子无声到按键错乱的完整排查链路6.1 现象一完全没有声音PWM输出引脚量不到波形这是最常遇到的第一个问题。我的排查顺序是先软件后硬件先时钟后输出。第一步用示波器或者万用表量PA6引脚看有没有方波。如果完全没有波形查TIM3的时钟使能有没有开。标准库容易犯的错是RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE)这行漏了Timer寄存器写什么都没反应。如果时钟开了但依然没波形查GPIO模式。PA6必须配置为复用推挽输出GPIO_Mode_AF_PP。很多人照抄GPIO初始化把模式写成了普通推挽输出结果PA6被当成普通IO控制永远输出的是0或1的固定电平不是PWM波形。如果波形有但扬声器不响问题就出在驱动电路。用万用表量三极管基极电压PA6输出高电平时基极应该在0.6~0.7V左右如果量到1V以上说明基极电阻太小电流过大如果量到0V检查三极管基极到PA6的走线有没有虚焊。6.2 现象二部分按键按下去没反应或者按一个键同时触发另外的键这大概率是矩阵扫描的“鬼键”问题发生在同时按下多个键时。举个例子行0和行1、列0和列1构成一个矩形当按下行0列0和行1列1时扫描行0读列1可能发现低电平因为列1通过行1列1键、行1、以及行0列1形成了一条意外的导电路径但这需要行0列1也被按下才成立。更普遍的情况是我扫描某一行时其他行的悬空或上拉状态干扰了列检测。解决方案有两个一是扫描列输入时把其他行全部置为高阻输入而不是推挽输出二是扫描时给列引脚配置内部上拉。我实测下来最有效的是在每一行的扫描周期里把非当前扫描行配置为GPIO_Mode_IPU上拉输入只把当前行设为推挽输出低电平。这样电流无法从非当前行穿过鬼键路径被切断。代价是每次扫描都要重新配置GPIO模式多花几十个时钟周期对10ms扫描周期来说微不足道。6.3 现象三按键松手后还在持续发声这通常是按键释放检测的逻辑问题。在我的状态机里PRESSED状态必须等到矩阵扫描返回0xFF无按键才回到IDLE如果扫描函数在按键释放瞬间返回了错误值比如因为抖动又检测到一次按下状态机就会卡在PRESSED声音一直不断。解决办法是给释放检测也加消抖连续两次扫描到无按键才确认释放。这和我按下时的两次确认是对称的。另外如果用的蜂鸣器模块自带三极管驱动而我在PA6上配置了PWM输出释放时如果把占空比设为0或者直接关闭TIM3输出扬声器会立即静音这也是一个更干脆的做法void Note_Stop(void) { TIM3-CCR1 0; // 占空比归零 }注意这里不能TIM_Cmd(TIM3, DISABLE)因为频繁启停定时器会有几微秒的重新同步时间并且每次启动时计数从0开始实际发出的第一个半周期可能不完整听感上有爆音。把CCR1设为0让PWM停止翻转但定时器继续跑下次发声时只需要改ARR和CCR1响应速度最快音头也最干净。6.4 调试过程中最值得分享的一个通用技巧整个项目调试下来我强烈建议在工程里保留一个“测试模式”的宏开关。平时开发时把SysTick中断里的扫描逻辑打印到串口人眼看串口输出确认按键状态和模式状态是否正确。等确认逻辑无误了再把打印关掉恢复纯音频模式。// #define DEBUG_TRACE 1 #ifdef DEBUG_TRACE printf(Key: %d, Mode: %d, Octave: %d\r\n, key_val, cur_mode, octave); #endif这个习惯帮我省了大量时间。做嵌入式调试最怕的就是多个外设同时出问题而你无法判断是哪个环节。把每个关键环节的状态打出来就能把问题从“玄学”变成“逻辑可查”。比如模式下切后音高不对打印出octave变量一眼就能看出是八度变量的更新逻辑写错了而不是去怀疑定时器配置有问题。7. 后续还能怎么扩展这套项目做完之后我留了几个扩展方向给有兴趣继续折腾的朋友一些思路。第一个方向是加MIDI接口。STM32的USART可以解析标准的MIDI数据流把电子琴变成一台MIDI控制器接上电脑的DAW软件就能当编曲键盘用。这比纯自娱自乐播放预置歌曲有实用价值得多而且代码改造量并不大核心就是解析MIDI Note On和Note Off消息映射到现在的发声函数。第二个方向是换用DAC输出波形替代PWM方波。改成查表输出正弦波或者三角波音色会比方波柔和很多。F103没有内置DAC需要外挂一个MCP4725之类的I2C DAC芯片或者直接用内部DAC的F103RCT6。声音会从“游戏机音效”进化到“可以听的电子琴音色”。第三个方向是录音存储扩容。现在的录音存在SRAM里掉电就丢。把录音事件编码成二进制格式通过串口发给外部Flash或者SD卡就能实现乐曲存储和调取等于把电子琴升级成带“U盘”的版本。我个人实际操作下来的体会是这个项目的价值不在于它多高级而在于它把嵌入式开发的几个核心环节——GPIO、定时器、PWM、状态机、矩阵扫描、原理图设计——全部串起来形成一个完整闭环。如果你能独立把这套项目从原理图到代码全部跑通你对STM32的理解会上一个台阶后面再去做更复杂的四轴飞行器驱动、电机控制、物联网网关很多思路都是相通的。最后再分享一个小技巧调音准的时候别全信耳朵手机装个频谱分析仪App把扬声器对着手机麦克风看频率峰值是不是落在标准音符频率附近这比凭感觉校音靠谱得多。本文还有配套的精品资源点击获取
返回列表