
1. 项目概述与核心思路作为一个玩了十几年吉他的老手我深知调音这事儿有多烦人。尤其是演出前手忙脚乱或者练琴时发现音不准用传统调音夹或者手机App总得一手按着琴一手去拧弦钮不仅慢还容易拧过头。所以当我看到用Arduino做自动调音器的想法时立刻就觉得这玩意儿太有搞头了。AutoTona这个项目本质上就是把我们耳朵听音、大脑判断、手去拧钮这个过程给自动化了。它的核心思路非常清晰用一个麦克风“听”吉他弦振动的声音用一块单片机Arduino当“大脑”分析这个声音的音高频率然后指挥一个电机伺服去自动拧动弦钮直到音高准确为止。这听起来简单但里面门道不少。首先你得让设备准确地“听”出弦的音高这涉及到信号处理和抗干扰其次电机拧弦的力道和精度要恰到好处不能拧断弦也不能拧不到位最后整个系统还得稳定、易用。原项目的分享者用Arduino Uno、一个DAOKI声音传感器、一个DF9GMS伺服电机和一个七段数码管就搭出了原型思路很正。但原帖更多是展示成果和基础步骤很多关键的细节比如怎么从嘈杂的音频里精准提取频率、伺服电机怎么控制才能平稳拧弦、程序里有哪些坑都说得比较简略。我这篇文章就打算结合我自己的折腾经验把这些坑一个个填上让你不仅能复现还能真正理解背后的原理甚至自己改进。2. 核心硬件选型与电路设计解析原项目给出的硬件清单比较基础但在实际动手前搞清楚每个部件为什么选它、以及有没有更好的选择能省下很多后期调试的麻烦。2.1 主控与传感器Arduino与麦克风模块主控选择Arduino Uno是入门级嵌入式项目的最优解。它开源、资料多、社区支持强大5V的工作电压也方便与大多数传感器模块对接。对于这个项目Uno的16MHz主频和2KB RAM处理音频FFT快速傅里叶变换是够用的但如果未来想增加更复杂的算法或显示界面可以考虑性能更强的Arduino Mega或ESP32。声音传感器是项目的“耳朵”。原项目用的DAOKI模块本质上是一个驻极体麦克风加了一个运算放大器电路输出的是模拟电压信号其幅值随声音强度变化。这里有个关键点这种模块输出的是音频信号的包络而不是原始的波形。对于判断“有没有声音”或者“声音大小”它很擅长但要做精确的音高频率分析它提供的信息是不够的因为频率信息蕴含在波形的周期性变化里而包络信号把这部分信息模糊掉了。注意这是原项目设计中的一个潜在瓶颈。为了实现可靠的音高检测更专业的做法是使用能输出原始模拟波形AUDIO OUT的模块例如MAX9814或INMP441I2S数字麦克风。MAX9814自带自动增益控制AGC能适应不同响度的弹奏而INMP441是数字接口抗干扰能力更强精度更高。如果坚持使用DAOKI这类简单模块后续的信号处理算法需要格外设计来补偿。2.2 执行机构DF9GMS 360度微型伺服电机执行机构选用了DF9GMS 360度连续旋转伺服电机。这和常见的180度位置伺服有本质区别。180度伺服你给一个角度信号它会转到并保持那个位置而360度连续伺服你给它的信号控制的是旋转速度和方向。信号脉宽在一个中间值比如1500μs时电机停止小于这个值向一个方向转大于则向反方向转差值越大转速越快。这正好符合调音的需求我们需要的是“拧动”这个动作而不是固定在一个角度。电机的供电需要特别注意。Arduino板载的5V稳压芯片通常只能提供500mA左右的电流而伺服电机在堵转或启动瞬间的电流可能超过1A。直接用Arduino给伺服供电轻则导致板子重启重则烧毁稳压芯片。因此必须为伺服电机准备独立的电源。原项目用了4节1.5V的AA电池组成6V电源这是正确的。要注意伺服的工作电压范围通常4.8V-6V和Arduino的5V逻辑电平要共地即把电池的负极和Arduino的GND连接在一起确保信号基准一致。2.3 人机交互与辅助电路七段数码管用于显示当前正在调哪根弦如显示数字1-6这比用几个LED指示灯更直观。原项目提到实际购买的数码管引脚排列可能与仿真软件不同这太常见了。拿到实物后第一件事就是用Arduino写个简单的段码测试程序逐个点亮各段确定引脚定义。按钮用于切换要调的音弦。这里建议使用硬件消抖或软件消抖。机械按钮在按下时触点会产生弹跳在几毫秒内产生多个通断信号会被单片机误判为多次按下。最简单的软件消抖就是在检测到按键按下后延时20-50毫秒再读取状态如果状态仍是按下则确认为一次有效动作。此外原项目提到了状态指示灯红/绿LED。我建议增加一个黄色LED作为“识别中”或“调整中”的状态指示让用户知道设备正在工作而不是死机了。整个系统的电路连接核心是确保动力电源伺服与逻辑电源Arduino、传感器分离但共地信号线连接正确。3. 核心算法从声音到频率的精准提取这是整个项目的技术核心也是最有挑战的部分。目标是从麦克风采集的模拟信号中计算出当前吉他弦振动的基频Fundamental Frequency。3.1 模拟信号采集与预处理Arduino Uno的ADC模数转换器精度是10位参考电压5V理论上最小能分辨约4.88mV的变化。我们将声音传感器的模拟输出接到一个模拟引脚如A0进行采样。首先需要确定采样频率。根据奈奎斯特采样定理要无失真地还原一个信号采样频率必须大于信号最高频率的两倍。吉他六根弦的标准音从低音E2约82.4Hz到高音E4约329.6Hz加上一些泛音我们关心的频率范围大概在80Hz到1000Hz。因此采样频率至少需要2kHz。但为了有更好的频率分辨率我们通常采样更快。Arduino Uno的ADC完成一次转换需要约100微秒因此理论最高采样频率约10kHz。在实际编程中我们可以通过analogRead()函数和定时器稳定在5kHz左右采样这对吉他调音来说足够了。采集到的原始数据是包含各种噪声环境噪声、电路噪声、手指摩擦声等的时域信号。直接进行FFT效果会很差。必须进行预处理直流偏置移除传感器输出可能有一个电压基准比如2.5V。我们需要减去这个平均值让信号以0为中心上下波动。// 假设采集了N个样本存入数组adc_buffer[N] long sum 0; for(int i0; iN; i) { sum adc_buffer[i]; } int dc_offset sum / N; // 计算直流分量 for(int i0; iN; i) { adc_buffer[i] - dc_offset; } // 移除直流偏置加窗处理FFT默认假设我们分析的信号段是无限长信号的一个周期片段。如果截取的不是整数个周期就会发生“频谱泄漏”导致频率能量分散到多个频点上。加窗函数如汉宁窗可以缓解这个问题。// 应用汉宁窗 for(int i0; iN; i) { float window 0.5 * (1 - cos(2*PI*i/(N-1))); adc_buffer[i] * window; }3.2 快速傅里叶变换FFT与基频查找预处理后的数据通过FFT转换到频域。Arduino处理FFT可以使用现成的库如arduinoFFT。这个库高效且易于使用。FFT之后我们会得到一个复数数组其中包含了各个频率分量的幅度和相位信息。我们通常只关心幅度谱。#include arduinoFFT.h arduinoFFT FFT arduinoFFT(); #define SAMPLES 256 // 必须是2的幂 double vReal[SAMPLES]; double vImag[SAMPLES]; // ... 采集数据并存入vRealvImag数组清零 ... FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HANN, FFT_FORWARD); // 加窗 FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD); // 计算FFT FFT.ComplexToMagnitude(vReal, vImag, SAMPLES); // 计算幅度谱现在vReal数组的前一半SAMPLES/2包含了从0到采样频率一半奈奎斯特频率的幅度信息。每个数组元素对应的频率可以通过公式计算频率 (索引号) * (采样频率) / (SAMPLES)。接下来是基频查找。吉他弦振动的声音不是单一频率它包含一个最强的基频和一系列整数倍的泛音谐波。最简单的找基频方法是寻找幅度谱中第一个突出的峰值在预期频率范围内。但环境噪声或传感器特性可能导致低频部分有杂讯。因此我们需要结合吉他弦的理论频率范围进行搜索。根据当前要调的音弦如A弦目标频率110Hz设定一个搜索范围如90Hz-130Hz。在vReal数组中找到该频率范围内幅度最大的点其对应的频率即为检测到的基频。进行谐波验证可选但推荐检查这个疑似基频的整数倍频率2倍、3倍附近是否也有明显的峰值。如果有那么它是基频的可能性就大大增加。这能有效避免将某个泛音误判为基频。3.3 调音逻辑与伺服控制策略检测到当前频率后就需要判断它是“偏低”Flat、“偏高”Sharp还是“正确”In Tune。我们不能要求频率完全等于目标值因为那几乎不可能。需要设定一个容差范围比如±5音分Cent。音分是音乐上衡量音高差的单位一个半音等于100音分。5音分的误差人耳几乎无法分辨是合理的调音标准。计算频率差以音分为单位的公式为音分数 1200 * log2(当前频率 / 目标频率)如果音分数 -5则音偏低需要拧紧琴弦伺服正向旋转。 如果音分数 5则音偏高需要放松琴弦伺服反向旋转。 如果在±5音分之内则调音正确停止电机点亮绿灯。伺服电机的控制需要讲究策略不能简单地“有偏差就全速拧”。这就像拧水龙头快接近时要慢一点否则容易拧过头。可以采用比例控制策略偏差越大电机转速越快偏差越小转速越慢。当偏差进入一个很小的范围如±10音分时让电机以极低的速度微调这样能实现平稳、精准的到位避免在目标值附近来回振荡。// 伪代码示例比例控制伺服速度 float error_in_cents calculate_cents(current_freq, target_freq); int servo_speed 90; // 停止的脉宽值例如1500us对应90 int dead_zone 5; // 死区单位音分 int max_adjust_zone 50; // 最大调整区间 if (abs(error_in_cents) dead_zone) { // 比例计算速度误差越大speed_offset越大 float speed_offset (error_in_cents / max_adjust_zone) * 30; // 30是最大速度偏移量 speed_offset constrain(speed_offset, -30, 30); // 限制范围 if (error_in_cents 0) { // 音低需要拧紧伺服值应小于90假设90是停止 servo_speed 90 - abs(speed_offset); } else { // 音高需要放松伺服值应大于90 servo_speed 90 abs(speed_offset); } // 将servo_speed映射到实际的伺服脉宽如1300us-1700us并输出 } else { // 误差在死区内停止电机点亮绿灯 servo_speed 90; digitalWrite(GREEN_LED, HIGH); }4. 机械结构设计与装配要点原项目用3D打印了底座Base和琴钮接口Peg Interface这是非常巧妙的思路。机械部分的核心要求是稳固和有效传递扭矩。4.1 底座与伺服固定底座的作用是固定伺服电机并提供整个设备在吉他上的支撑点。设计时需要考虑伺服舱位尺寸必须与DF9GMS伺服电机的外形紧密配合最好能卡住防止电机工作时自身转动。可以在舱体内侧设计一些肋条或卡槽。重心与防滑设备需要放置在吉他琴头或琴身上底座底部应增加橡胶垫或设计成防滑纹理防止在伺服电机反作用力下设备自己移动。底座的重量分布也要合理避免头重脚轻。线材管理预留走线槽或孔洞让伺服电机的三根长线原项目用了3英尺能整洁地引出连接到控制盒Arduino部分。4.2 琴钮接口设计与扭矩传递这是机械部分最关键的部件它直接与吉他的弦钮Tuning Peg耦合。吉他弦钮的造型多样有金属钮、塑料钮有标准式、封闭式。一个通用的设计挑战很大。原项目的“Peg Interface” likely是一个能与伺服电机输出轴通过伺服舵盘连接并套在吉他弦钮上的套筒。这里有几个经验材料与强度必须使用有足够强度和韧性的材料3D打印例如PETG或尼龙PA。PLA材料较脆在持续扭矩下可能断裂。内部形状套筒内部形状需要匹配常见弦钮的六角形或圆形。可以设计成可更换的适配头就像扳手套装一样以适应不同的吉他。最简单的通用方案是设计一个内部为正方形或六边形的套筒然后找一段尺寸匹配的金属套筒扳手嵌入其中利用金属的硬度和现成的形状来适配弦钮。连接可靠性与伺服舵盘的连接必须绝对牢固。除了使用螺丝紧固还可以在接口件上设计一个“D型孔”或带键槽的孔与伺服输出轴的形状匹配防止打滑。螺丝也要上紧并考虑在震动环境下加螺丝胶如Loctite 222。同心度伺服轴、接口件、吉他弦钮三者的中心轴应尽量对准。严重的不同心会导致额外的径向力加快磨损甚至卡死。在结构设计上要保证装配后的同心度。原项目中用折叠的硬纸块作为伺服电机与底座之间的填充物这是一个快速原型制作的妙招能消除空隙并减震。但在最终版本中应设计专门的、带弹性如硅胶的固定夹或垫片。5. 软件系统实现与代码剖析有了硬件和算法基础我们将它们整合成一个完整的、可运行的Arduino程序。程序需要高效地管理状态、处理输入、执行核心算法并控制输出。5.1 程序状态机与主循环设计一个好的嵌入式程序结构应该是清晰的状态机。对于AutoTona可以定义以下几个主要状态STATE_IDLE: 空闲状态等待用户按下按钮选择琴弦或开始调音。STATE_SELECT_STRING: 通过按钮循环选择要调的第几根弦1-6并在数码管上显示。STATE_LISTENING: 正在采集音频并分析频率。此时黄色LED闪烁。STATE_TUNING: 频率已识别且偏离目标正在控制伺服电机进行调音。红色LED左或右点亮。STATE_IN_TUNE: 音准在容差范围内调音完成。绿色LED常亮伺服停止。STATE_ERROR: 无法识别有效频率如环境太吵或未拨弦。所有LED闪烁提示。主循环loop()函数就围绕着这些状态进行切换和任务执行。// 状态定义 enum TunerState { IDLE, SELECT_STRING, LISTENING, TUNING, IN_TUNE, ERROR }; TunerState currentState IDLE; // 目标频率数组对应吉他6根弦的标准音高从高音E到低音E float targetFreq[6] {329.63, 246.94, 196.00, 146.83, 110.00, 82.41}; int currentString 0; // 当前选择的弦0对应第1弦高音E void loop() { switch(currentState) { case IDLE: // 检测按键切换到SELECT_STRING状态 break; case SELECT_STRING: // 处理按键改变currentString更新数码管显示 // 长时间无操作自动进入LISTENING状态 break; case LISTENING: // 采集音频运行FFT计算当前频率 // 如果成功得到可靠频率进入TUNING或IN_TUNE状态 // 如果失败进入ERROR状态 break; case TUNING: // 根据频率差用比例控制算法计算并输出伺服信号 // 每隔一定时间如200ms跳回LISTENING状态重新检测 break; case IN_TUNE: // 保持绿灯等待一段时间后自动返回IDLE或SELECT_STRING状态 break; case ERROR: // LED闪烁报警等待用户干预如重新拨弦后返回LISTENING break; } // 处理其他实时性要求不高的任务如按键消抖检测 handleButton(); }5.2 关键功能模块代码实现1. 频率检测函数 (detectFrequency)这是最核心的函数它封装了ADC采样、预处理、FFT和寻峰算法。float detectFrequency() { const int samplingFrequency 5000; // 5kHz采样率 const int samples 256; // 采样点数 static double vReal[samples]; static double vImag[samples] {0}; // 1. 采样 unsigned long startTime micros(); for (int i 0; i samples; i) { vReal[i] analogRead(MIC_PIN); vImag[i] 0; // 粗略的定时采样更精确的方法应使用定时器中断 while (micros() - startTime i * (1000000 / samplingFrequency)); } // 2. 预处理移除直流偏置、加窗 removeDCOffset(vReal, samples); applyHanningWindow(vReal, samples); // 3. 执行FFT FFT.Windowing(vReal, samples, FFT_WIN_TYP_HANN, FFT_FORWARD); FFT.Compute(vReal, vImag, samples, FFT_FORWARD); FFT.ComplexToMagnitude(vReal, vImag, samples); // 4. 寻峰在目标频率附近搜索 float targetFreq targetFreq[currentString]; float searchLow targetFreq * 0.85; // 搜索范围下限 float searchHigh targetFreq * 1.15; // 搜索范围上限 int lowBin (searchLow * samples) / samplingFrequency; int highBin (searchHigh * samples) / samplingFrequency; lowBin constrain(lowBin, 1, samples/2 -1); // 避开直流分量 highBin constrain(highBin, 1, samples/2 -1); int peakBin lowBin; for (int i lowBin; i highBin; i) { if (vReal[i] vReal[peakBin]) { peakBin i; } } // 5. 谐波验证简单版检查峰值是否显著高于噪声 float noiseFloor calculateNoiseFloor(vReal, samples, lowBin, highBin); if (vReal[peakBin] noiseFloor * 3) { // 信噪比太低 return -1.0; // 返回错误值 } // 6. 计算峰值对应频率 float detectedFreq (peakBin * samplingFrequency) / (float)samples; return detectedFreq; }2. 伺服控制函数 (controlServo)根据频率误差采用比例控制算法输出PWM信号。void controlServo(float currentFreq, float targetFreq) { const float deadZone 5.0; // 死区±5音分 const float maxZone 50.0; // 最大调整区间±50音分 const int servoStop 90; // 对应1500us停止 const int servoMin 70; // 对应1300us全速反转放松弦 const int servoMax 110; // 对应1700us全速正转上紧弦 float errorInCents 1200 * log2(currentFreq / targetFreq); if (fabs(errorInCents) deadZone) { // 音准了 myServo.write(servoStop); digitalWrite(GREEN_LED, HIGH); digitalWrite(RED_LED_LEFT, LOW); digitalWrite(RED_LED_RIGHT, LOW); currentState IN_TUNE; return; } // 比例计算 float speedRatio constrain(fabs(errorInCents) / maxZone, 0.0, 1.0); int speedOffset (int)(speedRatio * (servoMax - servoStop)); int servoPos; if (errorInCents 0) { // 音低需要上紧 servoPos servoStop speedOffset; digitalWrite(RED_LED_LEFT, HIGH); // 点亮“偏低”红灯 digitalWrite(RED_LED_RIGHT, LOW); } else { // 音高需要放松 servoPos servoStop - speedOffset; digitalWrite(RED_LED_RIGHT, HIGH); // 点亮“偏高”红灯 digitalWrite(RED_LED_LEFT, LOW); } servoPos constrain(servoPos, servoMin, servoMax); myServo.write(servoPos); digitalWrite(GREEN_LED, LOW); }5.3 系统优化与高级功能拓展基础功能实现后可以考虑以下优化自动增益控制AGC在detectFrequency函数中动态调整ADC采样的参考值或对采样值进行缩放确保不同响度的弹奏信号都能较好地利用ADC的量程。移动平均滤波对连续几次检测到的频率值进行移动平均可以平滑掉偶然的误差使读数更稳定。学习模式增加一个按钮进入学习模式。用户手动将吉他调到标准音然后让设备读取每根弦的频率并存储到EEPROM中。这样就能适配非标准调弦或一些特殊调式的吉他。电池电量检测通过分压电路检测伺服电池电压当电压过低时在数码管上显示警告防止因电量不足导致电机扭矩不够调音不准。6. 系统集成、调试与实战心得把所有硬件和软件拼装在一起才是挑战的开始。调试过程就是不断发现问题、解决问题的循环。6.1 分阶段集成与测试不要试图一次性连接所有部件并期望它工作。建议分阶段进行核心控制测试先不接伺服和麦克风。只连接Arduino、按钮和数码管。编写程序测试按钮切换琴弦选择并在数码管上正确显示。确保人机交互基础功能正常。伺服电机测试单独连接伺服电机到独立电源和Arduino。写一个简单程序让伺服按指令正转、反转、停止。观察其运转是否平滑扭矩是否足够可以空载测试再试着轻轻用手指捏住舵盘测试。特别注意一定要确保机械结构安装牢固后再让电机带载运行否则可能打滑或损坏部件。频率检测测试连接麦克风模块。在安静环境下用手机或电脑播放标准频率的声音如440Hz的A音运行频率检测程序并通过串口监视器打印出检测到的频率。调整代码中的参数如采样点数、寻峰范围直到能稳定、准确地检测出播放的频率。闭环系统联调将所有部件连接。选择一根弦手动将其调至明显不准。运行完整程序观察设备能否正确识别状态亮哪个灯伺服能否正确启动。首次联调时建议用手轻轻扶着伺服接口件感受其转动方向和力度随时准备断电防止因程序逻辑错误导致电机朝错误方向猛转拉断琴弦。6.2 常见问题排查速查表在实际调试中你几乎一定会遇到下面这些问题。这里提供一个快速排查指南问题现象可能原因排查步骤与解决方案伺服电机不转或抽搐1. 电源功率不足。2. 信号线接触不良或接错。3. 程序PWM信号输出引脚不对或库未正确初始化。1. 用万用表测量伺服电源电压带载时是否跌落到4.5V以下更换容量更大的电池或改用稳压电源。2. 检查接线信号线黄/橙接Arduino PWM引脚红线接电池正极黑/棕线接电池负极并与Arduino GND相连。3. 确认代码中Servo.attach()的引脚号与实际一致。尝试用Servo.write()输出几个固定值如45, 90, 135测试。频率检测始终不准或跳变1. 环境噪音太大。2. 麦克风模块不适用于频率检测。3. FFT参数采样率、点数设置不当。4. 寻峰算法范围不对或未做谐波验证。1. 移至安静环境测试。尝试对着麦克风轻轻吹气或发出稳定的“啊——”声看频率值是否稳定。2. 换用MAX9814等带原始输出的模块测试。3. 通过串口打印原始ADC值观察波形。调整samplingFrequency和samples权衡频率分辨率和实时性。4. 打印整个幅度谱观察目标频率附近是否有明显峰值。检查searchLow和searchHigh计算是否正确。调音过程在目标值附近振荡1. 伺服电机惯性或机械回差。2. 控制算法过于激进比例系数太大。3. 频率检测更新太慢反馈延迟。1. 在机械连接处减少旷量。在控制算法中加入“死区”Dead Zone当误差很小时不动作。2. 减小比例控制中的speedOffset计算系数让微调阶段更柔和。3. 优化代码提高detectFrequency函数的执行速度或降低其调用频率但每次调用后立即调整伺服。设备识别错误琴弦1. 当前环境中有其他相近频率的干扰音。2. 琴弦老化或品质差泛音过强基频反而不突出。1. 在detectFrequency函数中加强谐波验证逻辑。例如要求检测到的峰值频率的2倍频处也有一定幅度的峰值。2. 更换一根新琴弦测试。在软件中可以尝试在寻峰前对频谱进行加权提升低频部分的权重。数码管显示乱码或不亮1. 引脚连接错误。2. 限流电阻缺失或太大。3. 共阴/共阳类型与代码驱动方式不匹配。1. 使用万用表通断档或简单程序逐个点亮各段确定引脚定义。2. 数码管每段LED通常需要5-20mA电流直接接5V需串联220Ω-1kΩ电阻。3. 确认数码管是共阴Common Cathode还是共阳Common Anode代码中给段码和位码的电平要相反。6.3 实战心得与进阶建议踩过这些坑之后我总结出几条宝贵的经验电源是王道伺服电机的独立电源一定要给力。我后来改用了一节18650锂电池配一个降压模块调到6V比AA电池组持久稳定得多。在电机电源和Arduino的5V之间可以加一个大容量如100μF的电解电容能有效吸收电机启停产生的电压波动防止单片机复位。机械适配是难点3D打印的琴钮接口很难做到通用。最好的办法是准备一套不同尺寸的金属六角套筒然后用一个强力的磁性联轴器或弹性联轴器连接到伺服舵盘上。这样既能适配多种弦钮又能缓冲一些不同心带来的应力保护电机轴。软件滤波是关键在detectFrequency函数返回最终结果前对连续5-10次的检测结果取中值滤波或均值滤波能极大提升稳定性。虽然牺牲了一点速度但调音不是毫秒级响应的应用稳定性更重要。用户体验细节给“调音完成”状态加一个短暂的提示音可以用无源蜂鸣器比只看绿灯更直观。在STATE_ERROR状态可以让数码管显示“E”而不是简单的闪烁让用户更清楚问题。这个项目做下来远不止是调准了一把吉他。它是对嵌入式系统开发全流程的一次绝佳实践从需求分析、方案选型、电路设计、机械建模、算法实现到软硬件联调、问题排查。当你终于听到伺服电机嗡嗡作响把一根跑调的弦自动拉回标准音高并且绿灯亮起的那一刻那种成就感是任何现成产品都无法给予的。它也许外观粗糙但每一个环节都凝聚了你的思考和汗水这才是Maker精神的真谛。