มัลติทาสกิ้งบนไมโครคอนโทรลเลอร์
โปรแกรมควบคุมที่ใช้ในคอมพิวเตอร์แบบฝังตัวนั้นมักต้องการให้มีการทำงานหลายส่วนขนานกันไป เรียกว่าเป็นการทำงานแบบ มัลติทาสกิ้ง (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_PC0, HIGH);
delay(700);
digitalWrite(PIN_PC0, LOW);
delay(300);
}
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 เอาไว้