Lập Trình Với Máy Trạng Thái (Phần 1)

(Phần 1) , (Phần 2)

1. Khái Niệm Về Máy Trạng Thái (State Machine)

Một hệ thống nhúng thường được thiết kế để thực hiện một công việc theo một quy trình cố định, lặp đi lặp lại. Tùy theo trạng thái các ngõ vào (input) và trạng thái (state) hiện hành của hệ thống mà các ngõ ra (output) sẽ có các giá trị thích hợp.

Ta lấy ví dụ như một hệ thống điều khiển máy bơm cho một bồn nước trong nhà như ở Hình 1.

Hình 1: Sơ đồ hệ thống bồn nước – statemachine_example1

Hệ thống sẽ có input là hai cảm biến mực nước (sensor) cho bồn chứa, một input điều khiển relay bật/tắt máy bơm. Giả sử khi có nước ngập qua, output của sensor sẽ bằng 1, và khi ngõ điều khiển bơm bằng 1, máy bơm sẽ chạy.

Hoạt động của hệ thống diễn ra như sau:

  • Nếu mực nước xuống thấp hơn sensor 1, hệ thống sẽ bơm cho đến khi mực nước vượt quá sensor hai thì dừng.
  • Khi hệ thống khởi động, nếu mực nước ở giữa sensor A và sensor B, hệ thống sẽ không bơm.

Khi phân tích tương quan giữa output điều khiển máy bơm và hai input từ sensor, ta thấy:

  • Khi mực nước xuống thấp hơn sensor 1, hệ thống sẽ bật máy bơm.
  • Khi mực nước vượt quá sensor 2, hệ thống sẽ tắt máy bơm.
  • Tuy nhiên, khi mực nước ở giữa hai sensor, tùy theo trạng thái của hệ thống lúc đó mà máy bơm sẽ được bật hay tắt. Nếu trước đó hệ thống đang bơm, thì khi mực nước đạt đến giữa hai sensor hệ thống vẫn bật máy bơm. Ngược lại, nếu trước đó máy bơm đang tắt thì khi mực nước nằm ở giữa hai sensor, hệ thống vẫn tắt máy bơm.
  • Nói cách khác, output của hệ thống không chỉ phụ thuộc vào input mà còn phụ thuộc vào trạng thái hiện hành của hệ thống.

Một hệ thống có tính chất như trên gọi là một hệ tuần tự. Để dễ dàng mô tả hoạt động của hệ thống, ta hay dùng máy trạng thái (state machine).

Máy trạng thái điều khiển máy bơm được mô tả như Hình 2.

Ta thấy bộ điều khiển chỉ có 2 trạng thái: STOP PUMP và PUMP. Khi reset, hệ thống đi vào trạng thái STOP PUMP. Khi mực nước xuống quá sensor 1 làm sensor 1 bằng 0, hệ thống chuyển sang trạng thái PUMP. Khi đang ở trạng thái PUMP, nếu mực nước vượt quá sensor 2 (sensor = 1), hệ thống chuyển sang trạng thái STOP PUMP.

(0: no water, 1: have water)
Sensor 1 | Sensor 2 | Pump State
---------+----------+-----------
0        |  0       |  PUMP
1        |  0       |  PUMP
1        |  1       |  STOP PUMP

Ở trạng thái STOP PUMP, PumpCtrl output bằng 0 và ở trạng thái PUMP, output này bằng 1. Ta thấy output này chỉ phụ thuộc vào trạng thái của hệ thống, không phụ thuộc vào input. Máy trạng thái này có output chỉ phụ thuộc trạng thái, được gọi là máy trạng thái kiểu Moore (Moore Machine). PumpCtrl output gọi là ngõ ra kiểu Moore.

Hình 2: Máy trạng thái cho bộ điều khiển máy bơm

Nếu output của máy trạng thái phụ thuộc cả vào trạng thái và input, output đó gọi là ngõ ra kiểu Mealy (Mealy Machine). Một hệ tuần tự có thể có cả hai loại output.

2. Lập Trình Hệ Thống Sử Dụng Máy Trạng Thái

Như đã nói ở trên, hầu hết các hệ thống nhúng được thiết kế để làm việc như một hệ tuần tự, thực hiện một công việc theo một chu trình cố định. Vì vậy máy trạng thái là công cụ hết sức hữu ích khi mô tả, thiết kế và thực thi hệ thống.

