มัลติทาสกิ้ง (Multitasking) คือการทำงานของโปรแกรม 2 โปรแกรมพร้อมกัน โดยผลที่เห็นได้คือกระบวนการทำงานที่วางไว้มากกว่า 1 การทำงานสามารถทำได้ไปพร้อม ๆ กัน เช่น การสั่งให้ไฟกระพริบพร้อม ๆ กับการสั่งให้อ่านค่าอุณหภูมิจากเซ็นเซอร์ โดยกระบวนการทั้ง 2 อย่างนี้จะแยกจากกันอย่างเด็ดขาดหมายความว่าโค้ดโปรแกรมที่ใช้แม้จะมีการเรียกใช้ฟังก์ชันหยุดโปรแกรม ก็จะไม่ทำให้กระบวนการอื่นที่แยกออกมาหยุดทำงานไปด้วย
การทำงานแบบมัลติทาสกิ้งอาจสามารถแบ่งได้ดังนี้
มัลติทาสกิ้งเทียม คือกระบวนการทำงานที่มากกว่า 1 กระบวนการ ถูกแบ่งเวลาการทำงานกันเพื่อให้เหมือนกับการทำงานแบบมัลติทาสกิ้ง เนื่องจาก CPU ในปัจจุบันมีความเร็วของสัญญาณนาฬิการสูงมาก ดังนั้นการแบ่งเวลาทำงานของแต่ละกระบวนการจึงไม่สามารถมองเห็นเป็นผลได้ชัดเจนในเชิงการใช้งาน แต่หากในกระบวนการทำงานจำเป็นต้องมีการวนรอบ หรือคำนวณตัวเลขจำนวนมาก จะส่งผลให้กระบวนการอื่น ๆ หยุดการทำงานเนื่องจากคอร์ของ CPU ไม่สามารถแบ่งเวลาเพื่อไปทำงานอื่นได้
การใช้งานมัลติทาสกิ้งเทียมจำเป็นจะต้องมีการแบ่งเวลาการทำงานกัน การใช้งานเบื้องต้นคือการใช้ฟังก์ชัน millis() ซึ่งใช้ดึงค่าเวลาตั้งแต่เริ่มต้นการทำงานโปรแกรมในคำสั่งแรกมาจนถึงขณะนี้ ในหน่วยมิลิวินาที การใช้ฟังก์ชัน millis() จะใช้เฉพาะกระบวนการทำงานที่มีกำหนดเวลาการทำงานที่แน่นอน
ตัวอย่างของกระบวนการที่สามารถนำฟังก์ชัน millis() มาใช้ได้ คือ การกำหนดให้หลอด LED ดวงแรกกระพริบทุก ๆ 0.5 วินาที และดวงที่ 2 กระพริบทุก ๆ 0.3 วินาที จะเห็นได้ว่ากระบวนการทำงานจะแบ่งออกเป็น 2 กระบวนการ คือกระบวนการทำงานของหลอด LED ดวงแรกที่กระพริบทุก ๆ 0.5 วินาที และกระบวนการทำงานที่สองของหลอด LED ดวงที่สองที่กระพริบทุก ๆ 0.3 วินาที หลักการในการเขียนโปรแกรมคือจะใช้ค่าที่ได้จากฟังก์ชัน millis() มาเทียบกับค่าเวลาก่อนหน้าว่าผ่านไป 0.5 วินาทีแล้วหรือยัง เมื่อผ่านไปแล้วจึงทำในโค้ดโปรแกรมของ LED ดวงแรก และเปรียบเทียบว่าค่าเวลาก่อนหน้าผ่านไป 0.3 วินาทีแล้วหรือยัง เมื่อผ่านไปแล้วจึงทำโค้ดโปรแกรมของ LED ดวงที่สอง
ในการทดลองจะต้องต่อหลอด LED เพิ่มตามวงจรต่อไปนี้
จากนั้นเข้าไปคัดลอกโปรแกรมตัวอย่างได้ที่ https://goo.gl/5wiJlh จากนั้นสามารถอัพโหลดลงบอร์ด NodeMCU-32S ได้เลย ผลที่ได้คือหลอด LED ที่อยู่บนบอร์ด NodeMCU-32S และหลอด LED ที่มีการต่อเพิ่มมีการกระพริบไม่พร้อมกัน และกระพริบได้ตามเวลาที่กำหนดไว้ คือ 0.5 วินาที และ 0.3 วินาที
สำหรับโค้ดโปรแกรมที่ได้ทดลองไปมีดังนี้
ส่วนสำคัญของโค้ดที่เกี่ยวข้องกับการทำงานแบบมัลติทาสกิ้งมีดังนี้
บรรทัดที่ 4 ประกาศตัวแปร last1 และ last2 เพื่อเก็บค่าเวลาที่โปรแกรมในกระบวนการของหลอด LED ดวงแรกและดวงที่สองทำงาน
บรรทัดที่ 12 ดึงค่าเวลาปัจจุบันมาจากฟังก์ชั่ millis() แล้วนำมาหักลบกับตัวแปร last1 แล้วเปรียบเทียบว่าเวลาผ่านไปครบ หรือมากกว่า 500 มิลิวินาทีแล้วหรือยัง ถ้าผ่านไปครบแล้ว ให้ทำคำสั่งในปีกกาของ if
บรรทัดที่ 13 นำค่าเวลาปัจจุบันจากฟังก์ชัน millis() มาเก็บลงตัวแปร last1 เพื่ออัพเดทเวลาที่โค้ดทำงานล่าสุดใหม่
บรรทัดที่ 14 สลับสถานะของหลอด LED ดวงแรก
บรรทัดที่ 16 ดึงค่าเวลาปัจจุบันมาจากฟังก์ชั่ millis() แล้วนำมาหักลบกับตัวแปร last2 แล้วเปรียบเทียบว่าเวลาผ่านไปครบ หรือมากกว่า 300 มิลิวินาทีแล้วหรือยัง ถ้าผ่านไปครบแล้ว ให้ทำคำสั่งในปีกกาของ if
บรรทัดที่ 17 นำค่าเวลาปัจจุบันจากฟังก์ชัน millis() มาเก็บลงตัวแปร last1 เพื่ออัพเดทเวลาที่โค้ดทำงานล่าสุดใหม่
บรรทัดที่ 18 สลับสถานะของหลอด LED ดวงที่สอง
สาเหตุที่ครบเวลาที่กำหนดใน if แล้วจะต้องมีการอัพเดทตัวแปร last1 last2 ทันที เพราะหลังจากทำโปรแกรมไปแล้วอาจจะทำให้ค่าเวลาคลาดเคลื่อนจากค่าที่ได้ใน millis() ใน if แล้วจะส่งผลให้โค้ดโปรแกรมทำงานตามเวลาผิดพลาดไป
Timer คือวงจรไฟฟ้าภายในไมโครคอนโทรลเลอร์ที่มีหน้าที่นับเวลา เราสามารถกำหนดให้ Timer เริ่มนับเวลาเมื่อใดก็ได้ และกำหนดให้หยุดเมื่อใดก็ได้ การใช้ Timer จะใช้กับกระบวนการที่อ้างอิงกับเวลาเป็นหลัก จากในตัวอย่างที่แล้วมีหลอด LED จำนวน 2 ดวงที่มีเวลาในการทำงานที่แตกต่างกัน ดังนั้นจากกรณีของหลอด LED ทั้ง 2 ดวงที่ผ่านมาจึงสามารถนำ Timer มาใช้งานได้
การนำ Timer มาใช้งานจะเกี่ยวข้องกับการอินเตอร์รัพท์ คือเมื่อนับเวลาได้ครบตามที่กำหนด ตัว Timer จะเกิดเหตุการณ์ขึ้นแล้วไปเรียกฟังก์ชันที่กำหนดเป็นอินเตอร์รัพท์ไว้ ผลคือโปรแกรมที่ขณะนั้นทำงานอยู่จะหยุดลง และวิ่งเข้าไปทำงานในส่วนของฟังก์ชันอินเตอร์รัพท์แทน และทันทีที่โปรแกรมในฟังก์ชันอินเตอร์รัพท์หมดลงจะวิ่งกลับมาทำงานในส่วนของโปรแกรมหลักตามเดิม
การใช้ Timer เป็นการแบ่งเวลาการทำงานของโปรแกรมหลักไปทำโปรแกรมรอง ดังนั้น Timer จึงอาจจัดได้ว่าเป็นมัลติทาสกิ้งเทียม
การใช้งาน Timer มีฟังก์ชันที่เกี่ยวข้องดังนี้
ฟังก์ชันเริ่มใช้งาน Timer – ใช้ฟังก์ชัน timerBegin() มีรูปแบบการใช้งานดังนี้
hw_timer_t* timerBegin(byte timer, int divider, bool countUp);
มีรายละเอียดของพารามิเตอร์ดังนี้
ตอบค่ากลับเป็นข้อมูลชนิด hw_timer_t แบบพอยเตอร์ โดยค่าที่ได้จากฟังก์ชันนี้จะถูกนำไปใช้งานในฟังก์ชันอื่น ๆ ที่เกี่ยวข้องกับ Timer ต่อไป
ฟังก์ชันกำหนด Callback เมื่อนับเวลาได้ครบ – Callback หมายถึงการเข้าไปเรียกใช้ฟังก์ชันใด ๆ สร้างไว้รองรับการเกิดเหตุการณ์นั้น ๆ เช่น การใช้อินเตอร์รัพท์จะต้องสร้างฟังก์ชันไว้เพื่อ Callback เพื่อให้ไปทำโค้ดอื่น ๆ ต่อไป การกำหนดฟังก์ชัน Callback จะใช้ฟังก์ชัน timerAttachInterrupt() ในการกำหนด โดยมีรูปแบบการใช้งานดังนี้
void timerAttachInterrupt(hw_timer_t *timer, void (*fn)(void), bool edge);
มีรายละเอียดของพารามิเตอร์ดังนี้
และไม่มีการส่งค่ากลับ
ฟังก์ชันกำหนดเวลาให้เกิดเหตุการณ์ – เมื่อ Timer นับเวลาได้ครบตามที่กำหนดในฟังก์ชันต่อไปนี้ จะทำให้เกิดเหตุการณ์และ Callback กลับไปยังฟังก์ชันที่กำหนด โดยฟังก์ชันที่ใช้กำหนดเวลาคือ timerAlarmWrite() มีรูปแบบการใช้งานดังนี้
void timerAlarmWrite(hw_timer_t *timer, long interruptAt, bool autoreload);
มีรายละเอียดของพารามิเตอร์ดังนี้
และไม่มีค่าที่ตอบกลับ
ฟังก์ชันเปิดใช้ Timer – ใช้ฟังก์ชัน timerAlarmEnabled() ในการเปิดใช้งาน มีรูปแบบการใช้งานดังนี้
bool timerAlarmEnabled(hw_timer_t *timer);
มีรายละเอียดของพารามิเตอร์ดังนี้
และมีการตอบกลับเป็นผลของการเริ่มใช้ Timer นับเวลา
สำหรับการทดลองใช้งาน Timer จะใช้หลอด LED จำนวน 2 ดวงกระพริบที่เวลาต่างกันตามตัวอย่างที่แล้ว แต่เนื่องจากครั้งนี้ได้เปลี่ยนมาใช้ Timer จึงต้องเปลี่ยนกระบวนการทำงานของโปรแกรมใหม่ โดยหลอด LED ดวงแรกจะกำหนดให้อยู่ในฟังก์ชัน loop() เพื่อกระพริบโดยใช้โค้ดปกติ แต่สำหรับหลอด LED ดวงที่สองจะใช้ Timer เข้ามาเรียกให้ชุดโปรแกรมทำงานเพื่อให้หลอด LED ดวงที่สองสามารถกระพริบได้ตามต้องการ
ก่อนอื่นให้ต่อวงจรดังต่อไปนี้
จากนั้นเข้าไปคัดลอกโค้ดโปรแกรมตัวอย่างได้ที่ https://goo.gl/8nTZl1 จากนั้นอัพโหลดโปรแกรมเข้าบอร์ด NodeMCU-32S ได้เลย ผลคือหลอด LED ที่อยู่บนบอร์ด และหลอด LED ที่ต่อเพิ่มมีการกระพริบที่ความเร็วต่างกันแล้ว
สำหรับโค้ดโปรแกรมที่ได้ทดลองมีดังนี้
สำหรับโค้ดโปรแกรมนี้ จะอธิบายเฉพาะส่วนที่เกี่ยวข้องกับ Timer ซึ่งมีรายละเอียดดังนี้
บรรทัดที่ 4 สร้างตัวแปร timer แบบพอยเตอร์ชนิด hw_timer_t กำหนดให้มีค่าเป็น NULL (บรรทัดนี้จำเป็นต้องมีทุกครั้งที่ใช้ Timer และไม่แนะนำให้เปลี่ยนรูปแบบใด ๆ ในบรรทัดนี้)
บรรทัดที่ 6 สร้างฟังก์ชันย่อย onTimer สำหรับเป็นฟังก์ชัน Callback เมื่อเกิดเหตุการณ์ (นับเวลาได้ครบตามที่กำหนด) ฟังก์ชันนี้จะถูกเรียกขึ้นมา และทำโปรแกรมในฟังก์ชันนี้
บรรทัดที่ 14 กำหนดเริ่มใช้งาน Timer โดยใช้ Timer ตัวที่ 0 กำหนดตัวหาร 80 และกำหนดให้นับเวลาขึ้น นำค่าที่กำหนดไปเก็บไว้ในตัวแปร timer
บรรทัดที่ 15 กำหนดให้เมื่อ Timer ในตัวแปร timer เมื่อนับเวลาครบแล้วจะไปเรียกฟังก์ชัน onTimer ขึ้นมา
บรรทัดที่ 16 กำหนดให้เมื่อ Timer ในตัวแปร timer นับเวลาครบ 300000 ไมโครวินาที แล้วให้เกิดเหตุการณ์ และกำหนดให้เริ่มนับเวลาใหม่ทุกครั้งที่นับครบ
บรรทัดที่ 17 กำหนดให้ Timer ในตัวแปร timer เริ่มนับเวลา
จะเห็นได้ว่าโค้ดโปรแกรมจะแบ่งออกเป็น 2 ส่วนสำหรับ 2 กระบวนการทำงาน คือกระบวนการทำงานของหลอด LED ดวงแรกจะอยู่ใน loop และใช้ฟังก์ชันหน่วงเวลาในการกำหนดเวลากระพริบ แต่สำหรับหลอด LED ดวงที่สองจะใช้ Timer ในการนับเวลาเพื่อให้หลอด LED สามารถกระพริบได้ตามเวลาที่ต้องการ
ข้อเสียของการใช้ Timer คือ การ Callback จะหมายถึงการเกิดอินเตอร์รัพท์ ฟังก์ชันที่ถูกเรียกด้วยอินเตอร์รัพท์ ภายในฟังก์ชันนั้น จะไม่สามารถเรียกใช้ฟังก์ชันหน่วงเวลาได้เลย เพราะหากเรียกใช้ฟังก์ชันหน่วงเวลาจะทำให้ส่วนป้องกันการทำงานของโปรแกรมผิดพลาด (Watchdog timer :WDT) ทำงาน เนื่องจากฟังก์ชัน Callback ทำงานนานเกินไปจนไม่สามารถกลับไปทำโปรแกรมหลักได้ ผลที่ตามมาคือ ESP32 จะหยุดทำงานแล้วรีเซ็ตตัวเอง หรือแสดงข้อความว่า CPU halted.
ESP32 ใช้สิ่งที่เรียกว่า FreeRTOS ในการจัดการ ๆ ทำงานของโปรแกรมทั้งหมดบน ESP32 โดยตัว FreeRTOS จะทำหน้าที่กำหนดคำสั่งที่จะถูกกระทำก่อน หรือถูกกระทำทีหลัง โดยดูจากระดับความสำคัญ (Priority) โดยกระบวนการทำงานจะถูกเรียกว่า Task เช่น กระบวนการทำงานของ WiFi จะเรียกเป็น WiFi Task หรือกระบวนการทำงานของส่วนป้องกันการทำงานของโปรแกรมผิดพลาด (WDT) จะถูกเรียกว่า WDT Task ตัว FreeRTOS เปิดให้สามารถสร้าง Task ได้ไม่จำกัด แต่จะถูกจำกัดจำนวนพื้นที่แรม โดยจะใช้ได้เท่าที่จองไว้เท่านั้น
การสร้าง Task สามารถทำได้โดยใช้ฟังก์ชัน xTaskCreate() ซึ่งมีรูปแบบการใช้งานดังนี้
void xTaskCreate(TaskFunction_t pvTaskCode,
const char *pcName,
int usStackDepth,
void *pvParameters,
int uxPriority,
TaskHandle_t *pxCreatedTask
);
มีรายละเอียดของค่าพารามิเตอร์ดังนี้
และไม่มีค่าตอบกลับ
ภายใน Task ที่สร้างจะต้องเรียกใช้ฟังก์ชันหน่วงเวลาเสมอเมื่อโอกาศ เพื่อเปิดโอกาศให้ FreeRTOS ได้นำเวลาที่ถูกหน่วงไปทำงานใน Task อื่น ๆ ที่มีระดับความสำคัญรองลงมา หากภายใน Task ไม่มีการเรียกใช้ฟังก์ชันหน่วงเวลาอยู่เลย จะทำให้ Task อื่นไม่ทำงาน แล้วจะมีการแจ้งเตือนออกมาที่ Serial Monitor
ก่อนอื่นให้ตอวงจรสำหรับทดลองดังนี้
การทดลองให้เข้าไปคัดลองโปรแกรมตัวอย่างได้ที่ https://goo.gl/hYLoDI จากนั้นนำมาอัพโหลดลงบอร์ด NodeMCU-32S ผลที่ได้คือโปรแกรมสามารถทำงานได้ถูกต้องตามที่ต้องการคือหลอด LED จำนวน 2 ดวงติดสลับกันไปมาในเวลาที่ต่างกันได้อย่างถูกต้อง
โค้ดที่ได้ใช้ในการทดลองมีดังนี้
จากโค้ดจะเห็นได้ว่า คำสั่งและฟังก์ชันต่าง ๆ จะแยกออกจากกันอย่างชัดเจน และเหมือนกับการเขียนโปรแกรมแบบปกติ คือสามารถใช้งานฟังก์ชันหน่วงเวลาได้ตามต้องการ มีส่วน setup แยกออกมา ซึ่งรายละเอียดสามารถอธิบายได้ดังนี้
บรรทัดที่ 4 สร้างฟังก์ชันย่อยที่ชื่อ LEDTwo_Task() โดยมีพารามิเตอร์ p รับค่าแบบพอยเตอร์ ฟังก์ชันนี้ถูกสร้างเพื่อใช้เป็น Task แยกออกมาจากงานหลัก
บรรทัดที่ 5 ใช้ฟังก์ชัน pinMode() เพื่อกำหนดให้ขาตาม LED2 มีสถานะเป็นเอาต์พุต ในส่วนนี้จะเปรียบเสมือนส่วนที่ไว้ใส่ในฟังก์ชัน setup ที่แยกออกมาจากฟังก์ชัน setup จริง หากต้องการใช้ .begin() ใด ๆ ก็ตาม สามารถทำได้โดยวางในส่วนนี้
บรรทัดที่ 6 ใช้คำสั่ง while วนลูปโดยมีเงื่อนไขเป็น 1 ซึ่งจะหมายถึงการสร้างการวนลูปที่ไม่รู้จบ เปรียบได้กับการสร้างฟังก์ชัน loop ขึ้นมา หากต้องการให้คำสั่งใด ๆ ถูกทำซ้ำ ๆ ให้นำมาวางไว้ในปีกกาของ while
บรรทัดที่ 13 สร้าง Task ใหม่ โดยกำหนดให้สร้าง Task โดยใช้ฟังก์ชัน LEDTwo_Task กำหนดชื่อเป็น LEDTwo_Task กำหนดจองพื้นที่บนแรม 1KB ไม่มีการส่งค่าเข้าไปในพารามิเตอร์ของฟังก์ชัน Task กำหนดความสำคัญเป็นระดับ 10 และไม่มีการใช้ตัวแปรพอยเตอร์เพื่อนำไปควบคุม Task ในขั้นต่อไป
โดยส่วนตัวแล้วผู้เขียนแนะนำว่า หากงานมีมากกว่า 1 งาน หรือต้องการทำมัลติทาสกิ้ง การใช้วิธีสร้าง Task ใหม่จะทำให้ง่ายต่อการเขียนและออกแบบโปรแกรมมากที่สุด และเนื่องจาก FreeRTOS สามารถนำไปใช้งานได้กับไมโครคอนโทรลเลอร์หลากหลายตะกูล ตัว Arduino Uno R3 / Mage 2560 เองก็รองรับ FreeRTOS ทำให้หากต้องการทำมัลติทาสกิ้งแบบสร้าง Task ก็สามารถทำได้เช่นเดียวกัน
มัลติทาสกิ้งแท้ คือกระบวนการทำงานที่มากกว่า 1 กระบวนการ ถูกทำงานไปพร้อม ๆ กันจริง ๆ ซึ่งงานเหล่านั้นจะถูกกระทำโดยแยกคอร์ของ CPU ออกจากกันอย่างเด็ดขาด ทำให้สามารถทำโปรแกรมไปพร้อม ๆ กันได้จริง ๆ ตามเส้นเวลา การใช้มัลติทาสกิ้งแท้จะทำให้กระบวนการทำงานของโปรแกรมสามารถยึดพื้นที่การทำงานไปได้ทั้งหมด หมายความว่าแม้จะเรียกใช้คำสั่งวนลูปเพื่อหยุดการทำงานของโปรแกรมในคอร์ของ CPU ก็จะไม่กระทบกับกระบวนการทำงานอื่น ๆ ที่อยู่ในคอร์อื่น ๆ ของ CPU
มัลติทาสกิ้งแท้มีใช้งานอยู่แล้วกับฟังก์ชัน setup และ loop โดยถูกรันอยู่บน CPU คอร์ที่ 2 ดังนั้นการทดลองสร้างมัลติทาสกิ้งเทียมโดยสร้าง Task นั้น ตัว Task ที่สร้างขึ้นใหม่จะไปอยู่บนคอร์ที่ 1 ทั้งหมด และเนื่องจากคอร์ของ CPU ถูกใช้ไปจนครบแล้ว ทำให้ไม่สามารถทำมัลติทาสกิ้งแท้บน ESP32 เพิ่มได้อีก
มัลติทาสกิ้ง คือการทำงานมากกว่า 1 งานในเวลาพร้อม ๆ กัน เช่น ต้องการให้หลอด LED 2 ดวงกระพริบในเวลาที่ไม่เท่ากัน หรือต้องการให้มีการส่งค่าอุณหภูมิทุก ๆ 1 วินาที และในระหว่างนั้นก็สามารถควบคุมหลอด LED ผ่าน MQTT ได้ด้วย ซึ่งการใช้ฟังก์ชันหน่วงเวลาเพื่อกำหนดให้ค่าอุณหภูมิถูกส่งตามเวลา จะส่งผลให้ MQTT สามารถรับข้อมูลเข้ามาตามเวลาที่หน่วงด้วย ดังนั้นวิธีที่สุดคือการแยกงานของส่วนรอควบคุม LED และส่วนส่งอุณหภูมิออกจากกันอย่างเด็ดขาด การแยกงานทั้ง 2 งานออกจากกันอย่างเด็ดขาดและไม่รบกวนการทำงานซึ่งกันและกันนี้เองที่เรียกว่า มัลติทาสกิ้ง
มัลติทาสกิ้งบน ESP32 สามารถทำได้หลายรูปแบบ ทั้งการใช้ฟังก์ชัน millis() เพื่อดึงค่าเวลาปัจจุบันออกมา การใช้ TImer เพื่อให้โปรแกรมส่วนหนึ่งทำงานตามเวลา และวิธีสร้าง Task ใหม่ ในแต่ละวิธีผู้เขียนจะเสนอการทดลองและอธิบายโค้ดอย่างต่อเนื่อง บางส่วนของเนื้อหาอาจทำความเข้าใจได้ยาก ขอให้ผู้อ่านค่อย ๆ อ่านและทำความเข้าใจ เมื่อผู้อ่านเข้าใจแล้วจะทำให้ผู้อ่านสามารถสร้างโค้ดใหม่ ๆ แปลก ๆ ออกมาได้อีกมาก และสามารถนำไปประยุกต์เพื่อสร้างงานได้ตามต้องการ