การใช้งาน FreeRTOS ตอนที่ 2
ในตอนที่ผ่านมาได้แนะนำให้รู้จักกับ FreeRTOS ได้แนะนำวิธีการติดตั้งเพื่อใช้กับ Arduino และได้ทำโปรแกรมไฟกระพริบเทพ! กันไปแล้ว
ในตอนที่ 2 นี้จะเจาะลึกกันสักหน่อย โดยเริ่มจากเรื่องของ Task Function ถ้าหากย้อนกลับไปดูโปรแกรมไฟกระพริบเทพในตอนที่แล้ว Task Function ก็คือฟังก์ชัน redLedControllerTask, blueLedControllerTask และ yellowLedControllerTask นั่นเอง คำถามคือ ทุกฟังก์ชันสามารถเป็น Task ได้ทั้งหมดหรือไม่ คำตอบคือ ไม่
แล้วฟังก์ชันที่เป็น Task ได้ จะต้องมีลักษณะยังไง ฟังก์ชันที่มีลักษณะเป็น Task ต้องมีลักษณะดังนี้
- เป็นฟังก์ชันที่ทำงานเบ็ดเสร็จในตัวเอง คือ สามารถถูกเรียกใช้ และจะทำงานอยู่ในฟังก์ชันนั้นไปตลอด โดยไม่มีการ return ออกจากฟังก์ชัน ซึ่งหากกลับไปดูฟังก์ชันในโปรแกรมตัวอย่างจากตอนที่แล้ว ก็จะเห็นว่าเป็นฟังก์ชันที่ทำงานแบบ Forever
- เป็นฟังก์ชันที่ไม่มีทางจะทำงานไปจนถึงบรรทัดสุดท้ายของฟังก์ชัน เพราะไม่เช่นนั้นก็จะเกิดการ return นั่นเอง ดังนั้นในโปรแกรมตัวอย่างจากตอนที่แล้ว จึงได้ใช้ while(1) ครอบเอาไว้
นั่นแปลว่า เมื่อฟังก์ชันนั้นถูกเรียกขึ้นมาทำงาน และเปลี่ยนเป็น Task มันจะทำงานไปเรื่อยๆ ซึ่งอาจมีบางคนสงสัยว่า อ้าว! แล้วถ้ามันทำงานเสร็จแล้ว และอยากจะเลิกใช้งานมันต้องทำยังไงล่ะ คำตอบคือ ต้องลบ Task นั้นทิ้งไปครับ โดยใช้คำสั่ง vTaskDelete()
บางคนอาจจะเริ่มสงสัยว่าทุกฟังก์ชันทำงานพร้อมกัน มันเป็นไปได้ยังไง! เพราะ CPU ก็มีอยู่ตัวเดียว ก็ขอบอกความจริงว่า มันไม่ได้ทำงานพร้อมกันซะทีเดียวครับ แต่เสมือนว่าทำงานพร้อมกันเท่านั้น ดูรูปนี้อาจจะเข้าใจมากขึ้น
ก่อนอื่นก็ต้องขออธิบายว่า ในระบบคอมพิวเตอร์ที่ใช้ระบบปฏิบัติการใดๆ ผู้ที่เป็นเจ้าของ CPU ก็คือตัวระบบปฏิบัติการเอง (ต่อไปขอเรียกว่า OS) แต่คำว่า OS เป็นคำที่กว้างครอบคลุมทุกสิ่งทุกอย่าง โดยองค์ประกอบที่เป็นแก่นอยู่ใน OS ที่ทำหน้าที่เฉพาะจัดการ CPU และหน่วยความจำ จะเรียกว่าเคอร์เนล (Kernel) ดังนั้นอาจกล่าวได้ว่า เคอร์เนลนี่เองที่เป็นเจ้าของ CPU โดยเมื่อคอมพิวเตอร์บูตขึ้นมาจะทำงานอยู่ในโปรแกรมเคอร์เนล และเมื่อผู้ใช้เรียกโปรแกรมใดๆ ขึ้นมาทำงาน เคอร์เนลก็จะส่ง CPU ไปให้โปรแกรมนั้น โปรแกรมนั้นจึงสามารถใช้ CPU ได้
แต่เคอร์เนลก็จะไม่ยอมให้โปรแกรมใดๆ ครอบครอง CPU ตลอดไปหรอกนะครับ มันจะให้แค่แป๊บเดียว จากนั้นจะยึดคืนกลับมา ถามว่าในเมื่อให้ CPU กับโปรแกรมนั้นไปแล้ว จะยึดคืนกลับมาได้ยังไง คำตอบคือ ใช้กลไกของการอินเทอรัปต์ (Interrupt) โดยสำหรับคนที่ไม่รู้จัก ก็ขอเล่าสั้นๆ ว่าอินเทอรัปต์ คือ กลไกการทำงานหนึ่งของคอมพิวเตอร์ โดยในขณะที่คอมพิวเตอร์กำลังรันโปรแกรมอยู่ เมื่อเกิดอินเทอรัปต์ขึ้น ไม่ว่าคอมพิวเตอร์จะทำงานอะไรอยู่ จะต้องหยุดงานนั้น แล้วมาดูอินเทอรัปต์ซะก่อน (จะพูดให้เห็นภาพง่ายๆ ก็เหมือนมีคนมากดกริ่งหน้าบ้าน แล้วเราต้องหยุดงานแล้วไปดูว่าใครมากดนั่นแหละ)
โดยคอมพิวเตอร์จะมีนาฬิกาจับเวลาตัวหนึ่ง ที่ค่อยส่งสัญญาณอินเทอรัปต์ทุกๆ ช่วงเวลาที่กำหนด (เรียกว่า Tick Interrupt หรือ Timer Interrupt) ดังนั้นเมื่อครบเวลา Timer ตัวนี้ก็จะอินเทอรัปต์ CPU ทำให้สามารถกลับมาที่เคอร์เนลได้ จากนั้นเคอร์เนลค่อยพิจารณาว่าจะให้งานไหนทำต่อไป
หากดูจากรูป ก็จะเห็นว่าเรามี 2 งาน คือ Task1 และ Task2 โดยเริ่มต้น Task1 ทำงานอยู่จากนั้นเกิด Tick Interrupt ณ เวลา t2 ทำให้การทำงานกลับไปที่เคอร์เนล และเคอร์เนลตัดสินใจโยนการทำงานไปที่ t2 และเมื่อเวลา t3 ก็เกิด Tick Interrupt อีกครั้ง การทำงานกลับไปที่เคอร์เนล คราวนี้เคอร์เนลตัดสินใจโยนการทำงานกลับมาที่ t2 ทำให้ Task1 และ Task2 ได้เวลาในการทำงานสลับกันไปเรื่อยๆ และเนื่องจากการสวิตซ์นี้ เกิดขึ้นในเวลาสั้นมากๆ เช่น 100 มิลลิวินาที หรือ 1 ใน 10 ของวินาที ก็จะทำให้เสมือนกับว่า Task1 และ Task2 ทำงานพร้อมๆ กันไปทั้งคู่
ที่เล่ามายืดยาว ก็เพื่อให้เข้าใจแนวคิดของการทำงานแบบ Multitasking เพื่อประโยชน์ในการทำความเข้าใจกับ FreeRTOS ต่อไป
คราวนี้เราจะกลับมาทำความเข้าใจกับรายละเอียดของฟังก์ชัน xTaskCreate ซึ่งได้กล่าวถึงพารามิเตอร์ของฟังก์ชันไปตั้งแต่ตอนที่ 1 ก็ขอทบทวนดังนี้
- ตัวที่ 1 (pvTaskCode) ชื่อฟังก์ชันที่จะเรียกขึ้นมาทำงานเป็น Task ในระบบ
- ตัวที่ 2 (pcName) เป็นชื่อเรียกของ Task เป็นข้อความ ซึ่งไม่ได้ใช้ทำอะไร (ใช้ในการ debug)
- ตัวที่ 3 (usStackDepth) เป็นขนาดของ Stack ที่จะจองให้ Task นั้นใช้งาน โดยมีหน่วยเป็น word หรือ 4 ไบต์ เช่น หากกำหนดเป็น 100 แปลว่ามีขนาด 400 ไบต์ โดยทั่วไปหากใช้ FreeRTOS กับไมโครคอนโทรลเลอร์ มักจะใช้ขนาดเล็ก (หลักร้อย) แต่หากใช้ในระบบที่ใหญ่ขึ้นก็จะใช้ใหญ่ขึ้นมา (หลักพัน) แต่ต้องมีขนาดไม่เกิน 65536
- ตัวที่ 4 (pvParameters) เป็นพารามิเตอร์ที่จะส่งเข้าไปใน Task
- ตัวที่ 5 (uxPriority) เป็น Priority หรือลำดับความสำคัญของงาน
- ตัวที่ 6 (*pxCreatedTask) เป็น Task Handle ซึ่งจะเป็นค่าที่ใช้ในการอ้างถึง Task
คราวนี้จะมาดูการใช้งานของพารามิเตอร์ตัวที่ 4 คือ pvParameters พารามิเตอร์นี้ใช้ในการส่งค่าเข้าไปในฟังก์ชัน จากโปรแกรมในตอนที่แล้ว จะเห็นว่าในฟังก์ชัน redLedControllerTask, blueLedControllerTask และ yellowLedControllerTask มีการทำงานที่คล้ายกัน
เราจะลองยุบรวมฟังก์ชันให้เหลือเพียงฟังก์ชันเดียว สามารถเขียนได้ดังนี้
จากโปรแกรม ในบรรทัดที่ 7, 8 และ 9 จะเป็นการสร้าง ตัวแปรชนิด pointer ที่มีชื่อว่า *blueled, *redled และ *yellowled โดยกำหนดให้ชี้ไปที่ข้อมูล 3 ตัว คือ BLUE (8), RED(6) และ YELLOW (7) ตามลำดับ โดยในการสร้าง Task เราจะสร้างจากฟังก์ชันเดียวกัน แต่สร้างจำนวน 3 Task โดยแต่ละ Task จะส่งตัวแปรไปในพารามิเตอร์ตัวที่ 4 (pvParameters) ต่างกัน
ในแต่ละฟังก์ชันของแต่ละ Task ก็จะได้รับค่า pvParameters ที่ต่างกันไป ซึ่งก็คือขาของ LED แต่ละดวง ดังนั้นแต่ละ Task ก็จะสั่งให้ไฟ 3 ดวงกระพริบไปพร้อมๆ กัน คงจะเข้าใจวิธีการส่งข้อมูลเข้าไปใน Task แล้วนะครับ
คราวนี้เราจะมาดูพารามิเตอร์อีกตัวหนึ่ง คือ พารามิเตอร์ตัวที่ 5 (uxPriority) ซึ่งเป็นพารามิเตอร์ที่ใช้กำหนดลำดับความสำคัญของงาน โดยลำดับความสำคัญนี้ 0 มีค่าน้อยที่สุด และค่ามากที่สุด คือ 3 (ในแต่ละระบบที่เอา FreeRTOS ไปใช้อาจมีค่ามากที่สุดไม่เท่ากัน แต่สำหรับที่ใช้กับ Arduino จะมีค่า 3) โดยหลักการทำงานของพารามิเตอร์ตัวนี้ คือ Task ที่มีลำดับความสำคัญสูงกว่าจะได้ทำงานก่อน
ถ้าเราลองเปลี่ยนค่าพารามิเตอร์ตัวนี้ของไฟ LED สีแดงให้เป็น 2 ดังตัวอย่าง
xTaskCreate(LedControllerTask, “RED LED Task”, 128, (void *)redLed, 2, NULL);
เมื่อรันโปรแกรมนี้ จะพบว่าจากเดิมที่มีไฟกระพริบอยู่ 3 ดวง จะเหลือกระพริบเพียงดวงเดียว คือ ไฟ LED สีแดง ทั้งนี้เนื่องจาก Task นี้มีลำดับความสำคัญที่สูงกว่า คือ 2 ในขณะที่ Task อื่นๆ มีลำดับความสำคัญแค่ 1 เท่านั้น ดังนั้น Task อื่นจึงไม่ได้รับ CPU Time เพื่อทำงาน
เอาละครับ! ก็คงเข้าใจกับการใช้งานฟังก์ชัน xTaskCreate กันพอสมควรแล้วนะครับ สำหรับตอนที่ 2 ก็ขอจบเพียงเท่านี้ โปรดติดตามตอนต่อไป