站内搜索
发作品签到
EDA-Piano简易电子琴🎹
专业版

EDA-Piano简易电子琴🎹

简介

本项目基于ESP8266模组开发,是一款物联网电子琴,支持可视化教学及可视化播放等。

简介:本项目基于ESP8266模组开发,是一款物联网电子琴,支持可视化教学及可视化播放等。
复刻成本:20

开源协议

GPL 3.0

创建时间:2025-08-14 17:37:00更新时间:2025-11-19 09:53:39

描述

项目介绍
本项目基于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

image.png

image.png

触摸检测电路

采用SC12B触摸检测芯片,支持12路电容触摸检测,并且自带消抖处理,支持持自动校正,2.5V ~ 6.0V 宽电压。
通过I2C接口与主控通信,可同时检测多个按键按下状态,实现和弦演奏功能。

  • 检测通道:12路
  • 通信接口:I2C
  • 检测精度:高精度电容检测
  • 响应时间:<10ms
  • 支持同时多键检测

原理图

image.png

PCB设计

触控PAD正面

在电容触控的PCB设计中为了使其有较强的抗干扰能力,本项目触控PAD与铺地间距控制在1.5mm,使其有效平衡系统抗干扰度和触控灵敏度。

image.png
image.png

触控PAD背面

在电容触控PAD的背面做了镂空处理,减少寄生电容,改善灵敏度,在触控区和主电路区域放置地过孔隔离。
image.png

走线规则

对于相邻触摸信号线距离及铺地距离设置在15mil,避免串扰
image.png

image.png
对于触控信号线走线线宽设置为5mil
image.png
所有信号线均不跨越其他信号线,走线周围0.5mm内不走其他信号线
image.png
如果想让触控延时尽量保持一致,还可为每条触控信号线设置等长处理

封装

钢琴键已设计成封装,方便引用。
image.png
丝印部分参考:https://oshwhub.com/47uF/mini_piano 工程修改

显示电路
使用128x32像素的OLED显示屏,通过I2C接口连接。实时显示当前按键状态、音符信息、模式状态等。支持图形和文字混合显示。
  • 分辨率:128x32像素
  • 驱动芯片:SSD1306
  • 通信接口:I2C
  • 显示内容:钢琴键盘、音符、状态信息

image.png

音频输出电路
使用无源蜂鸣器作为音频输出设备,通过PWM信号驱动。支持不同频率的音符输出,可实现混音播放效果。
  • 输出设备:无源蜂鸣器
  • 驱动方式:PWM
  • 音域范围:5个八度
  • 支持功能:单音、和弦、混音

image.png

电池充电电路
使用TP4056锂电池充电芯片。

image.png

稳压电路
使用TP4056锂电池充电芯片。

image.png

接口
使用TP4056锂电池充电芯片。

image.png

image.png

软件开发

开发环境
  • 软件环境: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:主程序入口

使用方法

基础功能
  1. 触摸演奏: 直接触摸对应按键演奏
  2. OLED实时预览:预览按下的按键

2ab39e3e-1fa7-445c-b9ee-6079e7b7a51a.jpg

Web 界面功能
  1. 虚拟琴键: 支持web端远控弹奏
  2. 歌曲演奏: 支持对预载歌曲的音乐演奏功能
  3. 教学模式: 支持预载歌曲的钢琴弹奏教学,会在OLED屏实时反馈。
  4. 音频参数调节: 音符时长、八度偏移、灵敏度调节。

392cd09b-71a1-4690-9a5d-464370439b82.jpg

系统配置 (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控制。
image.png
image.png

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();
}

寄存器列表

image.png

触摸灵敏度调节

image.png
依照数据手册提供的寄存器配置,我们在软件端设置了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;

而在硬件中还有一个可调电容可以调节灵敏度
image.png

触摸检测

