บทนำ (Introduction)

สวัสดีครับน้องๆ วิศวกรและเพื่อนนักพัฒนาชาว www.123microcontroller.com ทุกคน! กลับมาพบกับวิศวกรขอบตาดำๆ กันอีกครั้งครับ วันนี้เราจะมาขยับความแอดวานซ์ขึ้นไปอีกขั้น เข้าสู่โลกของการจัดการระบบปฏิบัติการ (OS) และสถาปัตยกรรมซอฟต์แวร์ครับ

เวลาที่เราเขียนโปรแกรมไมโครคอนโทรลเลอร์ที่เริ่มมีความซับซ้อน เช่น ต้องอ่านเซ็นเซอร์อุณหภูมิ, อัปเดตหน้าจอ LCD, และส่งข้อมูลผ่าน UART ไปพร้อมๆ กัน เราจำเป็นต้องใช้เทคนิคที่เรียกว่า Multitasking ครับ ซึ่งก็คือการสร้าง “ภาพลวงตา” ว่า CPU สามารถทำงานหลายๆ อย่างขนานกันไปได้ (Pseudoparallel) ทั้งๆ ที่ความจริงแล้ว CPU มีแค่คอร์เดียวและทำได้ทีละคำสั่ง

เบื้องหลังเวทมนตร์นี้คือโปรแกรมจัดการคิวที่เรียกว่า Scheduler ครับ ซึ่งในโลกของวิศวกรรมซอฟต์แวร์ฝังตัว (Embedded Software) แหล่งข้อมูลระดับเซียนได้แบ่งปรัชญาการทำงานของ Scheduler ออกเป็น 2 ขั้วหลักๆ คือ Cooperative และ Pre-emptive วันนี้เราจะมาเจาะลึกกันว่า สองค่ายนี้ต่างกันอย่างไร มีข้อดีข้อเสียแบบไหน และเราควรเลือกใช้อะไรให้เหมาะกับโปรเจกต์ของเราครับ!

เนื้อหาหลัก (Core Concept): ศึกชิงซีพียูระหว่าง “คนสุภาพ” กับ “ผู้คุมกฎ”

ในบริบทที่กว้างขึ้นของสถาปัตยกรรมระบบปฏิบัติการ (OS Architecture) การที่ระบบจะสลับการทำงานจาก Task A ไปยัง Task B ได้นั้น (เรียกว่า Context Switch) จะต้องมีกฎเกณฑ์ที่ชัดเจนครับ ซึ่งแหล่งข้อมูลได้อธิบาย 2 รูปแบบไว้ดังนี้:

  • 1. Cooperative Multitasking (ระบบจัดการแบบสมานฉันท์):
    • หลักการ: เป็นระบบที่ “ไว้ใจ” โปรแกรมเมอร์ครับ ในระบบนี้ Task ที่กำลังรันอยู่จะมีสิทธิ์ครอบครอง CPU ไปเรื่อยๆ จนกว่าตัวมันเองจะทำงานเสร็จ หรือ “สมัครใจ” ยอมสละ CPU (Voluntarily yield) คืนให้กับ Scheduler เพื่อให้ Task ถัดไปได้รัน
    • การเปรียบเทียบ: เหมือนการประชุมที่สุภาพชนผลัดกันพูด เมื่อคนแรกพูดจบประเด็น ก็จะส่งไมค์ให้คนต่อไป
    • ข้อดี: เขียนง่าย ใช้ทรัพยากรน้อย (Low overhead) และไม่ต้องกังวลเรื่องการสลับ Task กลางคันจนข้อมูลพัง เพราะแต่ละ Task ควบคุมจังหวะการหยุดของตัวเองได้
    • จุดอ่อนร้ายแรง: หากมี Task ใด Task หนึ่งถูกเขียนมาไม่ดี เช่น มีการวนลูป while(1) เพื่อรอรับค่าจากฮาร์ดแวร์นานเกินไป (Busy waiting) หรือเกิดบั๊ก มันจะฮุบ CPU ไว้คนเดียว ทำให้ Task อื่นๆ ไม่ได้รันเลย (เกิดอาการ Starvation หรือระบบค้างไปเลย)
  • 2. Pre-emptive Multitasking (ระบบจัดการแบบผู้คุมเบ็ดเสร็จ):
    • หลักการ: นี่คือหัวใจของ Real-Time Operating System (RTOS) สมัยใหม่ครับ ระบบจะไม่สนใจว่า Task จะยอมคืน CPU หรือไม่ แต่ Scheduler (ซึ่งมักถูกกระตุ้นด้วย Timer Interrupt) จะมีสิทธิ์ “แทรกแซง (Pre-empt)” และยึด CPU คืนมาได้ทันทีเมื่อหมดเวลา (Time slice) หรือเมื่อมี Task ที่ “สำคัญกว่า (Higher Priority)” พร้อมที่จะทำงาน
    • การเปรียบเทียบ: เหมือนการโต้วาทีที่มีประธานคอยจับเวลา ถ้าหมดเวลาปุ๊บ ประธานจะตัดไมค์คุณทันที! หรือถ้ามีแขก VIP เดินเข้ามา ประธานก็จะเชิญ VIP ให้ขึ้นพูดแทรกได้เลย
    • ข้อดี: รับประกันการตอบสนองแบบ Real-time ได้ (Deterministic) Task ที่มีความสำคัญสูง (เช่น ตรวจจับเหตุฉุกเฉิน) จะได้รับ CPU ทันทีโดยไม่ต้องรอให้ Task อื่นทำงานเสร็จ
    • จุดอ่อน: กินทรัพยากร CPU และ Memory มากกว่า เพราะทุกครั้งที่สลับ Task ระบบต้องทำการ “Context Switch” คือการก๊อปปี้ค่า Registers และ Stack ทั้งหมดของ Task ปัจจุบันไปเก็บไว้ใน Memory และโหลดค่าของ Task ถัดไปขึ้นมา ที่สำคัญคือโปรแกรมเมอร์ต้องระวังเรื่องการแย่งกันใช้ตัวแปร (Race Conditions) ขั้นสุดครับ!

