01076051Lecture 12

Real-Time Operating System (RTOS)

บทที่ 12: ระบบปฏิบัติการเรียลไทม์ (RTOS) ด้วย FreeRTOS

เมื่อระบบฝังตัวต้องทำหลายงานพร้อมกัน (เล่นเพลง + อ่านไฟล์ + รับปุ่ม + วาดจอ) การเขียน scheduling เองบน CPU คอร์เดียวจะยากมาก. บทนี้แนะนำ RTOS ที่ช่วยสลับงานให้ดูเหมือนทำพร้อมกัน พร้อมแนวคิด task, context switch, ความเป็น real-time, และการจัดการทรัพยากรร่วมด้วย mutex / semaphore / queue บน FreeRTOS กับบอร์ด NUCLEO-F767ZI.

ผลการเรียนรู้ (Learning Outcomes)

  • อธิบายว่าทำไมระบบที่ต้องทำหลายงานพร้อมกันจึงต้องการ RTOS แทนการเขียน scheduling เอง
  • นิยาม real-time, deterministic และแยก non-real-time, soft real-time, hard real-time พร้อมตัวอย่าง
  • อธิบายแนวคิด task, scheduler, context switch, task state และ priority
  • สร้างและรัน task ด้วย FreeRTOS (xTaskCreate, vTaskDelay) ให้ LED กะพริบอิสระกัน
  • อธิบายปัญหา race condition และ critical section เมื่อหลาย task แชร์ทรัพยากรเดียวกัน
  • เลือกใช้ mutex, semaphore (รวมถึงแก้ producer-consumer), หรือ queue เพื่อคุมการเข้าถึงทรัพยากรร่วม

§12.1 ทำไมต้องมี RTOS

ลองนึกถึงเครื่องเล่น MP3. มันต้องทำหลายงาน พร้อมกัน: โหลดไฟล์จาก flash เข้า memory, decode ไฟล์บีบอัด (MP3/AAC) กลับเป็นเสียง, ปรับคุณภาพเสียง (equalizer), รับปุ่ม play/pause/next, และวาด UI/animation. เพลงต้องเล่นต่อเนื่องไม่สะดุด แปลว่าทุกงานต้องทันเวลา.

ถ้า CPU มีคอร์เดียว การเขียนเองให้ทุกงานดูเหมือนทำพร้อมกันนั้นยากมาก ต้องคอยจับเวลาและสลับงานเอง. ถ้ามี OS เราไม่ต้องคิดเรื่องนี้มาก ปล่อยให้ OS เป็นคน scheduling process ให้ แต่ละงานก็ทำตามจังหวะของมันไป.

อีกตัวอย่างคือรถยนต์ที่มี ECU (Electronic Control Unit) หลายตัวแยกกันคุมแต่ละงาน. งานเหล่านี้เป็น real-time แต่ความสำคัญไม่เท่ากัน: ระบบควบคุมเครื่องยนต์และ ABS สำคัญมากเพราะรถกำลังวิ่ง, ระบบถุงลม (airbag) สำคัญเฉพาะตอนชน, ส่วน infotainment หรือกุญแจไร้สายสำคัญรองลงไป. การแยกระดับความสำคัญนี้นำไปสู่แนวคิด real-time หลายระดับในหัวข้อถัดไป.

VIZ-62ไทม์ไลน์ตัวจัดคิวงาน RTOS
RunningReadyBlockedcontext switch
CPU
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED1
LED2
serialTask
0
2
4
6
8
10
12
14
16
18
tick 0

แต่ละงานสลับกันถือ CPU ทีละ slice. จุดสลับ (เส้นตั้ง) คือ context switch — เซฟสถานะงานเก่า โหลดงานใหม่ ซึ่งมีต้นทุนเวลา.

CPU เดียวรันทีละงาน. เลือกโหมดเพื่อดูสถานะงาน, การสลับงานที่ 'ดูเหมือนพร้อมกัน', ผลของการหน่วงแบบยึด CPU vs คืน CPU, และ race condition กับการแก้ด้วย mutex. เลื่อน playhead เพื่อไล่เวลา.

§12.2 Real-Time, Deterministic และ 3 ระดับ

