Đố toán nhanh trên OLED 4 nút với ESP32: trò chơi luyện tính nhẩm cho học sinh

Làm máy đố toán nhanh bằng ESP32 và màn hình OLED kèm 4 nút: cộng trừ đến 20, bảng nhân 2–9, 4 đáp án cho 4 nút, đếm câu đúng và thời gian, cách tạo đáp án sai gần đúng.

Luyện tính nhẩm sẽ vui hơn nhiều khi biến thành trò chơi có điểm, có đồng hồ. Bài viết hướng dẫn làm máy đố toán nhanh bằng ESP32 và màn hình OLED 0.96 inch kèm 4 nút: màn hình hiện một phép tính và 4 đáp án, người chơi bấm nút có đáp án đúng. Có ba cấp độ: cộng trừ trong phạm vi 20, bảng nhân 2–9 và trộn cả hai; mỗi lượt 10 câu, máy đếm số câu đúng và thời gian.

Ngoài luyện toán, dự án cho học sinh thấy một chương trình "ra đề" như thế nào: số ngẫu nhiên, mảng đáp án, và cách chọn đáp án sai sao cho không đoán bừa được.

1. Chuẩn bị

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; màn hình hiện bảng chọn cấp độ cộng trừ đến 20, bảng nhân 2 đến 9, trộn cả hai
Hình minh họa: nối các chân cùng màu.

Cách nối giống hệt trò chơi đo phản xạ và xúc xắc điện tử: I2C dùng D21 (SDA), D22 (SCL); bốn nút K1–K4 nối D32, D33, D25, D26; VCC nối 3V3. Mỗi lô mạch có thể đổi chỗ VCC và GND, nên luôn đọc chữ in cạnh hàng chân trước khi cắm dây nguồn.

3. Chương trình đố toán nhanh

// PYWORLD.VN - ESP32 + OLED 0.96 inch kèm 4 nút: đố toán nhanh, luyện tính nhẩm
// Mỗi câu có 4 đáp án ứng với 4 nút (^ v # *); 10 câu, tính số câu đúng và thời gian
// 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_CAU = 10;
int mucNghi[SO_NUT];
int capDo = 0;                  // 0: cộng trừ trong 20; 1: bảng nhân; 2: trộn cả hai

// 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;
}

// Chờ người chơi bấm một nút rồi nhả ra; trả về số thứ tự nút
int choBamNut() {
  int nut;
  while ((nut = nutDangBam()) == -1) delay(5);
  while (nutDangBam() != -1) delay(10);
  delay(30);                    // chờ hết dội phím
  return nut;
}

// Tạo một câu hỏi: ghi đề vào de[], ghi "bước" để tạo đáp án nhiễu, trả về đáp án đúng
int taoCauHoi(char* de, int doDai, int& buoc) {
  int loai = (capDo == 0) ? random(2) : (capDo == 1 ? 2 : random(3));
  int a, b;
  if (loai == 0) {              // phép cộng, tổng không quá 20
    a = random(1, 11); b = random(1, 11);
    snprintf(de, doDai, "%d + %d", a, b);
    buoc = 1;
    return a + b;
  }
  if (loai == 1) {              // phép trừ, kết quả không âm
    a = random(5, 21); b = random(1, a + 1);
    snprintf(de, doDai, "%d - %d", a, b);
    buoc = 1;
    return a - b;
  }
  a = random(2, 10); b = random(2, 10);   // bảng nhân 2–9
  snprintf(de, doDai, "%d x %d", a, b);
  buoc = a;                     // nhầm sang hàng bên cạnh của bảng nhân: lệch đúng a
  return a * b;
}

