ทำหุ่นยนต์มีหน้า แสดงอารมณ์บนจอ OLED ด้วย ESP32
หุ่นยนต์ตั้งโต๊ะที่มีตาบนจอ OLED SSD1306 กะพริบตาเองได้ หันหัวตามแสงด้วย servo และเปลี่ยนอารมณ์ตามเซนเซอร์ เข้าใกล้แล้วตกใจ เจอแสงแล้วดีใจ มีวงจรและโค้ดครบ
หุ่นยนต์ตัวนี้ไม่มีล้อ วางบนโต๊ะแล้วทำตัวเหมือนสัตว์เลี้ยง: มีตาสองข้างบนจอ OLED ที่กะพริบเองเป็นระยะ
หันหัวตามแสงด้วย servo SG90 ตกใจเมื่อมีมือเข้าใกล้ ดีใจเมื่อห้องสว่าง เศร้าเมื่อมืด และยิ้มเมื่อถูกลูบหัว (กดปุ่ม)
เป็นโปรเจกต์ที่ใช้ชิ้นส่วนไม่กี่ชิ้น แต่ได้ฝึกครบทั้งจอ I²C, servo, เซนเซอร์ analog และการเขียนโปรแกรมแบบไม่ใช้ delay()
หุ่นตัวนี้ทำงานอย่างไร
- ตาบนจอ วาดตาเป็นสี่เหลี่ยมมุมมนสองอัน แล้วเปลี่ยนรูปร่างตามอารมณ์ ไม่ต้องใช้ไฟล์รูป
- กะพริบตา ทุก 2–5 วินาทีแบบสุ่ม ตาจะหรี่ลงแล้วเปิดกลับภายในราว 0.15 วินาที รายละเอียดเล็กๆ นี้ทำให้หุ่นดูมีชีวิตที่สุด
- หัวหันตามแสง LDR สองตัวติดบนหัวหันเฉียงซ้ายขวา servo หมุนหัวไปทางตาที่สว่างกว่าจนสองตาอ่านค่าใกล้กัน
- อารมณ์จากเซนเซอร์ เลือกตามลำดับความสำคัญ:
| เงื่อนไข | อารมณ์ |
|---|---|
| ของอยู่ใกล้กว่า 20 cm (HC-SR04) | ตกใจ: ตาโตขึ้น |
| ถูกกดปุ่มบนหัวไม่เกิน 3 วินาทีที่ผ่านมา | ดีใจ: ตายิ้ม |
| แสงต่ำกว่า 20 % | เศร้า: เปลือกตาตกด้านนอก |
| แสงมากกว่า 60 % | ดีใจ |
| ของอยู่ในระยะ 20–50 cm | สงสัย: ตาข้างหนึ่งหรี่ |
| ไม่มีอะไรเกิดขึ้น | ปกติ |
ชิ้นส่วน
| ชิ้นส่วน | จำนวน | ราคาประมาณ | ร้าน |
|---|---|---|---|
| ESP32 DevKit V1 (30 ขา) | 1 | ฿130–฿250 | Shopee |
| Ultrasonic HC-SR04 | 1 | ฿30–฿60 | Shopee |
| โมดูลวัดแสง LDR (มีขา AO) | 2 | ฿15–฿40 | Shopee |
| ปุ่มกด tactile 12 mm + ฝาครอบ | 1 | ฿5–฿15 | Shopee |
| Servo SG90 | 1 | ฿40–฿80 | Shopee |
| OLED SSD1306 0.96" I²C | 1 | ฿80–฿150 | Shopee |
| ถ่าน Li-ion 18650 | 2 | ฿60–฿150 | Shopee |
| รางถ่าน 18650 แบบ 2 ก้อน (2S) | 1 | ฿20–฿60 | Shopee |
| Buck converter 7.4 V → 5 V 3 A | 1 | ฿30–฿90 | Shopee |
| สวิตช์เปิด-ปิด | 1 | ฿5–฿20 | Shopee |
| สายจัมเปอร์ ผู้-ผู้ / ผู้-เมีย ชุด 40 เส้น | 1 | ฿30–฿60 | Shopee |
| ตัวต้านทาน 1 kΩ + 2 kΩ (แบ่งแรงดัน 5 V → 3.3 V) | 1 | ฿2–฿10 | Shopee |
| แนะนำเพิ่ม | |||
| ที่ชาร์จถ่าน 18650 | 1 | ฿100–฿250 | Shopee |
| รวมโดยประมาณ | ฿522–฿1,175 | ||
ราคาประมาณการ ณ 2026-10-31 ยังไม่ได้ตรวจกับร้าน · บางลิงก์เป็นลิงก์ affiliate (อ่านเพิ่ม)
ถ้าหุ่นวางอยู่บนโต๊ะข้างคอมพิวเตอร์ตลอด ใช้ไฟจาก adapter USB 5 V 2 A แทนชุดแบตได้ แต่ servo ต้องรับไฟจากแหล่งที่จ่ายกระแสได้พอ (ดู ต่อ Servo SG90 กับ ESP32 ให้ไม่กระตุก)
วงจรและการต่อขา
| HC-SR04 Front sonar | VCC → 5VTRIG → GPIO26ECHO → GPIO34 (ผ่าน 1 kΩ / 2 kΩ)GND → GND |
|---|---|
| LDR Left eye LDR | VCC → 3V3GND → GNDAO → GPIO32 |
| LDR Right eye LDR | VCC → 3V3GND → GNDAO → GPIO33 |
| Tactile 12mm Head button | COM → GNDNO → GPIO27 |
| SG90 Head servo | GND → GNDVCC → 5VPWM → GPIO25 |
| SSD1306 Face Display | GND → GNDVCC → 3V3SCL → GPIO22SDA → GPIO21 |
ตัวตรวจวงจรในตัวจำลองเตือนเรื่องเดียว คือขา ECHO ของ HC-SR04 ส่งสัญญาณ 5 V ต้องผ่านตัวต้านทานแบ่งแรงดันก่อนเข้า ESP32 (วิธีต่ออยู่ใน บทความ HC-SR04):
- ! Front sonar ECHO ส่งสัญญาณ 5 V เข้าขา 3.3 V: ใส่ตัวต้านทานแบ่งแรงดัน 1 kΩ / 2 kΩ
- จอ SSD1306 ใช้ I²C ขา SDA = GPIO 21, SCL = GPIO 22 ซึ่งเป็นขา I²C มาตรฐานของ ESP32 address ส่วนใหญ่คือ
0x3C - LDR ใช้ GPIO 32 และ 33 ซึ่งเป็น ADC1 อ่านค่าได้แม้ภายหลังจะเปิด WiFi
- ปุ่มต่อระหว่าง GPIO 27 กับ GND ใช้ pull-up ในตัว อ่านได้ LOW เมื่อกด
โค้ด (Arduino IDE)
ติดตั้งไลบรารี Adafruit SSD1306 (จะติดตั้ง Adafruit GFX ให้ด้วย) และ ESP32Servo จาก Library Manager
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Servo.h>
// ขาตามตารางด้านบน
const int LDR_LEFT = 32, LDR_RIGHT = 33;
const int TRIG = 26, ECHO = 34; // ECHO ผ่านตัวต้านทาน 1 kΩ / 2 kΩ
const int BUTTON = 27, SERVO_PIN = 25;
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
Servo head;
enum Mood { NEUTRAL, HAPPY, SAD, SURPRISED, CURIOUS };
/* ---------- ตา ---------- */
// open: 1 = ลืมตาเต็มที่, ค่าน้อยลง = กำลังกะพริบ
void drawEyes(Mood mood, float open) {
oled.clearDisplay();
for (int side = -1; side <= 1; side += 2) { // -1 = ตาซ้าย, +1 = ตาขวา
int cx = 64 + side * 28, cy = 30;
int w = 32, h = 28;
if (mood == SURPRISED) { w = 30; h = 40; }
if (mood == CURIOUS && side > 0) h = 16; // ตาขวาหรี่ = สงสัย
h = max(2, int(h * open));
oled.fillRoundRect(cx - w / 2, cy - h / 2, w, h, min(8, h / 2), SSD1306_WHITE);
if (mood == HAPPY) // ตัดขอบล่างเป็นโค้ง = ตายิ้ม
oled.fillCircle(cx, cy + h / 2 + 10, w / 2 + 4, SSD1306_BLACK);
if (mood == SAD) { // เปลือกตาตกด้านนอก
int outer = cx + side * w / 2, inner = cx - side * w / 2, top = cy - h / 2;
oled.fillTriangle(outer, top, inner, top, outer, cy + 2, SSD1306_BLACK);
}
}
oled.display();
}
float blink() { // คืนค่าการลืมตา 0.1–1 ตามจังหวะกะพริบ
static unsigned long next = 3000, start = 0;
unsigned long now = millis();
if (now > next) { start = now; next = now + random(2000, 5000); }
unsigned long t = now - start;
if (t < 75) return 1.0 - t / 75.0 * 0.9; // หลับตาลง
if (t < 150) return 0.1 + (t - 75) / 75.0 * 0.9; // ลืมตาขึ้น
return 1.0;
}
/* ---------- เซนเซอร์ ---------- */
float distanceCm() {
digitalWrite(TRIG, LOW); delayMicroseconds(2);
digitalWrite(TRIG, HIGH); delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long us = pulseIn(ECHO, HIGH, 25000); // ไม่ได้ยินเสียงสะท้อนใน 25 ms = ไกลเกินวัด
return us ? us / 58.0 : 999;
}
int lightPercent(int pin) { return analogRead(pin) * 100 / 4095; }
/* ---------- หัวหันตามแสง ---------- */
float headAngle = 90;
void trackLight() {
int l = lightPercent(LDR_LEFT), r = lightPercent(LDR_RIGHT);
if (l + r < 5) return; // มืดเกินไป ไม่รู้ว่าแสงอยู่ทางไหน
float diff = float(r - l) / (r + l); // -1..1: บวก = ขวาสว่างกว่า
if (abs(diff) < 0.05) return; // สองตาใกล้กันแล้ว: หันถูกทาง
headAngle = constrain(headAngle + diff * 3, 10, 170); // ถ้าหัวหันหนีแสง ให้เปลี่ยน + เป็น -
head.write(int(headAngle));
}
/* ---------- โปรแกรมหลัก ---------- */
unsigned long patted = 0;
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
pinMode(BUTTON, INPUT_PULLUP);
Wire.begin(21, 22);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); // จอไม่ขึ้น: ลอง 0x3D
head.setPeriodHertz(50);
head.attach(SERVO_PIN, 500, 2400);
head.write(int(headAngle));
randomSeed(analogRead(LDR_LEFT));
}
void loop() {
static unsigned long lastSense = 0;
static Mood mood = NEUTRAL;
if (digitalRead(BUTTON) == LOW) patted = millis();
if (millis() - lastSense > 50) { // อ่านเซนเซอร์ 20 ครั้งต่อวินาที
lastSense = millis();
trackLight();
float near = distanceCm();
int light = max(lightPercent(LDR_LEFT), lightPercent(LDR_RIGHT));
if (near < 20) mood = SURPRISED;
else if (millis() - patted < 3000) mood = HAPPY;
else if (light < 20) mood = SAD;
else if (light > 60) mood = HAPPY;
else if (near < 50) mood = CURIOUS;
else mood = NEUTRAL;
}
drawEyes(mood, blink()); // วาดทุกรอบ การกะพริบจึงลื่น
}
ค่าแสง 20 % และ 60 % ขึ้นกับห้องและโมดูล LDR แต่ละตัว ให้เปิด Serial แล้วพิมพ์ค่า lightPercent() ตอนเปิดไฟและปิดไฟ
แล้วตั้งเกณฑ์ให้อยู่ระหว่างสองค่านั้น โมดูล LDR บางรุ่นให้ค่ายิ่งมากยิ่งมืด ถ้าเป็นแบบนั้นให้ใช้ 100 - …
ปัญหาที่พบบ่อย
- จอไม่ขึ้นอะไรเลย เช็กสาย SDA / SCL ว่าไม่สลับกัน ลอง address
0x3Dและดูว่าจอเป็นรุ่น SSD1306 ไม่ใช่ SH1106 (จอ 1.3" มักเป็น SH1106 ต้องใช้ไลบรารีอื่น) - ตาขาดๆ หรือภาพเลื่อน สาย I²C ยาวเกินไป ให้สั้นไม่เกิน 20–30 cm
- หัวสั่นไปมาไม่หยุด เพิ่มค่า
0.05ในtrackLight()หรือลดตัวคูณ3ลง - บอร์ดรีเซ็ตตอน servo หมุน ไฟไม่พอ อ่าน วิธีจ่ายไฟให้ servo
ลองในตัวจำลอง
หุ่นในตัวจำลองมีชิ้นส่วนครบตามบทความ: จอ SSD1306, หัว servo, LDR สองตา, HC-SR04 และปุ่มบนหัว โปรแกรมตัวอย่างในหน้า Code ทำงานแบบเดียวกับโค้ด Arduino ด้านบน ลองทำแบบนี้แล้วดูหน้าเปลี่ยน:
- ลากหลอดไฟไปรอบๆ หุ่น หัวจะหันตาม
- ลากหลอดไฟไปมุมขวาบนของสนาม แสงที่ตาจะเหลือไม่ถึง 20 % หน้าจะเศร้า
- ลากกล่องสีส้มมาไว้หน้าหุ่น ห่างราว 30 cm จะสงสัย ใกล้กว่า 20 cm จะตกใจ
- กดปุ่ม Head button ที่แถบ Drive ใต้ภาพจำลอง หุ่นจะยิ้ม