Ở phần 1, ta đã nói về máy trạng thái và mô tả một ví dụ trong đó ta dùng máy trạng thái để mô tả một bộ điều khiển máy bơm nước. Ở phần này, ta sẽ đi vào lập trình máy trạng thái đã mô tả ở trên.

Hình 3 mô tả hàm stateMachineUpdate(), là hàm thực thi các chuyển trạng thái và các ngõ ra của máy trạng thái.

Các trạng thái được định nghĩa bằng kiểu state_t, là một tập hợp các trạng thái có thể có, trong ví dụ này là hai trạng thái S_STOPPUMP, S_PUMP. Biến trạng thái state được khai báo trong file state.c, có kiểu static vì biến này chỉ được sử dụng nội bộ trong module state.c.

Các hàm sensorl()sensor2() được gọi để lấy được trạng thái của các sensor. Trong ví dụ này, ta mô phỏng hai sensor sử dụng hai nút nhấn LEFT và RIGHT trên board Tiva-C launchpad. Hàm pumpCtrl() được dùng để điều khiển motor, ở đây ta mô phỏng bằng LED xanh lục trên board.

Hàm initState() sẽ khởi động biến state ở trạng thái S_STOPPUMP, là trạng thái khi khởi động. Các chuyển trạng thái được thực thi bằng lệnh switchcase ở đầu hàm stateMachineUpdate(). Các chuyển trạng thái phụ thuộc vào trạng thái hiện tại và các ngõ vào như ở Hình 2.

// state.h
typedef enum {S_STOPPUMP, S PUMP} state_t;
typedef enum {WATERUNDER, WATEROVER} sensor_t;

//function prototype
void  initState(void); 
void stateMachineUpdate(void);
void SensorsInit(void);
sensor_t sensorl(void);
sensor_t sensor2(void);
void pumpCtrlInit(void);
void pumpCtrl(int state);
// state.c
#include "state.h"
static state_t State;
void initState()
{
  State = S_STOPPUMP;
}

void stateMachineUpdate(void)
{
  switch (State)
  {
    case S_STOPPUMP: 
      if (sensorl() == WATERUNDER) 
        State = S PUMP;
      break;
    case S_PUMP:
      if (sensor2() == WATEROVER) 
      State = S_STOPPUMP;
      break;
  }

  switch (State)
  {
    case S_STOPPUMP: 
      pumpCtrl(PUMPOFF); 
      break;
    case S_PUMP:
      pumpCtrl(PUMPON);
      break;
  }
}

Hình 3: Thực thi máy trạng thái trong ví dụ statemachine_example1, file state.h và state.c

Sau khi các trạng thái được cập nhật, các output được cập nhật dựa vào trạng thái hiện tại và các input cũng bằng một lệnh switchcase. Ở đây các output là output kiểu Moore, nên chúng chỉ phụ thuộc vào trạng thái hiện tại.

Sau khi cấu hình các ngoại vi, trạng thái được khởi động ở giá trị S_STOPPUMP bằng lệnh initState(), máy trạng thái được cập nhật liên tục bằng cách gọi hàm stateMachineUpdate() trong một vòng lặp vô tận trong module main.c như ở Hình 4.

// main.c
#include "state.h" 
int main(void)
{
  SensorsInit();
  pumpCtrlInit();
  pumpCtrl(PUMPOFF);
  initState();
  while (1)
  {
    stateMachineUpdate();
  }
}

Hình 4: Hàm main của ví dụ statemachine_example1

3. Máy trạng thái có sự kiện thời gian

Các chuyển trạng thái của máy trạng thái ở trên chỉ phụ thuộc các tín hiệu input.Trong thực tế, thông thường một hệ thống nhúng ngoài các tín hiệu input, còn phụ thuộc vào các sự kiện liên quan đến thời gian.

Máy trạng thái cho hệ thống này ngoài các input còn phải có một software timer, đây là timer thực thi bằng phần mềm chứ không phải là các ngoại vi timer phần cứng gắn trên CPU.

