← กลับหน้าบทความ

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 ด้วยตัวเองดูว่า จริง ๆ แล้วระหว่างโค้ดหนึ่งบรรทัดกับไฟที่ติดขึ้นมาหนึ่งดวง ข้างในไมโครคอนโทรลเลอร์มันเกิดอะไรขึ้นกันแน่