Trò chơi đo phản xạ với ESP32 và màn hình OLED 4 nút: đo thời gian, tính trung bình

Làm trò chơi đo phản xạ bằng ESP32 và màn hình OLED kèm 4 nút: kiểm tra nút, đo thời gian bằng millis, chờ ngẫu nhiên, tính trung bình 5 lượt, thí nghiệm cho học sinh.

Màn hình hiện một ký hiệu, người chơi bấm đúng nút nhanh nhất có thể, mạch đo xem mất bao nhiêu mili giây. Trò chơi đo phản xạ rất hợp với học sinh: dễ làm, chơi được ngay, và mỗi lượt chơi là một lần đo đạc thật. Bài viết hướng dẫn làm trò chơi bằng ESP32 và màn hình OLED 0.96 inch có sẵn 4 nút nhấn, rồi dùng nó làm vài thí nghiệm khoa học nhỏ.

Qua dự án, trẻ học được cách máy tính đo thời gian, vì sao trò chơi cần số ngẫu nhiên, cách tính trung bình cộng từ nhiều lần đo, và cách đặt một câu hỏi rồi làm thí nghiệm để trả lời.

1. Chuẩn bị

  • Một mạch ESP32 DevKit V1 và dây USB nạp code.
  • Một màn hình OLED 0.96 inch SSD1306, chọn loại kèm 4 nút nhấn (hàng 8 chân: GND, VCC, SCL, SDA, K4, K3, K2, K1).
  • Tám dây cắm test board loại hai đầu cái.
  • Arduino IDE đã cài gói esp32, chọn board DOIT ESP32 DEVKIT V1. Vào Tools > Manage Libraries, tìm và cài thư viện Adafruit SSD1306; khi được hỏi, chọn cài kèm Adafruit GFX Library và Adafruit BusIO.

2. Nối dây

Sơ đồ nối ESP32 DevKit V1 với màn hình OLED kèm 4 nút: GND với GND, VCC với 3V3, SCL với D22, SDA với D21, K1 với D32, K2 với D33, K3 với D25, K4 với D26; các chân cùng màu; màn hình đang hiện chương trình kiểm tra nút với dòng K3 BAM
Hình minh họa: nối các chân cùng màu.
Nối màn hình OLED kèm 4 nút với ESP32
Chân moduleChân ESP32Vai trò
GNDGNDnối đất
VCC3V3nguồn 3,3 V
SCLD22xung nhịp I2C
SDAD21dữ liệu I2C
K1D32nút K1
K2D33nút K2
K3D25nút K3
K4D26nút K4

Màn hình nói chuyện với ESP32 qua I2C, chỉ cần hai dây SDA, SCL; D21 và D22 là hai chân I2C mặc định của ESP32. Bốn nút K1–K4 nối vào bốn chân bất kỳ còn trống. Lưu ý: mỗi lô mạch có thể đổi chỗ hai chân VCC và GND, nên luôn đọc chữ in cạnh hàng chân trên mạch của mình trước khi cắm dây nguồn.

3. Chương trình 1: kiểm tra 4 nút

Trên module, bốn nút được đánh dấu bằng ký hiệu ^ v # *, còn hàng chân ghi K1–K4. Chương trình nhỏ này giúp biết nút nào nối với chân nào, đồng thời kiểm tra dây và màn hình trước khi viết trò chơi.

// PYWORLD.VN - ESP32 + màn hình OLED 0.96 inch kèm 4 nút (K1–K4): kiểm tra nút
// OLED: SDA = D21, SCL = D22, địa chỉ 0x3C; nút: K1 = D32, K2 = D33, K3 = D25, K4 = D26
// Bấm lần lượt từng nút để biết nút nào là K1, K2, K3, K4 trên mạch của bạn

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const int SO_NUT = 4;
const int CHAN_NUT[SO_NUT] = {32, 33, 25, 26};    // K1, K2, K3, K4
int mucNghi[SO_NUT];                               // mức đọc được khi không bấm

