การเขียนโปรแกรมแบบ Multi-Tasking: TinyGo Goroutines vs. FreeRTOS#
Keywords: Go / TinyGo, Microcontroller Programming, Raspberry Pico, RP2040, FreeRTOS, Arduino Core for RP2040
▷ การเขียนโปรแกรมแบบ Multi-tasking#
บทความนี้นำเสนอตัวอย่างการเขียนโค้ดแบบ "มัลติทาสก์" (Multi-Tasking) โดยใช้ Goroutines และ Channels ในภาษา Go และคอมไพล์โค้ดด้วย TinyGo Compiler (ใช้เวอร์ชัน v0.35.0) เพื่อนำมาเปรียบเทียบกับการเขียนโค้ด Arduino Sketch โดยใช้ FreeRTOS สำหรับชิปไมโครคอนโทรลเลอร์ RP2040
เราจะใช้รูปแบบการทำงานของโปรแกรมสำหรับไมโครคอนโทรลเลอร์ โดยแบ่งออกเป็น 2 งานย่อย หรือที่เรียกว่า ทาสก์ (Tasks) ซึ่งมีความสำคัญเท่ากัน ทั้งสองทาสก์มีหน้าที่สลับสถานะลอจิกของขา GPIO ที่ทำหน้าที่เป็นเอาต์พุตของชิป RP2040 โดยไม่มีการหน่วงเวลา อุปกรณ์ออสซิลโลสโคปแบบดิจิทัลจะถูกนำมาใช้เพื่อวัดสัญญาณเอาต์พุตที่ขา GPIO และใช้ในการวิเคราะห์พฤติกรรมการทำงานของโปรแกรมบนฮาร์ดแวร์จริง
การทำงานแบบ Multi-tasking สำหรับภาษา Go ใช้ฟังก์ชันประเภทที่เรียกว่า Goroutines อย่างไรก็ตาม หากเขียนโค้ดด้วย Arduino Core for RP2040 (ทดลองใช้เวอร์ชัน v4.3.0) ก็อาจใช้ FreeRTOS port for RP2040 ซึ่งเป็นระบบปฏิบัติการเวลาจริง (RTOS: Real-Time Operating System) หรือที่เรียกว่า Real-Time Kernel โดยนำมาสร้างและใช้งานสิ่งที่เรียกว่า FreeRTOS Tasks
รูป: โมเดลการทำงานแบบมัลติทาสก์สำหรับการทดสอบโดยใช้ TinyGo Goroutines + Go Channels
รูป: โมเดลการทำงานแบบมัลติทาสก์สำหรับการทดสอบโดยใช้ FreeRTOS Tasks + Binary Semaphores
▷ การเขียนโปรแกรมสำหรับ TinyGo#
เริ่มต้นด้วยโค้ดตัวอย่างสำหรับ TinyGo โดยเลือกใช้ขา GPIO15
และ GPIO14
เป็นขาเอาต์พุต และอ้างอิงโดยใช้ชื่อตัวแปรเป็น
A
และ B
ตามลำดับ มีการสร้างฟังก์ชัน toggleLED(...)
เพื่อใช้เป็นฟังก์ชัน Goroutine
func toggleLED(led machine.Pin, chanIn, chanOut chan bool)
led
มีชนิดข้อมูลเป็นmachine.Pin
และเป็นขา GPIO ที่จะใช้เป็นเอาต์พุตchanIn
และchanOut
เป็น Go Channel ใช้เป็นช่องทางสำหรับอ่าน (ช่องสัญญาณเข้า) และช่องทางสำหรับเขียน (ข่องสัญญาณออก) ข้อมูลแบบbool
ตามลำดับ
ในฟังก์ชัน toggleLED()
มีการใช้คำสั่ง for { ... }
ภายในมีการใช้คำสั่งแรก <-chanIn
เพื่อรออ่านค่าแบบ bool
จากช่อง chanIn
ถ้ายังไม่มีข้อมูล การฟังก์ชันนี้จะถูกหยุดไว้ชั่วคราวจนกว่าจะได้ระบข้อมูลเข้ามา
แต่ถ้าได้รับข้อมูลแล้วจึงสลับสถานะของลอจิกที่ขาเอาต์พุต และส่งข้อมูล true
ไปออกทาง chanOut
การส่งข้อมูลออกทาง chanOut
จะทำให้ฟังก์ชัน Goroutine ถัดไปที่รอรับข้อมูลอยู่ เปลี่ยนสถานะเป็นพร้อมทำงาน
for {
<-chanIn // Wait for the next token
led.Set(!led.Get()) // Toggle the LED output
chanOut <- true // Send a token
}
การทำงานของฟังก์ชัน toggleLED()
จะต้องอ่านข้อมูลจาก chanIn
ถ้าไม่มีข้อมูลเข้ามา จะต้องรอ
และเมื่ออ่านข้อมูลได้หนึ่งครั้งแล้ว จึงจะสลับสถานะลอจิกของขา led
หนึ่งครั้ง แล้วจึงเขียนข้อมูลไปยัง chanOut
คำสั่งต่อไปนี้เป็นการสร้างช่องสัญญาณ Go Channel จำนวน 2 ช่อง ดังนี้
chan1 := make(chan bool)
chan2 := make(chan bool)
การเรียกฟังก์ชัน toggleLED()
เพื่อใช้งานแบบ Goroutine จะมีคำว่า go
นำหน้า ดังนี้
go toggleLED(A, chan1, chan2)
go toggleLED(B, chan2, chan1)
ฟังก์ชันแรกใช้กับขา A
อ่านข้อมูลจาก chan1
และเขียนข้อมูลไปยัง chan2
ในขณะที่ ฟังก์ชันที่สองใช้กับขา B
อ่านข้อมูลจาก chan2
และเขียนข้อมูลไปยัง chan1
เริ่มต้นจะมีการเขียนข้อมูลไปยัง chan1
ดังนั้นฟังก์ชัน Goroutine หมายเลข 1 สำหรับขา A
จะเริ่มทำงานก่อน
package main
import (
"machine"
"time"
)
var (
A = machine.Pin(15) // LED 1 on GPIO 15
B = machine.Pin(14) // LED 2 on GPIO 14
)
func toggleLED(led machine.Pin, chanIn, chanOut chan bool) {
for {
<-chanIn // Wait for the next token
led.Set(!led.Get()) // Toggle the LED output
chanOut <- true // Send a token
}
}
func main() {
println("TinyGo RP2040 Demo...")
time.Sleep(time.Second)
println("CPU freq. (MHz):", machine.CPUFrequency()/1e6) // 125MHz
// Initialize the LED pins
A.Configure(machine.PinConfig{Mode: machine.PinOutput})
B.Configure(machine.PinConfig{Mode: machine.PinOutput})
// Create two channels for goroutine synchronization
chan1 := make(chan bool)
chan2 := make(chan bool)
// Start the goroutines to toggle LEDs
go toggleLED(A, chan1, chan2) // Goroutine (1) for pin A
go toggleLED(B, chan2, chan1) // Goroutine (2) for pin B
// Initial communication to start the first LED
chan1 <- true // Send the first token
select {} // Wait forever
}
รูป: ตัวอย่างการเขียนโค้ดโดยใช้ VS Code IDE + TinyGo Compiler
ตัวอย่างรูปคลื่นสัญญาณ 2 ช่อง จากขา GPIO15
และ GPIO14
ที่วัดได้ด้วยออสซิลโลสโคป เป็นดังนี้
รูป: สัญญาณเอาต์พุตที่ได้จากการทำงานของ Goroutines
จากรูปจะเห็นได้ว่า เมื่อสัญญาณแรกมีการเปลี่ยนสถานะลอจิกหนึ่งครั้ง ถัดไปสัญญาณที่สองเกิดการเปลี่ยนสถานะลอจิกหนึ่งครั้ง โดยมีระยะเวลาห่างกันประมาณ 4 ไมโครวินาที การทำงานของ TinyGo Runtime จะเป็นตัวจัดลำดับการทำงานของ Goroutines ให้สลับการทำงาน
▷ การเขียนโปรแกรม FreeRTOS: Binary Semaphores#
ตัวอย่างถัดไปเป็นการเขียนโค้ดด้วย Arduino Core for RP2040 ซึ่งจะมีการสร้าง FreeRTOS Tasks จำนวน 2 ทาสก์ ที่มีความสำคัญเท่ากัน และการสื่อสารกันระหว่างทาสก์ จะใช้วิธีที่เรียกว่า Binary Semaphores และจะใช้เพื่อสื่อสารจากทากส์หนึ่งไปยังอีกทาสก์หนึ่งในแต่ละทิศทาง และมีทาสก์แรกที่ถูกสร้างก่อน เป็นฝ่ายเริ่มต้นทำงานก่อน
#include <Arduino.h>
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// LED Pins
#define LED1_PIN 15
#define LED2_PIN 14
// FreeRTOS Semaphore Handles for synchronization
SemaphoreHandle_t semaphore1, semaphore2;
// Struct to hold LED pin and semaphore handles
struct TaskParams {
int ledPin;
SemaphoreHandle_t semIn, semOut;
};
// Task to toggle an LED
void toggleLED(void *pvParameters) {
// Cast the parameter back to a pointer to TaskParams
TaskParams *p = (TaskParams *)pvParameters;
int ledPin = p->ledPin;
for (;;) {
// Wait for the semaphore
if (xSemaphoreTake(p->semIn, portMAX_DELAY) == pdTRUE) {
digitalWrite(ledPin, !digitalRead(ledPin)); // Toggle the LED
xSemaphoreGive(p->semOut); // Give the semaphore to the next task
}
}
}
void setup() {
// Initialize LED pins
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
// Create the binary semaphores
semaphore1 = xSemaphoreCreateBinary();
semaphore2 = xSemaphoreCreateBinary();
// Define struct instances for task parameters
TaskParams params1 = {LED1_PIN, semaphore1, semaphore2};
TaskParams params2 = {LED2_PIN, semaphore2, semaphore1};
// Create FreeRTOS tasks with struct as the argument
xTaskCreate(toggleLED, "Toggle LED1", 256, (void *)¶ms1, 1, NULL);
xTaskCreate(toggleLED, "Toggle LED2", 256, (void *)¶ms2, 1, NULL);
xSemaphoreGive(semaphore1);
vTaskStartScheduler(); // Start the FreeRTOS scheduler
}
void loop() {
}
รูป: การเขียนโค้ดด้วย Arduino IDE และตัวอย่างโค้ดที่ใช้ FreeRTOS Binary Semaphores
ตัวอย่างรูปคลื่นสัญญาณ 2 ช่อง จากขา GPIO15
และ GPIO14
ที่วัดได้ด้วยออสซิลโลสโคป เป็นดังนี้
รูป: สัญญาณเอาต์พุตที่ได้จากการทำงานของ FreeRTOS Tasks
จะเห็นได้ว่า รูปคลื่นสัญญาณทั้งสองคล้ายกับกรณีของ TinyGo กล่าวคือ เมื่อสัญญาณแรกมีการเปลี่ยนสถานะลอจิกหนึ่งครั้ง ก็จะสลับไปเกิดการเปลี่ยนสถานะลอจิกหนึ่งครั้งในสัญญาณที่สอง แต่ให้สังเกตว่า ระยะเวลาจากขอบขึ้นของสัญญาณแรกไปยังขอบขาขึ้นของสัญญาณที่สอง จะใช้เวลามากกว่ากรณีของ TinyGo
▷ การเขียนโปรแกรม FreeRTOS: Queues#
ตัวอย่างถัดไปเป็นการเปลี่ยนจาก Binary Semaphores มาเป็น FreeRTOS Queues เพื่อใช้เป็นช่องทางการอ่านและเขียนข้อมูล คล้ายกับกรณีของ TinyGo ที่มีการใช้ Go Channels อ่านข้อมูลเข้าและส่งข้อมูลออกแยกกัน
#include <Arduino.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
// LED Pins
#define LED1_PIN 15
#define LED2_PIN 14
// FreeRTOS Queue Handles for communication between tasks
QueueHandle_t queue1, queue2;
// Struct to hold LED pin and queue handles
struct TaskParams {
int ledPin;
QueueHandle_t queueIn, queueOut;
};
// Task to toggle an LED
void toggleLED(void *pvParameters) {
// Cast the parameter back to a pointer to TaskParams
TaskParams *p = (TaskParams *)pvParameters;
bool token;
int ledPin = p->ledPin;
for (;;) {
if (xQueueReceive(p->queueIn, &token, portMAX_DELAY)==pdTRUE) {
#if 0
digitalWrite(ledPin, !digitalRead(ledPin)); // Toggle the LED
#else
gpio_put(ledPin,!gpio_get(ledPin)); // Toggle the LED
#endif
xQueueSend(p->queueOut, &token, portMAX_DELAY);
}
}
}
void setup() {
// Initialize LED pins
#if 0
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
#else
gpio_init(LED1_PIN);
gpio_set_dir(LED1_PIN, GPIO_OUT);
gpio_init(LED2_PIN);
gpio_set_dir(LED2_PIN, GPIO_OUT);
#endif
// Create the queues for inter-task communication
queue1 = xQueueCreate(1, sizeof(bool));
queue2 = xQueueCreate(1, sizeof(bool));
// Define struct instances for task parameters
TaskParams params1 = {LED1_PIN, queue1, queue2};
TaskParams params2 = {LED2_PIN, queue2, queue1};
// Create FreeRTOS tasks with struct as the argument
xTaskCreate(toggleLED, "Toggle LED1", 256, (void *)¶ms1, 1, NULL);
xTaskCreate(toggleLED, "Toggle LED2", 256, (void *)¶ms2, 1, NULL);
bool startToken = true;
xQueueSend(queue1, &startToken, portMAX_DELAY); // Send the start token
vTaskStartScheduler(); // Start the FreeRTOS scheduler
}
void loop() {
}
รูป: การเขียนโค้ดด้วย Arduino IDE และตัวอย่างโค้ดที่ใช้ FreeRTOS Queues
ตัวอย่างรูปคลื่นสัญญาณ 2 ช่อง จากขา GPIO15
และ GPIO14
ที่วัดได้ด้วยออสซิลโลสโคป เป็นดังนี้
รูป: สัญญาณเอาต์พุตที่ได้จากการทำงานของ FreeRTOS Tasks
▷ การเขียนโปรแกรม FreeRTOS: Task Notification#
ตัวอย่างถัดไปเป็นการใช้วิธีที่เรียกว่า Task Notification ที่รองรับการใช้งานโดย FreeRTOS Kernel สำหรับการสื่อสารกันระหว่างทาสก์ และมีตัวอย่างการเขียนโค้ดดังนี้
#include <Arduino.h>
#include "FreeRTOS.h"
#include "task.h"
// LED Pins
#define LED1_PIN 15
#define LED2_PIN 14
// Struct to hold LED pin and task handle
struct TaskParams {
int ledPin;
TaskHandle_t nextTask; // Handle of the next task to notify
};
// Task handles
TaskHandle_t task1Handle, task2Handle;
// Task to toggle an LED
void toggleLED(void *pvParameters) {
TaskParams *p = (TaskParams *)pvParameters;
int ledPin = p->ledPin;
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // Wait for a notification
digitalWrite(ledPin, !digitalRead(ledPin)); // Toggle the LED
xTaskNotifyGive(p->nextTask); // Notify the next task
}
}
void setup() {
// Initialize LED pins
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
// Define struct instances for task parameters
TaskParams params1 = {LED1_PIN, NULL};
TaskParams params2 = {LED2_PIN, NULL};
// Create FreeRTOS tasks with struct as the argument
xTaskCreate(toggleLED, "Toggle LED1", 256, (void *)¶ms1, 1, &task1Handle);
xTaskCreate(toggleLED, "Toggle LED2", 256, (void *)¶ms2, 1, &task2Handle);
// Set the `nextTask` handles in the parameters
params1.nextTask = task2Handle;
params2.nextTask = task1Handle;
xTaskNotifyGive(task1Handle); // Notify the first task
vTaskStartScheduler(); // Start FreeRTOS scheduler
}
void loop() {
}
รูป: การเขียนโค้ดด้วย Arduino IDE และตัวอย่างโค้ดที่ใช้ FreeRTOS Task Notification
ตัวอย่างรูปคลื่นสัญญาณ 2 ช่อง จากขา GPIO15
และ GPIO14
ที่วัดได้ด้วยออสซิลโลสโคป เป็นดังนี้
รูป: สัญญาณเอาต์พุตที่ได้จากการทำงานของ FreeRTOS Tasks
▷ กล่าวสรุป#
บทความนี้ได้นำเสนอตัวอย่างการเขียนโค้ด TinyGo เปรียบเทียบกับการเขียนโค้ดด้วย Arduino Sketch โดยใช้ Arduino Core for RP2040 + FreeRTOS Kernel เพื่อสาธิตการสร้างและทำงานแบบมัลติทาสก์ (Multi-tasking)
Goroutines ในภาษา Go สามารถนำมาใช้ในการเขียนโค้ดแบบ Multi-tasking ได้สำหรับไมโครคอนโทรลเลอร์
แต่ก็มีข้อดีและข้อเสียแตกต่างจากการใช้ FreeRTOS เช่น การทำงานของ Goroutine
จะทำต่อเนื่องจนกว่าจะถึงคำสั่งที่จำเป็นต้องหยุดรอเงื่อนไข หรือ รอเวลา เช่น time.Sleep()
จึงจะมีการเปลี่ยนการทำงานไปยัง Goroutine ในลำดับถัดไปที่พร้อมจะทำงานต่อ แต่ละฟังก์ชัน Goroutine ทำงานเป็นหนึ่งทาสก์
และทาสก์เหล่านี้มีระดับความสำคัญเท่ากัน ซึ่งจะแตกต่างจากการทำงานของระบบที่ใช้ Preemptive Scheduling
ในระบบประเภทนี้ ทาสก์สามารถมีระดับความสำคัญแตกต่างกันได้ ทาสก์ที่มีความสำคัญสูงกว่าและพร้อมที่จะทำงาน
จะแทรกแซงการทำงานของทาสก์ที่กำลังทำงานในขณะนั้นและมีความสำคัญต่ำกว่าได้
จากโค้ดตัวอย่างและการวัดสัญญาณเอาต์พุตจริง จะเห็นได้ว่า โค้ดภาษา Go ที่ใช้ Goroutines จะสร้างสัญญาณเอาต์พุตที่มีความถี่ได้สูงกว่า การเขียนโค้ดด้วย Arduino Sketch และใช้ FreeRTOS Tasks
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Created: 2024-12-23 | Last Updated: 2024-12-24