top of page

การสร้างและใช้งานคิว (Queue) ใน FreeRTOS


คำแนะนำ: บทความนี้ลึกมาก ต้องมีความรู้เรื่องระบบปฏิบัติการ (OS) เรื่อง Task โครงสร้างข้อมูล และเข้าใจภาษา C/C++ ในระดับหนึ่ง หากอ่านบทความนี้ไม่เข้าใจแนะนำให้อ่านบทความภาษาอังกฤษประกอบ

คิว (Queue) คือ แนวคิดการจัดเก็บข้อมูลลงในพื้นที่ชั่วคราว เพื่อทะยอยดึงออกมาใช้ตามลำดับ โดยข้อมูลที่ถูกเก็บลงไปก่อน จะถูกนำออกไปก่อน และข้อมูลที่ถูกเก็บทีหลัง จะถูกนำออกไปทีหลัง (First In First Out หรือ FIFO)

การทำงานของคิว เทียบได้กับการต่อแถวซื้อของ ที่คนมาก่อน จะได้ของก่อน คนมาทีหลัง จะได้ของทีหลัง ซึ่งระบบคิวจะเกิดขึ้นและจำเป็นต้องใช้เมื่อมีการดำเนินงานบางอย่างที่ต้องใช้เวลามากกว่าข้อมูลรอบถัดไปจะมา หากเทียบจากชีวิตจริง หากคนที่มาซื้อของมีไม่มาก คนแรกที่เข้ามาซื้อเสร็จแล้วออกไป ไม่มีคนมารอ ระบบคิวจะไม่จำเป็นต้องใช้ เพราะไม่มีการรอ แต่หากคนแรกกำลังซื้อของอยู่ แล้วมีคนที่สองเข้ามา มีคนที่สามเข้ามา ระบบคิวจึงจะจำเป็นที่จะต้องนำมาใช้จัดการ เพื่อให้คนที่มาก่อนได้สิทธิ์ซื้อของก่อน มาทีหลังได้สิทธิ์ซื้อของทีหลัง (ตาม Common sense ปกติ)

คิว เป็นหนึ่งในรูปแบบการสื่อสารระหว่างงาน (Task) ในระบบปฎิบัติการ โดยนำมาใช้เพื่อจัดลำดับการกระทำของข้อมูล และช่วยให้ไม่ต้องแชร์ทรัพยากรบางอย่างระหว่าง Task เช่น Task แรกจำเป็นต้องอ่านค่าจากเซ็นเซอร์แสง ส่วน Task สอง จำเป็นต้องอ่านค่าจากเซ็นเซอร์อุณหภูมิ แต่เซ็นเซอร์ทั้ง 2 ต่อกับ I2C ช่องเดียวกัน หมายความว่าทั้ง 2 Task มีการใช้งานทรัพยากรร่วมกัน (ในที่นี้คือใช้ I2C ร่วมกัน) เพื่อป้องกันไม่ให้ทั้ง 2 Task มีการแย่งใช้ทรัพยากรเดียวกันในเวลาเดียวกัน จึงควรแยก I2C ออกมาเป็น Task ใหม่ แล้วใช้ระบบคิวในการจัดการ และสื่อสารระหว่าง Task แทน

หากไม่เข้าใจสามารถค้นหาคำว่า โครงสร้างข้อมูลแบบคิว ใน Google ได้เลย

การสร้างคิวใน FreeRTOS

ใช้คำสั่ง xQueueCreate() เพื่อสร้างคิวแบบใช้พื้นที่แรมที่เหลืออยู่ ณ ขณะนั้น โดยมีพารามิเตอร์ที่ต้องกำหนด 2 ตัว ดังนี้

  • (ตัวเลข) uxQueueLength - ขนาดของคิวที่จุชุดข้อมูลได้สูงสุด

  • (ตัวเลข) uxItemSize - ขนาดของชุดข้อมูลต่อ 1 หน่วยคิว มีหน่วยเป็นไบต์ (Byte)

ค่าที่ส่งกลับ: ข้อมูลชนิด QueueHandle_t โดย

  • หากสร้างคิวสำเร็จ - ส่งกลับ handle ของคิวนี้ จำเป็นต้องใช้ในคำสั่งส่งข้อมูลเข้าคิว (Enqueue) หรือดึงข้อมูลออกจากคิว (Dequeue) ต่อไป

  • หากไม่สำเร็จ (แรม ณ ขณะนั้นเหลือไม่พอให้สร้างคิวตามขนาดที่ร้องขอ) - ส่งกลับ NULL

ขนาดของแรมที่ใช้สร้างคิวสามารถคำนวนคร่าว ๆ ได้จาก uxQueueLength x uxItemSize เช่น สร้างคิวเก็บชุดข้อมูล 100 ชุด แต่ละชุดข้อมูลขนาด 2 ไบต์ หมายความว่า uxQueueLength = 100, uxItemSize = 2 ไบต์ ดังนั้น 100 x 2 = 200 ไบต์

ตัวอย่างโค้ดโปรแกรมสร้างคิวเก็บตัวเลข (int) 10 ชุด ในแพลตฟอร์ม Arduino (ESP32) มีดังนี้