void setup() {
  Serial.begin(115200);
  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("Khong thay OLED, thu dia chi 0x3D");
  }
  oled.setTextColor(SSD1306_WHITE);
  for (int i = 0; i < SO_NUT; i++) {
    pinMode(CHAN_NUT[i], INPUT_PULLUP);             // bật điện trở kéo lên bên trong ESP32
  }
  delay(50);
  for (int i = 0; i < SO_NUT; i++) {
    mucNghi[i] = digitalRead(CHAN_NUT[i]);          // không bấm nút lúc khởi động
    Serial.printf("K%d (chan %d) muc nghi: %d\n", i + 1, CHAN_NUT[i], mucNghi[i]);
  }
}

void loop() {
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.print("Kiem tra 4 nut");
  for (int i = 0; i < SO_NUT; i++) {
    bool bam = digitalRead(CHAN_NUT[i]) != mucNghi[i];   // khác mức nghỉ là đang bấm
    oled.setCursor(0, 16 + i * 12);
    oled.printf("K%d chan %2d: %s", i + 1, CHAN_NUT[i], bam ? "BAM" : "-");
  }
  oled.display();
  delay(50);
}
  • Nạp chương trình khi không bấm nút nào. Lúc khởi động, chương trình ghi lại mức điện của từng chân (biến mucNghi). Sau đó, chân nào có mức khác đi tức là nút đó đang được bấm. Nhờ vậy, chương trình không cần biết trước nút nối xuống GND hay lên nguồn.
  • INPUT_PULLUP bật điện trở kéo lên có sẵn trong ESP32, để chân không bị "bỏ lửng" khi chưa bấm.
  • Bấm lần lượt từng nút và ghi lại: nút ^ là K mấy, nút v là K mấy… Nếu màn hình không sáng, mở Serial Monitor (115200 baud) xem dòng báo lỗi, và thử đổi địa chỉ 0x3C thành 0x3D.

4. Chương trình 2: trò chơi đo phản xạ

Lưu đồ trò chơi: bắt đầu, màn hình Bam nut bat ky, lượt n chờ ngẫu nhiên 1,5 đến 4 giây; nếu bấm khi đang chờ thì hiện Bam som và chơi lại lượt; nếu không thì hiện ký hiệu ngẫu nhiên và bắt đầu bấm giờ; bấm sai nút thì hiện Sai nut và chơi lại lượt; bấm đúng thì hiện thời gian và cộng vào tổng; chưa đủ 5 lượt thì sang lượt sau; đủ thì hiện trung bình và nhanh nhất
Hình minh họa: các bước của trò chơi.

Mỗi ván có 5 lượt. Trong mỗi lượt, màn hình hiện "Cho..." trong một khoảng thời gian ngẫu nhiên, sau đó hiện to một ký hiệu. Người chơi bấm nút có ký hiệu đó. Hết 5 lượt, màn hình hiện thời gian trung bình (TB) và lượt nhanh nhất (Min).

// PYWORLD.VN - ESP32 + OLED 0.96 inch kèm 4 nút: trò chơi đo phản xạ
// Màn hình hiện một ký hiệu (^ v # *), bấm đúng nút càng nhanh càng tốt; 5 lượt, tính trung bình
// OLED: SDA = D21, SCL = D22; nút: K1 = D32, K2 = D33, K3 = D25, K4 = D26

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const int SO_NUT = 4;
const int CHAN_NUT[SO_NUT] = {32, 33, 25, 26};      // K1, K2, K3, K4
const char KY_HIEU[SO_NUT] = {'^', 'v', '#', '*'};  // ký hiệu in cạnh K1..K4; sửa theo chương trình kiểm tra nút
const int SO_LUOT = 5;
int mucNghi[SO_NUT];
unsigned long tongThoiGian = 0, nhanhNhat = 0;