image.png
image.png
对于触控的检查我们将使用上面的引脚,地址参考地址选择说明,本项目中ASEL浮空,INT用于中断检测,当INT触发硬件中断则说明有按键被触摸,此时发送IIC轮询找到对应通道即可。

/* ========== 中断驱动的触摸检测 ========== */
  if (iftouch) {
    iftouch = false;
    
    unsigned long currentTime = millis();
    
    if (currentTime - lastSampleTime &gt; SAMPLE_INTERVAL) {
      uint16_t keyValue = detectMultipleKeys();
      
      if (keyValue != previousKeys) {
        
        currentKeys = keyValue;
        previousKeys = keyValue;
        lastKeyTime = currentTime;
        
        int pressedKeys[12];
        int keyCount = 0;
        
        parseKeys(keyValue, pressedKeys, &amp;keyCount);
        
        // 教学模式处理
        if (teachingMode &amp;&amp; keyCount &gt; 0) {
          int* melody = getCurrentMelody();
          int melodyCount = getCurrentMelodyCount();
          int expectedNote = melody[currentNoteIndex];
          if (expectedNote == 0) {
            // 跳过休止符
            currentNoteIndex++;
            if (currentNoteIndex &lt; melodyCount) {
              expectedNote = melody[currentNoteIndex];
            }
          }
          
          if (keyCount == 1 &amp;&amp; pressedKeys[0] == expectedNote) {
            // 按对了
            currentNoteIndex++;
            if (currentNoteIndex &gt;= melodyCount) {
              showTeachingMode(0, true, "Complete!");
              teachingMode = false;
            } else {
              int nextNote = melody[currentNoteIndex];
              if (nextNote == 0 &amp;&amp; currentNoteIndex + 1 &lt; 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 &amp;&amp; currentNoteIndex + 1 &lt; melodyCount) {
            currentNoteIndex++;
            nextNote = melody[currentNoteIndex];
          }
          showTeachingMode(nextNote, false, "");
        } else {
          // 正常模式
          displayMultipleKeys(pressedKeys, keyCount);
        }
        
        if (keyCount &gt; 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-&gt;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-&gt;send(200, "application/json", json);
  });

  // 播放音符API
  for(int i = 1; i &lt;= 12; i++) {
    String path = "/play/" + String(i);
    server.on(path.c_str(), HTTP_GET, [i](AsyncWebServerRequest *request){ 
      webCommand = i; 
      webCommandPending = true; 
      request-&gt;send(200, "text/plain", "OK"); 
    });
  }

  // 和弦API
  server.on("/chord/1,5,8", HTTP_GET, [](AsyncWebServerRequest *request){ webCommand = 201; webCommandPending = true; request-&gt;send(200, "text/plain", "OK"); });
  server.on("/chord/6,10,1", HTTP_GET, [](AsyncWebServerRequest *request){ webCommand = 202; webCommandPending = true; request-&gt;send(200, "text/plain", "OK"); });
  server.on("/chord/8,12,3", HTTP_GET, [](AsyncWebServerRequest *request){ webCommand = 203; webCommandPending = true; request-&gt;send(200, "text/plain", "OK"); });

  // 歌曲播放API
  server.on("/song/play", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request-&gt;hasParam("id")) {
      int songId = request-&gt;getParam("id")-&gt;value().toInt();
      setCurrentSong(songId);
      webCommand = 300; webCommandPending = true; 
      request-&gt;send(200, "text/plain", "OK");
    } else {
      request-&gt;send(400, "text/plain", "Missing song ID");
    }
  });

  // 教学模式API
  server.on("/teaching/start", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request-&gt;hasParam("id")) {
      int songId = request-&gt;getParam("id")-&gt;value().toInt();
      setCurrentSong(songId);
      webCommand = 400; webCommandPending = true; 
      request-&gt;send(200, "text/plain", "OK");
    } else {
      request-&gt;send(400, "text/plain", "Missing song ID");
    }
  });

  // 控制命令
  server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request){
    webCommand = 100; webCommandPending = true; request-&gt;send(200, "text/plain", "OK");
  });

  // 设置API
  server.on("/set/duration", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request-&gt;hasParam("value")) {
      int dur = request-&gt;getParam("value")-&gt;value().toInt();
      if (dur &gt;= 100 &amp;&amp; dur &lt;= 2000) {
        noteDuration = dur;
        request-&gt;send(200, "text/plain", "OK");
      } else {
        request-&gt;send(400, "text/plain", "Invalid duration");
      }
    } else {
      request-&gt;send(400, "text/plain", "Missing value parameter");
    }
  });

  server.on("/set/octave", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request-&gt;hasParam("value")) {
      int oct = request-&gt;getParam("value")-&gt;value().toInt();
      if (oct &gt;= -2 &amp;&amp; oct &lt;= 2) {
        octaveShift = oct;
        request-&gt;send(200, "text/plain", "OK");
      } else {
        request-&gt;send(400, "text/plain", "Invalid octave");
      }
    } else {
      request-&gt;send(400, "text/plain", "Missing value parameter");
    }
  });

  server.on("/set/sensitivity", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request-&gt;hasParam("value")) {
      int sens = request-&gt;getParam("value")-&gt;value().toInt();
      if (sens &gt;= 0 &amp;&amp; sens &lt;= 15) {
        touchSensitivity = sens;
        applyTouchSensitivity();
        request-&gt;send(200, "text/plain", "OK");
      } else {
        request-&gt;send(400, "text/plain", "Invalid sensitivity");
      }
    } else {
      request-&gt;send(400, "text/plain", "Missing value parameter");
    }
  });

  server.begin();
}