#include <Arduino.h>

QueueHandle_t xQueue; // สร้างตัวแปรไว้เก็บ handle ของคิวที่สร้าง

void setup() {
   xQueue = xQueueCreate(10, sizeof(int)); // สร้างคิวเก็บข้อมูล 10 ชุด แต่ละชุดขนาดเท่าข้อมูลชนิด int (ใน ESP32 คือ 4 ไบต์)
}

ตัวอย่างโค้ดโปรแกรมสร้างคิวเก็บตัวแปร pointer int จำนวน 10 ชุด ในแพลตฟอร์ม Arduino (ESP32) มีดังนี้

#include <Arduino.h>

QueueHandle_t xQueue; // สร้างตัวแปรไว้เก็บ handle ของคิวที่สร้าง

void setup() {
   xQueue = xQueueCreate(10, sizeof(int*)); // สร้างคิวเก็บข้อมูล 10 ชุด แต่ละชุดขนาดเท่าข้อมูลชนิด int*
}

หมายเหตุ. ตัวแปร pointer ที่ส่งเข้าไปในคิว ควรต้องสร้างจาก malloc() เท่านั้น แต่หากจะส่งตัวแปร static หรือตัวแปรระดับ Global ก็ทำได้ แต่ต้องระวังไม่ให้ค่าในตัวแปรถูก set ใหม่ในระหว่างที่คิวยังไม่ถูกดึงออก (โดยสรุปคือเสี่ยงมีบัค แนะนำให้ใช้ malloc() อย่างเดียว) ตัวแปรในระดับ local ห้ามส่งเข้าไปเด็ดขาด เสี่ยงพื้นที่ในแรมถูกลบออกก่อนถูกดึงออกจากคิว

หมายเหตุ2. ตัวแปร pointer ที่ส่งเข้าไปในคิวที่สร้างจาก malloc() หลังถูกดึงออกจากคิวและใช้งานเสร็จแล้ว ต้องใช้คำสั่ง free() เพื่อคืนพื้นที่แรมด้วย

การเพิ่มข้อมูลลงในคิว

ใช้คำสั่ง xQueueSend() กรณีเพิ่มคิวนอกฟังก์ชั่นอินเตอร์รัพท์ และ xQueueSendFromISR() กรณีเพิ่มคิวในฟังก์ชั่นอินเตอร์รัพท์

คำสั่ง xQueueSend() เป็นคำสั่งแบบ blocking / synchronous (ทำงานเสร็จสิ้นก่อนจึงไปคำสั่งถัดไป) ห้ามเรียกใช้ในฟังก์ชั่นอินเตอร์รัพท์เนื่องจากเสี่ยง WDT (Watchdog Timer) ทำงาน หรือ CPU ตอบสนองต่ออินเตอร์รัพท์รอบถัดไปไม่ทัน หรือเสี่ยงเกิดอินเตอร์รัพท์ซ้อน มีพารามิเตอร์ที่ต้องกำหนด 3 ตัว ดังนี้

  • (QueueHandle_t) xQueue - handle ของคิวที่ได้จากคำสั่ง xQueueCreate()

  • (const void *) pvItemToQueue - ข้อมูลที่ต้องการเพิ่มลงคิว

  • (TickType_t) xTicksToWait - เวลาที่สามารถรอเพิ่มข้อมูลลงคิวได้นานสุด หากขณะนั้นคิวเต็ม

ค่าที่ส่งกลับ: ข้อมูลชนิด BaseType_t โดย

  • หากเพิ่มข้อมูลลงคิวสำเร็จ - ส่งกลับ pdTRUE

  • หากขณะนั้นคิวเต็ม - ส่งกลับ errQUEUE_FULL

ตัวอย่างโค้ดโปรแกรมเพิ่มเลขสุ่มลงในคิวทุก ๆ 1 นาที ในแพลตฟอร์ม Arduino (ESP32) มีดังนี้

void loop() {
  int num = random(1, 100); // สุ่มตัวเลข 0 ถึง 100 เก็บลงตัวแปร num
  xQueueSend(xQueue, &num, pdMS_TO_TICKS(100)); // ส่งค่าในตัวแปร num เข้าคิว โดยหากคิวเต็มให้รอคิวว่างนานสุด 100 mS
  delay(1000); // หน่วงเวลา 1 วินาที
}

ตัวอย่างโค้ดโปรแกรมเพิ่มเลขสุ่มจากตัวแปร pointer ลงในคิวทุก ๆ 1 นาที ในแพลตฟอร์ม Arduino (ESP32) มีดังนี้

void loop() {
  int *num = (int *) malloc(sizeof(int)); // ขอพื้นที่แรม ณ ขณะนั้นเท่าพื้นที่ที่ข้อมูลชนิด int ใช้
  *num = random(1, 100); // สุ่มตัวเลข 0 ถึง 100 เก็บลง pointer ในตัวแปร num
  xQueueSend(xQueue, &num, pdMS_TO_TICKS(100)); // ส่งค่า pointer ในตัวแปร num เข้าคิว โดยหากคิวเต็มให้รอคิวว่างนานสุด 100 mS
  delay(1000); // หน่วงเวลา 1 วินาที
}