Cooperative vs Pre-emptive Multitasking Architecture

ตัวอย่างโค้ด (Code Example):

มาดูความแตกต่างทางโครงสร้างโค้ดภาษา C สไตล์ Clean Code กันครับ สังเกตดูว่ารูปแบบการเขียนลูปของ 2 ระบบนี้จะต่างกันอย่างสิ้นเชิง

#include <stdint.h>
#include <stdbool.h>

/* ======================================================================
 * แบบที่ 1: Cooperative Multitasking 
 * (โปรแกรมเมอร์ต้องใส่ฟังก์ชัน rtos_yield() เพื่อยอมสละ CPU เสมอ)
 * ====================================================================== */
void task_sensor_coop(void) {
    while(1) {
        // อ่านค่าเซ็นเซอร์
        read_sensor();
        
        // 🛡️ สำคัญมาก! ถ้าไม่เรียก yield ระบบจะค้างอยู่ที่นี่ตลอดกาล
        // เป็นการส่งสัญญาณบอก Scheduler ว่า "ฉันขอพัก ให้คนอื่นรันบ้าง"
        rtos_yield(); 
    }
}

void task_display_coop(void) {
    while(1) {
        update_lcd();
        rtos_yield(); // สละ CPU ให้ Task อื่น
    }
}

/* ======================================================================
 * แบบที่ 2: Pre-emptive Multitasking (RTOS)
 * (OS จัดการสลับ Task ให้เองผ่าน Timer Interrupt โปรแกรมเมอร์โฟกัสแค่งาน)
 * ====================================================================== */
void task_critical_alarm_preemptive(void) {
    /* สมมติว่า Task นี้ถูกตั้ง Priority ไว้สูงสุด */
    while(1) {
        // รอรับ Signal หรือ Event อย่างใดอย่างหนึ่ง (OS จะจับ Task นี้ไป Block ไว้)
        os_wait_event(ALARM_EVENT); 
        
        // เมื่อมี Event มาถึง OS จะ "ตัดจบ (Pre-empt)" Task อื่นที่กำลังรันอยู่ 
        // และกระโดดมาทำบรรทัดนี้ทันที!
        trigger_siren(); 
    }
}

