# EDA-Piano简易电子琴🎹 文档:https://wiki.yang17.top/products/oshwhub/oshw-837f0514ff5f4ee795ad673a9fc98149/ 项目来源:https://oshwhub.com/course-examples/eda-piano2 分类:硬件项目 更新时间:2026-08-31T02:44:35.000Z 本项目基于ESP8266模组开发,是一款物联网电子琴,支持可视化教学及可视化播放等。 资料超详细 👇 项目介绍 硬件设计 软件设计 3D结构 烧录说明 附件下载 硬件工程 项目介绍 本项目基于ESP8266模组构建,是一个支持12键触摸检测的智能钢琴,具备OLED显示、Web控制、教学模式等功能。采用SC12B触摸芯片实现高精度多键检测,支持和弦演奏和混音播放。 本项目的硬件成本来说应该是比较低的,主要的部分就是ESP8266和SC12B,TP4056不需要也可以删掉。 项目功能 ESP8266具有强大的 WiFi 功能和丰富的 GPIO 接口,本项目充分利用这些特性,实现了一个功能完整的智能钢琴系统。通过 SC12B 触摸芯片实现 12 键同时检测,结合蜂鸣器音频输出和 OLED 显示,为用户提供完整的钢琴演奏体验。 固件功能 ✅ 支持12键同时触摸检测,实现和弦演奏 ✅ OLED实时显示当前按键和音符信息 ✅ Web界面远程控制,支持手机操作 ✅ 教学模式,内置小星星、两只老虎等经典曲目,OLED预览教学 ✅ 混音播放,支持多音符同时发声 ✅ 可调节音符持续时间、八度偏移、触摸灵敏度 ✅ 自动播放功能,可播放预设曲目 项目参数 采用ESP8266作为主控,内置WiFi功能 SC12B触摸芯片,支持12路电容触摸检测 128x32 OLED显示屏,实时显示演奏信息 无源蜂鸣器音频输出,支持多音符混音 支持5个八度音域,共60个音符 Web界面支持响应式设计,适配手机和电脑 硬件设计 主控电路 主控选用ESP8266,集成WiFi功能,提供丰富的GPIO接口。板载USB转串口芯片,方便程序下载和调试。 Flash:4MB 触摸检测电路 采用SC12B触摸检测芯片,支持12路电容触摸检测,并且自带消抖处理,支持持自动校正,2.5V ~ 6.0V 宽电压。 通过I2C接口与主控通信,可同时检测多个按键按下状态,实现和弦演奏功能。 检测通道:12路 通信接口:I2C 检测精度:高精度电容检测 响应时间:<10ms 支持同时多键检测 原理图 PCB设计 触控PAD正面 在电容触控的PCB设计中为了使其有较强的抗干扰能力,本项目触控PAD与铺地间距控制在1.5mm,使其有效平衡系统抗干扰度和触控灵敏度。 触控PAD背面 在电容触控PAD的背面做了镂空处理,减少寄生电容,改善灵敏度,在触控区和主电路区域放置地过孔隔离。 走线规则 对于相邻触摸信号线距离及铺地距离设置在15mil,避免串扰 对于触控信号线走线线宽设置为5mil 所有信号线均不跨越其他信号线,走线周围0.5mm内不走其他信号线 如果想让触控延时尽量保持一致,还可为每条触控信号线设置等长处理 封装 钢琴键已设计成封装,方便引用。 丝印部分参考:https://oshwhub.com/47uF/mini_piano 工程修改 显示电路 使用128x32像素的OLED显示屏,通过I2C接口连接。实时显示当前按键状态、音符信息、模式状态等。支持图形和文字混合显示。 分辨率:128x32像素 驱动芯片:SSD1306 通信接口:I2C 显示内容:钢琴键盘、音符、状态信息 音频输出电路 使用无源蜂鸣器作为音频输出设备,通过PWM信号驱动。支持不同频率的音符输出,可实现混音播放效果。 输出设备:无源蜂鸣器 驱动方式:PWM 音域范围:5个八度 支持功能:单音、和弦、混音 电池充电电路 使用TP4056锂电池充电芯片。 稳压电路 使用TP4056锂电池充电芯片。 接口 使用TP4056锂电池充电芯片。 软件开发 开发环境 软件环境:VSCode + PlatformIO 开发语言:C/C++ 框架:Arduino Framework 依赖库 通过以下开源库协助本项目开发 Adafruit SSD1306:用于OLED显示屏驱动 ESPAsyncWebServer:用于Web服务器功能 ArduinoJson:用于JSON数据处理 SC12B:基于liuquanli1970/SC12B开源库修改适配 模块化 本项目采用模块化设计,各功能模块独立开发,便于维护和扩展 audio.h/.cpp:音频播放和混音处理 display.h/.cpp:OLED显示控制 touch.h/.cpp:触摸检测和多键处理 network.h/.cpp:WiFi和Web服务器 music.h/.cpp:曲谱数据和播放控制 SC12B.h/.cpp:触摸芯片驱动 config.h:系统配置参数 main.cpp:主程序入口 使用方法 基础功能 触摸演奏: 直接触摸对应按键演奏 OLED实时预览:预览按下的按键 Web 界面功能 虚拟琴键: 支持web端远控弹奏 歌曲演奏: 支持对预载歌曲的音乐演奏功能 教学模式: 支持预载歌曲的钢琴弹奏教学,会在OLED屏实时反馈。 音频参数调节: 音符时长、八度偏移、灵敏度调节。 系统配置 (config.h) // 硬件引脚定义 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 32 #define TOUCH_INTERRUPT_PIN 14 #define BUZZER_PIN 12 // 网络配置 #define AP_SSID "EDA-Piano" #define AP_PASSWORD "" // 音频参数 #define DEFAULT_NOTE_DURATION 500 #define DEFAULT_OCTAVE_SHIFT 0 触摸控制接口选择 SC12B一共支持两种控制方法,分别是I2C控制和BCD端口输出,BCD端口输出很简单,只需要ADC检查电压就可以,但BCD的缺点也很明显,四个接口要配置四个不常见的电阻,而且只能单向输出触控信号,无法深入控制IC,并且BCD产生的模拟电压也无法检查多点触控,显然不符合本项目要求。因此这里我们选择IIC控制。 SC12B驱动库移植 基于liuquanli1970/SC12B提供的库移植修改 SC12B.h 这里我们将writeRegister移动到public公开,方便外部调用。 public: bool writeRegister(uint8_t reg, uint8_t value); SC12B.cpp 这里我们将begin函数中内容修改成如下,使用默认地址和默认IIC。 void SC12B::begin() { Wire.begin(); } 寄存器列表 触摸灵敏度调节 依照数据手册提供的寄存器配置,我们在软件端设置了16个触控等级 // 应用触摸灵敏度设置到SC12B芯片 void applyTouchSensitivity() { Sensitivity sensitivityLevel; switch(touchSensitivity) { case 0: sensitivityLevel = LEVEL0; break; case 1: sensitivityLevel = LEVEL1; break; case 2: sensitivityLevel = LEVEL2; break; case 3: sensitivityLevel = LEVEL3; break; case 4: sensitivityLevel = LEVEL4; break; case 5: sensitivityLevel = LEVEL5; break; case 6: sensitivityLevel = LEVEL6; break; case 7: sensitivityLevel = LEVEL7; break; case 8: sensitivityLevel = LEVEL8; break; case 9: sensitivityLevel = LEVEL9; break; case 10: sensitivityLevel = LEVEL10; break; case 11: sensitivityLevel = LEVEL11; break; case 12: sensitivityLevel = LEVEL12; break; case 13: sensitivityLevel = LEVEL13; break; case 14: sensitivityLevel = LEVEL14; break; case 15: sensitivityLevel = LEVEL15; break; default: sensitivityLevel = LEVEL0; break; } touchPannel.writeRegister(REG_Senset0, sensitivityLevel); touchPannel.writeRegister(REG_SensetCOM, sensitivityLevel); } touchPannel.writeRegister(REG_Senset0, sensitivityLevel); 写入传感器0的灵敏度寄存器 touchPannel.writeRegister(REG_SensetCOM, sensitivityLevel); 写入公共传感器的灵敏度寄存器 寄存器定义 (在 SC12B.h 中) // 寄存器地址定义 #define REG_Senset0 0x00 // 传感器0灵敏度寄存器 #define REG_SensetCOM 0x01 // 公共传感器灵敏度寄存器 // 灵敏度等级枚举 typedef enum { LEVEL0 = 0x04, // 最低灵敏度 LEVEL1 = 0x15, LEVEL2 = 0x25, // ... 其他等级 LEVEL15 = 0xFF // 最高灵敏度 } Sensitivity; 而在硬件中还有一个可调电容可以调节灵敏度 触摸检测 对于触控的检查我们将使用上面的引脚,地址参考地址选择说明,本项目中ASEL浮空,INT用于中断检测,当INT触发硬件中断则说明有按键被触摸,此时发送IIC轮询找到对应通道即可。 /* ========== 中断驱动的触摸检测 ========== */ if (iftouch) { iftouch = false; unsigned long currentTime = millis(); if (currentTime - lastSampleTime > SAMPLE_INTERVAL) { uint16_t keyValue = detectMultipleKeys(); if (keyValue != previousKeys) { currentKeys = keyValue; previousKeys = keyValue; lastKeyTime = currentTime; int pressedKeys[12]; int keyCount = 0; parseKeys(keyValue, pressedKeys, &keyCount); // 教学模式处理 if (teachingMode && keyCount > 0) { int* melody = getCurrentMelody(); int melodyCount = getCurrentMelodyCount(); int expectedNote = melody[currentNoteIndex]; if (expectedNote == 0) { // 跳过休止符 currentNoteIndex++; if (currentNoteIndex < melodyCount) { expectedNote = melody[currentNoteIndex]; } } if (keyCount == 1 && pressedKeys[0] == expectedNote) { // 按对了 currentNoteIndex++; if (currentNoteIndex >= melodyCount) { showTeachingMode(0, true, "Complete!"); teachingMode = false; } else { int nextNote = melody[currentNoteIndex]; if (nextNote == 0 && currentNoteIndex + 1 < melodyCount) { currentNoteIndex++; nextNote = melody[currentNoteIndex]; } showTeachingMode(nextNote, true, "Good!"); } } else { // 按错了 showTeachingMode(expectedNote, false, "Error!"); } } else if (teachingMode) { // 教学模式但没有按键,显示下一个要按的键 int* melody = getCurrentMelody(); int melodyCount = getCurrentMelodyCount(); int nextNote = melody[currentNoteIndex]; if (nextNote == 0 && currentNoteIndex + 1 < melodyCount) { currentNoteIndex++; nextNote = melody[currentNoteIndex]; } showTeachingMode(nextNote, false, ""); } else { // 正常模式 displayMultipleKeys(pressedKeys, keyCount); } if (keyCount > 0) { playMultipleNotes(pressedKeys, keyCount); } else { stopAllAudio(); } } lastSampleTime = currentTime; } } 主机发送: START -> 0x40(写) -> ACK -> 0x08(REG_OUTPUT1) -> ACK -> RESTART -> 0x41(读) -> ACK 从机响应: DATA1 -> ACK -> STOP 多次采样: 连续5次轮询避免单次采样的不稳定性 1ms间隔确保采样的时间分散性 防抖处理: 20ms防抖时间避免按键抖动 只有状态真正改变才处理 HTML页面显示 这部分不细讲了,和前面EDA-Robot的项目的基本一致,移植过来的, 通过路由创建RESTful API,然后页面按钮触发发get/post,由路由监听到后执行对应任务。 void setupWebServer() { // 主页面 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/html", generateMainPage()); }); // 状态API server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request){ String json = "{\"duration\":" + String(noteDuration) + ",\"octave\":" + String(octaveShift) + ",\"sensitivity\":" + String(touchSensitivity) + ",\"wifi\":" + String(wifiConnected ? "true" : "false") + "}"; request->send(200, "application/json", json); }); // 播放音符API for(int i = 1; i <= 12; i++) { String path = "/play/" + String(i); server.on(path.c_str(), HTTP_GET, [i](AsyncWebServerRequest *request){ webCommand = i; webCommandPending = true; request->send(200, "text/plain", "OK"); }); } // 和弦API server.on("/chord/1,5,8", HTTP_GET, [](AsyncWebServerRequest *request){ webCommand = 201; webCommandPending = true; request->send(200, "text/plain", "OK"); }); server.on("/chord/6,10,1", HTTP_GET, [](AsyncWebServerRequest *request){ webCommand = 202; webCommandPending = true; request->send(200, "text/plain", "OK"); }); server.on("/chord/8,12,3", HTTP_GET, [](AsyncWebServerRequest *request){ webCommand = 203; webCommandPending = true; request->send(200, "text/plain", "OK"); }); // 歌曲播放API server.on("/song/play", HTTP_GET, [](AsyncWebServerRequest *request){ if (request->hasParam("id")) { int songId = request->getParam("id")->value().toInt(); setCurrentSong(songId); webCommand = 300; webCommandPending = true; request->send(200, "text/plain", "OK"); } else { request->send(400, "text/plain", "Missing song ID"); } }); // 教学模式API server.on("/teaching/start", HTTP_GET, [](AsyncWebServerRequest *request){ if (request->hasParam("id")) { int songId = request->getParam("id")->value().toInt(); setCurrentSong(songId); webCommand = 400; webCommandPending = true; request->send(200, "text/plain", "OK"); } else { request->send(400, "text/plain", "Missing song ID"); } }); // 控制命令 server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request){ webCommand = 100; webCommandPending = true; request->send(200, "text/plain", "OK"); }); // 设置API server.on("/set/duration", HTTP_GET, [](AsyncWebServerRequest *request){ if (request->hasParam("value")) { int dur = request->getParam("value")->value().toInt(); if (dur >= 100 && dur <= 2000) { noteDuration = dur; request->send(200, "text/plain", "OK"); } else { request->send(400, "text/plain", "Invalid duration"); } } else { request->send(400, "text/plain", "Missing value parameter"); } }); server.on("/set/octave", HTTP_GET, [](AsyncWebServerRequest *request){ if (request->hasParam("value")) { int oct = request->getParam("value")->value().toInt(); if (oct >= -2 && oct <= 2) { octaveShift = oct; request->send(200, "text/plain", "OK"); } else { request->send(400, "text/plain", "Invalid octave"); } } else { request->send(400, "text/plain", "Missing value parameter"); } }); server.on("/set/sensitivity", HTTP_GET, [](AsyncWebServerRequest *request){ if (request->hasParam("value")) { int sens = request->getParam("value")->value().toInt(); if (sens >= 0 && sens <= 15) { touchSensitivity = sens; applyTouchSensitivity(); request->send(200, "text/plain", "OK"); } else { request->send(400, "text/plain", "Invalid sensitivity"); } } else { request->send(400, "text/plain", "Missing value parameter"); } }); server.begin(); } 安装结构 项目采用的是双外壳结构,由前盖、后盖构成 前盖 正面 背面 前盖板内部空间很足,可以放下喇叭和电池 圆角处理 为typeC开槽 后盖 正面 背面 后盖内部空间较足,可以放下电池 圆角处理 为PCB设置基座 实物展示 实物图 拆解图 教学模式-正反馈 教学模式-负反馈 拓展方向 本项目提供了一个完整的简易电子琴方案,你可以基于此项目进行以下拓展: 增加更多内置曲目和教学内容 支持MIDI输出,播放加钢琴教学 支持自定义音色和乐器声音 添加节拍器和调音器功能 添加功放或大喇叭扩音量 添加TF卡存储歌曲 其实我本来想做MIDI这个的,因为MIDI格式是可以映射到琴键的,可以接个max98357,然后上位机解析MIDI到json格式,再去把数据处理就能映射了,这样你就能直接导入MIDI格式的音乐去学习弹奏。 "tracks": [ { "startTime": 0, "duration": 0, "length": 0, "notes": [], "controlChanges": {}, "id": 0 }, { "startTime": 1.6640625, "duration": 197.63932291666669, "length": 746, "notes": [ { "name": "G1", "midi": 31, "time": 1.6640625, "velocity": 0.5118110236220472, "duration": 2.2604166666666665 }, { "name": "G2", "midi": 43, "time": 1.6653645833333333, "velocity": 0.5118110236220472, "duration": 2.260416666666667 }, { "name": "G3", "midi": 55, "time": 1.6770833333333333, "velocity": 0.4409448818897638, "duration": 2.26171875 }, { "name": "D3", "midi": 50, "time": 2.2109375, "velocity": 0.5826771653543307, "duration": 1.7278645833333335 }, { "name": "B3", "midi": 59, "time": 2.7877604166666665, "velocity": 0.6850393700787402, "duration": 1.143229166666667 }, 比如这组midi解析的数据,我们可以得到 G1 (midi: 31) - 低音G G2 (midi: 43) - 中音G G3 (midi: 55) - 高音G D3 (midi: 50) - D音 B3 (midi: 59) - B音 这就能映射到琴键上了,但我们这里的项目主要是入门为主,所以这里我给大家提供一个思路,大家可以自己去实现,如果后续有空的话可以出个pro版,当然如果你有更好的方法也可以自己尝试。 注意事项 烧录部分可以参考 EDA-Robot机器狗-烧录教程 除了触摸外,手尽量不要触碰到PCB下半部分,避免干扰触摸信号,建议装好外壳使用 如果声音太小建议更换喇叭或添加功放,外壳安装时把喇叭粘在外壳透声孔上。 如果想降低成本可以去掉TP4056充电电路,TypeC的5V直接接到LDO输入上。喇叭也可换成蜂鸣器替代,不需要显示也可以把屏幕去掉。