Xúc xắc điện tử với ESP32 và OLED: thí nghiệm xác suất cho học sinh
Làm xúc xắc điện tử bằng ESP32 và màn hình OLED kèm 4 nút: tung một lần, tung nhanh 60 lần, vẽ biểu đồ; xác suất lý thuyết, thực nghiệm và luật số lớn cho học sinh.
Tung một xúc xắc 6 lần, có chắc mỗi mặt ra đúng một lần không? Tung 600 lần thì sao? Câu hỏi đơn giản này là cửa vào của môn xác suất. Bài viết hướng dẫn làm xúc xắc điện tử bằng ESP32 và màn hình OLED 0.96 inch kèm 4 nút: bấm một nút để tung, bấm nút khác để tung nhanh 60 lần và xem biểu đồ. Nhờ mạch tung hàng trăm lần trong chớp mắt, học sinh tự quan sát được điều mà tung tay phải mất cả buổi.
Qua dự án, trẻ học được: máy tính tạo số ngẫu nhiên như thế nào, cách vẽ mặt xúc xắc bằng câu lệnh điều kiện, xác suất lý thuyết và xác suất thực nghiệm khác nhau ra sao, và vì sao tung càng nhiều lần thì kết quả càng "đều".
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.
- 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, cài thư viện Adafruit SSD1306 (kèm Adafruit GFX Library và Adafruit BusIO) trong Tools > Manage Libraries.
- Tùy chọn: một xúc xắc thật để so sánh.
2. Nối dây