void task_calc_preemptive(void) {
    /* Task ความสำคัญต่ำ */
    while(1) {
        // สามารถคำนวณสมการหนักๆ ได้เลย ไม่ต้องสนเรื่อง yield 
        // เพราะเดี๋ยวถ้าหมด Time slice หรือมี Alarm เข้ามา OS จะตัดบทเอง
        heavy_math_calculation(); 
    }
}

ข้อควรระวัง / Best Practices:

การเลือกใช้ระบบสลับ Task ทั้งสองแบบมีหลุมพรางที่หนังสือวิศวกรรมซอฟต์แวร์ระดับโลกและมาตรฐานการเข้ารหัสที่ปลอดภัยเตือนไว้ดังนี้ครับ:

  1. ระวังภัยเงียบ Priority Inversion (สำหรับ Pre-emptive): นี่คือบั๊กคลาสสิกของ RTOS! สมมติว่า Task-Low กำลังใช้งานตัวแปรผ่าน Mutex, แล้ว Task-High ดันอยากใช้ตัวแปรนี้ด้วย Task-High จึงต้องหยุดรอ (Block) แต่จู่ๆ Task-Medium ก็ทำงานขึ้นมาและแย่ง CPU จาก Task-Low ไป (เพราะ Medium ชนะ Low) ผลคือ… Task-High ที่สำคัญที่สุด กลับไม่ได้ทำงานเพราะถูก Task-Medium ขวางทางอ้อม! วิธีแก้คือต้องใช้ระบบจัดการ OS ที่รองรับ “Priority Inheritance” ครับ
  2. ปกป้องตัวแปรด้วย Mutex เสมอ (สำหรับ Pre-emptive): ในระบบ Pre-emptive การสลับ Task อาจเกิดขึ้น ระหว่างบรรทัดของ C code (เช่น ตอนกำลังบวกค่าตัวแปร) ถ้าหลาย Task แชร์ตัวแปร global ตัวเดียวกัน คุณจะเจอ Race Condition ทันที ต้องใช้ Mutual Exclusion (Mutex) หรือ Semaphore เพื่อสร้าง “พื้นที่หวงห้าม (Critical Section)” เสมอ
  3. อย่ากั๊ก CPU (สำหรับ Cooperative): ในระบบสมานฉันท์ กฎเหล็กคือห้ามใช้ฟังก์ชันจำพวก delay_ms() หรือเขียนลูป while(!flag) เด็ดขาด เพราะมันคือการยึด CPU ไว้โดยเปล่าประโยชน์ หากต้องรออะไรสักอย่าง ให้ใช้วิธีเปลี่ยน State ของ Task หรือส่ง yield() รัวๆ เพื่อให้ระบบโดยรวมเดินหน้าต่อไปได้ครับ

สรุป (Conclusion)

สรุปสั้นๆ คือ Cooperative เปรียบเสมือนการทำงานเป็นทีมที่ทุกคนรู้หน้าที่และแบ่งปันเวลากัน เหมาะกับระบบขนาดเล็กที่ควบคุมโค้ดได้ทั้งหมด ส่วน Pre-emptive คือระบบองค์กรที่มีหัวหน้าคอยสั่งการตัดจบงาน เหมาะกับระบบที่ซับซ้อนและต้องการการตอบสนองที่ฉับไวแบบ Real-time ครับ การเลือกใช้ปรัชญาที่ถูกต้อง คือก้าวแรกของสถาปัตยกรรมซอฟต์แวร์ฝังตัวที่ทรงพลัง!

หากเพื่อนๆ สนใจอยากดูวิธีการสร้าง OS Scheduler แบบ Cooperative ด้วยตัวเอง หรืออยากแชร์ประสบการณ์ปวดหัวกับบั๊ก Race Condition ใน RTOS แวะเข้ามาตั้งกระทู้คุยกันต่อ หรือเอาโค้ดมาอวดกันได้ที่เว็บบอร์ด www.123microcontroller.com ของพวกเราได้เลยนะครับ! แล้วพบกันใหม่ในบทความหน้า Happy Coding ครับทุกคน!