loc4atnt gửi vào
- 13300 lượt xem
Xin chào các bạn! Hôm nay mình sẽ giới thiệu với các bạn một cách lập trình, quản lý code khá thú vị và mới lạ, đó chính là State Machine hay trạng thái máy. Đây là một cách thức lập trình cũng được sử dụng khá nhiều cho các hệ thống, phần mềm, máy móc trong thực tế. Dưới đây, mình chỉ viết những gì mình biết và tìm hiểu được nên có gì sai sót, mong các bạn đã biết về state machine hãy góp ý cho mình bên dưới phần comment để bài viết hoàn thiện hơn. Bắt đầu thôi!
I. State machine là gì?
State machine là một cách thức lập trình theo mô hình quản lý quá trình chuyển đổi trạng thái của thiết bị. Với trạng thái máy, chúng ta sẽ tiếp cận vấn đề bằng cách chia quá trình vận hành của thiết bị thành các trạng thái, mỗi trạng thái gồm: Các hành động được thực hiện khi thiết bị ở trạng thái này, sự chuyển đổi trạng thái và điều kiện kích hoạt sự chuyển đổi trạng thái.
Sơ đồ mô tả
Ưu điểm: Với state machine chúng ta sẽ dễ dàng tiếp cận các vấn đề hơn, code được mạch lạc, logic hơn, đồng thời mọi người sẽ dễ hiểu hơn mô hình hoạt động của thiết bị của chúng ta, để qua đó dễ sửa đổi, bảo trì và nâng cấp thiết bị. Đặc biệt, với trạng thái máy, chúng ta có thể dễ dàng tạo ra được các tiến trình bất đồng bộ (Các chức năng chạy song song với nhau).
Việc chia ra các trạng thái còn được ứng dụng để tối ưu hóa tài nguyên (phần cứng, phần mềm) tốt hơn. Cụ thể, tại mỗi trạng thái, thiết bị chỉ cung cấp các tài nguyên đủ để thực hiện các hoạt động trong trạng thái đó, còn các tài nguyên khác sẽ được đưa vào trạng thái nghỉ.
Chính vì những ưu điểm này mà state machine rất hay được sử dụng trong các chương trình lập trình lớn.
Nói lý thuyết thì khó để hiểu lắm hehe nên ta đi thẳng vào vận hành luôn nhá!
II. Chuẩn bị
Trong bài viết này, mình cần các bạn chuẩn bị một số linh kiện đơn giản sau:
- Arduino - Chắc chắn rồi :D
- 2 bóng led
- 1 nút nhấn
- Điện trở, dây nối, breadboard
III. Ví dụ số 1 - Bài tập Hello World huyền thoại của Arduino:
Chúng ta cùng bắt đầu với bài tập cơ bản nhất của giới Arduino nha - Blink Led <3.
Công việc đầu tiên khi dùng phương pháp state machine để lập trình là chia hoạt động của thiết bị thành các trạng thái và mô tả thành một sơ đồ khối. Mình chia chức năng blink led thành sơ đồ như dưới đây (Màu đen là tên trạng thái, màu xanh là các hành động, màu đỏ là điều kiện và các hướng chuyển đổi trạng thái):
Lưu ý: Trong state machine, chúng ta không được sử dụng delay nha :D
Sau khi đã có sơ đồ trạng thái, chúng ta sẽ triển khai sơ đồ ấy thành code. Có 2 cách để viết code state machine: Dùng cú pháp switch case (Hoặc if else) và Dùng con trỏ (trỏ đến 1 chương trình con).
1. Sử dụng cú pháp Switch Case:
Code đây, mình có giải thích trong code rồi, không hiểu gì cứ comment nhe :D
// Khai báo (Định nghĩa) các trạng thái #define RESET 0 #define CHECK_TIMES 1 #define CHECK_LED_STATE 2 #define TURN_ON_LED 3 #define TURN_OFF_LED 4 #define BLINK_TIME 1200 unsigned long t0, t1; boolean ledState; int ledPin = 8; // Biến lưu trạng thái hiện tại của chương trình byte state; void setup() { pinMode(ledPin, OUTPUT); state = RESET;// Trạng thái ban đầu là Reset } void loop() { runningFunc();// Chạy hàm xử lý chức năng blink led } void runningFunc() { switch(state) { case RESET:// Nếu trạng thái là RESET // Các hành động của trạng thái RESET t0 = millis(); // Sự chuyển đổi trạng thái if (true)// Điều kiện chuyển trạng thái: True state = CHECK_TIMES;// Chuyển sang trạng thái CHECK_TIMES break; case CHECK_TIMES:// Nếu trạng thái là CHECK_TIMES // Các hành động của trạng thái CHECK_TIMES t1 = millis(); // Sự chuyển đổi trạng thái if ( (t1 - t0) > BLINK_TIME ) state = CHECK_LED_STATE; break; case CHECK_LED_STATE:// Nếu trạng thái là CHECK_LED_STATE // Các hành động của trạng thái CHECK_LED_STATE ledState = digitalRead(ledPin); // Sự chuyển đổi trạng thái if (ledState == HIGH)// Điều kiện chuyển trạng thái: Led sáng state = TURN_OFF_LED;// Chuyển sang trạng thái TURN_OFF else// Điều kiện chuyển trạng thái: Led tắt state = TURN_ON_LED;// Chuyển sang trạng thái TURN_ON break; case TURN_ON_LED:// Nếu trạng thái là TURN_ON_LED // Các hành động của trạng thái TURN_ON_LED digitalWrite(ledPin, HIGH); if (true) state = RESET; break; case TURN_OFF_LED:// Nếu trạng thái là TURN_OFF_LED // Các hành động của trạng thái TURN_OFF_LED digitalWrite(ledPin, LOW); if (true) state = RESET; break; } }
Tuy nhiên, với cú pháp switch case, khi chạy chương trình phải kiểm tra từ case đầu tiên đến case trạng thái hiện tại của thiết bị. Vì vậy, cách này chưa được coi là tối ưu. Để giải quyết vấn đề này, làm cho chương trình thực hiện thẳng các hành động của trạng thái hiện tại mà không phải trải qua bước kiểm tra xem trạng thái hiện tại là trạng thái gì, chúng ta sẽ sử dụng con trỏ. Tiếp tục nào!
2. Sử dụng con trỏ:
Nếu chưa biết con trỏ là gì, thì bạn hãy google tìm hiểu trước rồi hãy quay lại đây sau nhé, từ khóa: "con trỏ trong lập trình c".
Chúng ta sẽ sử dụng 1 con trỏ trỏ tới 1 chương trình con chứa các câu lệnh xử lý của một trạng thái. Để chuyển đổi trạng thái, chúng ta chỉ việc gán con trỏ đó tới chương trình con tương ứng với trạng thái mới. Như vậy, chỉ cần gọi hàm được trỏ bởi con trỏ đó thì các hành động của trạng thái đã được thực hiện mà không phải kiểm tra xem trạng thái hiện tại là gì rồi!
Code đây
#define BLINK_TIME 1200 unsigned long t0, t1; boolean ledState; int ledPin = 8; void (*runningFunc)();// Con trỏ trỏ tới hàm xử lý của một trạng thái void setup() { pinMode(ledPin, OUTPUT); runningFunc = &resetFunc;// Trạng thái ban đầu là Reset } void loop() { runningFunc();// Chạy hàm được trỏ bởi con trỏ } void resetFunc() {// Hàm xử lý của trạng thái RESET // Các hành động của trạng thái t0 = millis();// Cập nhật lại mốc thời gian t0 // Sự chuyển đổi trạng thái if (true) // Điều kiện chuyển trạng thái: True runningFunc = &checkTimesFunc;// Chuyển sang trạng thái CHECK_TIMES } void checkTimesFunc() {// Hàm xử lý của trạng thái CHECK_TIMES // Các hành động của trạng thái t1 = millis(); // Sự chuyển đổi trạng thái if ( (t1 - t0) > BLINK_TIME ) runningFunc = &checkLedStateFunc; } void checkLedStateFunc() {// Hàm xử lý của trạng thái CHECK_LED_STATE // Các hành động của trạng thái ledState = digitalRead(ledPin); // Sự chuyển đổi trạng thái if (ledState == HIGH)// Điều kiện chuyển trạng thái: Led sáng runningFunc = &turnOffLedFunc;// Chuyển sang trạng thái TURN_OFF else// Điều kiện chuyển trạng thái: Led tắt runningFunc = &turnOnLedFunc;// Chuyển sang trạng thái TURN_ON } void turnOnLedFunc() {// Hàm xử lý của trạng thái TURN_ON // Sự chuyển đổi trạng thái digitalWrite(ledPin, HIGH); // Sự chuyển đổi trạng thái if (true) runningFunc = &resetFunc; } void turnOffLedFunc() {// Hàm xử lý của trạng thái TURN_OFF // Sự chuyển đổi trạng thái digitalWrite(ledPin, LOW); // Sự chuyển đổi trạng thái if (true) runningFunc = &resetFunc; }
3. Điều khiển bất đồng bộ nhiều led:
Tiếp theo, mình cần điều khiển nhiều led blink với các chu kỳ khác nhau, cụ thể mình sẽ blink 1 led với chu kỳ 1 giây, 1 led với chu kỳ 0.1 giây. Ở 2 ví dụ trên, chúng ta đã thấy chương trình blink 1 led hoạt động theo 1 mô hình các trạng thái khép kín, mình sẽ gọi đó là 1 tiến trình. Như vậy mình sẽ cần 2 tiến trình chạy song song nhau (bất đồng bộ). Với state machine, việc này cực kỳ đơn giản, các bạn chỉ việc "nhân đôi" code ở ví dụ 2 lên và sửa lại chu kỳ blink, led pin thôi :D. Cơ mà, nếu vậy thì code cực kỳ dài luôn, với 3, 4, 5, 6 ... led thì sao? Lúc này bạn sẽ cần đến sức mạnh của lập trình hướng đối tượng (Vì vậy mình cần bạn phải biết lập trình hướng đối tượng trong c++ để tiếp tục đọc bài viết này nhé)
Mình sẽ đưa phần code thực hiện tiến trình blink 1 led thành 1 class và sử dụng các đối tượng từ class đó nhé! Code đây:
File LedTask.h:
class LedTask { private: int blinkTime; unsigned long t0, t1; boolean ledState; int ledPin; void(LedTask::*runningFunc)();// Con trỏ trỏ tới hàm xử lý của một trạng thái void resetFunc() { t0 = millis(); if (true) runningFunc = &checkTimesFunc; } void checkTimesFunc() { t1 = millis(); if ( (t1 - t0) > blinkTime ) runningFunc = &checkLedStateFunc; } void checkLedStateFunc() { ledState = digitalRead(ledPin); if (ledState == HIGH) runningFunc = &turnOffLedFunc; else runningFunc = &turnOnLedFunc; } void turnOnLedFunc() { digitalWrite(ledPin, HIGH); if (true) runningFunc = &resetFunc; } void turnOffLedFunc() { digitalWrite(ledPin, LOW); if (true) runningFunc = &resetFunc; } public: void init(int ledPin, int blinkTime) { this->ledPin = ledPin; this->blinkTime = blinkTime; pinMode(ledPin, OUTPUT); runningFunc = &resetFunc;// Trạng thái ban đầu là Reset } void run() { (this->*runningFunc)();// Chạy hàm được trỏ bởi con trỏ } };
File chính:
#include "LedTask.h" LedTask ledTask1, ledTask2;// Tạo 2 đối tượng tiến trình void setup() { ledTask1.init(8, 100);// Cài đặt tiến trình 1: Blink led ở chân số 8, chu kỳ 100ms ledTask2.init(9, 1000);// Cài đặt tiến trình 2: Blink led ở chân số 9, chu kỳ 1000ms } void loop() { ledTask1.run();// Chạy tiến trình ledTask2.run();// Chạy tiến trình }
Thành quả:
IV. Ví dụ số 2:
Ví dụ tiếp theo, mình sẽ thực hiện 1 chức năng phức tạp hơn: Nhấn nút nhấn (Thời gian giữ nút dưới 1 giây) thì sẽ chuyển trạng thái tắt/bật của cả 2 led số 1 và số 2. Còn khi giữ nút nhấn (Trên 1 giây) thì sẽ chuyển trạng thái tắt/bật tiến trình blink của led số 1, đồng thời trong thời gian giữ nút, led số 2 sẽ nháy nhanh (chu kỳ 0.1 giây).
Nếu lập trình theo cách thông thường thì sẽ khá phức tạp, tuy nhiên khi áp dụng phương pháp state machine thì mọi chuyện sẽ trở nên đơn giản hơn nhiều.
Đầu tiên chúng ta cần thay đổi tiến trình blink ở ví dụ số 1 lại 1 tí để có thể tắt/bật được tiến trình này. Sơ đồ mới:
Thêm điều kiện chuyển từ trạng thái RESET => CHECK_TIMES
Sửa code class tiến trình blink led lại 1 xí (File LedTask.h mới):
class LedTask { private: int blinkTime; unsigned long t0, t1; boolean ledState; int ledPin; boolean isTurnOn;// Biến xác định trạng thái tắt/bật của tiến trình void(LedTask::*runningFunc)();// Con trỏ trỏ tới hàm xử lý của một trạng thái void resetFunc() { t0 = millis(); if (isTurnOn) runningFunc = &checkTimesFunc; } void checkTimesFunc() { t1 = millis(); if ( (t1 - t0) > blinkTime ) runningFunc = &checkLedStateFunc; } void checkLedStateFunc() { ledState = digitalRead(ledPin); if (ledState == HIGH) runningFunc = &turnOffLedFunc; else runningFunc = &turnOnLedFunc; } void turnOnLedFunc() { digitalWrite(ledPin, HIGH); if (true) runningFunc = &resetFunc; } void turnOffLedFunc() { digitalWrite(ledPin, LOW); if (true) runningFunc = &resetFunc; } public: void init(int ledPin, int blinkTime) { this->ledPin = ledPin; this->blinkTime = blinkTime; isTurnOn = false; pinMode(ledPin, OUTPUT); runningFunc = &resetFunc;// Trạng thái ban đầu là Reset } void run() { (this->*runningFunc)();// Chạy hàm được trỏ bởi con trỏ } // Đổi trạng thái tắt/bật tiến trình (Task) void toggle(boolean toggleState) { isTurnOn = toggleState; // Nếu tắt task thì tắt luôn cả led và chuyển về trạng thái reset if (toggleState == false) { runningFunc = &resetFunc; digitalWrite(ledPin, 0); } } boolean isTaskOn() { return isTurnOn; } };
Với ví dụ số 2, mình sẽ chia ra 3 tiến trình: 2 tiến trình xử lý blink 2 led có sơ đồ trạng thái như trên và 1 tiến trình xử lý nút nhấn và có sơ đồ mô tả trạng thái như sau:
Code chính Arduino:
#include "LedTask.h" #define btnPin 2// Chân nút nhấn #define led1Pin 8 #define led2Pin 9 LedTask ledTask1, ledTask2;// Tạo 2 đối tượng tiến trình blink led void (*runningFunc)();// Con trỏ trỏ tới hàm xử lý của một trạng thái (Tiến trình 3 - nút nhấn) unsigned long t0, t1; void setup() { pinMode(btnPin, INPUT_PULLUP); ledTask1.init(led1Pin, 1000);// Cài đặt tiến trình 1: Blink led ở chân số 8, chu kỳ 1000ms ledTask2.init(led2Pin, 30);// Cài đặt tiến trình 2: Blink led ở chân số 9, chu kỳ 30ms runningFunc = &reset;// Trạng thái ban đầu của tiến trình 3: Reset } void loop() { runningFunc();// Chạy hàm được trỏ bởi con trỏ của tiến trình 3 ledTask1.run();// Chạy tiến trình 1 ledTask2.run();// Chạy tiến trình 2 } void reset() {// Trạng thái RESET t0 = millis(); t1 = t0; if (true) runningFunc = &checkButton; } void checkButton() {// Trạng thái CHECK_BUTTON boolean btnState = digitalRead(btnPin); if (btnState == LOW) // Nút được nhấn runningFunc = &updateHoldTime; else runningFunc = &checkHoldBtnTime; } void updateHoldTime() {// Trạng thái UPDATE_HOLD_TIME t1 = millis(); unsigned long deltaTime = t1 - t0; if (deltaTime > 1000) { runningFunc = &turnOnLed2BlinkTask; } else { runningFunc = &checkButton; } } void turnOnLed2BlinkTask() {// Trạng thái TURN_ON_BLINK_LED2 ledTask2.toggle(true); if (true) runningFunc = &checkButton; } void checkHoldBtnTime() {// Trạng thái CHECK_HOLD_TIME if (ledTask2.isTaskOn()){ ledTask2.toggle(false); } unsigned long deltaTime = t1 - t0; if (deltaTime > 1000) runningFunc = &toggleLed1BlinkTask; else if (deltaTime > 10)// debounce button (Giữ nút trên 10ms mới tính là 1 lần nhấn) runningFunc = &toggleBoth2Led; else runningFunc = &reset; } void toggleLed1BlinkTask() {// Trạng thái TOGGLE_BLINK_LED1_TASK boolean isLed1BlinkTaskOn = ledTask1.isTaskOn(); ledTask1.toggle(!isLed1BlinkTaskOn); if (true) runningFunc = &reset; } void toggleBoth2Led() {// Trạng thái TOGGLE_2_LED if (ledTask1.isTaskOn()){ ledTask1.toggle(false); } toggleLed(led1Pin); toggleLed(led2Pin); if (true) runningFunc = &reset; } void toggleLed(int pin) { boolean lastState = digitalRead(pin); digitalWrite(pin, !lastState); }
Thành quả:
V. Kết:
Như vậy là xong rồi. Phần còn lại là dành cho các bạn sáng tạo <3. Nếu có bất kỳ thắc mắc nào hãy comment bên dưới để mình giải đáp và hoàn thiện bài viết hơn. Cảm ơn các bạn đã theo dõi. Tạm biệt!