// Trả về số thứ tự nút đang bấm (0–3), hoặc -1 nếu không bấm nút nào
int nutDangBam() {
  for (int i = 0; i < SO_NUT; i++) {
    if (digitalRead(CHAN_NUT[i]) != mucNghi[i]) return i;
  }
  return -1;
}

void choNhaHet() {
  while (nutDangBam() != -1) delay(10);
  delay(30);                                        // chờ hết dội phím
}

void hienChu(const char* dong1, const char* dong2, int co2) {
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.print(dong1);
  oled.setTextSize(co2);
  oled.setCursor(0, 24);
  oled.print(dong2);
  oled.display();
}

// Chơi một lượt; trả về thời gian phản xạ (ms), 0 nếu bấm sớm hoặc bấm sai
unsigned long choiMotLuot(int luot) {
  char dong[24];
  snprintf(dong, sizeof(dong), "Luot %d/%d", luot, SO_LUOT);
  hienChu(dong, "Cho...", 2);
  unsigned long batDauCho = millis();
  unsigned long thoiGianCho = random(1500, 4000);   // chờ ngẫu nhiên 1,5–4 giây
  while (millis() - batDauCho < thoiGianCho) {
    if (nutDangBam() != -1) {                       // bấm trước khi hiện ký hiệu
      hienChu(dong, "Bam som!", 2);
      choNhaHet();
      delay(1000);
      return 0;
    }
  }
  int dich = random(SO_NUT);
  char kyHieu[2] = {KY_HIEU[dich], 0};
  oled.clearDisplay();
  oled.setTextSize(6);
  oled.setCursor(48, 12);
  oled.print(kyHieu);
  oled.display();
  unsigned long batDau = millis();
  int nut;
  while ((nut = nutDangBam()) == -1) delay(1);
  unsigned long phanXa = millis() - batDau;
  choNhaHet();
  if (nut != dich) {
    hienChu(dong, "Sai nut!", 2);
    delay(1000);
    return 0;
  }
  snprintf(dong, sizeof(dong), "%lu ms", phanXa);
  hienChu("Thoi gian phan xa:", dong, 3);
  Serial.printf("Luot %d: %lu ms\n", luot, phanXa);
  delay(1500);
  return phanXa;
}

void setup() {
  Serial.begin(115200);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  oled.setTextColor(SSD1306_WHITE);
  for (int i = 0; i < SO_NUT; i++) pinMode(CHAN_NUT[i], INPUT_PULLUP);
  delay(50);
  for (int i = 0; i < SO_NUT; i++) mucNghi[i] = digitalRead(CHAN_NUT[i]);
}

void loop() {
  hienChu("Do phan xa: bam dung", "Bam nut bat ky", 1);
  while (nutDangBam() == -1) delay(10);
  choNhaHet();

  tongThoiGian = 0;
  nhanhNhat = 0;
  int luot = 1;
  while (luot <= SO_LUOT) {
    unsigned long t = choiMotLuot(luot);
    if (t == 0) continue;                           // lượt hỏng: chơi lại lượt đó
    tongThoiGian += t;
    if (nhanhNhat == 0 || t < nhanhNhat) nhanhNhat = t;
    luot++;
  }

  char dong[24];
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.print("Ket qua 5 luot");
  oled.setTextSize(2);
  oled.setCursor(0, 18);
  snprintf(dong, sizeof(dong), "TB %lu", tongThoiGian / SO_LUOT);
  oled.print(dong);
  oled.setCursor(0, 40);
  snprintf(dong, sizeof(dong), "Min %lu", nhanhNhat);
  oled.print(dong);
  oled.display();
  Serial.printf("Trung binh %lu ms, nhanh nhat %lu ms\n", tongThoiGian / SO_LUOT, nhanhNhat);
  delay(1000);
  while (nutDangBam() == -1) delay(10);             // bấm nút bất kỳ để chơi lại
  choNhaHet();
}

