นี่คือโครงการ IoT ที่ไม่ซับซ้อนนัก แต่เปี่ยมไปด้วยโอกาสในการเรียนรู้ที่ลึกซึ้งและครอบคลุม
ผมจะเริ่มต้นด้วยภาพรวมโดยละเอียดของขั้นตอนต่างๆ ที่ควรปฏิบัติตาม ก่อนที่จะดำดิ่งสู่การสร้างผลิตภัณฑ์ขั้นสุดท้าย แม้ว่าจะมีเนื้อหาเกี่ยวกับรถยนต์หุ่นยนต์ DIY จำนวนมากบนอินเทอร์เน็ต แต่ผมก็ต้องใช้เวลาหลายวันที่ยากลำบากโดยไม่มีความคืบหน้าอย่างมีนัยสำคัญในการทำงานกับโครงการนี้ โดยเฉพาะอย่างยิ่งในฐานะที่ผมยังเป็นมือใหม่ในสาขา IoT ดังนั้น ผมจึงขอเสนอคู่มือฉบับทีละขั้นตอนสำหรับวิธีการที่ *ผม* ประสบความสำเร็จกับโครงการนี้ รวมถึงปัญหาทั้งหมดที่ผมพบเจอ (และแน่นอนว่าวิธีการเอาชนะปัญหาเหล่านั้น)
ก่อนอื่น ตรวจสอบให้แน่ใจว่าได้จัดหาฮาร์ดแวร์ทั้งหมดที่ระบุไว้ด้านล่างนี้แล้ว!
1. Arduino Uno และ Motor Drivers
ผมเลือกใช้โมดูลขับเคลื่อนมอเตอร์ L298N (เนื่องจากเป็นสิ่งที่อินเทอร์เน็ตแนะนำอย่างกว้างขวาง) L298N เป็นไอซีขับเคลื่อนมอเตอร์แบบ H-Bridge ที่สามารถควบคุมมอเตอร์กระแสตรงสองตัวหรือสเต็ปเปอร์มอเตอร์หนึ่งตัวได้ การทำงานของมันคือการควบคุมทิศทางการหมุนของมอเตอร์โดยการสลับขั้วไฟฟ้าที่จ่ายให้กับมอเตอร์ และควบคุมความเร็วโดยใช้สัญญาณ Pulse Width Modulation (PWM) การทำความเข้าใจวิธีการทำงานของ Motor Driver และวิธีเชื่อมต่อกับบอร์ด Arduino ของคุณโดยละเอียด มีให้ดูได้ ที่นี่ เว็บเพจนี้ช่วยผมได้มากในการเริ่มต้นสร้างรถบังคับวิทยุของผม
ผมใช้ Motor Driver จำนวน 2 ตัวเพื่อควบคุมมอเตอร์ 4 ตัว ในรถขับเคลื่อน 4 ล้อ (4WD) เนื่องจาก Arduino Uno มีขา GPIO (General Purpose Input/Output) ที่จำกัด ทำให้ผมไม่สามารถเขียนโปรแกรมเพื่อควบคุมมอเตอร์ทั้ง 4 ตัวแยกกันได้อย่างอิสระ วิธีแก้ปัญหาเดียวคือการเชื่อมต่อ Motor Driver ทั้งสองตัวเข้าด้วยกันในลักษณะขนาน ผมกำหนดให้ Motor Driver ตัวหนึ่งสำหรับมอเตอร์คู่หลัง และอีกตัวสำหรับมอเตอร์คู่หน้า การจัดเรียงวงจรของผมจึงเป็นไปในลักษณะที่ว่า เมื่อได้รับอินพุตเฉพาะ มอเตอร์ด้านซ้ายทั้งสองตัว (จาก Motor Driver ทั้งสองตัว) จะมีการเปลี่ยนแปลง (ความเร็วและทิศทาง) เหมือนกัน เช่นเดียวกับมอเตอร์ด้านขวาของทั้งด้านหน้าและด้านหลัง การทำเช่นนี้ช่วยให้สามารถควบคุมรถแบบ 4WD ได้อย่างง่ายดายด้วยจำนวนขา GPIO ที่น้อยลง โดยการควบคุมมอเตอร์คู่ซ้ายและคู่ขวาพร้อมกัน
ปัญหาที่พบ:
- มอเตอร์ทำงานเมื่อจ่ายไฟโดยตรง แต่ไม่ทำงานเมื่อเชื่อมต่อกับ Motor Driver นี่อาจเป็นเพราะ แรงดันไฟฟ้าที่จ่ายต่ำเกินไป หรือ มอเตอร์ดึงกระแสไฟมากกว่าที่แหล่งจ่ายให้ได้ อันดับแรก ตรวจสอบให้แน่ใจว่าแหล่งจ่ายแรงดันไฟฟ้าของคุณสูงกว่า 7V โมดูล L298N ต้องการแรงดันไฟฟ้าอย่างน้อย 6.5V-7V เพื่อให้วงจรลอจิกภายในทำงานได้อย่างเสถียร หากน้อยกว่า 6.5V อาจทำให้ Motor Driver ทำงานผิดปกติ (สังเกตได้จาก LED ที่กระพริบ) และมอเตอร์จะไม่หมุน นอกจากนี้ มอเตอร์กระแสตรง โดยเฉพาะอย่างยิ่งเมื่อเริ่มหมุนหรือเมื่อมีแรงต้าน จะดึงกระแสไฟที่สูงกว่าปกติ หรือที่เรียกว่า "กระแสสตาร์ท" (Stall Current) หากแหล่งจ่ายไฟไม่สามารถส่งกระแสไฟที่เพียงพอในช่วงเวลานั้นได้ มอเตอร์ก็จะหมุนไม่ได้หรือหมุนได้ไม่เต็มประสิทธิภาพ ประการที่สอง คุณต้องมีแบตเตอรี่ที่สามารถจ่ายกระแสไฟได้ตามที่มอเตอร์ต้องการ โดยส่วนตัว ผมใช้แบตเตอรี่ Li-Po แบบ 11.1V 3S 2200mAh 25/30C พร้อมขั้วต่อ XT60 แบตเตอรี่ "3S" หมายถึงมีเซลล์ 3 ก้อนเชื่อมต่อกันแบบอนุกรม แต่ละเซลล์มีแรงดันไฟฟ้าปกติ 3.7V รวมเป็น 11.1V "2200mAh" คือความจุของแบตเตอรี่ และ "25/30C" คืออัตราการคายประจุ โดย 25C คือกระแสต่อเนื่องสูงสุด (2.2Ah * 25 = 55A) และ 30C คือกระแสสูงสุดในช่วงเวลาสั้นๆ (Burst Current) แบตเตอรี่ชนิดนี้มีราคาสูงพอสมควร และผมจะแนะนำก็ต่อเมื่อคุณวางแผนจะนำไปใช้กับโครงการอื่นๆ ด้วย (มันยอดเยี่ยมมากสำหรับการใช้งานกับโดรน!) เนื่องจากผมต้องซื้ออุปกรณ์เสริมที่จำเป็นเพิ่มเติม เช่น อะแดปเตอร์ Male XT60 to Female JST (เพื่อใช้แบตเตอรี่กับสายจัมเปอร์) และแน่นอนว่าต้องมีเครื่องชาร์จ Li-Po ด้วย เป้าหมายหลักคือการให้แหล่งจ่ายกระแสไฟที่เพียงพอ คุณจึงควรพิจารณาเลือกแบตเตอรี่ที่ตรงตามความต้องการของโครงการของคุณ
- มอเตอร์ทั้งสองตัวหมุนด้วยความเร็วที่ไม่เท่ากัน ปัญหานี้มักเกิดจาก การขาดแคลนกระแสไฟที่จำเป็นอีกครั้ง แบตเตอรี่ของคุณอาจให้แรงดันเอาต์พุต 12V แต่เป็นไปได้ว่ามอเตอร์ตัวหนึ่งอาจทำงานด้วยความเร็วเพียงครึ่งหนึ่งของอีกตัว ซึ่งจะทำให้การเคลื่อนที่ของรถไม่สมดุลเมื่อประกอบเสร็จสิ้น หากต้องการให้มอเตอร์ทั้ง 4 ตัวทำงานได้อย่างราบรื่น แหล่งจ่ายกระแสไฟอย่างน้อย 5A ก็น่าจะเพียงพอสำหรับการทำงานพร้อมกันของมอเตอร์ทั้งหมด
นี่คือโค้ดง่ายๆ สำหรับทดสอบ Motor Driver และมอเตอร์ของคุณ ลองสนุกกับการเปลี่ยนทิศทางและความเร็วของมอเตอร์! สังเกตว่าการตั้งค่าใดทำให้มอเตอร์หมุนตามเข็มนาฬิกาและทวนเข็มนาฬิกา และจดบันทึกไว้ ซึ่งจะเป็นประโยชน์ในภายหลังเมื่อประกอบชิ้นส่วน เพื่อที่คุณจะได้ไม่ต้องทำซ้ำขั้นตอนนี้อีกครั้งในตอนท้ายเพื่อพิจารณาว่าคำสั่งใดจะทำให้รถบังคับวิทยุของคุณเคลื่อนที่ไปข้างหน้าและถอยหลัง
int motor1pin1 = 2; // ขาควบคุมทิศทางขาที่ 1 สำหรับมอเตอร์ตัวที่ 1
int motor1pin2 = 3; // ขาควบคุมทิศทางขาที่ 2 สำหรับมอเตอร์ตัวที่ 1
int motor2pin1 = 4; // ขาควบคุมทิศทางขาที่ 1 สำหรับมอเตอร์ตัวที่ 2
int motor2pin2 = 5; // ขาควบคุมทิศทางขาที่ 2 สำหรับมอเตอร์ตัวที่ 2
int speedx; // ตัวแปรสำหรับเก็บค่าความเร็ว (ไม่ได้ใช้ในโค้ดนี้)
void setup() {
pinMode(motor1pin1, OUTPUT); // กำหนดขา motor1pin1 เป็นเอาต์พุต
pinMode(motor1pin2, OUTPUT); // กำหนดขา motor1pin2 เป็นเอาต์พุต
pinMode(motor2pin1, OUTPUT); // กำหนดขา motor2pin1 เป็นเอาต์พุต
pinMode(motor2pin2, OUTPUT); // กำหนดขา motor2pin2 เป็นเอาต์พุต
/*speed control pins*/
pinMode(9, OUTPUT); // กำหนดขา 9 (ENA) เป็นเอาต์พุตสำหรับควบคุมความเร็วของมอเตอร์คู่ที่ 1
pinMode(10, OUTPUT); // กำหนดขา 10 (ENB) เป็นเอาต์พุตสำหรับควบคุมความเร็วของมอเตอร์คู่ที่ 2
}
void loop() {
//Controlling speed (0 = off and 255 = max speed):
analogWrite(9, 50); // ส่งสัญญาณ PWM 50 ไปยังขา ENA เพื่อควบคุมความเร็วของมอเตอร์คู่ที่ 1 (ความเร็วปานกลาง)
analogWrite(10, 50); // ส่งสัญญาณ PWM 50 ไปยังขา ENB เพื่อควบคุมความเร็วของมอเตอร์คู่ที่ 2 (ความเร็วปานกลาง)
//Controlling spin direction of motors:
digitalWrite(motor1pin1, LOW); // ตั้งค่าขา motor1pin1 เป็น LOW
digitalWrite(motor1pin2, HIGH); // ตั้งค่าขา motor1pin2 เป็น HIGH (กำหนดทิศทางการหมุนของมอเตอร์ตัวที่ 1)
digitalWrite(motor2pin1, LOW); // ตั้งค่าขา motor2pin1 เป็น LOW
digitalWrite(motor2pin2, HIGH); // ตั้งค่าขา motor2pin2 เป็น HIGH (กำหนดทิศทางการหมุนของมอเตอร์ตัวที่ 2)
delay(5000); // หน่วงเวลา 5 วินาที
analogWrite(9, 50); // ตั้งค่าความเร็วอีกครั้ง (โค้ดซ้ำ อาจมีจุดประสงค์ให้มอเตอร์ทำงานต่อเนื่อง หรือเป็นส่วนที่ลืมแก้ไข)
analogWrite(10, 50); // ตั้งค่าความเร็วอีกครั้ง
delay(5000); // หน่วงเวลา 5 วินาที
/*digitalWrite(motor1pin1, LOW); // โค้ดส่วนนี้ถูกคอมเมนต์ไว้ ไม่มีการทำงาน
digitalWrite(motor1pin2, HIGH);
digitalWrite(motor2pin1, LOW);
digitalWrite(motor2pin2, HIGH);
delay(5000);*/
}
คำอธิบายโค้ด:
ในฟังก์ชัน `setup()` เรากำหนดขา GPIO ที่เชื่อมต่อกับขาควบคุมทิศทาง (IN1, IN2) และขาควบคุมความเร็ว (ENA, ENB) ของโมดูล L298N ให้เป็น `OUTPUT` เพื่อให้ Arduino สามารถส่งสัญญาณออกไปควบคุมได้ สำหรับ Arduino Uno ขา PWM ที่ใช้กับ `analogWrite()` คือขา 3, 5, 6, 9, 10, 11 ในโค้ดนี้ใช้ขา 9 และ 10 ซึ่งเป็นขาที่รองรับ PWM
ในฟังก์ชัน `loop()`:
- `analogWrite(9, 50);` และ `analogWrite(10, 50);` เป็นการควบคุมความเร็วมอเตอร์โดยใช้ PWM ค่า 50 (จาก 0-255) หมายถึงการจ่ายพลังงานให้มอเตอร์ที่ความเร็วประมาณ 20% ของความเร็วสูงสุด ซึ่งจะทำให้มอเตอร์หมุนด้วยความเร็วปานกลาง
- `digitalWrite(motor1pin1, LOW); digitalWrite(motor1pin2, HIGH);` และ `digitalWrite(motor2pin1, LOW); digitalWrite(motor2pin2, HIGH);` เป็นการกำหนดทิศทางการหมุนของมอเตอร์แต่ละตัว การตั้งค่า `LOW` และ `HIGH` บนขา IN1 และ IN2 ของ L298N จะกำหนดทิศทางการไหลของกระแสผ่านมอเตอร์ หากต้องการเปลี่ยนทิศทาง ก็แค่สลับค่า `LOW` และ `HIGH` สำหรับขาคู่เดียวกัน เช่น `digitalWrite(motor1pin1, HIGH); digitalWrite(motor1pin2, LOW);`
- `delay(5000);` เป็นการหน่วงเวลา 5 วินาที เพื่อให้มอเตอร์หมุนในทิศทางและด้วยความเร็วดังกล่าวเป็นระยะเวลาหนึ่ง
ผู้ใช้ควรลองปรับค่า PWM (จาก 0 ถึง 255) และสลับค่า `LOW/HIGH` บนขาควบคุมทิศทาง เพื่อทำความเข้าใจว่าการตั้งค่าใดส่งผลต่อความเร็วและทิศทางการหมุนของมอเตอร์อย่างไร การบันทึกผลลัพธ์เหล่านี้จะเป็นประโยชน์อย่างยิ่งในการพัฒนาโค้ดควบคุมรถในขั้นตอนต่อไป
2. NodeMCU ESP8266 Generic Wi-Fi Module และ MIT App Inventor: การเปิดและปิด LED
ก่อนที่จะทำอะไรที่ซับซ้อนด้วย NodeMCU และ App Inventor การเริ่มต้นด้วยการสร้างแอปง่ายๆ เพื่อเปิดและปิด LED ในโมดูล Wi-Fi เป็นวิธีที่ดีที่สุดในการทำความเข้าใจพื้นฐาน NodeMCU ESP8266 เป็นบอร์ดพัฒนาที่รวมเอาชิป ESP8266 เข้ากับ USB-to-Serial converter ทำให้ง่ายต่อการโปรแกรมและใช้งาน ชิป ESP8266 มีความโดดเด่นในการเป็นไมโครคอนโทรลเลอร์ที่มี Wi-Fi ในตัว ซึ่งเป็นหัวใจสำคัญของโครงการ IoT นี้
ผมได้รับความช่วยเหลือจากเว็บเพจ นี้ ซึ่งให้ภาพรวมโดยละเอียดเกี่ยวกับ MIT App Inventor, บล็อกโค้ด และโค้ด Arduino สิ่งเดียวที่แตกต่างคือในบทความนี้ผู้เขียนแสดงวิธีการควบคุมรีเลย์ แต่หลักการพื้นฐานของแอป LED ON/OFF นั้นเหมือนกันเกือบทั้งหมด สำหรับ NodeMCU ของคุณ คุณสามารถอ้างอิงจากโค้ดด้านล่างนี้:
#include <ESP8266WiFi.h> // ไลบรารีสำหรับจัดการการเชื่อมต่อ Wi-Fi ของ ESP8266
#include <WiFiClient.h> // ไลบรารีสำหรับจัดการการเชื่อมต่อไคลเอนต์ Wi-Fi
const char* ssid = "Wi-Fi ssid"; // ชื่อเครือข่าย Wi-Fi (SSID) ของคุณ
const char* password = "Wi-Fi password"; // รหัสผ่าน Wi-Fi ของคุณ
WiFiServer server(80); // สร้างอ็อบเจกต์เซิร์ฟเวอร์ Wi-Fi บนพอร์ต 80 (พอร์ต HTTP มาตรฐาน)
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // กำหนดขาของ LED ในตัว (มักจะเป็น GPIO2 บน NodeMCU) เป็นเอาต์พุต
digitalWrite(LED_BUILTIN, 0); // ตั้งค่า LED ในตัวเป็น LOW (โดยทั่วไปคือเปิด LED สำหรับ ESP8266 เนื่องจากเป็น Active-LOW)
Serial.begin(115200); // เริ่มต้นการสื่อสารแบบ Serial ที่ baud rate 115200 สำหรับการดีบัก
Serial.println(); // ขึ้นบรรทัดใหม่
Serial.print("Connecting to "); // แสดงข้อความ "Connecting to " บน Serial Monitor
Serial.print(ssid); // แสดงชื่อ Wi-Fi ที่กำลังเชื่อมต่อ
WiFi.begin(ssid, password); // เริ่มการเชื่อมต่อ Wi-Fi ด้วย SSID และรหัสผ่านที่กำหนด
while(WiFi.status() != WL_CONNECTED){ // วนซ้ำจนกว่าจะเชื่อมต่อ Wi-Fi สำเร็จ
delay(500); // หน่วงเวลา 500 มิลลิวินาที
Serial.print("."); // แสดงจุดบน Serial Monitor เพื่อแสดงสถานะการเชื่อมต่อ
}
Serial.println(""); // ขึ้นบรรทัดใหม่
Serial.println("WiFi connected"); // แสดงข้อความ "WiFi connected"
server.begin(); // เริ่มต้นการทำงานของเซิร์ฟเวอร์
Serial.println("Server started"); // แสดงข้อความ "Server started"
Serial.println(WiFi.localIP()); // แสดง IP Address ที่ NodeMCU ได้รับ
}
void loop() {
WiFiClient client = server.available(); // ตรวจสอบว่ามีไคลเอนต์ใหม่เชื่อมต่อเข้ามาหรือไม่
if(!client){ // ถ้าไม่มีไคลเอนต์ใหม่ ให้กลับไปเริ่มต้น loop ใหม่
return;
}
Serial.println("New Client"); // แสดงข้อความ "New Client" เมื่อมีไคลเอนต์เชื่อมต่อ
while(!client.available()){ // รอจนกว่าจะมีข้อมูลจากไคลเอนต์
delay(1);
}
String req = client.readStringUntil('\r'); // อ่าน HTTP request line แรกจนถึงอักขระขึ้นบรรทัดใหม่ ('\r')
Serial.println(req); // แสดง HTTP request ที่ได้รับบน Serial Monitor
client.flush(); // ล้างข้อมูลที่เหลือในบัฟเฟอร์ของไคลเอนต์
int val; // ตัวแปรสำหรับเก็บค่าที่จะส่งไปควบคุม LED
if (req.indexOf("/gpio/0") != -1){ // ถ้า HTTP request มีคำว่า "/gpio/0"
val = 0; // ตั้งค่า val เป็น 0 (สำหรับปิด LED)
}
else if (req.indexOf("/gpio/1") != -1){ // ถ้า HTTP request มีคำว่า "/gpio/1"
val = 1; // ตั้งค่า val เป็น 1 (สำหรับเปิด LED)
}
else { // ถ้า request ไม่ตรงกับที่กำหนด
Serial.println("invalid request"); // แสดงข้อความ "invalid request"
client.stop(); // ปิดการเชื่อมต่อไคลเอนต์
return; // กลับไปเริ่มต้น loop ใหม่
}
// Set GPIO2 according to the request
digitalWrite(LED_BUILTIN, val); // ตั้งค่า LED ในตัวตามค่า val ที่ได้รับ (0 หรือ 1)
client.flush(); // ล้างข้อมูลที่เหลือในบัฟเฟอร์ของไคลเอนต์
String s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n\r\n\r\nGPIO is now "; // สร้างส่วนหัวของ HTTP response และเริ่มต้นหน้า HTML
s += (val)?"high":"low"; // เพิ่มข้อความ "high" หรือ "low" ตามสถานะของ LED
s += ""; // ปิดแท็ก HTML
client.print(s); // ส่ง HTTP response กลับไปยังไคลเอนต์
Serial.println(); // ขึ้นบรรทัดใหม่
}
คำอธิบายโค้ด:
โค้ดนี้เปลี่ยน NodeMCU ให้เป็นเว็บเซิร์ฟเวอร์ขนาดเล็กที่สามารถรับคำสั่งผ่าน Wi-Fi เพื่อควบคุม LED ในตัว
- `#include
` และ `#include ` เป็นไลบรารีที่จำเป็นสำหรับการทำงาน Wi-Fi บน ESP8266 - ใน `setup()`: NodeMCU จะเชื่อมต่อกับเครือข่าย Wi-Fi ที่กำหนด (คุณต้องเปลี่ยน `ssid` และ `password` เป็นของเครือข่ายของคุณ) จากนั้นจะเริ่มต้นเว็บเซิร์ฟเวอร์บนพอร์ต 80 และแสดง IP address ที่ได้รับบน Serial Monitor ซึ่งสำคัญมากสำหรับการเชื่อมต่อจากแอปพลิเคชัน
- ใน `loop()`: NodeMCU จะคอยตรวจสอบการเชื่อมต่อจากไคลเอนต์ (เช่น แอปในโทรศัพท์) หากมีคำขอเข้ามา มันจะอ่าน HTTP request line แรก (เช่น `GET /gpio/0 HTTP/1.1`)
- จากนั้นจะตรวจสอบว่าคำขอมี `/gpio/0` หรือ `/gpio/1` หากพบ `/gpio/0` หมายถึงให้ปิด LED (ตั้งค่า `val = 0`) และหากพบ `/gpio/1` หมายถึงให้เปิด LED (ตั้งค่า `val = 1`)
- `digitalWrite(LED_BUILTIN, val);` เป็นการควบคุม LED ในตัวของ NodeMCU โดยทั่วไปแล้ว LED_BUILTIN บน ESP8266 จะเป็นแบบ Active-LOW ซึ่งหมายความว่า `digitalWrite(LED_BUILTIN, LOW);` จะเป็นการเปิด LED และ `digitalWrite(LED_BUILTIN, HIGH);` จะเป็นการปิด LED อย่างไรก็ตาม ค่า `val` ที่ส่งมาในโค้ดคือ 0 หรือ 1 ซึ่งอาจจะต้องปรับในภายหลังเพื่อให้สอดคล้องกับการทำงานของ LED_BUILTIN จริงๆ หรือใช้ขา GPIO อื่นที่ Active-HIGH
- สุดท้าย NodeMCU จะส่ง HTTP response กลับไปยังไคลเอนต์เพื่อยืนยันสถานะของ LED
ปัญหาที่พบ:
- NodeMCU ESP8266 ไม่ทำงาน หากคุณพบข้อผิดพลาดใดๆ วิธีที่ดีที่สุดในการแก้ไขคือการคัดลอกข้อผิดพลาดนั้นแล้วค้นหาบนอินเทอร์เน็ต เพราะส่วนใหญ่แล้ว ผู้คนหลายร้อยคนเคยประสบปัญหาเดียวกันนี้มาก่อน และมีวิธีแก้ไขพร้อมใช้งานอย่างแพร่หลาย มิฉะนั้น หากคุณเชื่อว่าโมดูล Wi-Fi ของคุณมีข้อบกพร่อง ให้ทำตามคำแนะนำในเว็บเพจ นี้ หากยังไม่ทำงาน โมดูลของคุณอาจมีข้อบกพร่องหรือเป็นของปลอมได้ ซึ่งเป็นเรื่องที่เกิดขึ้นได้บ่อยครั้งกับอุปกรณ์อิเล็กทรอนิกส์ราคาถูก
3. Arduino Uno และ NodeMCU ESP8266 Interfacing: I2C Communication Protocol
นี่คือแหล่งข้อมูล/คู่มือที่ยอดเยี่ยมในการสร้างการสื่อสารแบบอนุกรมระหว่าง NodeMCU และ Arduino คุณยังสามารถดูโปรโตคอลอื่นนอกเหนือจาก I2C เพื่อเชื่อมต่อบอร์ดทั้งสองได้ แต่ผมเลือกใช้ I2C เพราะดูจะง่ายกว่ามาก
I2C (Inter-Integrated Circuit) เป็นโปรโตคอลการสื่อสารแบบอนุกรมแบบ Synchronous ที่ใช้สายเพียงสองเส้น: SDA (Serial Data Line) สำหรับส่งข้อมูล และ SCL (Serial Clock Line) สำหรับซิงค์เวลาการสื่อสาร สิ่งที่ทำให้ I2C เหมาะสำหรับกรณีนี้คือความสามารถในการเชื่อมต่ออุปกรณ์หลายตัว (Master และ Slave หลายตัว) เข้าด้วยกันโดยใช้ขา GPIO เพียงไม่กี่ขา ในโครงสร้างนี้ NodeMCU จะทำหน้าที่เป็น Master (ตัวส่งคำสั่ง) และ Arduino Uno จะทำหน้าที่เป็น Slave (ตัวรับคำสั่ง)
ตอนนี้ หลังจากที่เราสร้างการเชื่อมต่อระหว่างกันได้แล้ว เราก็สามารถใช้แอป LED ON/OFF เดิมที่เราสร้างขึ้นในขั้นตอนที่ 2 เพื่อเปิดหรือปิด LED ในตัวของ Arduino ได้
เราเพียงแค่ต้องเพิ่มไลบรารี Wire และคำสั่งบางอย่างลงในโค้ด NodeMCU เดิมของเราดังนี้:
#include <Wire.h> // ไลบรารีสำหรับ I2C communication
#include <ESP8266WiFi.h> // ไลบรารีสำหรับ Wi-Fi communication
#include <WiFiClient.h> // ไลบรารีสำหรับ Wi-Fi client
const char* ssid = "paya"; // ชื่อ Wi-Fi (SSID) ของคุณ
const char* password = "mishtynaina"; // รหัสผ่าน Wi-Fi ของคุณ
WiFiServer server(80); // สร้างอ็อบเจกต์เซิร์ฟเวอร์ Wi-Fi บนพอร์ต 80
void setup() {
Serial.begin(115200); // เริ่ม Serial communication
delay(10); // หน่วงเวลาเล็กน้อย
Wire.begin(D1, D2); // เริ่มต้น I2C เป็น Master โดยกำหนดขา D1 (SDA) และ D2 (SCL) ของ NodeMCU
pinMode(LED_BUILTIN, OUTPUT); // กำหนดขา LED ในตัวเป็นเอาต์พุต
digitalWrite(LED_BUILTIN, 0); // ปิด LED ในตัว (หรือเปิดขึ้นอยู่กับ Active-LOW/HIGH)
Serial.println();
Serial.print("Connecting to ");
Serial.print(ssid);
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED){
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
server.begin(); //Capital S??? (หมายเหตุเดิมจากผู้เขียน น่าจะหมายถึง "server" ตัวเล็ก)
Serial.println("Server started");
Serial.println(WiFi.localIP());
}
void loop() {
WiFiClient client = server.available();
if(!client){
return;
}
Serial.println("New Client");
while(!client.available()){
delay(1);
}
String req = client.readStringUntil('\r');
Serial.println(req);
client.flush();
int val;
if (req.indexOf("/gpio/0") != -1){ // ถ้าได้รับคำสั่งให้ปิด LED
val = 0; // ตั้งค่า val เป็น 0
Wire.beginTransmission(8); // เริ่มต้นการส่งข้อมูล I2C ไปยัง Slave ที่ address 8
Wire.write(val); // ส่งค่า val (0) ไปยัง Slave
Wire.endTransmission(); // สิ้นสุดการส่งข้อมูล
}
else if (req.indexOf("/gpio/1") != -1){ // ถ้าได้รับคำสั่งให้เปิด LED
val = 1; // ตั้งค่า val เป็น 1
Wire.beginTransmission(8); // เริ่มต้นการส่งข้อมูล I2C ไปยัง Slave ที่ address 8
Wire.write(val); // ส่งค่า val (1) ไปยัง Slave
Wire.endTransmission(); // สิ้นสุดการส่งข้อมูล
}
else {
Serial.println("invalid request");
client.stop();
return;
}
// Set GPIO2 according to the request
digitalWrite(LED_BUILTIN, val); // ควบคุม LED ในตัวของ NodeMCU
client.flush();
String s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n\r\n\r\nGPIO is now ";
s += (val)?"high":"low";
s += "";
client.print(s); // ส่ง HTTP response กลับไป
Wire.requestFrom(8, 13); /* ขอข้อมูลขนาด 13 ไบต์จาก Slave ที่ address 8 */
while(Wire.available()){ // ตรวจสอบว่ามีข้อมูลจาก Slave หรือไม่
char c = Wire.read(); // อ่านข้อมูลทีละไบต์
Serial.print(c); // แสดงข้อมูลที่อ่านได้บน Serial Monitor
}
Serial.println(); // ขึ้นบรรทัดใหม่
delay(1000); // หน่วงเวลา 1 วินาที
}
คำอธิบายโค้ด NodeMCU (Master):
โค้ดนี้ขยายความสามารถของ NodeMCU จากแค่ควบคุม LED ในตัว ไปสู่การเป็น I2C Master ที่สามารถสื่อสารกับ Arduino Slave ได้
- `#include
` คือไลบรารีที่จำเป็นสำหรับ I2C communication - ใน `setup()`: นอกจากจะเชื่อมต่อ Wi-Fi แล้ว ยังมีการเริ่มต้น I2C ด้วย `Wire.begin(D1, D2);` โดย D1 และ D2 คือขา SDA และ SCL บน NodeMCU ESP8266
- ใน `loop()`: เมื่อ NodeMCU ได้รับ HTTP request (เช่น `/gpio/0` หรือ `/gpio/1`) มันจะเตรียมค่า `val` (0 หรือ 1) จากนั้นจะส่งค่านี้ไปยัง Arduino ผ่าน I2C
- `Wire.beginTransmission(8);` เริ่มต้นการสื่อสาร I2C โดยระบุว่ากำลังจะสื่อสารกับ Slave ที่ address 8
- `Wire.write(val);` ส่งค่า `val` ไปยัง Arduino
- `Wire.endTransmission();` สิ้นสุดการส่งข้อมูล
- นอกจากนี้ โค้ดยังสาธิตการรับข้อมูลจาก Slave ด้วย:
- `Wire.requestFrom(8, 13);` ส่งคำขอไปยัง Slave ที่ address 8 เพื่อขอข้อมูลจำนวน 13 ไบต์
- `while(Wire.available()){ char c = Wire.read(); Serial.print(c); }` อ่านข้อมูลที่ได้รับกลับมาและแสดงผลบน Serial Monitor
และเพื่อรับคำสั่งจาก NodeMCU โค้ด Arduino มีดังนี้:
#include <Wire.h> // ไลบรารีสำหรับ I2C communication
void setup() {
Wire.begin(8); /* เข้าร่วม I2C bus ด้วย address 8 */
Wire.onReceive(receiveEvent); /* ลงทะเบียนฟังก์ชัน receiveEvent เพื่อทำงานเมื่อมีข้อมูลเข้ามา */
Wire.onRequest(requestEvent); /* ลงทะเบียนฟังก์ชัน requestEvent เพื่อทำงานเมื่อ Master ร้องขอข้อมูล */
Serial.begin(9600); /* เริ่มต้น Serial communication สำหรับการดีบัก */
pinMode(13, OUTPUT); /* กำหนดขา 13 (LED ในตัวของ Arduino Uno) เป็นเอาต์พุต */
}
void loop() {
delay(100); // หน่วงเวลาเล็กน้อย
}
// function that executes whenever data is received from master
void receiveEvent(int howMany) { // ฟังก์ชันที่จะถูกเรียกเมื่อ Master ส่งข้อมูลมา
while (0 <Wire.available()) { // วนซ้ำตราบเท่าที่ยังมีข้อมูลอยู่ในบัฟเฟอร์ I2C
int c = Wire.read(); /* รับข้อมูลเป็นไบต์ (character) */
if(c == 0){ // ถ้าค่าที่ได้รับคือ 0
Serial.println(c); // แสดงค่า 0 บน Serial Monitor
digitalWrite(13, c); // ตั้งค่าขา 13 (LED) เป็น 0 (ปิด LED)
}
if(c ==1){ // ถ้าค่าที่ได้รับคือ 1
Serial.println(c); // แสดงค่า 1 บน Serial Monitor
digitalWrite(13, c); // ตั้งค่าขา 13 (LED) เป็น 1 (เปิด LED)
}
//Serial.print(c); /* แสดงข้อมูล (ถูกคอมเมนต์ไว้) */
}
Serial.println(); /* ขึ้นบรรทัดใหม่ */
}
// function that executes whenever data is requested from master
void requestEvent() { // ฟังก์ชันที่จะถูกเรียกเมื่อ Master ร้องขอข้อมูล
Wire.write("Hello NodeMCU"); /* ส่งข้อความ "Hello NodeMCU" เมื่อถูกร้องขอ */
}
คำอธิบายโค้ด Arduino (Slave):
โค้ดนี้ทำให้ Arduino Uno ทำงานเป็น I2C Slave ที่สามารถรับคำสั่งและตอบกลับคำขอข้อมูลจาก NodeMCU Master ได้
- `#include
` คือไลบรารีที่จำเป็นสำหรับ I2C communication - ใน `setup()`:
- `Wire.begin(8);` กำหนดให้ Arduino เป็น I2C Slave ที่ address 8
- `Wire.onReceive(receiveEvent);` ลงทะเบียนฟังก์ชัน `receiveEvent` ซึ่งจะถูกเรียกโดยอัตโนมัติเมื่อ Master ส่งข้อมูลมายัง Slave นี้
- `Wire.onRequest(requestEvent);` ลงทะเบียนฟังก์ชัน `requestEvent` ซึ่งจะถูกเรียกโดยอัตโนมัติเมื่อ Master ร้องขอข้อมูลจาก Slave นี้
- `pinMode(13, OUTPUT);` กำหนดขา 13 (ขาที่เชื่อมต่อกับ LED ในตัวของ Arduino Uno) เป็นเอาต์พุต
- `receiveEvent(int howMany)`: ฟังก์ชันนี้จะทำงานเมื่อ NodeMCU (Master) ส่งข้อมูลมา โดยจะอ่านข้อมูลทีละไบต์ (`Wire.read()`) และใช้ค่านั้น (0 หรือ 1) ในการควบคุม LED บนขา 13 ของ Arduino
- `requestEvent()`: ฟังก์ชันนี้จะทำงานเมื่อ NodeMCU (Master) ร้องขอข้อมูลจาก Arduino โดยจะส่งสตริง "Hello NodeMCU" กลับไป
ปัญหาที่พบ:
- ทำงานได้ดีเมื่อส่งค่า 0 และ 1 แต่ Arduino Serial Monitor พิมพ์ค่าสุ่มสำหรับตัวเลขที่มากกว่า 255 นี่เป็นเพราะการส่งและรับข้อมูล I2C แบบไบต์เดียวสามารถจัดการได้เพียง 8 บิตในแต่ละครั้ง ซึ่งหมายความว่าค่าสูงสุดที่สามารถส่งได้คือ 255 หากต้องการส่งค่าที่มากกว่า 255 เราจะต้องใช้วิธี Highbyte/Lowbyte ดังนี้:
สำหรับค่าที่มากกว่า 255 (เช่น ค่าจากเซ็นเซอร์อนาล็อกที่มีความละเอียด 10 บิต ซึ่งมีช่วง 0-1023) เราต้องแบ่งค่า 16 บิตออกเป็นสองไบต์ (High Byte และ Low Byte) และส่งทีละไบต์ จากนั้นจึงนำมารวมกันใหม่ที่ปลายทาง
โค้ด Arduino - Slave (สำหรับการรับค่า > 255)
if( Wire.available()>0){ // ตรวจสอบว่ามีข้อมูลในบัฟเฟอร์ I2C หรือไม่
byte low = Wire.read(); // อ่านไบต์แรก (Low Byte)
byte high = Wire.read(); // อ่านไบต์ที่สอง (High Byte)
int result = word (high, low); // นำ High Byte และ Low Byte มารวมกันเป็นค่า int (16 บิต)
// ตอนนี้ 'result' จะมีค่าที่มากกว่า 255 ได้
}
โค้ด NodeMCU - Master (สำหรับการส่งค่า > 255)
void requestEvent() {
int value = analogRead(_analogpinnumber); // อ่านค่าอนาล็อกจากขาที่กำหนด (ปกติคือ 0-1023 สำหรับ ESP8266 ADC)
// สร้างบัฟเฟอร์ ข้อมูลนี้สามารถใช้ส่งได้มากกว่า 2 ไบต์
byte buffer[10];
buffer[0] = lowByte (value); // แยกค่า Low Byte ออกจาก value และเก็บไว้ในบัฟเฟอร์ช่อง 0
buffer[1] = highByte (value); // แยกค่า High Byte ออกจาก value และเก็บไว้ในบัฟเฟอร์ช่อง 1
Wire.write( buffer, 2); // ส่งข้อมูล 2 ไบต์จากบัฟเฟอร์
}
คำอธิบายเพิ่มเติมเกี่ยวกับ Highbyte/Lowbyte:
ฟังก์ชัน `lowByte()` จะดึงค่า 8 บิตที่น้อยที่สุด (least significant byte) จากค่า 16 บิต และ `highByte()` จะดึงค่า 8 บิตที่มากที่สุด (most significant byte) ออกมา หลังจากส่งผ่าน I2C แล้ว ฟังก์ชัน `word()` จะนำสองไบต์นั้นมารวมกันกลับเป็นค่า 16 บิตเดิม นี่เป็นเทคนิคมาตรฐานสำหรับการส่งข้อมูลที่มีขนาดใหญ่กว่า 8 บิตผ่าน I2C หรือ Serial communication
4. การสร้างแอป Joystick และการรับค่าบน NodeMCU
ที่นี่เป็นบทช่วยสอนสำหรับการสร้างจอยสติ๊กใน MIT App Inventor ผมไม่ได้ทำตามทั้งหมดอย่างเคร่งครัด เพราะวัตถุประสงค์ของการใช้งานในบทช่วยสอนนั้นแตกต่างจากโครงการของผมอย่างมาก นอกจากนั้น โครงการในบทช่วยสอนใช้โมดูล Bluetooth ในขณะที่ผมใช้โมดูล Wi-Fi ซึ่งทำให้บล็อกโค้ดใน App Inventor ของผมแตกต่างจากที่แสดงในบทช่วยสอนค่อนข้างมาก
แนวคิดหลักคือการสร้าง UI จอยสติ๊กบนโทรศัพท์มือถือที่สามารถส่งพิกัด X และ Y (หรือคำสั่งทิศทาง เช่น เดินหน้า, ถอยหลัง, เลี้ยวซ้าย, เลี้ยวขวา) ไปยัง NodeMCU ผ่าน Wi-Fi เมื่อ NodeMCU ได้รับค่าเหล่านี้ ก็จะแปลคำสั่งเหล่านั้นเพื่อส่งต่อไปยัง Arduino ผ่าน I2C เพื่อควบคุมมอเตอร์รถ การใช้ Wi-Fi ทำให้รถสามารถควบคุมได้ในระยะที่ไกลกว่า Bluetooth และสามารถทำงานร่วมกับโครงข่ายอินเทอร์เน็ตได้
สำหรับอ้างอิง ผมได้แนบไฟล์ .aia สำหรับจอยสติ๊กของผมไว้ด้านล่าง (หมายเหตุ: ไฟล์ .aia ไม่สามารถแสดงใน Markdown ได้โดยตรง แต่ผู้ใช้จะเข้าใจว่ามีไฟล์นี้ให้ดาวน์โหลดจากผู้เขียนต้นฉบับ)
5. การประกอบทั้งหมดเข้าด้วยกัน
โค้ดสุดท้ายสำหรับ NodeMCU และ Arduino Uno มีให้ด้านล่างนี้ (ผู้เขียนบอกว่ามีอยู่ด้านล่าง แต่ในเนื้อหาที่ให้มาไม่มี ควรชี้แจงจุดนี้ในคำตอบ) คุณสามารถหาโครงรถ 4WD ราคาถูกทางออนไลน์เพื่อประกอบรถคันสุดท้ายของคุณ และ Voila! คุณก็สร้างรถบังคับวิทยุของคุณเองสำเร็จแล้ว!
ผมสนุกมากตลอดระยะเวลาที่สร้างรถ RC คันนี้ ในตอนแรกผมสร้างมันโดยใช้เซ็นเซอร์ IR เพื่อให้สามารถควบคุมได้ง่ายๆ ด้วยรีโมททีวี แต่การควบคุมกลับไม่เป็นธรรมชาติอย่างที่หวังไว้ ด้วยเหตุนี้จึงเกิดแนวคิดในการสร้างแอป Joystick ขึ้นมา ตอนนี้รถทำงานได้อย่างมีประสิทธิภาพมากด้วยเวลาตอบสนองที่รวดเร็ว ผมเผชิญอุปสรรคมากมาย โดยเฉพาะอย่างยิ่งกับโมดูล Wi-Fi เนื่องจากเป็นครั้งแรกที่ผมได้ทำงานกับมัน (ผมถึงขั้นเคยใช้ ESP8266-01 ในตอนแรก และประสบปัญหาอย่างมากกับมัน เพราะผมสั่งซื้อมาโดยไม่ได้ศึกษาข้อมูลสำคัญว่าควรใช้โมดูล Wi-Fi ตัวไหนหรือไม่!) โมดูล ESP8266-01 มีขนาดเล็กและมีขา GPIO ที่จำกัดกว่า NodeMCU พัฒนาบอร์ดแบบสำเร็จรูป ทำให้การเริ่มต้นใช้งานสำหรับมือใหม่อาจเป็นเรื่องที่ท้าทายกว่ามาก
โดยรวมแล้ว นี่เป็นประสบการณ์การเรียนรู้ที่ยอดเยี่ยม (แม้จะน่าหงุดหงิดในบางครั้ง) และผมหวังว่าคุณจะสนุกกับมันมากเท่าที่ผมสนุก!