外观
软件开发
项目介绍
本项目基于ESP8266 NodeMCU开发板构建,是一个支持12键触摸检测的智能钢琴,具备OLED显示、Web控制、教学模式等功能。采用SC12B触摸芯片实现高精度多键检测,支持和弦演奏和混音播放。
开发环境
- 软件环境: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实时预览**:预览按下的按键 Web 界面功能
1. **虚拟琴键**: 支持web端远控弹奏 2. **歌曲演奏**: 支持对预载歌曲的音乐演奏功能 3. **教学模式**: 支持预载歌曲的钢琴弹奏教学,会在OLED屏实时反馈。 4. **音频参数调节**: 音符时长、八度偏移、灵敏度调节。 系统配置 (config.h)
cpp
// 硬件引脚定义
#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
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
触摸控制接口选择
SC12B一共支持两种控制方法,分别是I2C控制和BCD端口输出,BCD端口输出很简单,只需要ADC检查电压就可以,但BCD的缺点也很明显,四个接口要配置四个不常见的电阻,而且只能单向输出触控信号,无法深入控制IC,并且BCD产生的模拟电压也无法检查多点触控,显然不符合本项目要求。因此这里我们选择IIC控制。
SC12B驱动库移植
基于[liuquanli1970/SC12B](https://github.com/liuquanli1970/SC12B)提供的库移植修改 SC12B.h 这里我们将writeRegister移动到public公开,方便外部调用。
cpp
public:
bool writeRegister(uint8_t reg, uint8_t value);
1
2
2
SC12B.cpp 这里我们将begin函数中内容修改成如下,使用默认地址和默认IIC。
cpp
void SC12B::begin() {
Wire.begin();
}
1
2
3
2
3
寄存器列表
触摸灵敏度调节
依照数据手册提供的寄存器配置,我们在软件端设置了16个触控等级
cpp
// 应用触摸灵敏度设置到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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
touchPannel.writeRegister(REG_Senset0, sensitivityLevel);写入传感器0的灵敏度寄存器 touchPannel.writeRegister(REG_SensetCOM, sensitivityLevel);写入公共传感器的灵敏度寄存器
寄存器定义 (在 SC12B.h 中)
cpp
// 寄存器地址定义
#define REG_Senset0 0x00 // 传感器0灵敏度寄存器
#define REG_SensetCOM 0x01 // 公共传感器灵敏度寄存器
// 灵敏度等级枚举
typedef enum {
LEVEL0 = 0x04, // 最低灵敏度
LEVEL1 = 0x15,
LEVEL2 = 0x25,
// ... 其他等级
LEVEL15 = 0xFF // 最高灵敏度
} Sensitivity;
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
而在硬件中还有一个可调电容可以调节灵敏度
触摸检测
对于触控的检查我们将使用上面的引脚,地址参考地址选择说明,本项目中ASEL浮空,INT用于中断检测,当INT触发硬件中断则说明有按键被触摸,此时发送IIC轮询找到对应通道即可。
cpp
/* ========== 中断驱动的触摸检测 ========== */
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
主机发送: START -> 0x40(写) -> ACK -> 0x08(REG_OUTPUT1) -> ACK -> RESTART -> 0x41(读) -> ACK
从机响应: DATA1 -> ACK -> STOP
多次采样: 连续5次轮询避免单次采样的不稳定性 1ms间隔确保采样的时间分散性
防抖处理: 20ms防抖时间避免按键抖动 只有状态真正改变才处理
HTML页面显示
这部分不细讲了,和前面EDA-Robot的项目的基本一致,移植过来的,通过路由创建RESTful API,然后页面按钮触发发get/post,由路由监听到后执行对应任务。
cpp
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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
模块介绍
1. audio.h/.cpp
主要功能:
- 基础音频频率生成 (12 个半音)
- 八度调节 (-2 到+2 八度)
- 多音符混音播放
- 音符持续时间控制
音频频率计算:
cpp
// 获取调整八度后的频率
int getNoteFrequency(int noteIndex) {
if (noteIndex < 1 || noteIndex > 12) return 0;
float freq = baseFrequencies[noteIndex];
// 八度调节:每个八度频率翻倍或减半
for (int i = 0; i < abs(octaveShift); i++) {
if (octaveShift > 0) {
freq *= 2.0; // 升高八度
} else if (octaveShift < 0) {
freq /= 2.0; // 降低八度
}
}
return (int)freq;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
多音符播放控制:
cpp
void playMultipleNotes(int* keys, int keyCount) {
// 停止之前的播放
isMixPlaying = false;
noTone(BUZZER_PIN);
if (keyCount == 0) return;
if (keyCount == 1) {
// 单音符使用tone函数,使用统一的音长设置
tone(BUZZER_PIN, getNoteFrequency(keys[0]), noteDuration);
} else {
// 多音符使用简化混音
mixKeyCount = keyCount;
mixStartTime = millis();
lastMixUpdate = micros();
// 复制按键数组
for (int i = 0; i < keyCount; i++) {
mixKeys[i] = keys[i];
}
isMixPlaying = true;
updateMixedTone(); // 立即开始播放
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
混音播放算法:
cpp
void updateMixedTone() {
if (!isMixPlaying || mixKeyCount == 0) return;
// 计算混合频率(简单平均)
float avgFrequency = 0;
for (int i = 0; i < mixKeyCount; i++) {
avgFrequency += getNoteFrequency(mixKeys[i]);
}
avgFrequency /= mixKeyCount;
// 使用tone函数播放平均频率
tone(BUZZER_PIN, (int)avgFrequency, 100);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
基础音频频率数组:
cpp
// 基础音频频率(第4八度)
int baseFrequencies[] = {
0, // 占位,索引从1开始
262, // 1 do (C4)
277, // 2 升do (C#4)
294, // 3 re (D4)
311, // 4 升re (D#4)
330, // 5 mi (E4)
349, // 6 fa (F4)
370, // 7 升fa (F#4)
392, // 8 so (G4)
415, // 9 升so (G#4)
440, // 10 la (A4)
466, // 11 升la (A#4)
494 // 12 si (B4)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2. display.h/.cpp
主要功能:
- 钢琴键盘可视化显示
- 网络状态信息显示
- 教学模式界面
- 智能熄屏管理
显示界面:
- 钢琴键盘: 7 个白键 + 5 个黑键的完整显示
- 按键反馈: 实时高亮显示按下的键
- 状态信息: 八度、灵敏度、音符时长显示
- 教学模式: 引导式学习界面
熄屏功能:
- 支持 5 种熄屏时间设置 (10 秒-30 分钟)
- 触摸自动唤醒
- 活动时间自动跟踪
钢琴键盘绘制算法:
cpp
void drawPianoKeyboard(int* pressedKeys, int keyCount) {
display.clearDisplay();
// 创建按键状态映射数组
bool keyPressed[13] = {false};
for (int i = 0; i < keyCount; i++) {
if (pressedKeys[i] >= 1 && pressedKeys[i] <= 12) {
keyPressed[pressedKeys[i]] = true;
}
}
// 白键和黑键映射
int whiteKeys[] = {1, 3, 5, 6, 8, 10, 12};
int blackKeys[] = {2, 4, 7, 9, 11};
int blackKeyPositions[] = {0, 1, 3, 4, 5};
// 绘制白键
for (int i = 0; i < 7; i++) {
int keyX = i * keyWidth;
int keyNum = whiteKeys[i];
display.drawRect(keyX, keyboardY, keyWidth, whiteKeyHeight, SSD1306_WHITE);
if (keyPressed[keyNum]) {
display.fillRect(keyX + 1, keyboardY + 1, keyWidth - 2, whiteKeyHeight - 2, SSD1306_WHITE);
}
}
// 绘制黑键
for (int i = 0; i < 5; i++) {
int whiteKeyIndex = blackKeyPositions[i];
int keyX = whiteKeyIndex * keyWidth + keyWidth * 2/3;
int keyNum = blackKeys[i];
if (keyPressed[keyNum]) {
display.fillRect(keyX, keyboardY, blackKeyWidth, blackKeyHeight, SSD1306_WHITE);
} else {
display.fillRect(keyX, keyboardY, blackKeyWidth, blackKeyHeight, SSD1306_WHITE);
display.drawRect(keyX + 1, keyboardY + 1, blackKeyWidth - 2, blackKeyHeight - 2, SSD1306_BLACK);
}
}
display.display();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
熄屏管理:
cpp
void checkScreenTimeout() {
if (screenOffMode == SCREEN_OFF_NONE || !screenOn) return;
unsigned long currentTime = millis();
unsigned long timeout = 0;
switch (screenOffMode) {
case SCREEN_OFF_10SEC: timeout = 10 * 1000UL; break;
case SCREEN_OFF_1MIN: timeout = 1 * 60 * 1000UL; break;
case SCREEN_OFF_5MIN: timeout = 5 * 60 * 1000UL; break;
case SCREEN_OFF_15MIN: timeout = 15 * 60 * 1000UL; break;
case SCREEN_OFF_30MIN: timeout = 30 * 60 * 1000UL; break;
}
if (currentTime - lastActivityTime >= timeout) {
turnOffScreen();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3. touch.h/.cpp
主要功能:
- 12 通道电容触摸检测
- 多点触摸支持
- 触摸灵敏度调节
- 防抖处理
多触控算法:
- 采用 5 次采样合并算法提高检测精度
- 位运算解析多键同时按下
- 16 级灵敏度调节 (LEVEL0-LEVEL15)
- 中断驱动 + 轮询双重检测机制
多键检测(5 次采样合并):
cpp
uint16_t detectMultipleKeys() {
uint16_t combinedKeys = 0;
for (int i = 0; i < 5; i++) {
uint16_t keyValue = touchPannel.getKeyValue();
combinedKeys |= keyValue; // 位运算合并多次采样
delay(1);
}
return combinedKeys;
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
键值解析(位运算解码):
cpp
void parseKeys(uint16_t keyValue, int* pressedKeys, int* keyCount) {
*keyCount = 0;
if (keyValue == 0) return;
// 标准多键检测 - 位4到位15对应键1到键12
for (int i = 4; i < 16; i++) {
if (keyValue & (1 << i)) { // 位运算检测
int keyIndex = i - 3;
if (keyIndex >= 1 && keyIndex <= 12) {
pressedKeys[*keyCount] = keyIndex;
(*keyCount)++;
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
触摸中断处理:
cpp
void IRAM_ATTR checkkey() {
iftouch = true; // 中断标志位
}
void initTouch() {
pinMode(TOUCH_INTERRUPT_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(TOUCH_INTERRUPT_PIN), checkkey, CHANGE);
touchPannel.begin();
touchPannel.init();
applyTouchSensitivity();
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
4. network.h/.cpp
主要功能:
- WiFi 热点模式 (AP 模式)
- Web 服务器 (端口 80)
- RESTful API 接口
- 实时参数调节
Web 功能:
- 虚拟钢琴: 网页端可视化钢琴键盘
- 参数控制: 音符时长、八度、灵敏度在线调节
- 歌曲播放: 预置歌曲自动播放
- 教学模式: 网页端启动学习模式
API 接口设计:
GET /play/{1-12} - 播放单个音符
GET /chord/{keys} - 播放和弦
GET /song/play?id={id} - 播放歌曲
GET /teaching/start?id={id} - 启动教学
GET /set/duration?value={ms} - 设置音符时长
GET /set/octave?value={-2~2} - 设置八度
GET /set/sensitivity?value={0~15} - 设置触摸灵敏度
GET /status - 获取系统状态
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Web 服务器核心实现:
cpp
void setupWebServer() {
// 主页面 - 响应式钢琴界面
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", generateMainPage());
});
// 动态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("/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");
}
}
});
server.begin();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
命令队列处理机制:
cpp
// Web命令队列变量
volatile int webCommand = 0;
volatile bool webCommandPending = false;
void processWebCommands() {
if (!webCommandPending) return;
webCommandPending = false;
if (webCommand >= 1 && webCommand <= 12) {
// 播放单个音符
int keys[] = {webCommand};
playMultipleNotes(keys, 1);
}
else if (webCommand == 201) {
// C大调和弦 [1,5,8]
int keys[] = {1, 5, 8};
playMultipleNotes(keys, 3);
}
else if (webCommand == 300) {
// 启动自动播放模式
autoPlayMode = true;
autoPlayIndex = -1;
lastAutoPlayTime = millis() - autoPlayInterval;
}
webCommand = 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
5. music.h/.cpp
主要功能:
- 预置歌曲曲谱存储
- 自动播放控制
- 教学模式支持
内置歌曲:
- 一闪一闪亮晶晶: 42 个音符的完整曲谱
- 两只老虎: 32 个音符的经典儿歌
播放模式:
- 自动播放: 按曲谱顺序自动播放
- 教学模式: 逐个音符引导学习
- 实时反馈: 正确/错误提示
#include "music.h"
// "一闪一闪亮晶晶"曲谱 (C大调) - 完整版
int twinkleMelody[] = {
1, 1, 8, 8, 10, 10, 8, // 一闪一闪亮晶晶 (C C G G A A G)
6, 6, 5, 5, 3, 3, 1, // 满天都是小星星 (F F E E D D C)
8, 8, 6, 6, 5, 5, 3, // 挂在天空放光明 (G G F F E E D)
8, 8, 6, 6, 5, 5, 3, // 好像许多小眼睛 (G G F F E E D)
1, 1, 8, 8, 10, 10, 8, // 一闪一闪亮晶晶 (C C G G A A G)
6, 6, 5, 5, 3, 3, 1 // 满天都是小星星 (F F E E D D C)
};
int twinkleMelodyCount = 42;
// "两只老虎"曲谱 (C大调)
int tigerMelody[] = {
1, 3, 5, 1, // 两只老虎 (C D E C)
1, 3, 5, 1, // 两只老虎 (C D E C)
5, 6, 8, // 跑得快 (E F G -)
5, 6, 8, // 跑得快 (E F G -)
8, 10, 8, 6, 5, 1, // 一只没有眼睛 (G A G F E C)
8, 10, 8, 6, 5, 1, // 一只没有尾巴 (G A G F E C)
3, 1, 1, // 真奇怪 (C C C -)
3, 1, 1, // 真奇怪 (C C C -)
};
int tigerMelodyCount = 32;
// 当前教学曲目选择 (0=一闪一闪亮晶晶, 1=两只老虎)
int currentSong = 0;
// 获取当前教学曲谱
int* getCurrentMelody() {
return currentSong == SONG_TWINKLE ? twinkleMelody : tigerMelody;
}
int getCurrentMelodyCount() {
return currentSong == SONG_TWINKLE ? twinkleMelodyCount : tigerMelodyCount;
}
void setCurrentSong(int songId) {
if (songId == SONG_TWINKLE || songId == SONG_TIGER) {
currentSong = songId;
}
}
String getCurrentSongName() {
return currentSong == SONG_TWINKLE ? "一闪一闪亮晶晶" : "两只老虎";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
6. SC12B.h/.cpp
主要功能:
- SC12B 芯片底层驱动
- I2C 通信协议实现
- 寄存器配置管理
技术特性:
- 12 通道电容触摸检测
- 16 级灵敏度调节
- I2C 地址: 0x40
- 支持同时多键检测
寄存器配置:
cpp
#define REG_Senset0 0x00 // 灵敏度设置
#define REG_SensetCOM 0x01 // 公共灵敏度
#define REG_OUTPUT1 0x08 // 输出寄存器1
#define REG_OUTPUT2 0x09 // 输出寄存器2
1
2
3
4
2
3
4
7. config.h - 系统配置参数
硬件配置:
cpp
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define TOUCH_INTERRUPT_PIN 14
#define BUZZER_PIN 12
1
2
3
4
2
3
4
功能参数:
cpp
#define DEFAULT_NOTE_DURATION 500 // 默认音符时长
#define DEFAULT_OCTAVE_SHIFT 0 // 默认八度
#define DEFAULT_TOUCH_SENSITIVITY 0 // 默认触摸灵敏度
1
2
3
2
3
8. main.cpp
主要功能:
- 系统初始化协调
- 主循环任务调度
- 多任务并发处理
系统初始化流程:
cpp
void setup() {
Serial.begin(115200);
// 按顺序初始化各个模块
initAudio(); // 音频系统初始化
initTouch(); // 触摸检测初始化
initDisplay(); // OLED显示初始化
initNetwork(); // WiFi网络初始化
// 显示启动信息
showStartupScreen();
showNetworkInfo();
setupWebServer();
delay(3000);
displayMultipleKeys(0, 0); // 显示初始界面
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
主循环任务调度:
cpp
void loop() {
/* ========== 混音播放状态维护 ========== */
if (isMixPlaying) {
unsigned long currentMicros = micros();
if (currentMicros - lastMixUpdate >= MIX_UPDATE_INTERVAL) {
updateMixedTone();
lastMixUpdate = currentMicros;
}
}
/* ========== 熄屏检测 ========== */
checkScreenTimeout();
/* ========== 触摸控制检测 ========== */
unsigned long currentTime = millis();
if (currentTime - lastSampleTime > SAMPLE_INTERVAL) {
uint16_t keyValue = detectMultipleKeys();
if (keyValue != previousKeys && currentTime - lastKeyTime > KEY_DEBOUNCE_TIME) {
updateScreenActivity(); // 触摸时唤醒屏幕
// 解析按键并处理
int pressedKeys[12];
int keyCount = 0;
parseKeys(keyValue, pressedKeys, &keyCount);
// 教学模式逻辑处理
if (teachingMode && keyCount > 0) {
handleTeachingMode(pressedKeys, keyCount);
} else {
displayMultipleKeys(pressedKeys, keyCount);
}
// 音频播放控制
if (keyCount > 0) {
playMultipleNotes(pressedKeys, keyCount);
} else {
stopAllAudio();
}
}
}
/* ========== 自动播放处理 ========== */
if (autoPlayMode) {
handleAutoPlay();
}
/* ========== Web命令处理 ========== */
processWebCommands();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50