Trước khi nạp, sửa mảng KY_HIEU theo kết quả của chương trình kiểm tra nút: phần tử thứ nhất là ký hiệu in cạnh nút K1, thứ hai là ký hiệu cạnh nút K2… Nếu không sửa, trò chơi vẫn chạy nhưng ký hiệu trên màn hình có thể không khớp với ký hiệu in cạnh nút.

  • Đo thời gian bằng millis(): hàm này trả về số mili giây kể từ lúc mạch khởi động. Ghi lại millis() lúc ký hiệu hiện ra và lúc nút được bấm, lấy hiệu hai số là thời gian phản xạ. Vòng chờ nút có delay(1) nên số đo chính xác tới khoảng 1 ms.
  • Vì sao phải chờ ngẫu nhiên: random(1500, 4000) cho một số từ 1500 đến 3999. Nếu lúc nào cũng chờ đúng 2 giây, người chơi sẽ đếm nhịp và bấm trước, kết quả không còn là phản xạ.
  • Chống gian lận: bấm nút trong lúc chờ thì màn hình báo "Bam som!" và lượt đó chơi lại. Bấm sai nút cũng chơi lại lượt.
  • Chờ nhả nút: hàm choNhaHet() chờ người chơi nhả hết nút rồi chờ thêm 30 ms. Nút cơ khí khi bấm, nhả thường "dội" vài lần trong vài mili giây; khoảng chờ này giúp một lần bấm không bị đếm thành nhiều lần.
Sáu màn hình OLED mô phỏng: 1 chữ Do phan xa bam dung và Bam nut bat ky; 2 Luot 1/5 và Cho; 3 ký hiệu dấu thăng cỡ lớn; 4 Thoi gian phan xa 520 ms; 5 Luot 2/5 Bam som; 6 Ket qua 5 luot, TB 526, Min 463
Hình mô phỏng: chữ và cỡ chữ theo đúng chương trình, thời gian là số minh họa.

5. Từ 5 lượt đến trung bình cộng

Biểu đồ cột 5 lượt chơi 520, 463, 612, 488, 547 mili giây, cột lượt 2 màu xanh lá là nhanh nhất, đường đỏ nét đứt là trung bình 526; tổng 2630, trung bình 2630 chia 5 bằng 526 mili giây, nhanh nhất 463 mili giây; ô thí nghiệm nhỏ: tay thuận và tay không thuận, một ký hiệu và bốn ký hiệu, chơi 3 lần liền
Hình minh họa: số liệu giả định để luyện cách tính.

Mỗi lần đo cho một kết quả hơi khác, vì người chơi lúc tập trung, lúc lơ đãng. Vì vậy, nhà khoa học không dựa vào một lần đo mà đo nhiều lần rồi lấy trung bình cộng: cộng tất cả rồi chia cho số lần đo. Học sinh lớp 4 đã học phép tính này, và có thể tự tính lại để kiểm tra con số trên màn hình.

Chương trình chia hai số nguyên (tongThoiGian / SO_LUOT), nên phần thập phân bị bỏ đi. Ví dụ tổng 2633 ms chia 5 được 526,6 nhưng màn hình hiện 526.

6. Biến trò chơi thành thí nghiệm

Với người lớn, khi chỉ cần bấm một nút ngay khi thấy tín hiệu, thời gian phản xạ thường khoảng 0,2–0,3 giây. Trò chơi này khó hơn vì người chơi phải nhận ra ký hiệu và chọn đúng 1 trong 4 nút, nên kết quả thường lâu hơn. Gợi ý ba thí nghiệm; với mỗi thí nghiệm, cho trẻ đoán trước kết quả, chơi mỗi trường hợp ít nhất 2 ván, ghi TB vào bảng rồi so sánh:

  • Tay thuận và tay không thuận: chơi một ván bằng tay phải, một ván bằng tay trái. Chênh nhau bao nhiêu mili giây?
  • Một ký hiệu và bốn ký hiệu: đổi dòng int dich = random(SO_NUT); thành int dich = 0; để lần nào cũng chỉ hiện ký hiệu của nút K1. Khi không phải chọn nút, người chơi nhanh hơn bao nhiêu? Đây là ý chính của định luật Hick trong tâm lý học: càng nhiều lựa chọn, càng mất nhiều thời gian quyết định.
  • Luyện tập: chơi 5 ván liên tiếp, vẽ biểu đồ TB của từng ván. Kết quả có tốt dần lên không?