RTOS ย่อจาก Real-Time Operating System. จุดเน้นไม่ใช่ความเร็วอย่างเดียว แต่คือความ deterministic — ต้องตอบสนองงานให้เสร็จ ภายในเวลาที่กำหนด (deadline) อย่างคาดเดาได้. เช่นระบบหัวฉีดต้องตอบสนองภายในเวลาที่กำหนด, ABS เมื่อตรวจเจอล้อล็อคต้องเริ่มทำงานภายในเวลาที่กำหนด.

คุณสมบัติอื่นเช่นความเร็วประมวลผล จำนวนฟีเจอร์ หรือขนาด footprint ไม่ได้เป็นตัวตัดสินความเป็น real-time โดยตรง. ตัวตัดสินคือการรับประกัน deadline.

แบ่งระบบตามความเป็น real-time ได้ 3 ระดับ. Non-real-time: ไม่มี deadline เสร็จเมื่อไรก็ได้. Soft real-time: มี deadline แต่ถ้าเลยบ้างก็แค่ไม่น่าพอใจ (เช่นภาพกระตุก) ไม่เกิดความเสียหาย. Hard real-time: ถ้าเลย deadline เกิดความเสียหายจริง.

ตัวอย่าง soft real-time: เครื่องเล่น DVD, เครื่องเล่นเพลงพกพา, แอปนำทาง (ถ้าบอกให้เลี้ยวช้าไปก็แค่หงุดหงิด). ตัวอย่าง hard real-time: ระบบส่งกำลัง/เปลี่ยนเกียร์ (ช้าไปเครื่องสะดุด), ABS (ช้าไปรถเสียหลัก), airbag (ช้าหรือเร็วไปก็ป้องกันได้ไม่เต็มที่), ระบบหล่อเย็นเตาปฏิกรณ์นิวเคลียร์, และ motor control ในสายพานการผลิต.

ระดับมี deadline?ถ้าเลย deadlineตัวอย่าง
Non-real-timeไม่มีไม่มีผลงานประมวลผลทั่วไป
Soft real-timeมีไม่พอใจ แต่ไม่เสียหายเครื่องเล่นเพลง, นำทาง
Hard real-timeมีเกิดความเสียหายABS, airbag, เตาปฏิกรณ์
3 ระดับของระบบตามความเป็น real-time
VIZ-62ไทม์ไลน์ตัวจัดคิวงาน RTOS
RunningReadyBlockedcontext switch
CPU
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED1
LED2
serialTask
0
2
4
6
8
10
12
14
16
18
tick 0

แต่ละงานสลับกันถือ CPU ทีละ slice. จุดสลับ (เส้นตั้ง) คือ context switch — เซฟสถานะงานเก่า โหลดงานใหม่ ซึ่งมีต้นทุนเวลา.

CPU เดียวรันทีละงาน. เลือกโหมดเพื่อดูสถานะงาน, การสลับงานที่ 'ดูเหมือนพร้อมกัน', ผลของการหน่วงแบบยึด CPU vs คืน CPU, และ race condition กับการแก้ด้วย mutex. เลื่อน playhead เพื่อไล่เวลา.

§12.3 Task, Scheduler และ Context Switch

แนวคิดหลักของ RTOS คือ task (บางที่เรียก thread หรือ process). เราเขียนแต่ละงานเป็น task แยกกัน ให้แต่ละ task มี loop ไม่รู้จบของตัวเอง แล้วให้ scheduler ของ OS เป็นคนตัดสินว่าตอนนี้ task ไหนได้ใช้ CPU.

เนื่องจาก CPU คอร์เดียวรันได้ทีละ task scheduler จึงสลับไป-มาเร็ว ๆ (คนละ task ทำทีละนิด) ทำให้ ดูเหมือน ทำพร้อมกัน — เรียกว่า concurrency ไม่ใช่ parallel จริง. การสลับจาก task หนึ่งไปอีก task เรียก context switch ซึ่งต้องเซฟสถานะ (register, stack) ของ task เดิมแล้วโหลดของ task ใหม่ จึงมีต้นทุนเวลาอยู่บ้าง.

