Đàn piano trái cây với ESP32: 6 phím cảm ứng, còi phát nốt nhạc cho học sinh
Dự án STEM âm nhạc: nối sáu quả vào chân cảm ứng ESP32, chạm quả nào còi phát nốt đó; tần số Đô Rê Mi Fa Son La, giai điệu viết bằng hai mảng nốt và phách.
Chạm vào quả táo, mạch phát nốt Đô; chạm quả cam, mạch phát nốt Rê… Đàn piano trái cây là dự án STEM được trẻ rất thích vì vừa có âm nhạc, vừa có "phép màu" của điện. Bài viết hướng dẫn làm đàn 6 phím bằng ESP32, sáu quả bất kỳ và một còi nhỏ, rồi lập trình cho mạch tự chơi một giai điệu lưu trong mảng.
Qua dự án, học sinh học được ba điều: chân cảm ứng của ESP32 nhận biết cái chạm tay qua một vật dẫn điện, âm thanh cao hay thấp là do tần số, và một bản nhạc có thể viết thành hai dãy số: nốt nào và dài bao lâu.
1. Chuẩn bị
- Một mạch ESP32 DevKit V1 và dây USB nạp code.
- Tám dây cắm test board loại một đầu cái, một đầu đực: sáu dây cho sáu quả, hai dây cho còi.
- Một còi thụ động (còn gọi là loa áp điện, buzzer thụ động). Loại này phát được nhiều nốt khác nhau. Còi chủ động chỉ kêu một tiếng cố định nên không dùng được.
- Sáu quả hoặc rau củ mọng nước: táo, cam, chuối, thanh long, ổi, chanh… Đặt trên đĩa hoặc tờ giấy khô.
- Arduino IDE đã cài gói esp32 và chọn board DOIT ESP32 DEVKIT V1.
Cách phân biệt hai loại còi: cấp điện 3,3 V trực tiếp vào hai chân, còi chủ động kêu "bíp" liên tục, còi thụ động chỉ kêu "tách" một tiếng. Nếu dùng còi thụ động loại cuộn dây (điện từ) thay vì loại áp điện, nên dùng dạng module có sẵn transistor để không lấy dòng quá lớn từ chân ESP32.
2. Nối dây

| Chân ESP32 | Nối với | Nốt |
|---|---|---|
| D32 | quả táo | Đô |
| D33 | quả cam | Rê |
| D27 | quả chuối | Mi |
| D14 | quả thanh long | Fa |
| D13 | quả ổi | Son |
| D4 | quả chanh | La |
| D25 | chân + của còi | |
| GND | chân − của còi |
- Cắm đầu cái của dây vào chân mạch, đầu đực cắm sâu khoảng 1–2 cm vào quả.
- Các quả không được chạm nhau, và không đặt trên đĩa ướt hay mặt bàn kim loại: nước hoặc kim loại nối các quả lại thì chạm một quả, mạch nhận ra nhiều phím.
- Sáu chân D32, D33, D27, D14, D13, D4 đều là chân cảm ứng. Bài không dùng D2, D12, D15 (cũng là chân cảm ứng) vì D2 nối với đèn trên mạch, còn D12, D15 ảnh hưởng tới cách mạch khởi động.
3. Vì sao chạm vào quả thì mạch biết?
Quả chứa nhiều nước và muối khoáng nên dẫn điện. Khi nối vào chân cảm ứng, quả giống một tấm kim loại lớn gắn ở đầu dây. Khi ngón tay chạm vào quả, khả năng tích điện (điện dung) của chân đó tăng lên, và con số ESP32 đọc được bằng lệnh touchRead() thay đổi rõ rệt.
Mỗi quả có kích thước và độ ẩm khác nhau nên con số lúc chưa chạm cũng khác nhau. Vì vậy, lúc khởi động, chương trình đo giá trị nền của từng phím, rồi so độ lệch: lệch quá 30 % là đang chạm, trở về dưới 15 % là đã nhả tay. Hai ngưỡng khác nhau giúp nốt nhạc không bị ngắt quãng khi tay hơi run.
4. Mỗi phím một tần số