Cách thông thường nhất để thực thi timer là sử dụng một timer phần cứng để tạo ra một ngắt sau mỗi khoảng thời gian nhất định. Cứ mỗi khi xảy ra ngắt timer thì số đếm của software timer sẽ bị trừ đi 1 cho đến khi nó bằng 0. Với cách này, độ phân giải của software timer chính là thời gian giữa hai lần ngắt timer. Các dòng ARM Cortex hỗ trợ ngắt Systick, rất phù hợp cho việc tạo ra soffware timer. Với các vi điều khiển không hỗ trợ ngắt System Tick như họ 8051 hay PIC, ta có thể sử dụng bất cứ timer phần cứng nào để tạo ngắt.

Độ phân giải của software timer phụ thuộc vào yêu cầu, cách thiết kế hệ thống và các timer phần cứng của CPU.

Ta lấy ví dụ một hệ thống điều khiển máy phát điện tự động như sau:

  • Đầu tiên khi khởi động, máy phát điện tắt
  • Nếu như điện lưới bị mất trong thời gian 20s, hệ thống bật máy phát điện.
  • Nếu máy phát điện đang chạy mà điện lưới có lại trong thời gia 20s, tắt máy phát điện.
  • Trạng thái điện lưới được báo bởi tín hiệu GRID_ST. Nếu tín hiệu này bằng 0 thì có nghĩa là có điện lưới.
  • Máy phát được điều khiển bởi tiến hiệu GEN_CTRL.
  • Trong ví dụ example_stateMachine_2, tín hiệu GRID_ST được mô phỏng bởi nút nhấn SW1, GEN_CTRL được mô phỏng bởi LED RED có sẵn trên board mạch.

Hình 5: Máy trạng thái cho ví dụ example_stateMachine_2

Hình 5 mô tả máy trạng thái cho bộ điều khiển trên. Khi hệ thống đang ở trạng thái GRID_ON, nếu tín hiệu GRID_ST mang giá trị OFF (mất điện lưới), hệ thống chuyển sang trạng thái GRID_OFF_WAIT và bật timer với giá trị 20. Timer sẽ giảm đi 1 sau mỗi giây. Trong hai trạng thái GRID_ONGRID_OFF_WAIT, máy phát không chạy.

Khi đang ở trạng thái GRID_OFF_WAIT, sau thời gian 20s (timer bằng 0) hệ thống sẽ chuyển sang trạng thái GRID_OFF. Nếu trong khoảng thời gian này mà điện lưới có lại, hệ thống sẽ chuyển về trạng thái GRID_ON.

Khi hệ thống đang ở trạng thái GRID_OFF, nếu tin hiệu GRID_ST mang giá trị ON (có diện lưới), hệ thống chuyển sang trạng thái GRID_ON_WAIT và bật timer với giá trị 20. Timer sẽ giảm đi 1 sau mỗi giây. Trong hai trạng thái GRID_OFFGRID_OFF_WAIT, máy phát được chạy.

Khi đang ở trạng thái GRID_OFF_WAIT, sau thời gian 20s (timer bằng 0) hệ thống sẽ chuyển về trạng thái GRID_ON. Nếu trong khoảng thời gian này mà điện lưới lại bị mất, hệ thống sẽ chuyển về trạng thái GRID_OFF.

Ở đây, ta có thể chọn độ phân giải cho timer là một giây. Đương nhiên, ta có thể chọn độ phân giải nhỏ hơn như là 0.5s hay lớn hơn như hai giây, thậm chỉ là 20s vì các độ dài thời gian được yêu cầu ở đây là 20s.

//state.h
typedef enum {S_GRIDON, S_GRIDOFF_WAIT, S_GRIDOFF, S_GRIDON_WAIT} genState_t;
typedef enum {S_LEDON, S_LEDOFF} ledState_t;
typedef enum {GRIDON, GRIDOFF} gridState_t;
#define GENON 1
#define GENOFF 0

//function prototype
void initGenState(void);
void genStateMachineUpdate(void); 
void SensorsInit(void);
gridState_t gridState(void);
void genCtrlInit(void ); 
void genCtrl(int state);
//state.c 
#include "state.h"

static genState_t genState;
static unsigned int genTimerCount;