แต่ละ task มี state เช่น Running (กำลังใช้ CPU), Ready (พร้อมรันรอคิว), Blocked/Suspended (รอเหตุการณ์หรือถูกหน่วงเวลา). แต่ละ task ยังมี priority; task ที่ priority สูงกว่าจะได้ใช้ CPU ก่อน. ถ้าทุก task priority เท่ากัน scheduler จะแบ่งเวลาให้เท่า ๆ กันแล้วสลับวนไป.

RTOS ต้องมี timer ของตัวเองเสมอ เพราะถ้าไม่มี timer จับเวลา ก็ทำ context switch ตามจังหวะไม่ได้. ใน CubeMX ค่า default จะเลือก timer ให้ (เช่น TIM8 ซึ่งเป็น basic timer) แยกจาก SysTick ปกติ.

ในแล็บใช้ FreeRTOS ซึ่งรวมมากับ CubeMX. ARM ยังกำหนด API กลางชื่อ CMSIS-RTOS (เทียบได้กับ CMSIS ฝั่งเพอริเฟอรัลที่ให้ชื่อ register/macro กลางไม่ต้องรู้ address จริง) โดย RTOS แต่ละเจ้าทำ wrapper ให้เรียกผ่าน API กลางนี้ได้. เราจะใช้ API ตรงของ FreeRTOS เป็นหลัก (เช่น vTaskDelay) และ CMSIS บ้าง (เช่น osDelay).

VIZ-62ไทม์ไลน์ตัวจัดคิวงาน RTOS
RunningReadyBlockedcontext switch
CPU
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED1
LED2
serialTask
0
2
4
6
8
10
12
14
16
18
tick 0

แต่ละงานสลับกันถือ CPU ทีละ slice. จุดสลับ (เส้นตั้ง) คือ context switch — เซฟสถานะงานเก่า โหลดงานใหม่ ซึ่งมีต้นทุนเวลา.

CPU เดียวรันทีละงาน. เลือกโหมดเพื่อดูสถานะงาน, การสลับงานที่ 'ดูเหมือนพร้อมกัน', ผลของการหน่วงแบบยึด CPU vs คืน CPU, และ race condition กับการแก้ด้วย mutex. เลื่อน playhead เพื่อไล่เวลา.

§12.4 สร้าง Task แรกให้ LED กะพริบ

ใน CubeMX เปิดใช้ FreeRTOS แล้ว gen code จะได้โครงมาให้: ส่วน init ฮาร์ดแวร์, init FreeRTOS, สร้าง task เริ่มต้นชื่อ defaultTask, แล้วเรียก start scheduler. โค้ดจะวนอยู่ใน scheduler ตลอด ไม่กลับมาที่ while(1) ใน main อีก ถ้าหลุดออกมาแปลว่ามีปัญหา.

คำสั่งสร้าง task คือ xTaskCreate(). พารามิเตอร์หลักได้แก่: (1) ชื่อฟังก์ชันที่เป็น body ของ task, (2) ชื่อบรรยาย (description) เป็นสตริง, (3) ขนาด stack ของ task, (4) พารามิเตอร์ที่ส่งเข้า task, (5) priority, และ (6) pointer ชนิด TaskHandle_t ที่ชี้ไปยัง task เพื่อเอาไว้ควบคุมภายหลัง (เช่นลบหรือเปลี่ยน priority).

ตัวอย่างแรกสร้าง 3 task ให้กะพริบ LED ทั้งสามดวงอิสระกัน: LD1 ที่ PB0, LD2 ที่ PB7, LD3 ที่ PB14. ภายในแต่ละ task คือ loop ไม่รู้จบที่ toggle ขา LED แล้วหน่วงเวลา. ตั้งคาบต่างกัน (200, 500, 800 ms) เพื่อให้เห็นชัดว่ากะพริบไม่พร้อมกัน.

จุดสำคัญคือการหน่วงเวลา: อย่าใช้ HAL_Delay() เพราะมันเป็น busy-wait ที่ บล็อก CPU ทั้งก้อน ช่วงนั้นไม่มีงานอื่นเกิดขึ้นเลย. ให้ใช้ vTaskDelay() (หรือ osDelay ของ CMSIS) ซึ่งจะ suspend task นี้ชั่วคราวคืน CPU ให้ task อื่นทำงานในช่วงที่รอ. ข้อควรจำ: vTaskDelay นับเป็น tick (จำนวน tick × เวลาต่อ tick) ส่วน osDelay รับหน่วยเป็น ms มาให้แล้ว.

