Arduino ตอนที่ 2
Arduino ตอน 2 จริง ๆ แล้วเราเขียนอะไรลงไปใน Arduino?
จากตอนแรกเรารู้กันไปแล้วว่า Arduino คืออะไร มีที่มาอย่างไร และทำไมมันถึงกลายเป็นบอร์ดที่คนจำนวนมากเลือกใช้ในการเริ่มต้นเรียนรู้ไมโครคอนโทรลเลอร์ ทีนี้พอเรามีบอร์ด Arduino อยู่ในมือแล้ว สิ่งต่อมาที่เราต้องทำก็คือเขียนโปรแกรมลงไปในบอร์ด ซึ่งตรงนี้หลายคนน่าจะเริ่มต้นเหมือนกัน คือเปิด Arduino IDE ขึ้นมา ลองโปรแกรม Blink แล้วกด Upload จากนั้น LED บนบอร์ดก็กะพริบตามเวลาที่เรากำหนด
โค้ดแรกที่เราเจอก็มักจะมีหน้าตาประมาณนี้
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
}
ถ้าเราเพิ่งเริ่มต้นใช้งาน Arduino สิ่งที่ต้องรู้ในตอนแรกอาจมีแค่ว่า setup() ทำงานครั้งเดียวตอนเปิดเครื่อง ส่วน loop() จะทำงานวนซ้ำไปเรื่อย ๆ และถ้าต้องการกำหนดให้ขาไหนเป็น Output ก็ใช้ pinMode() ถ้าต้องการสั่งให้ขานั้นเป็น HIGH หรือ LOW ก็ใช้ digitalWrite() ซึ่งแค่นี้ก็เพียงพอแล้วสำหรับการเริ่มต้นทดลองอะไรหลายอย่าง
แต่พอใช้ไปสักพัก ถ้าเราลองกลับมาดูโค้ดชุดเดิมอีกครั้ง มันจะเริ่มมีเรื่องให้สงสัยอยู่เหมือนกัน อย่างเช่น setup() กับ loop() มาจากไหน ใครเป็นคนเรียกสองฟังก์ชันนี้ขึ้นมาทำงาน หรือทำไมโปรแกรม C/C++ ที่เราเคยเห็นถึงมี main() แต่พอมาเขียน Arduino กลับไม่เห็น main() อยู่ในโปรแกรมเลย รวมถึงฟังก์ชันอย่าง digitalWrite() ที่เราเรียกใช้กันอยู่ตลอด จริง ๆ แล้วมันทำอะไรกับไมโครคอนโทรลเลอร์บ้าง
ตรงนี้ต้องทำความเข้าใจก่อนอย่างหนึ่งว่า สิ่งที่เราเขียนบน Arduino ไม่ได้เป็นภาษาโปรแกรมที่ชื่อว่า "ภาษา Arduino" อย่างที่บางครั้งเราเรียกกันจนติดปาก ตัวโปรแกรมยังคงเขียนด้วย C/C++ เพียงแต่ Arduino เตรียม Framework รวมถึงฟังก์ชันต่าง ๆ เอาไว้ให้เราใช้งาน ทำให้คนเขียนโปรแกรมไม่จำเป็นต้องลงไปจัดการรายละเอียดของไมโครคอนโทรลเลอร์โดยตรงทุกครั้ง
ตัวอย่างที่เห็นได้ง่ายที่สุดก็คือ
digitalWrite(13, HIGH);
เวลาเขียนแบบนี้ เรารู้เพียงว่าต้องการให้ Digital Pin 13 มีสถานะเป็น HIGH แต่ในระดับของไมโครคอนโทรลเลอร์มันไม่ได้รู้จักคำว่า Pin 13 ในแบบเดียวกับที่เราเห็นบนบอร์ด และไม่ได้มีคำสั่ง digitalWrite() ฝังอยู่ในตัวไมโครคอนโทรลเลอร์ด้วย ฟังก์ชันนี้เป็นส่วนหนึ่งที่ Arduino เตรียมเอาไว้ เพื่อรับหมายเลข Pin ที่เราใช้แล้วนำไปจัดการกับ Hardware ของไมโครคอนโทรลเลอร์อีกทีหนึ่ง
เพราะฉะนั้นสิ่งที่ Arduino ทำได้ดีมากก็คือ มันเอารายละเอียดที่ยังไม่จำเป็นสำหรับคนเริ่มต้นซ่อนเอาไว้ก่อน เราจึงสามารถสั่งเปิดปิดขา อ่านค่าจากเซ็นเซอร์ ใช้งาน Serial หรือควบคุมอุปกรณ์ต่าง ๆ ได้ โดยที่ในช่วงแรกยังไม่จำเป็นต้องรู้เลยว่า Register ตัวไหนกำลังถูกแก้ไข หรือ Bit ไหนในไมโครคอนโทรลเลอร์กำลังเปลี่ยนสถานะ
ทีนี้กลับมาที่ setup() กับ loop() กันอีกครั้ง ถ้า Arduino ใช้ C/C++ แล้ว main() หายไปไหน จริง ๆ แล้ว main() ไม่ได้หายไป เพียงแต่ Arduino เตรียมส่วนนี้เอาไว้ให้แล้ว เราจึงไม่ต้องเขียนมันขึ้นมาเองทุกครั้ง
ถ้ามองเฉพาะหลักการ โครงสร้างข้างในจะมีลักษณะประมาณนี้
int main(void)
{
init();
setup();
while (true) {
loop();
}
}
โค้ดจริงอาจมีรายละเอียดมากกว่านี้และแตกต่างกันตาม Core หรือไมโครคอนโทรลเลอร์ที่ใช้งาน แต่แนวคิดหลักคือ เมื่อระบบเริ่มทำงาน Arduino จะเตรียมส่วนที่จำเป็นก่อน จากนั้นจึงเรียก setup() ของเราหนึ่งครั้ง แล้วเข้าสู่ Loop ที่เรียก loop() ซ้ำไปเรื่อย ๆ นั่นจึงเป็นเหตุผลว่าทำไมสิ่งที่เราเขียนอยู่ภายใน loop() ถึงทำงานวนซ้ำตลอดเวลา
พอเข้าใจตรงนี้แล้ว เราจะเริ่มเห็นว่า Arduino ไม่ได้ทำงานด้วยวิธีพิเศษอะไร มันเพียงสร้างโครงสร้างพื้นฐานขึ้นมาครอบการทำงานของไมโครคอนโทรลเลอร์ไว้อีกชั้นหนึ่ง เพื่อให้เราเริ่มเขียนโปรแกรมได้ง่ายขึ้น ซึ่งวิธีนี้สะดวกมากสำหรับการเรียนรู้และการทำ Prototype เพราะเราไม่ต้องเสียเวลาไปกับรายละเอียดระดับล่างตั้งแต่เริ่มต้น
อย่างไรก็ตาม เมื่อเราเริ่มทำโปรเจกต์ที่ซับซ้อนขึ้น รายละเอียดที่ Arduino ซ่อนเอาไว้ก็จะเริ่มมีความสำคัญขึ้นมา เช่น ทำไมการใช้ Library บางตัวถึงไปกระทบกับ PWM ทำไม Timer บางตัวถึงใช้งานไม่ได้หลังจากเปิด Library บางอย่าง ทำไม Interrupt ถึงไม่ควรเขียนโค้ดยาว ๆ อยู่ข้างใน หรือแม้แต่คำถามง่าย ๆ อย่าง delay(1000) จริง ๆ แล้วไมโครคอนโทรลเลอร์รู้ได้อย่างไรว่าผ่านไปหนึ่งวินาทีแล้ว
คำถามพวกนี้ตอบได้ยากถ้าเรามองเฉพาะโค้ดที่อยู่ใน Arduino Sketch เพราะคำตอบส่วนหนึ่งอยู่ใน Arduino Core และอีกส่วนหนึ่งอยู่ในตัวไมโครคอนโทรลเลอร์เอง ดังนั้นถ้าเราอยากเข้าใจ Arduino มากกว่าการเรียกใช้ฟังก์ชันที่มีให้ ขั้นต่อไปก็คือเริ่มมองผ่าน Framework ลงไปดูว่า Hardware ข้างล่างทำงานอย่างไร
ไม่ได้หมายความว่าเราจะต้องเลิกใช้ digitalWrite() หรือกลับไปเขียน Register เองทั้งหมด เพราะในงานทั่วไปฟังก์ชันที่ Arduino เตรียมไว้ให้ก็สะดวกและเพียงพออยู่แล้ว แต่การรู้ว่าข้างหลังฟังก์ชันเหล่านี้เกิดอะไรขึ้น จะช่วยให้เราเข้าใจปัญหาได้ง่ายขึ้นเวลาที่โปรแกรมเริ่มทำงานไม่เป็นอย่างที่คิด และยังช่วยให้เราเลือกได้ด้วยว่า ตอนไหนควรใช้สิ่งที่ Arduino เตรียมไว้ให้ และตอนไหนที่เราควรลงไปจัดการกับ Hardware ด้วยตัวเอง
ลองกลับไปมองโค้ดบรรทัดเดิมที่เราใช้กันมาตั้งแต่เริ่มต้นอีกครั้ง
digitalWrite(13, HIGH);
เราเขียนแค่บรรทัดเดียว แล้วไฟก็ติด เรื่องมันดูเหมือนจะไม่มีอะไร แต่ถ้าตัด digitalWrite() ออกไปล่ะ?
ถ้าเราไม่ใช้ฟังก์ชันของ Arduino เลย เราจะสั่งให้ขา 13 เป็น HIGH ได้ไหม แล้วเลข 13 ที่เขียนอยู่ตรงนี้ จริง ๆ แล้วมันคือขา 13 ของอะไร? มันไปตรงกับขาไหนของ ATmega328P และสุดท้ายคำสั่งเพียงบรรทัดเดียวนี้ไปเปลี่ยนอะไรข้างในไมโครคอนโทรลเลอร์ ถึงทำให้แรงดันไฟฟ้าออกมาที่ขาหนึ่งของชิปได้?
ที่น่าสนใจกว่านั้นคือ ถ้า digitalWrite() เป็นเพียงฟังก์ชันที่ Arduino เขียนครอบเอาไว้ แสดงว่าเราน่าจะข้ามมันไปแล้วคุยกับ Hardware โดยตรงได้เหมือนกัน
แล้วถ้าทำแบบนั้นได้จริง...
เรายังจำเป็นต้องมี digitalWrite() อยู่หรือเปล่า?
ตอนต่อไปเราจะลองเอามันออก แล้วสั่งขา I/O ของ ATmega328P ด้วยตัวเองดูว่า จริง ๆ แล้วระหว่างโค้ดหนึ่งบรรทัดกับไฟที่ติดขึ้นมาหนึ่งดวง ข้างในไมโครคอนโทรลเลอร์มันเกิดอะไรขึ้นกันแน่