Số đo của trò chơi có một sai số nhỏ: màn hình cần một khoảng thời gian rất ngắn để vẽ xong ký hiệu. Sai số này giống nhau ở mọi lượt, nên vẫn so sánh công bằng giữa các lần chơi trên cùng một mạch.

7. Xử lý lỗi thường gặp

Lỗi thường gặp và cách xử lý
Hiện tượngNguyên nhân thường gặpCách xử lý
Màn hình không sángĐảo VCC và GND; lỏng dây SDA, SCL; địa chỉ khác 0x3CĐọc lại chữ in trên mạch; cắm chắc dây; thử địa chỉ 0x3D
Báo lỗi biên dịch không tìm thấy Adafruit_SSD1306.hChưa cài thư việnCài Adafruit SSD1306 kèm Adafruit GFX Library và Adafruit BusIO
Vừa khởi động đã báo "Bam som!" liên tụcĐang giữ nút lúc khởi động nên mức nghỉ bị ghi saiBỏ tay ra, bấm nút EN trên ESP32 để khởi động lại
Bấm nút mà dòng trong chương trình kiểm tra không đổiDây nút lỏng hoặc sai chân; nút nối lên nguồn mà mạch không có điện trở kéo xuốngKiểm tra dây; thử đổi INPUT_PULLUP thành INPUT_PULLDOWN ở cả hai chương trình
Bấm đúng ký hiệu mà màn hình báo "Sai nut!"Mảng KY_HIEU chưa sửa theo mạchChạy chương trình kiểm tra nút, sửa thứ tự trong KY_HIEU

8. Gợi ý cho phụ huynh và giáo viên

  • So với chính mình: khuyến khích trẻ ghi "kỷ lục cá nhân" và cố vượt qua nó, thay vì xếp hạng cả lớp. Phản xạ phụ thuộc nhiều vào tuổi và độ mệt, so sánh giữa các bạn dễ gây áp lực.
  • Liên môn: Toán (trung bình cộng, biểu đồ cột), Khoa học (đặt giả thuyết, đo nhiều lần, so sánh), Tin học (biến, vòng lặp, câu lệnh điều kiện, số ngẫu nhiên).
  • Phân vai: một bạn chơi, một bạn ghi số vào bảng, một bạn vẽ biểu đồ; sau mỗi ván đổi vai.
  • Mở rộng cho học sinh lớn: thêm chế độ hai người chơi (mỗi người hai nút), hoặc lưu kỷ lục để tắt nguồn vẫn còn.
  • Thời lượng: 2 buổi 45 phút. Buổi 1: nối dây, kiểm tra nút, chơi thử. Buổi 2: đọc chương trình, làm một thí nghiệm và trình bày kết quả.

9. Xem thêm

  • Cách dùng thư viện vẽ chữ, hình trên màn hình OLED: màn hình OLED 0.96 inch SSD1306 với Arduino Nano (các lệnh của thư viện giống trên ESP32).
  • Hai dây SDA, SCL hoạt động thế nào: giao tiếp I2C trên ESP32.
  • Đọc nút nhấn nâng cao, chống dội: ngắt GPIO 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ó: xúc xắc điện tử và xác suất với ESP32 và OLED (dùng lại đúng sơ đồ nối của bài này), đàn piano trái cây với ESP32, 5 dự án Scratch đầu tiên cho trẻ.

Tin tức liên quan

Sạc pin Li-ion 1S ở dòng 1 A: chọn dung lượng pin, thời gian sạc và an toàn
Sạc pin Li-ion 1S ở dòng 1 A: chọn dung lượng pin, thời gian sạc và an toàn