เคล็ดลับ: 3 task กะพริบ LED ยุบเหลือฟังก์ชันเดียวได้ โดยอาศัยพารามิเตอร์ที่ส่งเข้า task (ส่ง 1/2/3) ให้ฟังก์ชันเดียวกันรู้ว่าตัวเองคุม LED ดวงไหน แทนการเขียนสามฟังก์ชันซ้ำ.

FreeRTOS: สร้าง task กะพริบ LED (ย่อ)c
TaskHandle_t h1, h2, h3;

void LEDTask1(void *arg) {
  for (;;) {                         // loop ไม่รู้จบของ task
    HAL_GPIO_TogglePin(LD1_GPIO_Port, LD1_Pin);
    vTaskDelay(pdMS_TO_TICKS(200));  // คืน CPU ให้ task อื่นระหว่างรอ
  }
}
// LEDTask2 → LD2 (PB7) คาบ 500ms, LEDTask3 → LD3 (PB14) คาบ 800ms

int main(void) {
  // ... init ฮาร์ดแวร์ ...
  xTaskCreate(LEDTask1, "LED1", 128, NULL, 1, &h1);
  xTaskCreate(LEDTask2, "LED2", 128, NULL, 1, &h2);
  xTaskCreate(LEDTask3, "LED3", 128, NULL, 1, &h3);
  vTaskStartScheduler();             // ไม่ return — scheduler วนภายใน
  while (1) { }                      // ถึงบรรทัดนี้ = มีปัญหา
}
VIZ-62ไทม์ไลน์ตัวจัดคิวงาน RTOS
RunningReadyBlockedcontext switch
CPU
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED1
LED2
serialTask
0
2
4
6
8
10
12
14
16
18
tick 0

แต่ละงานสลับกันถือ CPU ทีละ slice. จุดสลับ (เส้นตั้ง) คือ context switch — เซฟสถานะงานเก่า โหลดงานใหม่ ซึ่งมีต้นทุนเวลา.

CPU เดียวรันทีละงาน. เลือกโหมดเพื่อดูสถานะงาน, การสลับงานที่ 'ดูเหมือนพร้อมกัน', ผลของการหน่วงแบบยึด CPU vs คืน CPU, และ race condition กับการแก้ด้วย mutex. เลื่อน playhead เพื่อไล่เวลา.

§12.5 Race Condition และ Critical Section

ตัวอย่างที่สองเพิ่ม task พิมพ์ข้อความออก UART. สร้าง serialTask ส่งพารามิเตอร์เข้าไป task แรกให้พิมพ์เลข '1' ติดกัน 100 ตัวแล้วขึ้นบรรทัดใหม่, task สองพิมพ์เลข '2' แบบเดียวกัน. ทั้งคู่ priority เท่ากันและใช้ UART ตัวเดียวกัน.

ผลที่ได้คือเลข 1 กับ 2 ปนกันมั่ว ไม่เป็นชุดละ 100 ตัวตามที่ตั้งใจ. สาเหตุคือ scheduler สลับ task กลางคันขณะที่ยังพิมพ์ไม่ครบชุด เพราะทั้งสองแชร์ resource เดียวกัน (UART) โดยไม่มีการคุมการเข้าถึง. ปัญหานี้เรียก race condition — ผลลัพธ์ขึ้นกับจังหวะที่ task แย่งกันเข้าใช้ resource.

ส่วนของโค้ดที่เข้าถึง resource ร่วมและต้องทำให้เสร็จโดยไม่ถูกแทรกเรียก critical section. หลักการแก้คือต้อง mutual exclusion (mutex) — บังคับให้เข้า critical section ได้ทีละ 1 task เท่านั้น task อื่นต้องรอ.

วิธีคืน CPU แบบจงใจเมื่อพิมพ์ครบชุดคือ taskYIELD() (สละ CPU ให้ task อื่นทันทีโดยไม่รอหมดเวลา). แต่ yield อย่างเดียวไม่พอถ้ามีการแชร์ resource — ยังต้องมีกลไกล็อกครอบ critical section อยู่ดี.

