การใช้งาน FreeRTOS ตอนที่ 4
สำหรับตอนที่ 4 เป็นต้นไป ก็จะเข้าสู่โซน Advanced กันสักหน่อย โดยตอนนี้จะกล่าวถึงการใช้ Queue
คำถามแรก ที่ควรจะถาม คือ Queue คืออะไร (ต่อไปขอเรียกแบบไทยๆ ว่า คิว) ในทางคอมพิวเตอร์ คิว เป็นโครงสร้างข้อมูลแบบหนึ่ง โดยมีการทำงาน คือ ข้อมูลอะไรที่เข้าไปในคิวก่อน ข้อมูลนั้นจะต้องออกจากคิวก่อน ซึ่งแสดงได้ตามรูป
คำถามต่อมา คือ คิวเอาไว้ทำอะไร คิวอาจมีประโยชน์หลายอย่าง แม้กระทั่ง คิวสำหรับเข้าแถวซื้ออาหาร ก็ถือเป็นประโยชน์ของคิวเช่นนั้น แต่ในที่นี้ คิว จะใช้ในการส่งข้อมูลระหว่าง Task
ทำไมต้องเขียนโปรแกรมให้ส่งข้อมูลระหว่าง Task ด้วยล่ะ ก็ขออธิบายดังนี้นะครับ ถ้าเขียนโปรแกรมแบบ Single Task เราต้องเอางานทุกอย่างไปไว้ใน Task เดียว แล้วใช้เทคนิคการเขียนโปรแกรมจัดการกับงานหลายๆ อย่าง ซึ่งผลที่ตามมาก็คือ ความซับซ้อนของโปรแกรมที่เพิ่มขึ้น
ไม่ต้องเอาโปรแกรมซับซ้อนอะไรมาก สมมติว่า เราจะเขียนโปรแกรมให้รับปุ่ม 3 ปุ่ม เพื่อควบคุมไฟ LED จำนวน 3 ดวง ปุ่ม A เมื่อกด จะทำให้ไฟ LED X กระพริบ กดอีกทีให้ดับ ปุ่ม B เมื่อกด จะทำให้ไฟ LED Y กระพริบ กดอีกทีให้ดับ ปุ่ม C ก็เช่นเดียวกัน แค่นี้โปรแกรมก็เริ่มซับซ้อนแล้วใช้มั้ยครับ
แต่ถ้าเราเขียนเป็น Multitask เราจะแยกออกเป็น 6 Task แต่ละ Task จะรับผิดชอบงานง่ายๆ คือ รับปุ่มแต่ละปุ่ม จำนวน 3 Task และกระพริบไฟแต่ละดวงจำนวน 3 Task โปรแกรมก็จะง่ายขึ้นมาก ช่วงต่อไปจะแสดงให้ดูครับ
อาจจะมีผู้อ่านบางคนบอกว่า ใช้วิธีกำหนดตัวแปรแบบ Global ก็สามารถส่งข้อมูลระหว่าง Task ได้เช่นกัน ก็จริงครับ! แต่ปัญหาก็มีอยู่ว่า Task ตัวรับจะรู้ได้ยังไงว่าตัวส่งได้ส่งมาแล้ว และ Task ตัวส่งจะรู้ได้ยังไงว่าข้อมูลที่เอาไปเก็บไว้ที่ตัวแปร มีการอ่านออกไปแล้ว พูดง่ายๆ คือ มีปัญหาในการ Synchronization
ด้วยเหตุนี้ เขาจึงออกแบบโครงสร้างคิวขึ้นมายังไงละครับ จากภาพด้านบน แสดงการทำงานของคิว โดยเมื่อ Task A จะส่งข้อมูลใน Task B ข้อมูลก็จะไปรอที่ต้นคิว และเมื่อ Task A ส่งไปอีก ข้อมูลก็จะไปรอที่คิวอันดับ 2 จากนั้นเมื่อ Task B รับข้อมูลไป ก็จะดึงจากส่วนหัวของคิว และข้อมูลลำดับ 2 (20) ก็จะเลื่อนมาอยู่ที่ส่วนหัวของคิว ก็เป็นเรื่องที่เราคุ้นเคยกันอยู่แล้ว คงเข้าใจได้ไม่ยากนะครับ
FreeRTOS Queue
ก่อนอื่นก็ขออธิบาย คิวของ RTOS ก่อน โดยจะมีคุณลักษณะดังนี้
- คิวจะมีจำนวนจำกัดค่าหนึ่งเสมอ เรียกว่า ความยาวคิว
- ข้อมูลจะต้องถูกนำออกจากส่วนหัวของคิวเสมอ
- คิวจะเป็นหน่วยความจำของ FreeRTOS เมื่อสร้างคิวขึ้นมาแล้ว ไม่ต้องจองเพิ่มอีกเมื่อมีการใช้งาน
- เมื่อส่งข้อมูลเข้าไปในคิว จะเป็นลักษณะของการ copy ข้อมูลทั้งชุด ไม่ใช่แค่การส่ง address แบบ pointer และเมื่อส่งข้อมูลในคิวแล้ว สามารถจะใช้งานตัวแปรที่ส่งเข้าไปในคิวซ้ำได้ทันที (เพราะถูก copy ข้อมูลเข้าไปในคิวแล้ว)
- เมื่อข้อมูลถูกนำเข้าไปใส่ในคิวจนเต็ม จะใส่เข้าเพิ่มเข้าไปอีกไม่ได้ และในทำนองเดียวกัน เมื่อข้อมูลนำออกจากคิวจนหมด (คิวว่าง) จะไม่สามารถอ่านข้อมูลออกจากคิวได้
- Task ผู้ส่ง และ Task ผู้รับ จะแยกกันอย่างเด็ดขาดผ่านคิว ดังนั้นผู้เขียนโปรแกรมไม่ต้องกังวลว่า Task ใดจะทำหน้าที่เป็นผู้รับผิดชอบข้อมูล Task เพียงส่งหรือรับข้อมูลจากคิวเท่านั้น
- คิว 1 คิว สามารถจะใช้งานได้หลาย Task พร้อมๆ กัน เช่น เราอาจกำหนดให้มี 1 คิว และมี 3 Task ที่รับข้อมูลและนำมาใส่ในคิวเดียวกัน
- ในขณะที่ Task รับข้อมูลออกจากคิว จะมี Task เดียวที่อ่านได้ในเวลาหนึ่ง เช่น ถ้าคิวมีข้อมูลอยู่ Task แรกจะอ่านข้อมูลได้ ส่วน Task อื่นๆ (ถ้ามี) จะต้องรอ โดยจะในสภาวะ block แต่หากคิวไม่มีข้อมูล ทุก Task จะอยู่ในสภาวะ block ทั้งหมด จนกว่าจะมีข้อมูลเข้ามาในคิว Task ที่มี Priority สูงสุดจะเปลี่ยนเป็นสภาวะ Ready ซึ่งจะเข้าไปอ่านข้อมูลในคิวได้ แต่หากมี Task ที่มี Priority เท่ากัน Task ที่คอยมานานที่สุด จะเป็น Task ที่ได้เข้าไปในคิว และเพื่อป้องกันไม่ให้ Task ต้องคอยนานเกินไป อาจจะกำหนด block time เอาไว้ได้ โดยหาก Task ต้องรอเกินระยะเวลาที่กำหนดใน block time ก็จะเกิด time out และกลับสู่สภาวะ ready (แต่จะไม่ได้ข้อมูลไป) เพื่อทำงานต่อไป เช่น กลับไปแสดงผลว่าไม่มีข้อมูล เป็นต้น
- ในขณะที่มีการส่งข้อมูลเข้าไปในคิว (Write Queue) จะมีการ lock คิว เอาไว้ ดังนั้น Task อื่นๆ จะเข้าใช้งานคิวไม่ได้ (ต้องรอในสภาวะ block) ซึ่งแปลว่าในขณะใดขณะหนึ่ง จะมีเพียง Task เดียว ที่ส่งข้อมูลคิวได้ กรณีที่มีหลาย Task ถูก block พร้อมกัน เมื่อคิวมีที่ว่าง Task ที่มี Priority สูงสุด จะได้เข้าไปเขียนในคิวก่อน แต่หากมี Task ที่มี Priority เท่ากัน Task ที่คอยมานานที่สุด จะเป็น Task ที่ได้เข้าไปเขียนในคิว และเช่นเดียวกัน สามารถกำหนด timeout สำหรับการรอเขียน (กรณีที่คิวเต็ม) ได้ เพื่อไม่ให้ Task รอเขียนข้อมูลนานเกินไป
คราวนี้ก็มาดูส่วนของการเขียนโปรแกรม มีฟังก์ชันที่ใช้ดังนี้
การสร้างคิว
xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize ) เป็นฟังก์ชันที่ใช้ในการสร้างคิว
- uxQueueLength ใช้กำหนดความยาวของคิวที่จะสร้าง
- uxItemSize ใช้กำหนดขนาดของแต่ละคิว
- return value ถ้าเป็น NULL แสดงว่าสร้างคิวไม่สำเร็จ
ฟังก์ชันฝั่งส่ง
xQueueSendToFront( QueueHandle_t xQueue, const void * pvItemToQueue,
TickType_t xTicksToWait );
xQueueSendToBack( QueueHandle_t xQueue, const void * pvItemToQueue,
TickType_t xTicksToWait );
- xQueue เป็นตัวแปรของคิวที่จะส่งข้อมูลเข้าไป (Handle)
- pvItemToQueue พอยเตอร์ที่ชี้ไปยังข้อมูลที่จะส่งเข้าไปในคิว
- xTicksToWait เป็นระยะเวลาสูงสุดที่จะรอเพื่อส่งข้อมูลเข้าในคิว ถ้ามีค่าเป็น 0 ก็จะไม่รอ แม้จะส่งข้อมูลเข้าในคิวไม่ได้
อนึ่ง ฟังก์ชัน xQueueSendToFront จะส่งข้อมูลเข้าไปที่หัวของคิว และ xQueueSendToBack จะส่งข้อมูลเข้าไปที่ส่วนท้ายของคิว เนื่องจากปกติแล้วเราจะส่งข้อมูลเข้าไปที่ส่วนท้ายของคิว ดังนั้น FreeRTOS จึงได้สร้างฟังก์ชัน xQueueSend() ขึ้นมา ซึ่งจะเหมือนกับ xQueueSendToBack
ค่าที่ส่งกลับจากฟังก์ชัน มี 2 แบบ คือ pdPASS ซึ่งแปลว่าส่งข้อมูลได้สำเร็จ และ errQUEUE_FULL คือ ส่งข้อมูลไม่สำเร็จเนื่องจากคิวเต็ม
ฟังก์ชันฝั่งรับ
xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer,
TickType_t xTicksToWait );
- xQueue เป็นตัวแปรของคิวที่จะรับข้อมูลออกมา (Handle)
- pvBuffer เป็นพอยเตอร์ที่ใช้รับข้อมูลที่ copy ออกมาจากคิว
- xTicksToWait เป็นระยะเวลาสูงสุดที่จะรอเพื่อรับข้อมูลจากคิว ถ้ามีค่าเป็น 0 ก็จะไม่รอ แม้จะส่งไม่มีข้อมูลในคิว
ค่าที่ส่งกลับจากฟังก์ชัน มี 2 แบบ คือ pdPASS ซึ่งแปลว่าส่งข้อมูลได้สำเร็จ และ errQUEUE_FULL คือ ส่งข้อมูลไม่สำเร็จเนื่องจากคิวเต็ม
Simple Queue
จะเริ่มด้วยโปรแกรม Simple Queue ซึ่งเป็นโปรแกรมคิวอย่างง่าย
โปรแกรมจะประกอบด้วยส่วน setup() ซึ่งเป็นการสร้างคิวขึ้นมา 1 คิว ชื่อ ledQueue จากนั้นก็สร้าง Task ง่ายๆ ขึ้นมา 2 Task ชื่อ vSenderTask และ vReceiverTask
vSenderTask จะทำหน้าที่อ่าน Switch ซึ่งต่อเอาไว้ที่ขา 7 และส่งข้อมูลเข้าไปในคิว โดยโปรแกรมกำหนดให้ delay 10 มิลลิวินาที
vReceiverTask จะทำหน้าที่รับข้อมูลจากคิว และสั่งให้ LED สว่างหรือดับ ขึ้นกับข้อมูลที่ได้รับมาจากคิว
โปรแกรมก็ไม่มีอะไรซับซ้อน ซึ่งจะเห็นได้ว่าโดยการแยก Task เราสามารถจะแยกการรับ Input กับการแสดงผล Output ออกจากกันได้
Multiple Source Queue
สำหรับตัวอย่างโปรแกรมต่อไป จะเป็นโปรแกรมที่ซับซ้อนขึ้น โดยสร้าง Task ขึ้นมา 3 Task สำหรับรับ Input จำนวน 2 Task และเป็น Output จำนวน 1 Task โดย Input แต่ละ Task จะข้อมูลจาก Switch จำนวน Task ละ 1 ตัว และสั่งให้ไฟที่ Task Output กระพริบ จำนวน 1 ครั้ง ทุกครั้งที่กด
จากโปรแกรมเราจะเริ่มต้นด้วยการกำหนด Pin ของ LED โดยมี 2 สี คือ สีแดงต่อกับขา 6 และสีเหลืองต่อกับขา 5 จากนั้นก็จะเป็นสวิตซ์ 2 ตัวต่อกับขา 7–8 ตามลำดับ
จากนั้นจะเป็นการกำหนดตัวแปรคิว ชื่อ BlinkQueue
ในส่วนของ Setup จะเริ่มจากสร้างคิว โดยมีความจุ 5 ช่อง จากนั้นจะสร้าง Task ขึ้นมา 3 Task โดย 2 Task แรกจะเป็น Sender และ 1 Task สุดท้ายจะเป็น Receiver โดยในส่วนของ Task Sender จะมีการส่งค่าพารามิเตอร์ เป็นหมายเลข Pin ของสวิตซ์เข้าไปด้วย
คราวนี้มาดูในส่วนของ Sender กัน เริ่มจากกำหนด pinMode ให้ Pin ที่รับมาเป็น Input จากนั้นจะเข้าสู่ While Loop ที่เป็น Infinity Loop โดยใน Loop จะเช็คว่ามีการกดสวิตซ์หรือไม่ ถ้ามีการกดจะให้ค่าของตัวแปร valueToSend มีค่าเท่ากับ Pin ที่มีการกด ไม่งั้นก็จะมีค่าเป็น 0 จากนั้นจะมีการตรวจสอบว่า หาก valueToSend มีค่าไม่เท่ากับ 0 (คือมีการกด) ก็ให้ส่งค่าเข้าไปในคิว โดยตรวจสอบด้วยว่าส่งข้อมูลเข้าไปในคิวสำเร็จหรือไม่ หากไม่สำเร็จจะแจ้ง Error มาทาง Serial จากนั้นจะรอ 100 ms แล้วค่อยไปรับคีย์ใหม่
ในส่วนของ Receiver จะเริ่มจากกำหนด pinMode ของ LED ทั้งสีแดงและสีเหลืองให้เป็น Output จากนั้นจะเข้าสู่ While Loop ที่เป็น Infinity Loop โดยใน Loop จะอ่านข้อมูลออกจากคิว และตรวจสอบว่าอ่านได้หรือไม่ ถ้าอ่านไม่ได้ (คิวว่าง) จะแสดงผ่าน Serial หลังจากนั้นจะแสดงว่าที่รับมาได้คืออะไร และหากรับมาได้เป็น 7 ก็จะสั่งให้ไฟสีแดงติด 100 ms และหากรับมาได้เป็น 8 ก็จะสั่งให้ไฟสีเหลืองติด 100 ms
สรุป
โดยสรุป โปรแกรมนี้จะเป็นการรับสวิตซ์ 2 ตัว แบบอิสระกัน หากกดสวิตซ์ตัวใด ก็ให้ไฟของสวิตซ์ตัวนั้นติดและดับ เพียงแต่เราทำผ่านกลไกที่เรียกว่าคิว ซึ่งจะเห็นว่าหลาย Task สามารถจะใช้งานคิวเดียวกันในการส่งข้อมูลได้
สำหรับเนื้อหาของตอนนี้ ก็ขอจบลงเพียงเท่านี้ แล้วพบกันใหม่ในตอนต่อไปครับ