| Chân module | Chân ESP32 | Chân module | Chân ESP32 |
|---|---|---|---|
| GND | GND | K1 | D32 |
| VCC | 3V3 | K2 | D33 |
| SCL | D22 | K3 | D25 |
| SDA | D21 | K4 | D26 |
Cách nối giống hệt trò chơi đo phản xạ trong cùng loạt bài, nên có thể dùng chung một bộ mạch cho cả hai dự án. Mỗi lô mạch có thể đổi chỗ VCC và GND: 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 xúc xắc và thống kê
// PYWORLD.VN - ESP32 + OLED 0.96 inch kèm 4 nút: xúc xắc điện tử và thí nghiệm xác suất
// K1: tung 1 lần; K2: tung nhanh 60 lần; K3: xem biểu đồ số lần ra mỗi mặt; K4: xóa thống kê
// 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
int mucNghi[SO_NUT];
unsigned long soLanRa[7]; // soLanRa[1]..soLanRa[6]
unsigned long tongSoLan = 0;
int nutVuaBam() { // chờ nhả rồi mới trả về, mỗi lần bấm tính một lần
for (int i = 0; i < SO_NUT; i++) {
if (digitalRead(CHAN_NUT[i]) != mucNghi[i]) {
while (digitalRead(CHAN_NUT[i]) != mucNghi[i]) delay(10);
delay(30);
return i;
}
}
return -1;
}
// Vẽ mặt xúc xắc có số chấm n (1–6) trong ô vuông 56 x 56 ở góc trái trên (x, y)
void veMat(int x, int y, int n) {
oled.drawRoundRect(x, y, 56, 56, 8, SSD1306_WHITE);
int a = x + 14, b = x + 28, c = x + 42; // ba cột
int d = y + 14, e = y + 28, f = y + 42; // ba hàng
if (n % 2 == 1) oled.fillCircle(b, e, 5, SSD1306_WHITE); // chấm giữa: 1, 3, 5
if (n >= 2) { oled.fillCircle(a, d, 5, SSD1306_WHITE); oled.fillCircle(c, f, 5, SSD1306_WHITE); }
if (n >= 4) { oled.fillCircle(c, d, 5, SSD1306_WHITE); oled.fillCircle(a, f, 5, SSD1306_WHITE); }
if (n == 6) { oled.fillCircle(a, e, 5, SSD1306_WHITE); oled.fillCircle(c, e, 5, SSD1306_WHITE); }
}
void hienMotLan(int mat) {
oled.clearDisplay();
veMat(4, 4, mat);
oled.setTextSize(1);
oled.setCursor(70, 8);
oled.print("Ra mat:");
oled.setTextSize(3);
oled.setCursor(84, 24);
oled.print(mat);
oled.setTextSize(1);
oled.setCursor(70, 54);
oled.printf("Lan %lu", tongSoLan);
oled.display();
}
void hienBieuDo() {
unsigned long lonNhat = 1;
for (int m = 1; m <= 6; m++) if (soLanRa[m] > lonNhat) lonNhat = soLanRa[m];
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.printf("Tong %lu lan", tongSoLan);
for (int m = 1; m <= 6; m++) {
int cao = (int)(40 * soLanRa[m] / lonNhat); // cột cao nhất 40 điểm ảnh
int x = 4 + (m - 1) * 21;
oled.fillRect(x, 54 - cao, 14, cao, SSD1306_WHITE);
oled.setCursor(x + 4, 56);
oled.print(m);
}
oled.display();
Serial.printf("Tong %lu:", tongSoLan);
for (int m = 1; m <= 6; m++) Serial.printf(" %d=%lu", m, soLanRa[m]);
Serial.println();
}
int tung() {
int mat = random(1, 7); // số ngẫu nhiên từ 1 đến 6
soLanRa[mat]++;
tongSoLan++;
return mat;
}
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]);
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.print("K1: tung 1 lan\nK2: tung 60 lan\nK3: bieu do\nK4: xoa");
oled.display();
}
void loop() {
int nut = nutVuaBam();
if (nut == 0) { // K1: tung một lần, có hiệu ứng lăn
for (int i = 0; i < 8; i++) {
oled.clearDisplay();
veMat(4, 4, random(1, 7));
oled.display();
delay(60 + i * 15);
}
int mat = tung();
hienMotLan(mat);
Serial.printf("Lan %lu: %d\n", tongSoLan, mat);
} else if (nut == 1) { // K2: tung nhanh 60 lần
for (int i = 0; i < 60; i++) tung();
hienBieuDo();
} else if (nut == 2) { // K3: xem biểu đồ
hienBieuDo();
} else if (nut == 3) { // K4: xóa thống kê
for (int m = 0; m <= 6; m++) soLanRa[m] = 0;
tongSoLan = 0;
oled.clearDisplay();
oled.setCursor(0, 0);
oled.print("Da xoa thong ke");
oled.display();
}
delay(10);
}
Nạp chương trình khi không bấm nút nào. Màn hình hiện bảng chọn bốn chức năng:
- K1: tung một lần. Mặt xúc xắc "lăn" qua 8 khung hình, chậm dần (60 ms, 75 ms, 90 ms…) rồi dừng ở kết quả.
- K2: tung nhanh 60 lần và hiện biểu đồ. Bấm K2 nhiều lần thì số lần tung cộng dồn: 60, 120, 180…
- K3: xem lại biểu đồ. K4: xóa thống kê để làm thí nghiệm mới.
Bốn nút trên module có ký hiệu ^ v # *, còn chương trình gọi theo tên chân K1–K4. Nếu chưa biết nút nào là K1, cứ bấm thử: nút làm xúc xắc lăn là K1, nút hiện biểu đồ ngay là K2 hoặc K3.
Máy tính tung xúc xắc thế nào?
Lệnh random(1, 7) cho một số nguyên từ 1 đến 6 (số 7 không bao giờ ra). Khác với Arduino Uno, ESP32 có sẵn bộ tạo số ngẫu nhiên bằng phần cứng, và hàm random() của gói esp32 lấy số từ bộ này, nên mỗi lần bật mạch không lặp lại cùng một dãy số.
Vẽ 6 mặt bằng 4 câu lệnh "nếu"
Hàm veMat() không vẽ riêng từng mặt mà dùng quy luật của các chấm, một bài tư duy rất hay cho học sinh:
- Mặt lẻ (1, 3, 5) có chấm ở giữa:
n % 2 == 1. - Từ mặt 2 trở lên có hai chấm ở đường chéo: trên trái và dưới phải.
- Từ mặt 4 trở lên có thêm đường chéo còn lại.
- Riêng mặt 6 có thêm hai chấm ở giữa hai bên.
Hỏi trẻ: mặt 5 được vẽ bởi những câu lệnh nào? Vì sao mặt 3 không có chấm ở góc trên phải?

Biểu đồ trên màn hình tự co giãn: mặt ra nhiều nhất luôn cao 40 điểm ảnh, các mặt khác cao theo tỉ lệ. Muốn biết số lần chính xác, mở Serial Monitor ở 115200 baud: mỗi lần hiện biểu đồ, chương trình in một dòng dạng Tong 61: 1=11 2=4 3=12 4=10 5=13 6=11.
4. Xác suất lý thuyết và xác suất thực nghiệm

- Xác suất lý thuyết: xúc xắc cân đối có 6 mặt khả năng như nhau, nên mỗi mặt có xác suất 1/6, khoảng 16,7 %.
- Xác suất thực nghiệm: làm thí nghiệm thật, lấy số lần ra một mặt chia cho tổng số lần tung. Trong Chương trình Toán 2018, học sinh lớp 6 học khái niệm này.
Với 60 lần tung, mỗi mặt "nên" ra khoảng 10 lần, nhưng thực tế thường lệch vài lần. Đó không phải lỗi của chương trình: ngẫu nhiên nghĩa là không đoán trước được từng lần.
5. Tung càng nhiều, càng gần một phần sáu

