= ESP 32 RTOS = Ref [https://attaphon.medium.com/%E0%B8%A5%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%82%E0%B8%B5%E0%B8%A2%E0%B8%99%E0%B9%82%E0%B8%9B%E0%B8%A3%E0%B9%81%E0%B8%81%E0%B8%A3%E0%B8%A1%E0%B9%81%E0%B8%9A%E0%B8%9A-multi-tasking-%E0%B8%9A%E0%B8%99-esp32-%E0%B8%81%E0%B8%B1%E0%B8%99-rtos-fd098f798f8f here] ช่วงนี้กลับมาเล่นพวกบอร์ด ESP32 อีกครั้ง หลังจากโดนทวงงานที่ดองค้างไว้นานน(น.หนู 32 ตัว) ซึ่งในงานนี้มีส่วนที่ต้องศึกษาการเขียนโปรแกรมแบบ Multi-tasking บน ESP32 จึงเป็นโอกาสที่จะเขียนเก็บไว้เป็น log ดูเล่นเผื่อวันหลังเรากลับมาดูโปรแกรมที่ตัวเองเขียนไว้จะได้ไม่งง เนื่องจากสเปกของ บอร์ด ESP32 มันมี 2 CPU ครับ (CPU0 และ CPU1) ด้วยเหตุนี้เราจึงอยากจะลองเขียนโปรแกรมเพื่อแบ่งงานให้ CPU 2 ตัวนี้ไปช่วยกันทำงานโดยใช้ RTOS ครับ Note : บทความนี้อธิบายบนพื้นฐานการใช้งานบอร์ด ESP32 และ Arduino IDE ครับ ใครที่ยังไม่เคยเล่น Arduino อาจจะต้องไปศึกษาพื้นฐานเพิ่มเติมครับ ESP32 รองรับการการเขียนโปรแกรมเพื่อใช้ FreeRTOS อยู่แล้วครับ เราจะมาเรียนรู้ตัวอย่างการใช้งานแบบง่ายๆกัน แต่ก่อนอื่นมาเข้าใจกันก่อนว่า … RTOS คืออะไร ? เจ้า RTOS (Real-Time Operating System ) มันเป็น Kernel ที่ช่วยให้บอร์ด ESP32 สามารถเขียนโปรแกรมเพื่อแบ่งเวลาแบ่งทรัพยากรในการประมวลผลงานต่างๆ ในแต่ละ CPU ได้ การเขียนโปรแกรมโดยใช้ RTOS เราจะต้องสร้างหน่วยทำงานที่เรียกว่า Task ซึ่งแต่ละ Task สามารถแยกกันทำงานแบบอิสระต่อกันได้ RTOS มีวิธีจัดการ Task ต่างๆ โดยการดูระดับความสำคัญ ( Priority ) ซึ่ง Task ที่มีระดับความสำคัญสูงสุดจะได้ทำงานก่อน และใช้ Time slicing ในการแบ่งเวลากันทำงาน ซึ่งทำให้ ESP32 สามารถทำงานคล้ายแบบ Multi-tasking ได้ Time slicing RTOS มีฟังก์ชั่นคล้ายๆ Delay() แต่เป็นรูปแบบ Software Timer ชื่อว่า `vDelayTask()` ด้วยฟังก์ชันนี้ทำให้เราสามารถสั่งให้ Task หยุดพักการทำงาน โดยไม่ทำให้ CPU หยุดพักการทำงานไปด้วย ( Delay() เป็น Hardware Timer ) ทำให้แต่ละ Task แบ่งเวลากันทำงานเป็นรูปแบบ Time Slicing คือ ถ้ายังมี Task ที่รอทำงานต่อก็ จับมา Run ได้เลยครับ ขอยกตัวอย่างครับ เช่นว่า เราต้องการเขียนโปรแกรมสั่ง ESP32 ให้ทำงานอยู่ 2 งาน คือ [ 1 – สั่งให้ไปเก็บค่าจาก Sensor ] และ [ 2 - สั่งให้ส่ง SMS รายงานผล] ตามรูปนี้… ทั่วไป เราจะเขียนโปรแกรมแบบข้อ 1 เริ่มต้นโดยการสั่งให้ทำฟังก์ชั่นอ่านค่าจาก Analog sensor ก่อน แล้วใส่ Delay() เพื่อรออ่านค่าจาก sensor หลังจากนั้นจึงทำฟังก์ชันส่ง SMS ต่อ ซึ่งก็ใส่ Delay() อีกครั้งเพื่อรอ GSM module ทำงานให้เสร็จ ในช่วงที่ฟังก์ชัน Delay() ทำงาน ช่วงนั้น CPU จะหยุดการประมวลผล ซึ่งหาก Delay มีค่าที่ตั้งไว้นาน จะเกิดช่วงเวลาที่สูญเปล่าเพราะไม่ได้ทำงานฟังก์ชันอื่นต่อเนื่องเลย แต่ถ้าเราเปลี่ยนไปเขียนโปรแกรมแบบข้อ 2 ( เขียนเป็น Task โดยใช้ RTOS ) และใช้ `vTaskDelay()` แทน เมื่อ Taskอ่านค่าSensor ทำงานเสร็จ Taskส่งSMS ก็จะทำงานต่อทันที และระหว่างช่วงที่ Taskส่งSMS ถูก Blocked รอทำงานให้เสร็จ Taskอ่านค่าsensor ก็สามารถทำงานวนรอไปเรื่อยๆได้ เนื่องจาก `vTaskDelay()` จะหยุดการทำงานของ Task ที่ระดับ Software จึงทำให้ CPU ประมวลผล Task อื่นต่อเนื่องได้ == Task == การสร้าง Task ใน RTOS เราจะเขียน function ที่เราต้องการให้ทำงานไว้ก่อน แล้วก็สร้าง Task เพื่อไปเรียก function มาใช้งานอีกที พวก function ที่เราเขียนควรจะมี infinite loop ภายในด้วยนะครับ Task จะได้ทำงานวนไปเรื่อยๆ แต่ถ้าเราเขียนโปรแกรมให้ฟังก์ชั่นทำงานครั้งเดียวจบ เราต้องลบ Task ที่ไม่ใช้งานด้วย Task ถ้ามองง่ายๆ มันก็คือ กล่องๆหนึ่งที่เราต้องเอา Function ไปผูกไว้ และมีการกำหนด Parameter ต่างๆเพิ่มเติมเข้ามา เช่น Priority เพื่อไว้ตัดสินว่ากล่อง Task ใหนจะได้ทำงานก่อน ส่วนประกอบเพื่อสร้าง Task เมื่อเราจะสร้าง Task ให้เราประกาศฟังก์ชันตามรูปแบบนี้ {{{ xTaskCreate(TaskFunction,TaskName,StackDepth,(void*)PassParameters,TaskPriority,TaskHandle) }}} มาดูตัวแปรที่ใช้ตอนสร้าง Task ทีละตัวกัน {{{ 1. TaskFunction : ชื่อฟังก์ชันที่จะให้ทำงานเป็น Task ซึ่งในฟังก์ชันนั้นควรจะเขียนโปรแกรมแบบทำงานวนลูป( infinite loop ) 2. TaskName : ชื่อของ Task ( ข้อมูลตรงส่วนนี้จะใช้ตอน Debug ไม่มีผลต่อการทำงานของโปรแกรมโดยตรง ) 3. StackDepth : ขนาด Stack ของ Task (เพื่อจอง memory) การกำหนดขนาดของ Task ถ้ากำหนดไว้น้อยเกินไปจะทำให้ ESP32 Restart ตัวเองตลอดเวลา แต่ถ้ากำหนดไว้มากไปก็ทำให้เสีย memory ทิ้งเปล่าๆ การกำหนดค่านี้แบบคร่าวๆ คือ ลอง complie ดูขนาดของ function ที่ผูกกับ Task นี้ ลองดูจำนวน Byte ที่ใช้ไปครับ แล้วเอามาใส่ในตัวแปรนี้ 4. PassParameter : ชื่อตัวแปรที่จะส่งค่าเข้ามาทำงานต่อใน Task ( ดูวิธีการทำงานใน code ตัวอย่างเพิ่มเติมครับ ) 5. TaskPriority : กำหนดเลข Priority ให้ Task ซึ่งค่า 0 คือ Priority ที่ต่ำที่สุด 6. TaskHandle : ชื่อตัวแปรของ Task ที่จะนำไปใช้ในการ Handle ทำงานอื่นๆต่อ ( ดูวิธีการทำงานใน code ตัวอย่างเพิ่มเติมครับ ) }}} แต่ถ้าอยากกำหนดให้ลึกว่า CPU ใหนจะทำงาน Task นั้นๆ ให้ประกาศฟังก์ชันตามรูปแบบนี้ xTaskCreatePinnedToCore(TaskFunction,TaskName,StackDepth,(void*)PassParameters,TaskPriority,TaskHandle,Core) {{{ 7. Core : สำหรับ ESP เราสามารถกำหนดให้ Task ทำงานที่ CPU 0 หรือ 1 }}} ลองมาดู code ตัวอย่างการสร้าง Task เพื่อความเข้าใจกันครับ โดยตัวอย่างนี้ เราจะเขียนโปรแกรมสร้าง Task ในบอร์ด ESP32 เพื่อปริ้นดูการทำงานของแต่ละ Task ผ่าน Serial port อธิบาย code ทีละส่วนกัน {{{ const TickType_t xDelay2000ms = pdMS_TO_TICKS(2000); }}} ฟังก์ชัน `vTaskDelay()` จะรับค่า Tick ซึ่งเป็นค่า Timer ของ CPU ดังนั้นเราจึงต้องแปลงค่า เวลา(หน่วยมิลลิวินาที)ให้เป็นค่า Tick ก่อน จึงนำมาใช้งานได้ เริ่มต้น เราจึงสร้างตัวแปรมาเก็บค่า Tick เพื่อใช้ในฟังก์ชั้น `vTaskDelay(…)` โดยเรียกใช้ฟังก์ชัน `pdMS_TO_TICKS(…)` ที่จะแปลงตัวเลขหน่วยมิลลิวินาทีเป็นจำนวน Tick (ในตัวอย่างนี้คือ `xDelay2000ms` เก็บค่า Tick ของ 2 วินาทีไว้ ) {{{ TaskHandle_t Task1 = NULL; TaskHandle_t Task2 = NULL; TaskHandle_t Task3 = NULL; }}} สร้างตัวแปรมาเก็บค่า TaskHandle ของแต่ละ Task ไว้ก่อน เดี๋ยวเอาไปใช้ตอนประกาศ Created Task {{{ int passValue = 0; }}} สร้างตัวแปรเก็บค่าตัวเลขไว้ เดี๋ยวจะเอาไปทดลองใช้ส่งค่าเข้าไปใน Task1 เพื่อนับจำนวนรอบที่ทำงาน {{{ void setup() { Serial.begin(115200); delay(1000); // Created 3 Task here xTaskCreatePinnedToCore(func1_Task,"func1_Task",1000,(void*) passValue,1,&Task1,0); xTaskCreatePinnedToCore(func2_Task,"func2_Task",1000,NULL,1,&Task2,0); xTaskCreatePinnedToCore(func3_Task,"func3_Task",1000,NULL,1,&Task3,0); } }}} มาถึงส่วนของฟังก์ชั่น setup(…) เราจะประกาศ Created Task กันในฟังก์ชันนี้ โดยใช้คำสั่ง `xTaskCreatePinnedToCore(…)` {{{ Task ที่ 1 : ผูกกับฟังก์ชั่น func1_Task() , ตั้งชื่อ Task ว่า “func1_Task”,DepthStack = 1000 ,ส่งตัวแปร PassValue เข้าไปใน Task นี้ด้วย , มีค่า Priority = 1 , ผูกกับ TaskHandle ชื่อว่า Task1 และให้ Task นี้ทำงานอยู่บน Core 0 Task ที่ 2 และ 3 : ผูกกับ func2_Task() และ func3_Task() ตามลำดับ โดยไม่มีการส่งตัวแปรเข้าไปใน Task , มี Priority = 1 และให้ Task ทำงานอยู่บน Core 0 เหมือนกัน }}} {{{ void loop() { } }}} ไม่จำเป็นต้องเขียนโปรแกรมใน Loop() {{{ void func1_Task(void *pvvalue){ int f1param = (int)pvvalue ; while(1){ Serial.println(String("hello from Task1 : count >> ") + f1param ); f1param++; vTaskDelay(xDelay2000ms); } } }}} ในส่วนของ `func1_Task()` ที่ผูกกับ Task1 ไว้ สร้างตัวแปร int ชื่อ f1param ไว้เก็บค่า `pvvalue` ที่ส่งเข้ามา(แปลง type ให้เป็น int ไว้แล้ว ) ปริ้นค่า “Hello from Task1” พร้อมกับจำนวนรอบที่ทำงาน ( ตัวแปร f1param ) ผ่าน Serial port อัพเดทค่า f1param +1 เข้าไป ซึ่งตัวแปร `passValue `ก็จะถูกอัพเดทค่าใหม่นี้ด้วย เพราะเป็น pointer ของ f1param ทำการ Delay Task1 ไว้ 2 วินาที แล้วจะวนมาทำงานใหม่ {{{ void func2_Task(void *pvParam){ while(1){ Serial.println(String("hello from Task2")); vTaskDelay(xDelay2000ms); } }void func3_Task(void *pvParam){ while(1){ Serial.println(String("hello from Task3")); vTaskDelay(xDelay2000ms); } } }}} ส่วนใน func2_Task() และ func3_Task() ก็จะสร้าง infinite loop ไว้ปริ้นค่า Hello from task ไว้เช่นกัน และ มี Delay ไว้ 2 วินาทีค่อยทำงานใหม่อีกครั้ง การทำงานของ code นี้ {{{ Task ถูกสร้างไล่เรียงลำดับเป็น Task1 , Task2 และ Task3 ภายใน Setup() เนื่องจากทุกTask มีค่า Priority เท่ากัน Task 1 ที่สร้างขึ้นมาก่อนจึงได้ทำงานก่อน และ Task2 และ Task3 จะทำงานเรียงต่อกันไป Task1 เริ่มทำงาน โดยการปริ้น “Hello from Task1” พร้อมกับแสดงจำนวนรอบที่ทำงาน โดยนำตัวแปรชื่อ passValue ที่ส่งผ่านเข้ามาใน Task มาแสดงค่า หลังจากนั้น เพิ่มค่า +1 ให้กับตัวแปรดังกล่าว แล้วทำการ Blocked Task1 ให้หยุดทำงานชั่วคราว 2 วินาที ด้วยคำสั่ง vTaskDelay() หลังจาก Task1 ถูก Blocked แล้ว Task2 จะเริ่มทำงาน โดยการปริ้น “Hello from Task2” แล้วทำการ blocked Task2 ให้หยุดทำงานชั่วคราว 2 วินาที หลังจาก Task2 ถูก Blocked แล้ว Task3 จะเริ่มทำงาน โดยการปริ้น “Hello from Task3” แล้วทำการ blocked Task3 ให้หยุดทำงานชั่วคราว 2 วินาที เมื่อ ครบ 2 วินาที Task1,Task2 และ Task3 จะกลับมาทำงานปกติ ทำงานวนไปเรื่อยๆ }}} STATE ของ TASK เพื่อความเข้าใจในการจัดการ Task เพิ่มขึ้น ควรจะมาดูเรื่อง State การทำงานของ Task กันต่อ ซึ่งมี 4 State ดังนี้ {{{ 1) Running state : Task ที่มี Priority สูงสุด จะได้ทำงานใน State นี้ และเป็น Task เดียวที่ได้ทำงานในเวลานั้นๆ 2) Ready state : Task ที่รอเข้าไปทำงานใน Running state จะกองกันอยู่ที่นี่ ( เป็นพวก Task ที่มี Priority น้อยว่า Task ใน Running state) 3) Blocked state : Task ที่อยู่ใน state นี้คือ Task ที่ถูก Blocked การทำงานชั่วคราว เช่น Task ที่ใช้คำสั่ง `vTaskDelay()` 4) Suspended state : Task ที่อยู่ใน state นี้คือ Task ที่โดนสั่งให้พักการทำงาน โดยใช้คำสั่ง `vTaskSuspend()` และหาต้องการให้ Task นั้นๆ กลับมาทำงานตามปกติ เราต้องส่งคำสั่ง vTaskResume() เพื่อปลดล็อค }}} RTOS API บางส่วนที่เราจะมาดูกันเพิ่ม เพื่อลองเขียนโปรแกรมเปลี่ยน State ของ Task มีดังนี้ {{{ xTaskCreated(…) : สร้าง Task ใหม่ vTaskDelete(…) : ลบ Task ที่เคยสร้างไว้ ( เลิกใช้งาน Task ) vTaskDelay(…) : สั่งให้ Task หยุดพการทำงานตามเวลาที่กำหนด vTaskSuspend(…) : สั่งให้ Task เข้าสู่ Suspend state ( หยุดทำงาน ) vTaskResume(…) : สั่งให้ Task เข้าสู่ Ready state ( ออกจาก Suspend state ) vTaskPriorityGet(…) : อ่านค่า Priority ของ Task vTaskPrioritySet(…) : เปลี่ยนค่า Priority ของ Task }}} มาลองดู code ตัวอย่างกันครับ คร่าวๆการทำงานของ code ตัวอย่าง เราจะสร้าง Task มาลองทำงานแบบสลับ State เมื่อทำงานจบ ก็จะลบ Task ทิ้งไป {{{ TaskHandle_t Task1 = NULL; TaskHandle_t Task2 = NULL; TaskHandle_t Task3 = NULL; TaskHandle_t Task4 = NULL; }}} เริ่มต้นด้วยการสร้างตัวแปรมาเก็บค่า TaskHandle ของแต่ละ Task {{{ void setup() { Serial.begin(115200); delay(3000); // Created 4 Task here xTaskCreatePinnedToCore(f4_Task,"func4_Task",1000,NULL,4,&Task3,0); xTaskCreatePinnedToCore(f3_Task,"func3_Task",1000,NULL,3,&Task3,0); xTaskCreatePinnedToCore(f2_Task,"func2_Task",1000,NULL,2,&Task2,0); xTaskCreatePinnedToCore(f1_Task,"func1_Task",1000,NULL,1,&Task1,0); } }}} ใน Setup() เราประกาศสร้าง Task จำนวน 4 Task โดยแต่ละ Task มี Priority ต่างกัน {{{ void f4_Task(void *pvParam){ Serial.println(String("Hello from TASK4 , Priority is : ") + uxTaskPriorityGet(Task4)); int newPriority = uxTaskPriorityGet(Task4) - 4; Serial.println(String("NOW WE CHANGE PRIORITY OF TASK4 TO BE : ") + newPriority ); vTaskPrioritySet(Task4,newPriority); Serial.println(String("Hello again from TASK4 , then delete TASK4 !")); vTaskDelete(NULL); } }}} ฟังก์ชั่น f4_Task() ที่ผูกกับ Task4 ปริ้น “Hello from Task4” ผ่าน Serial Port พร้อมทั้งปริ้นค่า Priority ของ Task ด้วยฟังก์ชั้น `uxTaskPriorityGet(…)` เปลี่ยน Priority ของ Task4 จาก 4 ให้เป็น 0 ( ต่ำสุด ) ด้วยฟังก์ชัน `vTaskPrioritySet(…)` ลบ Task4 ทิ้ง โดยคำสั่ง `vTaskDelete(…)` {{{ void f3_Task(void *pvParam){ Serial.println(String("Hello from TASK3 , then Suspend TASK2 & TASK3 !")); vTaskSuspend(Task2); vTaskSuspend(NULL);Serial.println(String("Hello again from TASK3 , then delete TASK3 !")); vTaskDelete(NULL); } }}} ฟังก์ชั่น f3_Task() ที่ผูกกับ Task3 ปริ้น “Hello from Task3” ผ่าน Serial Port Suspend Task2 และ Task3 ด้วยคำสั่ง `vTaskSuspend(…)` ลบ Task3 ทิ้ง โดยคำสั่ง `vTaskDelete(…)` {{{ void f2_Task(void *pvParam){ Serial.println(String("Hello from TASK2 , then Resume TASK3 !")); vTaskResume(Task3);Serial.println(String("Hello again from TASK2 , then delete TASK2 !")); vTaskDelete(Task2); } }}} ฟังก์ชั่น f2_Task() ที่ผูกกับ Task2 ปริ้น “Hello from Task2” ผ่าน Serial Port Resume Task3 ด้วยคำสั่ง `vTaskResume(…)` ลบ Task2 ทิ้ง โดยคำสั่ง `vTaskDelete(…)` {{{ void f1_Task(void *pvParam){ Serial.println(String("Hello from TASK1 , then Resume TASK2 !")); vTaskResume(Task2);Serial.println(String("Hello again from TASK1 , then delete TASK1 !")); vTaskDelete(NULL); } }}} ฟังก์ชั่น f1_Task() ที่ผูกกับ Task1 ปริ้น “Hello from Task1” ผ่าน Serial Port Resume Task2 ด้วยคำสั่ง `vTaskResume(…)` ลบ Task1 ทิ้ง โดยคำสั่ง `vTaskDelete(…)` การทำงานของตัวอย่าง code นี้ {{{ เริ่มต้น Task4 จะเริ่มทำงานก่อน โดยการปริ้น “Hello from Task4” พร้อมทั้งค่า Priority ของ Task4 เอง Task4 ทำการเปลี่ยนค่า Priority ของตัวเอง จาก 4 เป็น 0 ( ค่าต่ำสุด ) Task3 กลายเป็น Task ที่มี Priority สูงสุด จึงเลื่อนมาทำงานใน Running state. ส่วน Task4 ก็ถูกโยนกลับไปใน Ready state Task3เริ่มทำงานโดยการปริ้น “Hello from Task3” หลังจากนั้น ก็ suspend Task2 และ Task3(ตัวมันเอง) ทำให้ Task2&Task3 ถูกโยนไปอยู่ใน Suspended state ตอนนี้ Task1 ซึ่งมี Priority สูงสุด จะเริ่มทำงาน โดยการปริ้น “Hello from Task1” แล้วก็ทำการ resume Task2 กลับมา Task2 จะกลับมาเป็น Task ที่มี Priority สูงสุดอีกครั้ง จึงทำให้ Task1 ถูกโยนกลับไปที่ Ready state อีกครั้ง Task2 ก็เริ่มทำงานโดยการปริ้น “Hello from Task2” แล้วก็ทำการ resume Task3 กลับมาจาก suspended state Task3 กลับมาทำงานอีกครั้งเนื่องจาก Priority สูงสุด และทำงานต่อจากการทำงานเดิม โดยการ delete ตัวเอง พร้อมกับปริ้น “Hello again from Task3 , then delete Task3” Task2 กลับมาทำงานอีกครั้งเนื่องจาก Priority สูงสุด และทำงานต่อจากการทำงานเดิม โดยการ delete ตัวเอง พร้อมกับปริ้น “Hello again from Task2 , then delete Task2” Task1 กลับมาทำงานอีกครั้งเนื่องจาก Priority สูงสุด และทำงานต่อจากการทำงานเดิม โดยการ delete ตัวเอง พร้อมกับปริ้น “Hello again from Task1 , then delete Task1” Task4 กลับมาทำงานอีกครั้งเนื่องจาก Priority สูงสุด และทำงานต่อจากการทำงานเดิม โดยการ delete ตัวเอง พร้อมกับปริ้น “Hello again from Task4 , then delete Task4” ขั้นตอนนี้นี้ไม่เหลือ Task ให้ทำงานแล้ว จึงจบการทำงาน }}} เป็นไงครับ เห็นการทำงานโยนไปโยนมาของ Task ที่ไปอยู่ใน State ต่างๆ หวังว่าผู้อ่านคงเข้าใจเพิ่มขึ้นเกี่ยวกับการทำงานของ Task และเผื่อจะเป็นไอเดียเอาไปประยุกต์ใช้ในโปรเจ็คตัวเองนะครับ ถ้าโปรเจ็คของท่านไม่ได้มีความซับซ้อนมาก วิธีการเขียนโปรแกรมแบบเดิมอาจจะเป็นคำตอบที่ดีอยู่แล้วก็ได้ แต่ถ้าโปรแกรมที่เราเขียนมีความซับซ้อนและเกี่ยวเนื่องกับการทำงานหลายๆอย่างพร้อมกันกัน RTOS น่าจะเป็นคำตอบที่ดีสำหรับโปรเจ็คแบบนั้นครับ เพราะจะทำให้เราเขียน code ได้เข้าใจง่าย สุดท้ายนี้ ขอลง Reference แหล่งที่มาที่ศึกษาการใช้งาน RTOS ที่ผมไปอ่านแล้วลองทำตามดู ใครสนใจรายละเอียดต้นทาง ลองเข้าไปอ่านกันได้ครับ Tasks: CreateTasks Get started with creating a basic task in FreeRTOS with the ESP32 and ESP-IDF Before starting make sure that you have… coder137.github.io FreeRTOS With Arduino 06 : Task Suspend and Resume In earlier tutorials, we saw how to create, use and delete the tasks. In this tutorial, we will see how to Suspend and… www.instructables.com การใช้งาน FreeRTOS ตอนที่ 1 FreeRTOS พัฒนาขึ้นมาโดยบริษัท Real Time Engineer โดย FreeRTOS…