// Xếp 4 đáp án: 1 đúng, 3 sai gần đúng (không âm, không trùng); trả về vị trí đáp án đúng
int xepDapAn(int dung, int buoc, int dapAn[]) {
  int ungVien[8] = {dung - 1, dung + 1, dung - 2, dung + 2, dung - buoc, dung + buoc, dung + 10, dung - 10};
  for (int i = 7; i > 0; i--) { // trộn thứ tự các ứng viên
    int j = random(i + 1);
    int t = ungVien[i]; ungVien[i] = ungVien[j]; ungVien[j] = t;
  }
  int viTriDung = random(SO_NUT);
  for (int k = 0; k < SO_NUT; k++) dapAn[k] = -1;
  dapAn[viTriDung] = dung;
  int o = 0;
  for (int i = 0; i < 8; i++) {
    while (o < SO_NUT && dapAn[o] != -1) o++;  // tìm ô còn trống
    if (o >= SO_NUT) break;
    int v = ungVien[i];
    bool boQua = (v < 0);
    for (int k = 0; k < SO_NUT; k++) if (dapAn[k] == v) boQua = true;
    if (!boQua) dapAn[o] = v;
  }
  return viTriDung;
}

void manHinhCau(int cau, int diem, const char* de, int dapAn[]) {
  char dong[24];
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  snprintf(dong, sizeof(dong), "Cau %d/%d   Dung %d", cau, SO_CAU, diem);
  oled.print(dong);
  oled.setTextSize(2);
  oled.setCursor(0, 12);
  snprintf(dong, sizeof(dong), "%s=?", de);
  oled.print(dong);
  for (int k = 0; k < SO_NUT; k++) {
    oled.setCursor((k % 2) * 64, 32 + (k / 2) * 16);
    snprintf(dong, sizeof(dong), "%c %d", KY_HIEU[k], dapAn[k]);
    oled.print(dong);
  }
  oled.display();
}

void hienHaiDong(const char* dong1, const char* dong2) {
  oled.clearDisplay();
  oled.setTextSize(2);
  oled.setCursor(0, 8);
  oled.print(dong1);
  oled.setCursor(0, 36);
  oled.print(dong2);
  oled.display();
}

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() {
  // Chọn cấp độ bằng 3 nút đầu
  char dong[24];
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 0);
  oled.print("Do toan nhanh");
  const char* TEN_CAP[3] = {"Cong tru den 20", "Bang nhan 2-9", "Tron ca hai"};
  for (int k = 0; k < 3; k++) {
    oled.setCursor(0, 16 + k * 12);
    snprintf(dong, sizeof(dong), "%c %s", KY_HIEU[k], TEN_CAP[k]);
    oled.print(dong);
  }
  oled.display();
  int chon;
  while ((chon = choBamNut()) > 2) {}   // nút thứ tư không dùng ở đây
  capDo = chon;

  int diem = 0;
  unsigned long batDau = millis();
  for (int cau = 1; cau <= SO_CAU; cau++) {
    char de[16];
    int buoc;
    int dung = taoCauHoi(de, sizeof(de), buoc);
    int dapAn[SO_NUT];
    int viTriDung = xepDapAn(dung, buoc, dapAn);
    manHinhCau(cau, diem, de, dapAn);
    int nut = choBamNut();
    if (nut == viTriDung) {
      diem++;
      hienHaiDong("Dung roi!", "");
    } else {
      snprintf(dong, sizeof(dong), "%s=%d", de, dung);
      hienHaiDong("Chua dung", dong);
    }
    Serial.printf("Cau %d: %s = %d, chon %d -> %s\n", cau, de, dung, dapAn[nut], nut == viTriDung ? "dung" : "sai");
    delay(nut == viTriDung ? 600 : 1500);
  }
  unsigned long giay = (millis() - batDau) / 1000;

  snprintf(dong, sizeof(dong), "%d/%d cau", diem, SO_CAU);
  char dong2[24];
  snprintf(dong2, sizeof(dong2), "%lu giay", giay);
  hienHaiDong(dong, dong2);
  Serial.printf("Ket qua: %d/%d cau dung trong %lu giay\n", diem, SO_CAU, giay);
  delay(1000);
  choBamNut();                  // bấm nút bất kỳ để chơi lại
}

Trước khi nạp, sửa mảng KY_HIEU cho đúng mạch của mình: phần tử thứ nhất là ký hiệu in cạnh nút K1, thứ hai cạnh K2… Cách tìm nút nào là K1 có trong bài trò chơi đo phản xạ (chương trình kiểm tra 4 nút).