void genStateMachineUpdate(void)
{
  switch (genState)
  {
    case S_GRIDON:
      if (gridstate() == GRIDOFF)
      {
        genState S_GRIDOFF_WAIT:
        DBG("state changed from S_GRIDON to S_GRIDOFF_WAIT \n").
        genTimerCount = 20;
      }
      break;
    case S_GRIDOFF_WAIT:
      if (gridState() == GRIDON)
      {
        genState = S_GRIDON; 
        DBG("state changed from S_GRIDOFF_WAIT to S_GRIDON \n");
      }
      else if (genTimerCount == 0)
      {
        genState = S_GRIDOFF; 
        DBG("state changed from S_GRIDOFF_WAIT to S_GRIDOFF \n");
      }
      break;
    case S_GRIDOFF:
      if (gridState() == GRIDON)
      {
        genState = S_GRIDON_WAIT; 
        DBG("state changed from S_GRIDOFF to S_GRIDON_WAIT \n"); 
        genTimerCount = 20;
      }
      break;
    case S_GRIDON_WAIT: 
      if (gridState() == GRIDOFF)
      {
        genState = S_GRIDOFF:
        DBG("state changed from S_GRIDON_WAIT to S_GRIDOFF \n");
      }
      else if (genTimerCount = 0)
      {
        genState = S_GRIDON; 
        DBG("state changed from S_GRIDON_WAIT to S_GRIDON \n");
      }
      break;
  }

  switch (genState)
  {
    case S_GRIDON:
    case S_GRIDOFF_WAIT:
      genCtrl(GENOFF); 
      break;
    case S_GRIDOFF: 
    case S_GRIDON_WAIT:
      genCtrl(GENON);
      break;
  }
}

void SysTickIntHandler(void)
{
  //
  // Update the Timer counter.
  //
  if (genTimerCount != 0)
  {
    genTimerCount--; 
    DBG("genTimerCount = %d\n",genTimerCount);
  }
}

Hình 6: mô tả cách thực hiện máy trạng thái cho ví dụ example_statemachine_2, file state.h và state.c

Trình phục vụ ngắt SysTickIntHandler sẽ kiểm tra giá trị của biến gentimerCount mỗi giây một lần, nếu biến này khác 0 nó sẽ trừ biến này đi 1. Đây là cách để ta thực thi soffware timer với độ phân giải là 1s.

Tương tự như ví dụ example_statemachire_1Hình 3, hàm stateMachineUpdate sẽ thực hiện các chuyển trạng thái theo input và trạng thái software timer, đồng thời điều khiến output. Tương tự như dụ example_statemachire_1, sau khi khởi động hệ thống, hàm này được gọi liên tục trong module main.c như ở Hình 7.

#include "state.h" 
#ifdef DEBUG
void InitConsole(void)
{
  //
  // Enable GPIO port A which is used for UART0
  // pins.
  // TODO: change this to whichever GPIO port you
  // are using.
  //
  SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
  GPIOPinConfigure(GPIO_PA0_U0RX);
  GPIOPinConfigure(GPIO_PA1_U0TX);
  GRIOPinTypeUART(GPIO_PORTA_BASE, GPIO_PIN_0 | GPIO_PIN_1);

  //
  // Initialize the OART for console 1/0.
  //
  UARTStdioInit(0);
#endif
  
int main(void)
{
  SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_USE_OSC | SYSCTL_OSC_MAIN | SYSCTL_XTAL_16MHZ);
#ifdef DEBUG 
  InitConsole();
#endif
  // Set up the period for the SysTick timer. 
  // The SysTick timer period will 
  // be equal to the system clock, resulting in a 
  // period of 1 second.
  //
  SysTickPeriodSet(SysCtlClockGet());
  IntMasterEnable();
  SysTickIntEnable();
  SysTickEnable();

  SensorsInit();
  genCtrlInit();
  genCtrl(GENOFF);
  initGenState();
  while (1)
  {
    genStateMachineUpdate();
  }
}

Hình 7: Hàm main cho ví dụ example_statemachine_2

Tiếp theo – 4. Xử lý đa tác vụ với máy trạng thái

1 thought on “Lập Trình Với Máy Trạng Thái (Phần 1)”

  1. Pingback: Lập Trình Với Máy Trạng Thái (Phần 2) - Tạ Lục Gia Hoàng

Comments are closed.

Icons made by Freepik from www.flaticon.com