มัลติทาสกิ้งบนไมโครคอนโทรลเลอร์
โปรแกรมควบคุมที่ใช้ในคอมพิวเตอร์แบบฝังตัวนั้นมักต้องการให้มีการทำงานหลายส่วนขนานกันไป เรียกว่าเป็นการทำงานแบบ มัลติทาสกิ้ง (multitasking) อาทิเช่นการตรวจสอบสถานะของแสงเพื่อเปิดปิดไฟในขณะที่ต้องตรวจสอบสถานะการกดปุ่มสวิตช์ไปด้วยในเวลาเดียวกัน หรือการทำไฟกระพริบเป็นจังหวะเพื่อแสดงให้เห็นว่าอุปกรณ์กำลังทำงานในขณะที่ต้องคอยวนตรวจสอบข้อมูลที่มาจากพอร์ท USB เป็นต้น ในสถานการณ์เหล่านี้แม้ว่าแต่ละงานย่อยจะมีการทำงานที่ตรงไปตรงมา แต่การทำงานย่อยหลาย ๆ งานให้เสมือนว่าพร้อมกันบนไมโครคอนโทรลเลอร์ที่มีหน่วยประมวลผลเดียวโดยไม่มีระบบปฏิบัติการคอยช่วยเหลือเป็นเรื่องที่ค่อนข้างซับซ้อน
เนื้อหา
ตัวอย่างแอพลิเคชัน
พิจารณาตัวอย่างโปรแกรมควบคุม LED สองดวงให้กระพริบเป็นอิสระต่อกันดังนี้
- ตัวอย่าง
- เขียนเฟิร์มแวร์ที่ทำให้ LED สีเขียวบนบอร์ดพ่วงติด 1 วินาทีและดับ 0.5 วินาทีสลับกันไป ในขณะเดียวกันทำให้ LED สีแดงติด 0.7 วินาทีและดับ 0.3 วินาทีสลับกันไป
จะเห็นว่างานทั้งหมดประกอบด้วยงานย่อยสองงาน ที่ผ่านมานั้นหากใช้เฟรมเวิร์กของ Arduino การทำให้เพียง LED สีเขียวกระพริบตามที่กำหนดทำได้โดยการเขียนโค้ดในฟังก์ชัน loop
ลักษณะนี้
// LED สีเขียวติด 1 วินาที ดับ 0.5 วินาที
void loop()
{
digitalWrite(PIN_PC2, HIGH);
delay(1000);
digitalWrite(PIN_PC2, LOW);
delay(500);
}
ในขณะที่การทำให้ LED สีแดงกระพริบจะใช้โค้ดดังนี้
// LED สีแดงติด 0.7 วินาที ดับ 0.3 วินาที
void loop()
{
digitalWrite(PIN_PC0, HIGH);
delay(700);
digitalWrite(PIN_PC0, LOW);
delay(300);
}
อย่างไรก็ตาม การให้ LED ทั้งสองดวงกระพริบตามจังหวะของตัวเองขนานกันไปนั้นไม่อาจทำได้โดยการรวมงานทั้งคู่เข้าด้วยกันอย่างตรงไปตรงมาเช่นนี้ได้
// *** โค้ดด้านล่างไม่ได้ทำงานตามที่พึงประสงค์ ***
void taskGreen()
{
digitalWrite(PIN_PC2, HIGH);
delay(1000);
digitalWrite(PIN_PC2, LOW);
delay(500);
}
void taskRed()
{
digitalWrite(PIN_PC0, HIGH);
delay(700);
digitalWrite(PIN_PC0, LOW);
delay(300);
}
void loop()
{
taskGreen();
taskRed();
}
ทั้งนี้เนื่องจากว่าฟังก์ชัน taskRed()
ไม่มีการถูกเรียกจนกว่าฟังก์ชัน taskGreen()
จะทำงานเสร็จในหนึ่งรอบซึ่งใช้เวลาทั้งสิ้น 1.5 วินาที ในทำนองเดียวกัน ระหว่างที่ฟังก์ชัน taskRed()
ทำงานอยู่นั้นฟังก์ชัน taskGreen()
ก็ไม่มีโอกาสได้ทำงานเช่นกัน
เครื่องจักรสถานะ
เวลาที่สูญเสียไปกับฟังก์ชันทั้งคู่ตามตัวอย่างข้างต้นนั้นเกือบทั้งหมดเกิดจากการใช้คำสั่ง delay ซึ่งเป็นการหยุดรอโดยไม่ทำอะไรทั้งสิ้น จึงส่งผลกระทบให้งานอื่นที่ต้องการการประมวลผลหยุดชะงักลงด้วย เราจึงต้องออกแบบทั้งสองฟังก์ชันใหม่เพื่อให้ทำงานเสร็จสิ้นในการเรียกแต่ละครั้งให้เร็วที่สุดเพื่อให้งานอื่นมีโอกาสได้ประมวลผล แนวคิดที่ใช้กันอย่างแพร่หลายคือมองงานแต่ละงานในรูป เครื่องจักรสถานะจำกัด (Finite State Machine) หรือเรียกสั้น ๆ ว่าเครื่องจักรสถานะ ผังภาพด้านล่างแสดงเครื่องจักรสถานะของงานควบคุม LED สีเขียว
กลไกการทำงานของงาน (หรือเครื่องจักร) ข้างต้นเป็นดังนี้
- เริ่มทำงานโดยเข้าสู่สถานะ ON ซึ่งมีการสั่งให้ LED สีเขียวติด และบันทึกเวลาปัจจุบันเป็นมิลลิวินาทีจากฟังก์ชัน
millis()
ไว้ในตัวแปรts
(ย่อมาจาก timestamp)- (หมายเหตุ: ฟังก์ชัน millis() เป็นฟังก์ชันที่เฟรมเวิร์ก Arduino มีให้ใช้สำหรับตรวจสอบว่าไมโครคอนโทรลเลอร์ได้ทำงานมาเป็นระยะเวลานานกี่มิลลิวินาที)
- ตรวจสอบเวลาที่อยู่ในสถานะนี้โดยคำนวณค่า
millis()-ts
เกินค่า 1000 มิลลิวินาที- หากยังไม่เกินให้อยู่ในสถานะเดิม
- หากเกินแล้ว ให้เข้าสู่สถานะ OFF โดยบันทึกค่า
ts
เป็นเวลาที่เข้าสู่สถานะใหม่ใน และดับ LED สีเขียว
- ดำเนินการในลักษณะเดียวกันเมื่ออยู่ในสถานะใหม่
โค้ดเครื่องจักรสถานะ สำหรับ LED สีเขียว
เรานำเครื่องจักรสถานะที่ออกแบบไว้ข้างต้นมาเขียนเป็นโค้ดภาษาซี/Arduino ได้ดังนี้
#include <Practicum.h>
enum State { ON, OFF };
State taskGreen_state;
//////////////////////////////////////////////////
void taskGreen()
{
static uint32_t ts = 0;
if (taskGreen_state == ON)
{
if (millis() - ts >= 1000)
{
ts = millis();
digitalWrite(PIN_PC2, LOW);
taskGreen_state = OFF;
}
}
if (taskGreen_state == OFF)
{
if (millis() - ts >= 500)
{
ts = millis();
digitalWrite(PIN_PC2, HIGH);
taskGreen_state = ON;
}
}
}
//////////////////////////////////////////////////
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);
// กำหนดสถานะเริ่มต้น
taskGreen_state = ON;
digitalWrite(PIN_PC2, HIGH);
}
//////////////////////////////////////////////////
void loop()
{
taskGreen();
}
โปรแกรมข้างต้นสามารถคอมไพล์และรันได้จริง แต่จะมีเพียงงานของการกระพริบ LED สีเขียวเท่านั้น อย่างไรก็ตามโปรแกรมนี้ถูกเขียนในรูปแบบแตกต่างจากโปรแกรมไฟกระพริบที่ผ่าน ๆ มา และมีจุดที่น่าสังเกตหลายจุดดังนี้
- ฟังก์ชัน
taskGreen
นั้นทำงานตามที่ได้ออกแบบไว้ในเครื่องจักรสถานะทุกประการ ซึ่งจะเห็นว่าไม่มีการใช้งานคำสั่ง delay จึงทำให้ทำงานเสร็จแทบจะทันทีที่ถูกเรียกในแต่ละครั้ง อันเป็นพฤติกรรมที่เราต้องการในการทำงานแบบมัลติทาสกิ้ง - คำสั่ง enum เป็นการกำหนดชนิดข้อมูลแบบใหม่ชื่อ
State
เพื่อเอาไว้สร้างตัวแปรเก็บสถานะปัจจุบันของเครื่องจักร โดยมีค่าที่เป็นไปได้คือ ON และ OFF - ตัวแปร
ts
ถูกประกาศให้เป็นตัวแปรแบบโลคัล แต่ต้องมีการคงค่าเดิมไว้แม้การทำงานจะออกจากฟังก์ชันtaskGreen()
ไปแล้ว จึงต้องมีการระบุคีย์เวิร์ด static เอาไว้
โค้ดที่สมบูรณ์: LED สองสีกระพริบอิสระ
โค้ดข้างต้นนำมาเพิ่มเครื่องจักรสถานะสำหรับ LED สีแดงเข้าไปได้อย่างตรงไปตรงมา ทำให้ได้โค้ดที่สมบูรณ์ดังนี้
#include <Practicum.h>
enum State { ON, OFF };
State taskGreen_state;
State taskRed_state;
////////////////////////////////////
// เครื่องจักรสถานะสำหรับ LED สีเขียว
////////////////////////////////////
void taskGreen()
{
static uint32_t ts = 0;
if (taskGreen_state == ON)
{
if (millis() - ts >= 1000)
{
ts = millis();
digitalWrite(PIN_PC2, LOW);
taskGreen_state = OFF;
}
}
if (taskGreen_state == OFF)
{
if (millis() - ts >= 500)
{
ts = millis();
digitalWrite(PIN_PC2, HIGH);
taskGreen_state = ON;
}
}
}
////////////////////////////////////
// เครื่องจักรสถานะสำหรับ LED สีแดง
////////////////////////////////////
void taskRed()
{
static uint32_t ts = 0;
if (taskRed_state == ON)
{
if (millis() - ts >= 700)
{
ts = millis();
digitalWrite(PIN_PC0, LOW);
taskRed_state = OFF;
}
}
if (taskRed_state == OFF)
{
if (millis() - ts >= 300)
{
ts = millis();
digitalWrite(PIN_PC0, HIGH);
taskRed_state = ON;
}
}
}
//////////////////////////////////////////////
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);
// กำหนดสถานะเริ่มต้นให้กับเครื่องจักรสถานะทั้งคู่
taskGreen_state = ON;
digitalWrite(PIN_PC2, HIGH);
taskRed_state = ON;
digitalWrite(PIN_PC0, HIGH);
}
//////////////////////////////////////////////
void loop()
{
taskGreen();
taskRed();
}
การอิมพลิเมนต์เครื่องจักรสถานะด้วยคำสั่ง goto
โค้ดที่เขียนขึ้นจากเครื่องจักรสถานะนั้นจะดูตรงไปตรงมาก็ต่อเมื่อมีผังภาพเครื่องจักรสถานะดังเช่นรูปข้างต้นมาพิจารณาประกอบ แต่ถ้าหากดูจากตัวโค้ดเพียงอย่างเดียวนั้นแทบจะไม่สามารถถอดความได้ทันทีว่าผลลัพธ์จะเป็นอย่างไร เห็นได้จากตารางเปรียบเทียบโค้ดเดิมที่ทำงานแบบซิงเกิลทาสกิ้ง (ซ้ายมือ) และโค้ดที่เขียนตามเครื่องจักรสถานะเพื่อรองรับการทำงานแบบมัสติทาสกิ้ง (ขวามือ)
โค้ดดั้งเดิม (แบบใช้ delay) | โค้ดที่อิมพลิเมนต์ตามเครื่องจักรสถานะ |
---|---|
void taskGreen()
{
while (1)
{
digitalWrite(PIN_PC2, HIGH);
delay(1000);
digitalWrite(PIN_PC2, LOW);
delay(500);
}
}
|
void taskGreen()
{
static uint32_t ts = 0;
if (taskGreen_state == ON)
{
if (millis() - ts >= 1000)
{
ts = millis();
digitalWrite(PIN_PC2, LOW);
taskGreen_state = OFF;
}
}
if (taskGreen_state == OFF)
{
if (millis() - ts >= 500)
{
ts = millis();
digitalWrite(PIN_PC2, HIGH);
taskGreen_state = ON;
}
}
}
|
ความต้องการของการเขียนโค้ดตามแบบด้านขวามือนั้นคือการทำให้ taskGreen
ประมวลผลและออกจากฟังก์ชันให้เร็วที่สุดในการเรียกแต่ละครั้ง โดยจดจำสถานะปัจจุบันของตนเอาไว้เพื่อดำเนินการต่อในการถูกเรียกประมวลผลครั้งถัดไป โค้ดที่ได้จึงมีลักษณะเป็นคำสั่ง if หลาย ๆ คำสั่งกระจายอยู่ทั่วไปในฟังก์ชัน และบดบังลำดับการทำงานที่ต้องการจนแทบไม่เหลือเค้าเดิม ส่วนโค้ดทางด้านซ้ายนั้นแม้จะสื่อให้เห็นถึงพฤติกรรมการทำงานได้อย่างชัดเจน แต่การวนลูปแบบไม่รู้จบและหยุดรอของคำสั่ง delay นั้นเป็นสิ่งที่ยอมรับไม่ได้ในการทำงานแบบมัลติทาสกิ้ง
ทางเลือกที่ใช้ข้อดีจากทั้งสองฝ่ายคือการหากลไกที่ทดแทนคำสั่ง delay โดยให้โปรแกรมทำงานต่อในบรรทัดถัดไปเมื่อเวลาผ่านไปครบถ้วนแล้ว และบังคับให้ออกจากฟังก์ชันทันทีหากยังไม่ถึงเวลาหน่วงที่กำหนดแต่ให้บันทึกตำแหน่งปัจจุบันเอาไว้ เพื่อที่ว่าการเรียกฟังก์ชันครั้งถัดไปจะได้กระโดดมาทำงาน ณ จุดที่ออกจากฟังก์ชันไปต่อได้ แนวคิดนี้ทำได้โดยอาศัยคำสั่ง goto
ซึ่งมีให้อยู่แล้วในภาษาซี
#include <Practicum.h>
void* taskGreen_state;
//////////////////////////////////////////////////
// เครื่องจักรสถานะสำหรับ LED สีเขียว
//////////////////////////////////////////////////
void taskGreen()
{
static uint32_t ts;
// กระโดดไปยังตำแหน่งที่บันทึกเอาไว้ก่อนออกจากฟังก์ชันครั้งล่าสุด (ถ้ามี)
if (taskGreen_state != NULL) goto *taskGreen_state;
while (1)
{
digitalWrite(PIN_PC2, HIGH);
// สามบรรทัดนี้ทำงานเทียบเท่ากับการใช้ delay(1000) โดยไม่หยุดรอ
ts = millis();
ON: taskGreen_state = &&ON; // จดจำบรรทัดปัจจุบัน
if (millis()-ts < 1000) return;
digitalWrite(PIN_PC2, LOW);
// สามบรรทัดนี้ทำงานเทียบเท่ากับการใช้ delay(500) โดยไม่หยุดรอ
ts = millis();
OFF:taskGreen_state = &&OFF; // จดจำบรรทัดปัจจุบัน
if (millis()-ts < 500) return;
}
}
//////////////////////////////////////////////////
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);
taskGreen_state = NULL;
}
//////////////////////////////////////////////////
void loop()
{
taskGreen();
}
- หมายเหตุ
- โค้ดด้านบนคอมไพล์และรันได้จริง
- การใช้คำสั่ง
goto
ในรูปแบบด้านบนเรียกว่าการทำ computed goto ซึ่งไม่จัดอยู่ในภาษาซีมาตรฐาน แต่รองรับโดยคอมไพเลอร์ส่วนใหญ่เช่น GCC คำสั่งในภาษาซีมาตรฐานที่มีพฤติกรรมใกล้เคียง computed goto มากที่สุดคือคำสั่ง switch..case - แม้ฟังก์ชัน
taskGreen
จะมีลูปอนันต์อยู่ จะมีการออกจากฟังก์ชันเสมอด้วยคำสั่งreturn
จึงทำให้ทำงานร่วมกับงานอื่นแบบมัลติทาสกิ้งได้ - เป็นที่ถกเถียงกันเรื่องความเลวร้ายของการใช้คำสั่ง goto แต่การใช้คำสั่งนี้ในสถานการณ์ที่เหมาะสมจะทำให้โปรแกรมสั้นและทำความเข้าใจได้ง่ายขึ้น
ฟังก์ชัน taskGreen
ที่เขียนใหม่นั้นยังสามารถถูกมองในรูปของเครื่องจักรสถานะได้ เพียงแต่สถานะต่าง ๆ นั้นถูกฝังลงไปในจุดต่าง ๆ ของโค้ด ณ ตำแหน่งที่มีการกำกับด้วย ON:
และ OFF:
ลองเปรียบเทียบโค้ดที่ปรับแก้แล้วกับโค้ดดั้งเดิมและโค้ดที่เขียนขึ้นจากเครื่องจักรสถานะตรง ๆ
โค้ดเครื่องจักรสถานะแบบใช้ goto | โค้ดดั้งเดิม (แบบใช้ delay) | โค้ดเครื่องจักรสถานะแบบใช้ if |
---|---|---|
void taskGreen()
{
static uint32_t ts;
if (taskGreen_state != NULL)
goto *taskGreen_state;
while (1)
{
digitalWrite(PIN_PC2, HIGH);
// เทียบเท่า delay(1000)
ts = millis();
ON: taskGreen_state = &&ON;
if (millis()-ts < 1000) return;
digitalWrite(PIN_PC2, LOW);
// เทียบเท่า delay(500)
ts = millis();
OFF:taskGreen_state = &&OFF;
if (millis()-ts < 500) return;
}
|
void taskGreen()
{
while (1)
{
digitalWrite(PIN_PC2, HIGH);
delay(1000);
digitalWrite(PIN_PC2, LOW);
delay(500);
}
}
|
void taskGreen()
{
static uint32_t ts = 0;
if (taskGreen_state == ON)
{
if (millis() - ts >= 1000)
{
ts = millis();
digitalWrite(PIN_PC2, LOW);
taskGreen_state = OFF;
}
}
if (taskGreen_state == OFF)
{
if (millis() - ts >= 500)
{
ts = millis();
digitalWrite(PIN_PC2, HIGH);
taskGreen_state = ON;
}
}
}
|
จะเห็นว่าโค้ดรูปแบบใหม่ทางด้านซ้ายมือแม้ว่าจะยาวกว่าเดิมก็ตาม แต่ก็มีลักษณะที่คล้ายคลึงกับโค้ดดั้งเดิมในช่องกลางมากขึ้น โดยเฉพาะอย่างยิ่งถ้ามีการรวบ 3 คำสั่งที่คอมเม้นต์ไว้ว่าทำงานเทียบเท่าคำสั่ง delay เอาไว้ให้เป็นคำสั่งเดียวได้จะได้โค้ดภายในลูป while ที่เหมือนกันเกือบทุกประการกับโค้ดเดิม จึงเหลือเพียงการทำให้โค้ดดูง่ายขึ้นโดยการรวมคำสั่งหลาย ๆ คำสั่งให้เสมือนเป็นคำสั่งเดียวโดยใช้ระบบมาโครของภาษาซี ทั้งหมดนี้มีโปรแกรมเมอร์หลายคนได้พัฒนาเอาไว้พร้อมใช้งานในรูปไลบรารีเรียบร้อยแล้ว หนึ่งในนั้นคือไลบรารี Protothreads
ไลบรารี Protothreads
Protothreads พัฒนาขึ้นโดย Adam Dunkels โดยมีวัตถุประสงค์เพื่อให้นักพัฒนาโค้ดสามารถเขียนโปรแกรมแบบมัลติทาสกิ้งในรูปแบบที่เข้าใจได้ง่ายบนอุปกรณ์ที่มีทรัพยากรจำกัดอย่างเช่นไมโครคอนโทรลเลอร์ ไลบรารีนี้นำเอาเทคนิคการบันทึกสถานะร่วมกับการใช้คำสั่ง goto ตามที่ได้กล่าวไปแล้วมารวมไว้เป็นชุดมาโครในภาษาซี ทำให้รูปแบบโค้ดของงานย่อยแต่ละงานมีลักษณะราวกับการทำงานแบบซิงเกิลทาสก์
การติดตั้งไลบรารี Protothreads เพื่อใช้งานร่วมกับ Arduino
ดาวน์โหลดไฟล์ [1] แล้วแตกเอาไว้ในไดเรคตอรี ~/sketchbook/libraries/
$ cd ~/sketchbook/libraries/ $ wget http://www.cpe.ku.ac.th/~cpj/204223/pt.tgz $ tar zxf pt.tgz
ตัวอย่างโปรแกรม
โปรแกรมต่อไปนี้เขียนขึ้นโดยอาศัยไลบรารี Protothreads ซึ่งให้พฤติกรรมการทำงานที่เหมือนกับตัวอย่างในหัวข้อที่แล้วทุกประการ
#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
นั่นเอง