Thí nghiệm với mạch: bấm K4 để xóa, bấm K2 một lần (60 lần tung) và ghi số lần ra từng mặt. Bấm K2 thêm 9 lần (600 lần) và ghi lại. Tính tỉ lệ phần trăm của từng mặt và so sánh. Các tỉ lệ sẽ tiến gần 16,7 % hơn. Các nhà toán học gọi hiện tượng này là luật số lớn: làm thí nghiệm càng nhiều lần, xác suất thực nghiệm càng gần xác suất lý thuyết.
6. Thí nghiệm và thử thách mở rộng
- So với xúc xắc thật: mỗi nhóm tung xúc xắc thật 30 lần và ghi bảng, cả lớp cộng kết quả lại. Kết quả có giống xúc xắc điện tử không?
- Số "giả" ngẫu nhiên: thêm dòng
randomSeed(42);vào cuốisetup(). Tung 5 lần, ghi lại, rồi bấm nút EN để khởi động lại và tung tiếp 5 lần. Hai dãy giống hệt nhau! Khi gọirandomSeed(), chương trình chuyển sang cách tính số "giả ngẫu nhiên": cùng một số khởi đầu thì cùng một dãy. - Xúc xắc gian lận: trong hàm
tung(), đổi thànhint mat = random(1, 8); if (mat == 7) mat = 6;. Mặt 6 bây giờ có xác suất 2/7. Một bạn sửa chương trình bí mật, bạn khác dùng thống kê để phát hiện xúc xắc có gian lận không. Cần tung bao nhiêu lần mới chắc chắn? - Hai xúc xắc: xem hình bên dưới.

Tung hai xúc xắc và cộng hai mặt, tổng nào hay ra nhất? Bảng 36 ô cho thấy tổng 7 có 6 cách, còn tổng 2 và 12 chỉ có 1 cách. Học sinh lớn có thể sửa chương trình: tung hai lần random(1, 7), cộng lại, đếm 11 loại tổng (mảng có 13 phần tử, dùng từ chỉ số 2 đến 12) và vẽ biểu đồ 11 cột hẹp hơn, rồi so với biểu đồ lý thuyết.
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ý |
|---|---|---|
| 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; đổi 0x3C thành 0x3D |
| Báo lỗi không tìm thấy Adafruit_SSD1306.h | Chưa cài thư viện | Cài Adafruit SSD1306 kèm Adafruit GFX Library và Adafruit BusIO |
| Vừa bật mạch, xúc xắc tự lăn liên tục | Đang giữ nút lúc khởi động nên mức nghỉ bị ghi sai | Bỏ tay ra, bấm nút EN để khởi động lại |
| Bấm nút không có phản ứng | Dâ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ống | Kiểm tra dây; thử đổi INPUT_PULLUP thành INPUT_PULLDOWN |
| Bấm K3 thấy "Tong 0 lan" và không có cột nào | Chưa tung lần nào, hoặc vừa bấm K4 xóa thống kê | Bấm K1 hoặc K2 để tung trước, rồi mới xem biểu đồ |
8. Gợi ý cho phụ huynh và giáo viên
- Đoán trước, đo sau: trước khi bấm K2, cho trẻ đoán mỗi mặt sẽ ra bao nhiêu lần trong 60 lần, viết ra giấy rồi so với kết quả.
- Liên môn: Toán (phân số, tỉ số phần trăm, biểu đồ cột, xác suất thực nghiệm lớp 6), Tin học (câu lệnh điều kiện, mảng đếm, vòng lặp), và cả Khoa học (thí nghiệm lặp lại nhiều lần).
- Trò chơi lớp học: dùng xúc xắc điện tử thay xúc xắc thật trong các trò chơi cờ, rồi thảo luận: xúc xắc điện tử có "công bằng" hơn không?
- Không cổ vũ cờ bạc: nếu trẻ liên hệ với trò chơi ăn tiền, đây là dịp tốt để giải thích bằng số liệu vì sao người chơi lâu dài luôn thua: luật chơi được tính để nhà cái có lợi.
- Thời lượng: 2 buổi 45 phút. Buổi 1: nối dây, chơi xúc xắc, đọc hàm
veMat(). Buổi 2: thí nghiệm 60 và 600 lần, tính phần trăm, một thử thách mở rộng.
9. Xem thêm
- Lệnh vẽ chữ, hình trên màn hình OLED: màn hình OLED 0.96 inch SSD1306 với Arduino Nano.
- Hai dây SDA, SCL hoạt động thế nào: giao tiếp I2C 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ó: trò chơi đo phản xạ với ESP32 và màn hình OLED (có chương trình kiểm tra 4 nút), thuật toán qua trò chơi kiểu Bebras, số nhị phân cho trẻ với thẻ chấm.
Xem thêm