Dòng sạc 1 A hợp với pin 18650 và pin Li-po từ khoảng 2000 mAh, quá lớn với pin vài trăm mAh. Bài hướng dẫn chọn dung lượng pin, ước tính thời gian sạc, cách mắc pin và quy tắc an toàn.

Áp suất tuyệt đối, dư, chênh, chân không: đơn vị, hiệu chuẩn cảm biến tuyệt đối và an toàn áp cao
Áp suất tuyệt đối, dư, chênh, chân không: đơn vị, hiệu chuẩn cảm biến tuyệt đối và an toàn áp cao

Phân biệt áp suất tuyệt đối, dư, chênh và chân không, đổi đơn vị bar, kgf/cm², psi, hiệu chuẩn cảm biến tuyệt đối bằng chuẩn áp dư và khí áp kế, năng lượng khí nén và quy tắc an toàn.

Các nguồn sai số khi đo lực bằng load cell: lệch tâm, rão, trễ, nhiệt độ và ngân sách sai số
Các nguồn sai số khi đo lực bằng load cell: lệch tâm, rão, trễ, nhiệt độ và ngân sách sai số

Vì sao đo lực thực tế sai hơn phiếu thông số load cell: lệch tâm và gá lắp, rão, trễ, phi tuyến, nhiệt độ, cáp và bộ đọc; ví dụ ngân sách sai số và danh sách kiểm tra nhanh.

Mức logic 3,3 V và 5 V: khi nào cần chuyển mức và cách nối an toàn với ESP32
Mức logic 3,3 V và 5 V: khi nào cần chuyển mức và cách nối an toàn với ESP32

Giải thích mức logic 3,3 V và 5 V: ngưỡng mức thấp, mức cao của ESP32, Arduino và các họ IC; khi nào nối thẳng được, khi nào cần mạch chia áp hoặc mạch chuyển mức MOSFET. Kèm chương trình đo kiểm tra.

Encoder tuyệt đối: cấu tạo, mã Gray, đơn vòng, đa vòng, cách chế tạo và đọc AS5600 bằng ESP32
Encoder tuyệt đối: cấu tạo, mã Gray, đơn vòng, đa vòng, cách chế tạo và đọc AS5600 bằng ESP32

Encoder tuyệt đối hoạt động thế nào: đĩa mã nhị phân và mã Gray, đơn vòng, đa vòng, giao tiếp SSI, encoder từ tính, cách chế tạo và đọc cảm biến góc AS5600 bằng ESP32.

Số nhị phân cho trẻ: đếm bằng 5 thẻ chấm, giải mật thư như máy tính
Số nhị phân cho trẻ: đếm bằng 5 thẻ chấm, giải mật thư như máy tính

Dạy trẻ số nhị phân bằng 5 thẻ chấm 1, 2, 4, 8, 16: viết số, đếm và tìm quy luật, giải mật thư chữ cái, liên hệ với bit, màn hình và mạch ESP32. Hoạt động không cần máy tính.

5 dự án Scratch đầu tiên cho trẻ: từ thiệp động đến trò chơi bắt táo
5 dự án Scratch đầu tiên cho trẻ: từ thiệp động đến trò chơi bắt táo

Năm dự án Scratch đầu tiên cho học sinh lớp 3–5 từ dễ đến khó: thiệp động, nhân vật theo chuột, nhạc cụ ảo, đố vui phép nhân, bắt táo; khối chính, khái niệm và thử thách.

Encoder tương đối: cấu tạo, tín hiệu A B Z, cách chế tạo và đếm xung bằng PCNT của ESP32
Encoder tương đối: cấu tạo, tín hiệu A B Z, cách chế tạo và đếm xung bằng PCNT của ESP32

Encoder tương đối hoạt động thế nào: đĩa mã, mặt nạ, kênh A B lệch 90°, vạch Z, đếm x4, ngõ ra NPN, HTL, RS-422, cách chế tạo đĩa mã và đếm encoder bằng PCNT của ESP32.


Bình luận
Đã thêm vào giỏ hàng