Âm thanh là dao động. Màng còi rung bao nhiêu lần trong một giây thì gọi là tần số, đơn vị héc (Hz). Rung càng nhanh, âm nghe càng cao. Nốt La chuẩn dùng để lên dây đàn là 440 Hz. Lệnh tone(chân, tần số) cho còi rung đúng tần số đó, lệnh noTone(chân) tắt tiếng.
5. Chương trình 1: đàn piano 6 phím
// PYWORLD.VN - ESP32 DevKit V1: đàn piano trái cây 6 phím cảm ứng
// Sáu chân cảm ứng nối với sáu quả; chạm quả nào, còi phát nốt đó (Đô Rê Mi Fa Son La)
// Còi thụ động (loa nhỏ áp điện) nối D25 và GND; lúc khởi động không chạm vào quả
const int CHAN_COI = 25;
const int SO_PHIM = 6;
const int CHAN_PHIM[SO_PHIM] = {32, 33, 27, 14, 13, 4}; // D32, D33, D27, D14, D13, D4
const int TAN_SO[SO_PHIM] = {262, 294, 330, 349, 392, 440}; // Hz: Đô, Rê, Mi, Fa, Son, La (quãng 4)
const char* TEN_NOT[SO_PHIM] = {"Do", "Re", "Mi", "Fa", "Son", "La"};
const float LECH_CHAM = 0.30; // lệch hơn 30 % so với giá trị nền: đang chạm
const float LECH_NHA = 0.15; // lệch dưới 15 %: đã nhả
uint32_t giaTriNen[SO_PHIM];
bool dangCham[SO_PHIM];
int notDangPhat = -1; // -1: đang im lặng
uint32_t docTrungBinh(int chan, int soLan) {
uint32_t tong = 0;
for (int i = 0; i < soLan; i++) {
tong += touchRead(chan);
delay(2);
}
return tong / soLan;
}
float doLech(int phim, uint32_t giaTri) {
float chenh = (float)giaTri - (float)giaTriNen[phim];
if (chenh < 0) chenh = -chenh;
return chenh / giaTriNen[phim];
}
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("Dung cham vao qua, dang do gia tri nen...");
for (int p = 0; p < SO_PHIM; p++) {
giaTriNen[p] = docTrungBinh(CHAN_PHIM[p], 30);
if (giaTriNen[p] == 0) giaTriNen[p] = 1;
dangCham[p] = false;
Serial.printf("Phim %s (chan %d): nen %u\n", TEN_NOT[p], CHAN_PHIM[p], (unsigned)giaTriNen[p]);
}
Serial.println("San sang! Cham vao qua de choi dan");
}
void loop() {
int notMoi = -1;
for (int p = 0; p < SO_PHIM; p++) {
float lech = doLech(p, docTrungBinh(CHAN_PHIM[p], 3));
if (!dangCham[p] && lech > LECH_CHAM) dangCham[p] = true;
else if (dangCham[p] && lech < LECH_NHA) dangCham[p] = false;
if (dangCham[p] && notMoi == -1) notMoi = p; // chạm nhiều quả: phát nốt thấp nhất
}
if (notMoi != notDangPhat) { // chỉ đổi âm thanh khi có thay đổi
if (notMoi == -1) {
noTone(CHAN_COI);
} else {
tone(CHAN_COI, TAN_SO[notMoi]);
Serial.println(TEN_NOT[notMoi]);
}
notDangPhat = notMoi;
}
delay(10);
}
Nạp chương trình khi tay không chạm vào quả nào, vì 30 lần đọc đầu tiên dùng để đo giá trị nền. Mở Serial Monitor ở 115200 baud để xem giá trị nền của từng phím, sau đó chạm từng quả: còi phát nốt, Serial Monitor in tên nốt.
- Ba mảng song song:
CHAN_PHIM,TAN_SO,TEN_NOTcó cùng thứ tự, nên phần tử thứ nhất của cả ba cùng nói về phím Đô. Muốn đổi quả nào phát nốt nào, chỉ cần đổi thứ tự trong mảng. - Chạm nhiều quả cùng lúc: còi chỉ phát được một âm mỗi lúc, nên chương trình chọn nốt thấp nhất trong các quả đang chạm.
- Chỉ đổi âm khi có thay đổi: biến
notDangPhatnhớ nốt đang kêu. Nếu không có biến này, chương trình gọitone()liên tục mỗi 10 ms và âm thanh có thể bị rè.
6. Chương trình 2: mạch tự chơi một giai điệu