4. Trò chơi trên màn hình

Năm màn hình OLED mô phỏng: bảng chọn cấp độ; câu 3 trên 10 với phép tính 7 x 8 và bốn đáp án 63, 56, 49, 58 kèm ký hiệu; Dung roi; Chua dung kèm 7 x 8 = 56; kết quả 8 trên 10 câu trong 47 giây
Hình mô phỏng: chữ và cỡ chữ theo đúng chương trình; câu hỏi và kết quả là ví dụ.
  1. Màn hình đầu tiên cho chọn cấp độ bằng ba nút đầu.
  2. Mỗi câu hiện ở cỡ chữ lớn, bốn đáp án xếp thành hai hàng, mỗi đáp án có ký hiệu của một nút.
  3. Bấm đúng: "Dung roi!" trong 0,6 giây rồi sang câu mới. Bấm sai: hiện lại phép tính kèm đáp án đúng trong 1,5 giây để trẻ kịp đọc.
  4. Hết 10 câu, màn hình hiện số câu đúng và số giây đã dùng. Bấm nút bất kỳ để chơi lại.

5. Chương trình tạo đáp án sai như thế nào?

Với câu 7 x 8 bằng 56, chương trình lập 8 ứng viên: 55, 57, 54, 58, 49 là 7 x 7, 63 là 7 x 9, 66 và 46; trộn ngẫu nhiên, chọn ô cho đáp án đúng, lấy lần lượt ứng viên điền 3 ô còn lại, bỏ số âm và số đã có
Hình minh họa: danh sách ứng viên đúng như hàm xepDapAn.
  • Hàm taoCauHoi() chọn loại phép tính theo cấp độ, chọn hai số ngẫu nhiên và trả về đáp án đúng. Phép trừ luôn chọn số bị trừ lớn hơn số trừ để kết quả không âm.
  • Hàm xepDapAn() lập 8 ứng viên quanh đáp án đúng: lệch 1, lệch 2, lệch 10, và với phép nhân thì lệch đúng một hàng của bảng nhân (7 x 7, 7 x 9). Đây là những chỗ trẻ hay nhầm nhất, nên đáp án sai trông "rất giống thật".
  • Thuật toán trộn dùng ở đây là đổi chỗ ngẫu nhiên từ cuối mảng lên đầu, một cách trộn đều được dùng rất nhiều trong lập trình trò chơi.

6. Thử thách mở rộng

  • Thêm phép chia: chọn a, b ngẫu nhiên rồi hỏi "a x b : b", để kết quả luôn là số nguyên.
  • Đếm ngược: mỗi câu chỉ có 5 giây; hết giờ tính là sai. Gợi ý: dùng millis() như trò chơi đo phản xạ.
  • Ghi nhớ câu sai: lưu các câu trả lời sai vào mảng, cuối lượt hỏi lại những câu đó.
  • Kỷ lục: lưu thời gian tốt nhất bằng thư viện Preferences để tắt nguồn vẫn còn.

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

  • Theo lớp: lớp 1–2 chơi cấp cộng trừ đến 20; lớp 3 trở lên chơi bảng nhân; trộn cả hai để ôn tập.
  • So với chính mình: khuyến khích trẻ phá kỷ lục cá nhân về số câu đúng và thời gian, không xếp hạng cả lớp.
  • Đổi vai: học sinh lớn đọc chương trình và sửa cho máy ra đề theo ý mình (chỉ bảng nhân 7, chỉ phép trừ có nhớ…) để em nhỏ luyện tập.
  • Thời lượng: 1–2 buổi 45 phút: nối dây, nạp chương trình, chơi; buổi sau đọc hàm xepDapAn() và làm một thử thách.

8. Xem thêm


Tin tức liên quan

Biến trở, quang trở và nhiệt điện trở NTC: cách dùng và đọc bằng ESP32
Biến trở, quang trở và nhiệt điện trở NTC: cách dùng và đọc bằng ESP32