การรับข้อมูลจากคิว

ใช้คำสั่ง xQueueReceive() สำหรับนอกฟังก์ชั่นอินเตอร์รัพท์ และ xQueueReceiveFromISR() กรณีรับข้อมูลจากคิวในฟังก์ชั่นอินเตอร์รัพท์

คำสั่ง xQueueReceive() มีพารามิเตอร์ที่ต้องกำหนด 3 ตัว ดังนี้

  • QueueHandle_t xQueue - handle ของคิวที่ได้จากคำสั่ง xQueueCreate()

  • void *pvBuffer - ตัวแปรที่ใช้เก็บค่าที่อ่านได้จากในคิว

  • TickType_t xTicksToWait - เวลาที่สามารถรอรับข้อมูลจากคิวได้นานสุด หากขณะนั้นคิวว่าง (Empty)

ค่าที่ส่งกลับ: ข้อมูลชนิด BaseType_t โดย

  • หากรับข้อมูลจากคิวสำเร็จ - ส่งกลับ pdTRUE

  • หากขณะนั้นคิวว่าง - ส่งกลับ pdFALSE

ตัวอย่างโค้ดโปรแกรมรับตัวเลขจากในคิว ในแพลตฟอร์ม Arduino (ESP32) มีดังนี้

static void queue_process_task(void*) {
  for (;;) {
    int num; // สร้างตัวแปร num ไว้รับค่าจากคิว
    xQueueReceive(xQueue, &num, pdMS_TO_TICKS(100)); // รับค่าจากคิวเก็บลงตัวแปร num โดยหากคิวว่าง ให้รอข้อมูลเข้านานสุด 100 mS
    Serial.printf("Receive from queue: %d\n", num);
  }
  
  vTaskDelete(NULL);
}

ตัวอย่างโค้ดโปรแกรมรับตัวเลขจากตัวแปร pointer จากคิวทุก ในแพลตฟอร์ม Arduino (ESP32) มีดังนี้

static void queue_process_task(void*) {
  for (;;) {
    int *num = NULL; // สร้างตัวแปร num ไว้รับค่าจากคิว
    xQueueReceive(xQueue, &num, pdMS_TO_TICKS(100)); // รับค่าจากคิวเก็บลงตัวแปร pointer num โดยหากคิวว่าง ให้รอข้อมูลเข้านานสุด 100 mS
    if (num) { // เช็คเบื้องต้นว่าได้รับ pointer ที่ถูกต้องมาหรือไม่
      Serial.printf("Receive from queue: %d\n", *num);
      free(num); // คืนพื้นที่แรมหลังใช้ข้อมูลเสร็จ
    }
  }
  
  vTaskDelete(NULL);
}

โค้ดโปรแกรมทดสอบฉบับเต็ม

ใช้โค้ดโปรแกรมจากทุกหัวข้อมารวมกัน โดยทดลองสร้างคิว แล้วแยกให้ void loop() มีหน้าที่ส่งข้อมูลเข้าคิว ส่วนใน Task queue_process_task มีหน้าที่แสดงผลตัวเลขที่ได้รับผ่าน Serial


#include <Arduino.h>

QueueHandle_t xQueue; // สร้างตัวแปรไว้เก็บ handle ของคิวที่สร้าง

static void queue_process_task(void*) {
  for (;;) {
    int num; // สร้างตัวแปร num ไว้รับค่าจากคิว
    xQueueReceive(xQueue, &num, portMAX_DELAY); // รับค่าจากคิวเก็บลงตัวแปร num โดยหากคิวว่างให้รอจนกว่าจะไม่ว่าง
    Serial.printf("Receive from queue: %d\n", num);
  }
  
  vTaskDelete(NULL);
}

void setup() {
  Serial.begin(115200);
  
  xQueue = xQueueCreate(10, sizeof(int)); // สร้างคิวเก็บข้อมูล 10 ชุด แต่ละชุดขนาดเท่าข้อมูลชนิด int (ใน ESP32 คือ 4 ไบต์)
  xTaskCreate(queue_process_task, "queue_process_task", 1000, NULL, 20, NULL);
}

void loop() {
  int num = random(1, 100); // สุ่มตัวเลข 0 ถึง 100 เก็บลงตัวแปร num
  Serial.printf("Send %d to queue\n", num);
  xQueueSend(xQueue, &num, pdMS_TO_TICKS(100)); // ส่งค่าในตัวแปร num เข้าคิว โดยหากคิวเต็มให้รอคิวว่างนานสุด 100 mS
  delay(1000); // หน่วงเวลา 1 วินาที
}

ผลลัพท์ที่ได้หลังจากอัพโหลดโปรแกรมลงบอร์ด มีดังนี้

จะเห็นว่า void loop() สามารถส่งค่าไปให้ queue_process_task ได้แล้ว

ข้อมูลเพิ่มเติม

ดู 564 ครั้ง0 ความคิดเห็น

Comments


bottom of page