race condition: 2 task พิมพ์ลง UART เดียวกันc
// ตั้งใจให้พิมพ์ '1'x100 เป็นชุด แล้ว '2'x100 เป็นชุด
void serialTask(void *arg) {
  char ch = *(char *)arg;            // รับ '1' หรือ '2' ผ่านพารามิเตอร์
  for (;;) {
    for (int i = 0; i < 100; i++)
      HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 10);
    HAL_UART_Transmit(&huart3, (uint8_t *)"\r\n", 2, 10);
    // ไม่มีการล็อก → scheduler สลับกลางชุด → 1 กับ 2 ปนกัน
  }
}
VIZ-62ไทม์ไลน์ตัวจัดคิวงาน RTOS
RunningReadyBlockedcontext switch
CPU
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED1
LED2
serialTask
0
2
4
6
8
10
12
14
16
18
tick 0

แต่ละงานสลับกันถือ CPU ทีละ slice. จุดสลับ (เส้นตั้ง) คือ context switch — เซฟสถานะงานเก่า โหลดงานใหม่ ซึ่งมีต้นทุนเวลา.

CPU เดียวรันทีละงาน. เลือกโหมดเพื่อดูสถานะงาน, การสลับงานที่ 'ดูเหมือนพร้อมกัน', ผลของการหน่วงแบบยึด CPU vs คืน CPU, และ race condition กับการแก้ด้วย mutex. เลื่อน playhead เพื่อไล่เวลา.

§12.6 Mutex, Semaphore และ Queue

mutex (mutual exclusion) คือ binary semaphore (มีค่าแค่ 0 กับ 1) ใช้ล็อกก่อนเข้า critical section และปลดล็อกเมื่อออก. ใน FreeRTOS คือ take ก่อนเข้า, give เมื่อออก. ครอบคำสั่งพิมพ์ UART ด้วย mutex ก็จะได้ชุด '1' และ '2' ครบไม่ปนกัน เพราะเข้าได้ทีละ task.

semaphore ทั่วไป (counting) เก็บค่าตามจำนวน slot ของ resource ไม่ได้จำกัดแค่ 0/1. มันแก้ปัญหาคลาสสิกของ inter-process communication (IPC) ได้ เช่นปัญหา producer-consumer.

producer-consumer: producer ผลิตของใส่ buffer, consumer ดึงออก. ถ้าทำด้วยตัวแปรนับ (count) กับ sleep/wake เฉย ๆ จะเจอบั๊ก lost wake-up: producer สั่ง wake-up ให้ consumer ก่อนที่ consumer จะเข้า sleep จริง (เพราะคำสั่งเช็กเงื่อนไขกับ sleep ไม่ได้อยู่ใน critical section เดียวกัน) สัญญาณ wake-up จึงหาย consumer หลับค้าง ระบบ deadlock.

แก้ด้วย semaphore ของ Dijkstra ที่มีฟังก์ชัน P (wait: ลดค่า ถ้าน้อยกว่า 0 ให้ sleep) กับ V (signal: เพิ่มค่า ถ้ามีคนรออยู่ให้ปลุกขึ้น). producer-consumer ใช้ 3 ตัว: full (นับช่องที่มีของ), empty (นับช่องว่าง), และ mutex (binary ล็อก critical section). เพราะ P/V เป็น atomic การ sleep จึงไม่หลุด สัญญาณ wake-up ไม่หาย.

สำหรับคนที่ยังไม่ชินกับ semaphore หลายตัว FreeRTOS มี queue ที่ใช้ง่ายกว่า — ส่งข้อมูลระหว่าง task ผ่านคิวได้เลยโดยไม่ต้องจัดการ mutex/semaphore เอง เพราะ queue จัดการ synchronization ให้ภายใน.

สรุปแนวทาง: ถ้ามีการแชร์ resource ควรมีกลไกคุมเสมอ — ใช้ mutex/lock ครอบ critical section, ใช้ semaphore สำหรับนับทรัพยากร/แก้ producer-consumer, หรือใช้ queue เพื่อความง่าย. ถ้าปล่อยให้แชร์โดยไม่คุมจะเกิดผลลัพธ์ที่ไม่พึงประสงค์ (race condition).

