มัลติทาสกิ้งด้วยไลบรารี Protothreads
Protothreads พัฒนาขึ้นโดย Adam Dunkels โดยมีวัตถุประสงค์เพื่อให้นักพัฒนาโค้ดสามารถเขียนโปรแกรมแบบมัลติทาสกิ้งในรูปแบบที่เข้าใจได้ง่ายบนอุปกรณ์ที่มีทรัพยากรจำกัดอย่างเช่นไมโครคอนโทรลเลอร์ ไลบรารีนี้นำเอาเทคนิคการบันทึกสถานะร่วมกับการใช้คำสั่ง goto ตามที่ได้กล่าวไปแล้วมารวมไว้เป็นชุดมาโครในภาษาซี ทำให้รูปแบบโค้ดของงานย่อยแต่ละงานมีลักษณะราวกับการทำงานแบบซิงเกิลทาสก์
เนื้อหา
การติดตั้งไลบรารี Protothreads เพื่อใช้งานร่วมกับ Arduino
ดาวน์โหลดไฟล์ pt.tgz แล้วแตกเอาไว้ในไดเรคตอรี ~/sketchbook/libraries/
$ cd ~/sketchbook/libraries/ $ wget http://www.cpe.ku.ac.th/~cpj/204223/pt.tgz $ tar zxf pt.tgz
ตัวอย่างโปรแกรม
โปรแกรมต่อไปนี้เขียนขึ้นโดยอาศัยไลบรารี Protothreads ซึ่งให้พฤติกรรมการทำงานที่เหมือนกับตัวอย่างตามวิกิ มัลติทาสกิ้งบนไมโครคอนโทรลเลอร์ ทุกประการ
- ตัวอย่าง
- เขียนเฟิร์มแวร์ที่ทำให้ LED สีเขียวบนบอร์ดพ่วงติด 1 วินาทีและดับ 0.5 วินาทีสลับกันไป ในขณะเดียวกันทำให้ LED สีแดงติด 0.7 วินาทีและดับ 0.3 วินาทีสลับกันไป
#include <Practicum.h>
#include <pt.h>
#define PT_DELAY(pt, ms, ts) \
ts = millis(); \
PT_WAIT_WHILE(pt, millis()-ts < (ms));
struct pt pt_taskRed;
struct pt pt_taskGreen;
///////////////////////////////////////////////////////
PT_THREAD(taskGreen(struct pt* pt))
{
static uint32_t ts;
PT_BEGIN(pt);
while (1)
{
digitalWrite(PIN_PC2, HIGH);
PT_DELAY(pt, 1000, ts);
digitalWrite(PIN_PC2, LOW);
PT_DELAY(pt, 500, ts);
}
PT_END(pt);
}
///////////////////////////////////////////////////////
PT_THREAD(taskRed(struct pt* pt))
{
static uint32_t ts;
PT_BEGIN(pt);
while (1)
{
digitalWrite(PIN_PC0, HIGH);
PT_DELAY(pt, 700, ts);
digitalWrite(PIN_PC0, LOW);
PT_DELAY(pt, 300, ts);
}
PT_END(pt);
}
///////////////////////////////////////////////////////
void setup()
{
pinMode(PIN_PC0, OUTPUT);
pinMode(PIN_PC1, OUTPUT);
pinMode(PIN_PC2, OUTPUT);
pinMode(PIN_PC3, INPUT_PULLUP);
pinMode(PIN_PC4, INPUT);
pinMode(PIN_PD3, OUTPUT);
PT_INIT(&pt_taskGreen);
PT_INIT(&pt_taskRed);
}
///////////////////////////////////////////////////////
void loop()
{
taskGreen(&pt_taskGreen);
taskRed(&pt_taskRed);
}
สิ่งที่น่าสนใจคืองานย่อยทั้งสองงาน (taskGreen และ taskRed) มีโค้ดที่เกือบจะเหมือนกับโค้ดดั้งเดิมที่เขียนสำหรับงานเดี่ยว
โค้ดดั้งเดิม (แบบใช้ delay) | โค้ดที่อิมพลิเมนต์ด้วยไลบรารี Protothreads |
---|---|
void taskGreen()
{
while (1)
{
digitalWrite(PIN_PC2, HIGH);
delay(1000);
digitalWrite(PIN_PC2, LOW);
delay(500);
}
}
|
PT_THREAD(taskGreen(struct pt* pt))
{
static uint32_t ts;
PT_BEGIN(pt);
while (1)
{
digitalWrite(PIN_PC2, HIGH);
PT_DELAY(pt, 1000, ts);
digitalWrite(PIN_PC2, LOW);
PT_DELAY(pt, 500, ts);
}
PT_END(pt);
}
|
การทำงานของไลบรารี Protothreads
รายละเอียดคร่าว ๆ ของโปรแกรมและมาโครต่าง ๆ ในไลบรารี Protothreads ที่นำมาใช้มีดังนี้
struct pt
ถูกนิยามไว้ในไฟล์เฮดเดอร์pt.h
ซึ่งภายในมีตัวแปรสมาชิกเพียงตัวเดียวที่เอาไว้เก็บสถานะปัจจุบันของงาน
// นิยามโดยย่อของ struct pt
struct pt
{
void* state;
};
- มาโคร
PT_THREAD(task(struct pt* pt))
ใช้สำหรับครอบการประกาศฟังก์ชันที่จะให้ทำหน้าที่เป็น protothread (เสมือนเป็นเครื่องจักรสถานะตัวหนึ่ง) ซึ่งมีผลเทียบเท่ากับการประกาศฟังก์ชันด้วยคำสั่ง
char task(struct pt* pt)
- ซึ่งไม่ต่างจากการประกาศฟังก์ชันทั่วไป แต่การประกาศฟังก์ชันผ่านมาโครนี้จะช่วยย้ำผู้เขียนโปรแกรมว่าฟังก์ชันนี้ทำหน้าที่เป็น protothread
- มาโคร
PT_BEGIN(pt)
ถูกนิยามไว้เป็นโค้ดที่เรียกใช้คำสั่งgoto
ตามสถานะที่เก็บไว้ในตัวแปรpt
ซึ่งเทียบเท่ากับการใช้คำสั่ง
if (pt->state != NULL) goto *(pt->state);
- มาโครนี้จึงต้องถูกเรียกเป็นคำสั่งแรกเสมอในฟังก์ชันที่จะทำหน้าที่เป็นเครื่องจักรสถานะ
- มาโคร
PT_END(pt)
ระบุจุดสิ้นสุดของ protothread โดยทำงานเทียบเท่ากับคำสั่ง return
return 3;
- ค่า 3 ถูกใช้ในไลบรารี Protothreads เป็นการภายในเพื่อระบุว่า protothread นี้จบการทำงานโดยสมบูรณ์
- มาโคร
PT_INIT(pt)
ทำหน้าที่กำหนดสถานะเริ่มต้นให้กับตัวแปรpt
ซึ่งมีผลเทียบเท่ากับการใช้คำสั่ง
pt->state = NULL;
- เมื่อพิจารณาคู่กับมาโคร
PT_BEGIN
แล้วจึงมีความหมายว่าให้เริ่มต้นทำงานตั้งแต่ต้นฟังก์ชัน
- มาโคร
PT_WAIT_WHILE(pt, cond)
ถูกนิยามไว้เป็นการตรวจสอบเงื่อนไขcond
ว่ายังเป็นจริงอยู่หรือไม่ หากเป็นจริงจะบันทึกสถานะบรรทัดปัจจุบันไว้ในตัวแปรpt
ก่อนที่จะ return ออกจากฟังก์ชัน ซึ่งเทียบเท่ากับการใช้คำสั่ง
L__LINE__: pt->state = &&L__LINE__; // __LINE__ ถูกคอมไพเลอร์แทนที่ด้วยหมายเลขบรรทัดปัจจุบัน
if (cond) return 0;
- มาโคร
PT_DELAY(pt, ms, ts)
ไม่ได้อยู่ในไลบรารี Protothreads แต่สร้างขึ้นเพื่อความสะดวกในการจำลองการทำงานของคำสั่ง delay ในแบบที่ไม่หยุดรอ นิยามไว้ให้เทียบเท่ากับการใช้คำสั่ง
ts = millis();
PT_WAIT_WHILE(pt, millis()-ts < (ms));
ดังนั้นส่วนของโปรแกรมที่นิยาม protothread ชื่อ taskGreen
PT_THREAD(taskGreen(struct pt* pt))
{
static uint32_t ts;
PT_BEGIN(pt);
while (1)
{
digitalWrite(PIN_PC2, HIGH);
PT_DELAY(pt, 1000, ts);
digitalWrite(PIN_PC2, LOW);
PT_DELAY(pt, 500, ts);
}
PT_END(pt);
}
เมื่อแทนที่มาโครต่าง ๆ เรียบร้อยแล้วจะมีผลเทียบเท่ากับโค้ดด้านล่าง
char taskGreen(struct pt* pt)
{
static uint32_t ts;
// มาโคร PT_BEGIN(pt)
if (pt->state != NULL) goto *(pt->state);
while (1)
{
digitalWrite(PIN_PC2, HIGH);
// มาโคร PT_DELAY(pt, 1000, ts);
ts = millis();
L43: pt->state = &&L43; // สมมติว่า 43 คือเลขบรรทัดนี้
if (millis()-ts < 1000) return 0;
digitalWrite(PIN_PC2, LOW);
// มาโคร PT_DELAY(pt, 500, ts);
ts = millis();
L45: pt->state = &&L45; // สมมตว่า 45 คือเลขบรรทัดนี้
if (millis()-ts < 500) return 0;
}
// มาโคร PT_END(pt);
return 3;
}
ซึ่งเหมือนกับโค้ดที่อิมพลิเมนต์เครื่องจักรสถานะแบบใช้ goto
นั่นเอง
ข้อควรระวังในการใช้ไลบรารี Protothreads
เนื่องจากโค้ดที่เขียนในรูป protothread จะมีหน้าตาคล้ายกับโค้ดที่ทำงานเดี่ยวเป็นอย่างมาก จึงเป็นการง่ายที่จะเผลอเขียนโค้ดที่ไม่ได้คำนึงถึงการทำงานร่วมกับงานอื่น ด้านล่างเป็นข้อผิดพลาดที่เกิดขึ้นได้ง่ายในการใช้ Protothreads
- ตัวแปรแบบโลคัลถูกทำลาย จำไว้เสมอว่าฟังก์ชันมีการเรียกใช้ return ตลอดเวลา ดังนั้นค่าของตัวแปรที่ถูกประกาศแบบโลคัลจะสูญหายทันที โค้ดด้านล่างแสดงส่วนของโปรแกรมที่พยายามทำให้ LED สีเขียวกระพริบ 5 ครั้ง แล้วจึงให้ LED สีแดงกระพริบอีก 3 ครั้ง แต่โปรแกรมจะไม่ทำงานตามที่คาดหวัง
1 PT_THREAD(taskBlink(struct pt* pt))
2 {
3 static uint32_t ts;
4 int i; // <- ถูกทำลายและสร้างใหม่ตลอดเวลา
5
6 PT_BEGIN(pt);
7
8 // สีเขียวกระพริบ 5 ครั้ง
9 for (i = 0; i < 5; i++)
10 {
11 digitalWrite(PIN_PC2, HIGH);
12 PT_DELAY(pt,100,ts);
13 digitalWrite(PIN_PC2, LOW);
14 PT_DELAY(pt,100,ts);
15 }
16
17 // สีแดงกระพริบ 3 ครั้ง
18 for (i = 0; i < 3; i++)
19 {
20 digitalWrite(PIN_PC0, HIGH);
21 PT_DELAY(pt,100,ts);
22 digitalWrite(PIN_PC0, LOW);
23 PT_DELAY(pt,100,ts);
24 }
25 PT_END(pt);
26 }
เหตุที่โปรแกรมไม่ทำงานตามที่คาดหวังเนื่องจากตัวแปร i
ที่ประกาศไว้ในบรรทัดที่ 4 เป็นตัวแปรแบบโลคัลธรรมดา ภายในมาโคร PT_DELAY
มีคำสั่ง return ซึ่งมีผลทำให้ตัวแปร i
ถูกทำลาย และถูกสร้างใหม่เมื่อฟังก์ชันถูกเรียกให้ทำงานต่อ ดังนั้นค่าของ i
ในลูป for แรกจึงเป็นศูนย์เสมอ วิธีที่ถูกต้องคือประกาศให้ตัวแปร i
เป็นแบบ static
4 static int i;
- ลูปอนันต์ไม่เปิดโอกาสให้ออกจากฟังก์ชัน การใช้ลูปแบบ
while (1)
ทำได้ใน protothread ก็จริง แต่ต้องให้ฟังก์ชันได้ return เพื่อให้โอกาสงานอื่น ๆ ทำงานด้วยเช่นกัน โค้ดด้านล่างเป็นตัวอย่างการตีความค่าแสง 2 ระดับและแสดงผลลัพธ์บน LED สีแดงตลอดเวลา
1 PT_THREAD(taskLight(struct pt* pt))
2 {
3 PT_BEGIN(pt);
4
5 while (1)
6 {
7 uint16_t light = analogRead(PIN_PC4);
8 digitalWrite(PIN_PC0, light/512);
9 }
10
11 PT_END(pt);
12 }
จะเห็นว่าเมื่อลูป while เริ่มทำงานแล้วจะไม่มีการเปิดโอกาสให้ออกจากฟังก์ชันได้อีกเลย นั่นหมายถึงงานอื่น ๆ ที่ต้องการให้ทำควบคู่กันไปจะหยุดชะงักลงทันที หากไม่ต้องการหน่วงเวลาใด ๆ อย่างน้อยที่สุดต้องมีการใช้คำสั่ง PT_YIELD
เพื่อฟังก์ชันจะได้มีโอกาส return และให้งานอื่น ๆ ได้ทำงานบ้าง
1 PT_THREAD(taskLight(struct pt* pt))
2 {
3 PT_BEGIN(pt);
4
5 while (1)
6 {
7 uint16_t light = analogRead(PIN_PC4);
8 digitalWrite(PIN_PC0, light/512);
9 PT_YIELD(pt):
10 }
11
12 PT_END(pt);
13 }