Chương trình thứ hai chơi giai điệu dân gian Frère Jacques, ở Việt Nam quen thuộc với tên bài hát thiếu nhi "Kìa con bướm vàng". Bản nhạc được viết thành hai mảng: NOT_NHAC ghi tần số từng nốt, PHACH ghi mỗi nốt dài mấy phách. Còi giữ nguyên nối D25 và GND; quả chanh ở D4 dùng làm nút "phát lại".
// PYWORLD.VN - ESP32 DevKit V1: phát giai điệu lưu trong mảng
// Giai điệu dân gian Frère Jacques (ở Việt Nam quen với tên "Kìa con bướm vàng")
// Còi thụ động nối D25 và GND; chạm dây ở D4 (hoặc quả nối D4) để phát lại
const int CHAN_COI = 25;
const int CHAN_CHAM = 4;
const int NHIP = 300; // độ dài một phách (mili giây); số nhỏ hơn thì nhạc nhanh hơn
// Tần số các nốt (Hz); 0 là lặng
const int SON3 = 196, DO = 262, RE = 294, MI = 330, FA = 349, SON = 392, LA = 440;
// Giai điệu: mỗi nốt đi kèm số phách
const int NOT_NHAC[] = {DO, RE, MI, DO, DO, RE, MI, DO, MI, FA, SON, MI, FA, SON,
SON, LA, SON, FA, MI, DO, SON, LA, SON, FA, MI, DO, DO, SON3, DO, DO, SON3, DO};
const float PHACH[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 2,
0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1, 2, 1, 1, 2};
const int SO_NOT = sizeof(NOT_NHAC) / sizeof(NOT_NHAC[0]);
uint32_t giaTriNen = 1;
void phatGiaiDieu() {
Serial.println("Phat giai dieu");
for (int i = 0; i < SO_NOT; i++) {
int thoiGian = (int)(PHACH[i] * NHIP);
if (NOT_NHAC[i] > 0) tone(CHAN_COI, NOT_NHAC[i]);
delay(thoiGian * 9 / 10); // phát 90 % độ dài nốt
noTone(CHAN_COI);
delay(thoiGian / 10); // ngắt 10 % để hai nốt giống nhau vẫn nghe tách ra
}
}
void setup() {
Serial.begin(115200);
delay(500);
uint32_t tong = 0;
for (int i = 0; i < 30; i++) {
tong += touchRead(CHAN_CHAM);
delay(2);
}
giaTriNen = tong / 30;
if (giaTriNen == 0) giaTriNen = 1;
Serial.printf("Giai dieu co %d not\n", SO_NOT);
phatGiaiDieu();
}
void loop() {
uint32_t giaTri = touchRead(CHAN_CHAM);
float lech = ((float)giaTri - (float)giaTriNen) / giaTriNen;
if (lech < 0) lech = -lech;
if (lech > 0.30) { // chạm vào dây D4: phát lại
phatGiaiDieu();
delay(500);
}
delay(20);
}
- Nhịp: mỗi phách 300 ms. Đổi
NHIPthành 200 thì nhạc nhanh hơn, thành 450 thì chậm hơn. - Nốt kêu 90 %, nghỉ 10 %: nếu không có khoảng nghỉ ngắn, hai nốt Đô liền nhau sẽ nghe thành một nốt dài.
- Đặt tên cho tần số: dòng
const int SON3 = 196, DO = 262, …giúp mảng giai điệu dễ đọc như bản nhạc.SON3là nốt Son thấp hơn một quãng tám (196 Hz, bằng một nửa 392 Hz).
7. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Không có tiếng | Dùng còi chủ động; dây còi cắm sai chân | Đổi sang còi thụ động; kiểm tra dây ở D25 và GND |
| Còi kêu ngay khi không chạm | Lúc khởi động đang chạm vào quả; hai quả chạm nhau | Tách các quả, bỏ tay ra rồi bấm nút EN trên mạch để đo lại giá trị nền |
| Chạm quả không kêu | Dây chưa cắm sâu; quả khô, nhỏ | Cắm dây sâu hơn, đổi quả mọng nước; giảm LECH_CHAM xuống 0.20 |
| Chạm một quả, kêu sai nốt | Quả đặt trên đĩa ướt hoặc mặt kim loại; thứ tự dây khác bảng nối | Đặt quả trên giấy khô; so lại bảng nối với mảng CHAN_PHIM |
| Âm kêu chập chờn khi giữ tay | Tay chạm nhẹ, độ lệch dao động quanh ngưỡng | Chạm chắc tay hơn, hoặc giảm LECH_NHA xuống 0.10 |
8. Thử thách mở rộng
- Viết bản nhạc riêng: giai điệu Twinkle Twinkle Little Star bắt đầu bằng Đô Đô Son Son La La Son (nốt cuối dài 2 phách), Fa Fa Mi Mi Rê Rê Đô (nốt cuối dài 2 phách). Hãy viết hai mảng cho đoạn này.
- Đàn quãng cao: nhân đôi các số trong mảng
TAN_SO(524, 588…) rồi nghe lại. Nhận xét: nốt vẫn "giống" nhưng cao hơn. - Thí nghiệm vật dẫn: thay một quả bằng cốc nước, miếng giấy bạc, cọng rau, cục tẩy. Vật nào làm phím hoạt động, vật nào không? Vì sao?
- Đàn người: cầm đầu dây thay cho quả. Chạm vào tay bạn đang cầm dây thì còi có kêu không?
9. Gợi ý cho phụ huynh và giáo viên
- Liên môn: dự án nối môn Âm nhạc (tên nốt, cao độ, phách), Khoa học tự nhiên lớp 7 (độ cao của âm và tần số) và Tin học (mảng, vòng lặp).
- Bắt đầu với một quả: học sinh nhỏ nên làm một phím trước, rồi tăng dần lên sáu phím. Người lớn nối dây còi, trẻ cắm dây vào quả.
- Phân vai trong nhóm: một bạn nối dây, một bạn sửa mảng giai điệu, một bạn biểu diễn. Cuối buổi, mỗi nhóm chơi một bài cho cả lớp đoán tên.
- An toàn: mạch chạy bằng nguồn USB 5 V nên an toàn khi chạm. Không để nước trái cây chảy vào mạch; rửa tay sau khi chơi và không ăn quả đã cắm dây.
- Thời lượng: 2 buổi 45 phút. Buổi 1: nối dây, đàn 6 phím. Buổi 2: giai điệu trong mảng và viết bản nhạc riêng.
10. Xem thêm
Hàm tone() tạo âm thanh bằng tín hiệu xung, cùng loại tín hiệu trong bài PWM trên ESP32. Trong loạt phát triển tư duy cho trẻ bằng STEM và lập trình còn có các bài: chạm tay bật đèn với chân cảm ứng ESP32 (giải thích kỹ giá trị nền và hai ngưỡng), nháy đèn và mã Morse với ESP32, trò chơi đo phản xạ với ESP32 và màn hình OLED.
Xem thêm