State Machine Với Arduino

   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!

lên
10 thành viên đã đánh giá bài viết này hữu ích.
Các dự án được truyền cảm hứng

Bộ điều khiển PID - ứng dụng phần 2 - xe dò line dùng thuật toán PID

Tiép nối bài viết về xe dò line cảm ơn Đỗ Hữu Toàn đã viết hộ mình phần 4. hôm nay mình sẽ làm cho chiếc xe dò line đi mượt và có hồn hơn 

lên
34 thành viên đã đánh giá bài viết này hữu ích.
Các bài viết cùng tác giả

Giới thiệu về Femtoduino – Arduino nhỏ nhất hiện nay

Xin chào các bạn! Hôm nay mình xin giới thiệu với các bạn một board Arduino nhỏ nhất hiện nay, nó có tên là Femtoduino. Đây là sản phẩm của công ty Femtoduino, chứ không phải là của công ty Arduino. Nó có kích thước nhỏ hơn cả đồng xu.

lên
16 thành viên đã đánh giá bài viết này hữu ích.

Tự làm Xe điều khiển từ xa bằng Remote TV - Điều khiển xe bằng Hồng Ngoại khó hay dễ?

Đây là bài viết đầu tiên của mình nên có sai sót gì mong mọi người đóng góp. Vào vấn đề thôi ! Hiện nay, trên cộng đồng của mình đã có bài viết hướng dẫn làm xe điều khiển với cách điều khiển là dùng sóng nrf hoặc sóng bluetooth. Hôm trước mình đọc bình luận của một bạn, bạn ấy nói rằng bạn chỉ có 1 con arduino và cũng không có sờ-mát-phôn(Mình cũng thế :D), nên không thể sử dụng 2 cách điều khiển trên. Vì vậy hôm nay mình xin viết bài viết hướng dẫn làm xe điều khiển bằng remote TV (Nói chính xác hơn là bằng tín hiệu hồng ngoại) nhằm giúp cho các bạn có số phận như mình và bạn ấy.

lên
32 thành viên đã đánh giá bài viết này hữu ích.