กลไกค่า/ลักษณะใช้เมื่อ
Mutexbinary (0/1)ล็อก critical section ให้เข้าทีละ task
Counting semaphoreนับจำนวน slotนับทรัพยากรหลายช่อง / producer-consumer
Queueคิวข้อมูลส่งข้อมูลระหว่าง task แบบง่าย คุม sync ให้เอง
กลไกจัดการทรัพยากรร่วมใน FreeRTOS
ใช้ mutex ครอบ critical section (พิมพ์ UART)c
SemaphoreHandle_t uartMutex;
uartMutex = xSemaphoreCreateMutex();

void serialTask(void *arg) {
  char ch = *(char *)arg;
  for (;;) {
    xSemaphoreTake(uartMutex, portMAX_DELAY);   // ล็อกเข้า critical section
    for (int i = 0; i < 100; i++)
      HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 10);
    HAL_UART_Transmit(&huart3, (uint8_t *)"\r\n", 2, 10);
    xSemaphoreGive(uartMutex);                   // ปลดล็อก → ได้ชุดครบไม่ปนกัน
    taskYIELD();
  }
}
VIZ-62ไทม์ไลน์ตัวจัดคิวงาน RTOS
RunningReadyBlockedcontext switch
CPU
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED2
serialTask
LED1
LED1
LED2
serialTask
0
2
4
6
8
10
12
14
16
18
tick 0

แต่ละงานสลับกันถือ CPU ทีละ slice. จุดสลับ (เส้นตั้ง) คือ context switch — เซฟสถานะงานเก่า โหลดงานใหม่ ซึ่งมีต้นทุนเวลา.

CPU เดียวรันทีละงาน. เลือกโหมดเพื่อดูสถานะงาน, การสลับงานที่ 'ดูเหมือนพร้อมกัน', ผลของการหน่วงแบบยึด CPU vs คืน CPU, และ race condition กับการแก้ด้วย mutex. เลื่อน playhead เพื่อไล่เวลา.

แบบฝึกหัด

12 ข้อ — แต่ละข้อมีชนิดและระดับความยากกำกับ กดปุ่ม "ดูเฉลย" เพื่อดูคำตอบและคำอธิบาย

  1. 1.ปรนัยง่าย

    เหตุผลหลักที่ระบบฝังตัวซับซ้อนต้องการ RTOS คือข้อใด

  2. 2.ปรนัยง่าย

    คุณสมบัติสำคัญที่สุดที่นิยามความเป็น real-time คือข้อใด

  3. 3.ปรนัยง่าย

    ระบบใดจัดเป็น hard real-time

  4. 4.ถูก/ผิดง่าย

    บน CPU คอร์เดียว RTOS รันหลาย task พร้อมกันจริง ๆ แบบ parallel

  5. 5.ปรนัยกลาง

    context switch คืออะไร

  6. 6.ปรนัยกลาง

    ทำไมใน task ของ FreeRTOS จึงควรใช้ vTaskDelay แทน HAL_Delay

  7. 7.ปรนัยกลาง

    ถ้าทุก task มี priority เท่ากัน scheduler จะจัดการอย่างไร

  8. 8.เติมคำกลาง

    ปัญหาที่ผลลัพธ์ขึ้นกับจังหวะที่หลาย task แย่งกันเข้าใช้ resource ร่วมโดยไม่มีการคุม เรียกว่าอะไร และส่วนของโค้ดที่ต้องป้องกันเรียกว่าอะไร

  9. 9.ปรนัยยาก

    mutex ต่างจาก counting semaphore อย่างไร

  10. 10.ปรนัยยาก

    บั๊ก lost wake-up ในปัญหา producer-consumer เกิดจากอะไร

  11. 11.วิเคราะห์ยาก

    ในปัญหา producer-consumer ด้วย semaphore ใช้ full, empty, และ mutex อย่างละตัว จงอธิบายหน้าที่ของแต่ละตัว

  12. 12.วิเคราะห์กลาง

    สองตัวอย่าง task พิมพ์ '1' และ '2' ลง UART ตัวเดียวกันโดย priority เท่ากัน แล้วได้ผลลัพธ์เลขปนกัน จงอธิบายสาเหตุและวิธีแก้