安装结构

项目采用的是双外壳结构,由前盖、后盖构成

4f72522b-bb94-4589-b399-9d94545a10c4.png

前盖
正面背面
536385cd-3758-4292-8275-cade80bb6a28.png4119d78d-283d-4c9b-ac5c-8e36f6c3efbe.png
  • 前盖板内部空间很足,可以放下喇叭和电池
  • 圆角处理
  • 为typeC开槽
后盖
正面背面
d07afdbe-6c5b-4db7-853a-37b35007d90e.png79dfd6c6-1ab5-478e-8af9-070ee1af798e.png
  • 后盖内部空间较足,可以放下电池
  • 圆角处理
  • 为PCB设置基座
实物展示
实物图拆解图
096cd278-3e7a-4235-ad71-9202227c8af4.jpga2a600e6-c4a7-4a1e-9af5-45c2b90ed5a0.jpg
教学模式-正反馈教学模式-负反馈
6c536b08-25fa-4995-86b9-0262f342b7bc.jpg1da6fa0d-8211-49be-b5d4-96ecda7b5547.jpg
拓展方向
本项目提供了一个完整的简易电子琴方案,你可以基于此项目进行以下拓展:
  • 增加更多内置曲目和教学内容
  • 支持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版,当然如果你有更好的方法也可以自己尝试。

注意事项
  1. 烧录部分可以参考 EDA-Robot机器狗-烧录教程
  2. 除了触摸外,手尽量不要触碰到PCB下半部分,避免干扰触摸信号,建议装好外壳使用
  3. 如果声音太小建议更换喇叭或添加功放,外壳安装时把喇叭粘在外壳透声孔上。
  4. 如果想降低成本可以去掉TP4056充电电路,TypeC的5V直接接到LDO输入上。喇叭也可换成蜂鸣器替代,不需要显示也可以把屏幕去掉。

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

3D模型

序号文件名称下载次数
1
bt.STL
73
2
top.STL
51

附件

序号文件名称下载次数
1
演示视频.mp4
186
2
eda-piano资源包.zip
291
克隆工程
添加到专辑
0
0
分享
侵权投诉
知识产权声明&复刻说明

本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。

请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。

底部导航