Cấu tạo và cách mắc biến trở, biến trở B10K và A10K, quang trở LDR, nhiệt điện trở NTC, PTC, phương trình B, thí nghiệm đọc biến trở, LDR, NTC bằng ADC của ESP32 và chỉnh độ sáng LED.

Trẻ mấy tuổi nên học lập trình? Lộ trình từ 4 đến 15 tuổi cho phụ huynh
Trẻ mấy tuổi nên học lập trình? Lộ trình từ 4 đến 15 tuổi cho phụ huynh

Trẻ mấy tuổi nên học lập trình: lộ trình năm bước từ trò chơi không màn hình, ScratchJr, Scratch đến mạch ESP32 và Python, kèm dấu hiệu trẻ sẵn sàng và câu hỏi thường gặp.

Chuẩn chiều dài: căn mẫu song phẳng ISO 3650, giao thoa laser và nhiệt độ tham chiếu 20 °C
Chuẩn chiều dài: căn mẫu song phẳng ISO 3650, giao thoa laser và nhiệt độ tham chiếu 20 °C

Mét được hiện thực ra sao, căn mẫu song phẳng và các cấp K, 0, 1, 2 theo ISO 3650, hiệu chuẩn căn mẫu bằng giao thoa và so sánh, giao thoa laser cho máy CNC và vì sao đo ở 20 °C.

Jack Type-C sạc pin loại 4 dây: một cổng vừa sạc pin 1S vừa truyền dữ liệu USB
Jack Type-C sạc pin loại 4 dây: một cổng vừa sạc pin 1S vừa truyền dữ liệu USB

Jack Type-C tích hợp mạch sạc loại 4 dây có thêm D+ D− để một cổng vừa sạc pin 1S vừa nối USB với máy tính. Bài hướng dẫn xác định từng dây bằng đồng hồ, nối vào vi điều khiển và các lưu ý khi dùng chung cổng.

Xốp định hình cho vali nhôm, vali kéo: sắp xếp bộ dụng cụ nhiều chi tiết
Xốp định hình cho vali nhôm, vali kéo: sắp xếp bộ dụng cụ nhiều chi tiết

Gia công xốp định hình cho vali nhôm và vali kéo: khoang tròn, khoang chữ nhật, rãnh dài cho bộ dụng cụ nhiều chi tiết. Xem hai mẫu thực tế, bảng kích thước vali nhôm và cách đặt làm.

Chân D+ D− (DP, DM) trên jack USB Type-C: vị trí, cách nối và phân biệt với DisplayPort
Chân D+ D− (DP, DM) trên jack USB Type-C: vị trí, cách nối và phân biệt với DisplayPort

D+ D− (DP, DM) là cặp dữ liệu USB 2.0 nằm ở chân A6, A7, B6, B7 của cổng Type-C. Bài hướng dẫn nối jack Type-C 4 dây vào mạch có USB, vai trò của D+ D− khi sạc và cách phân biệt với DisplayPort.

Diode: tiếp giáp p-n, chỉnh lưu, chống cắm ngược nguồn và diode bảo vệ
Diode: tiếp giáp p-n, chỉnh lưu, chống cắm ngược nguồn và diode bảo vệ

Diode là gì: chất bán dẫn và tiếp giáp p-n, đặc tuyến diode silic và Schottky, thông số 1N4148, 1N4007, 1N5819, ứng dụng chỉnh lưu, chống cắm ngược, chọn nguồn, kẹp bảo vệ, cách kiểm tra.

Cảm biến pH: cấu tạo điện cực thủy tinh, nguyên lý Nernst, cách chế tạo và đo pH bằng ESP32
Cảm biến pH: cấu tạo điện cực thủy tinh, nguyên lý Nernst, cách chế tạo và đo pH bằng ESP32

Cảm biến pH hoạt động thế nào: thang pH, điện cực thủy tinh kết hợp, phương trình Nernst và bù nhiệt độ, hiệu chuẩn, bảo quản, cách chế tạo điện cực và đo pH bằng module với ESP32.


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