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

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

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

Một hệ thống nhúng thường xử lý đồng thời nhiều hơn một tác vụ. Các tác vụ này có thể liên quan hoặc hoàn toàn độc lập với nhau.

Để thiết kế các hệ thống như vậy, hành vi của hệ thống được mô tả gồm nhiều tác vụ hay còn gọi là task, chạy song song với nhau. Các task này có thể chia sẻ thông tin với nhau qua các biến dùng chung (shared variable) hay qua các cơ chế truyền thông điệp (message passing).

Các tác vụ thể được thiết kế như là các máy trạng thái độc lập, chia sẻ thời gian xử lý của CPU với nhau.

Phương pháp đơn giản nhất để thực hiện các tác vụ song song với nhau là tuần tự thực hiện chúng trong một vòng lặp vô tận như sau:

while (1)
{
  Task1(); 
  Task2();
}

Hoặc

while (1)
{
  if (eventl) Task1();

  if (event2) Task2();
}

Phương pháp này sẽ được thảo luận ngay sau đây.

Phương pháp này có thể áp dụng đối với các chương trình đơn giản, trong đó các task thực hiện các chức năng đơn giản, không chia sẻ tài nguyên của hệ thống với nhau. Tuy nhiên với các tác vụ đòi hỏi xử lý phức tạp, rất khó để việc thiết kế chương trình sao cho hệ thống thỏa mãn các yêu cầu về chức năng và tốc độ đáp ứng.

Ví dụ như Task1 đòi hỏi phải chờ một nút nhấn được nhấn trước khi tiếp tục xử lý. Khi đó, Task2 phải chờ Task1 thực hiện xong mới được thực hiện. Điều này làm cho thời gian đáp ứng của Task2 phụ thuộc vào Task1 và trở nên không xác định được.

Phương pháp thứ hai là sử dụng một chương trình quản lý task, còn gọi là scheduler để quản lý các task. Tuy nhiên, với phương pháp này, việc thiết kế các task để giải quyết vấn đề thời gian đáp ứng và chia sẻ tài nguyên cũng rất khó khăn đối với các chương trình phức tạp.

Phương pháp thứ ba là sử dụng một hệ điều hành để quản lý các task. Một hệ điều hành là một đoạn chương trình có nhiệm vụ sắp xếp lịch chạy cho các task (scheduling), lưu trữ trạng thái task khi task dừng, phục hồi trạng thái của nó khi nó được chạy trở lại, cùng với các chức năng khác như cấp phát bộ nhớ, giao tiếp ngoại vi… Một hệ điều hành thời gian thực (real-time operating system) là hệ điều hành cho phép xác định thời gian đáp ứng của task, và đảm bảo rằng thời gian này luôn được thỏa mãn. Một hệ điều hành như vậy sẽ được thảo luận ở một bài viết khác.

5. Máy trạng thái độc lập chạy song song

Ở ví dụ điều khiển máy phát điện như ở Phần 3, để báo hiệu cho người sử dụng biết là hệ thống đang chạy, một LED sẽ được chớp tắt liên tục theo cách sáng trong một giây rồi tắt trong 4s. Hệ thống sẽ được thiết kế thành hai máy trạng thái độc lập nhau. Máy trạng thái thứ nhất điều khiển máy phát dựa trên trạng thái điện lưới như ở Hình 8.

Hình 8: Máy trạng thái điều khiển LED

Ở ví dụ example_statemachine_3, máy trạng thái điều khiển LED được thực thì trong cùng module state.c với máy trạng thái điều khiển máy phát.

//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) gridSt_t;

#define GENON
#define GENOFF
#define LEDON   GPIOPinWrite(GPIO PORTF_BASE, GPIO_PIN_2, 1 << 2)
#define LEDOFF  GPIOPinWrite(GPIO PORTF_BASE, GPIO PIN_2, 0)

//function prototype
void initGenState(void) ;
void genStateMachineUpdate(void);
void SensorsInit(void);
gridSt_t gridState(void);
void genCtrlInit(void);
void genCtrl(int state);
void initLedState(void);
void ledStateMachineUpdate(void);


//state.c
#include "state.h"
static genState_t genState;
static ledState_t ledState;
static unsigned int genTimerCount;
static unsigned int ledTimerCount;

void genStateMachineUpdate(void);
{
  switch(genState)
  {
    case S_GRIDON:
      if(gridstate() == GRIDOFF)
      {
        genState = S_GRIDOFF_WAIT;
        genTimerCount = 20;
      }
      break;

    case S_GRIDOFF_WAIT:
      if(gridState() GRIDON) 
        genState = S_GRIDON;
      else if(genTimerCount == 0)
        genState = S_GRIDOFF;
      break;

    case S_GRIDOFF:
      if(gridState() == GRIDON)
        genState = S_GRIDON_WAIT; 
        genTimerCount = 20;
      break;

    case S_GRIDON_WAIT:
      if(gridState() == GRIDOFF) 
        genState = S_GRIDOFF;
      else if(genTimerCount == 0) 
        genState = S_GRIDON;
      break;
  }
  
  switch(genState)
  {
    case S_GRIDON:
    case S_GRIDOFF_WAIT:  
      genCtrl(GENOFF);
      break;
    case S_GRIDOFF:
    case S_GRIDON_WAIT:
      genCtrl(GENON); 
      break;
  }  
}

void ledStateMachineUpdate()
{
  switch(ledState)
  {
    case S_LEDON:
      if(ledTimerCount == 0) 
        ledState = S_LEDOFF; 
        ledTimerCount = 4; 
      break;
    case S_LEDOFF: 
      if(ledTimerCount == 0) 
        ledState = S_LEDON; 
        ledTimerCount = 1; 
      break; 
  }

  switch(ledState) 
  {
    case S_LEDON: 
      LEDON; 
      break; 
    case S_LEDOFF: 
      LEDOFF; 
      break; 
  }
}
 
void SysTickIntHandler(void) 
{
  //
  // Update the Timer counter. 
  // 
  if(genTimerCount != 0) genTimerCount--; 
  if(ledTimerCount != 0) ledTimerCount--; 
}


//main.c 
#include "state.h"

int main(void) 
{
  SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_USE_OSC | SYSCTL_OSC_MAIN | SYSCTL_XTAL_16MHZ); 
  // Set up the period for the SysTick Timer. The SysTick Timer period will 
  // be equal to the systemm clock, resulting in the period of 1 second
  SysTickPeriodSet(SysCtlClockSet());
  IntMasterEnable();
  SysTickIntEnable();
  SysTickEnable();

  SensorsInit();
  genCtrlInit();
  initGenState();
  initLedState();
  while(1)
  {
    genStateMachineUpdate();
    ledStateMachineUpdate();
  }
} 

Hình 9: Ví dụ example_stateMachine_3

Hình 9 mô tả cách thực thi ví dụ example_statemachine_3, trong đó có hai máy trạng thái chạy song song với nhau. Hai hàm genStateMachineUpdate()ledStateMachineUpdate() được tuần tự gọi liên tục trong module main(). Thời gian thực thi hai hàm này là rất ngắn, trong hai hàm này không có những vòng lặp chờ đợi sự kiện khác xảy ra, vì vậy khi thực thi ta có thể thấy hai máy trạng thái điều khiển máy phát và LED thực thi một cách đồng thời.

6. Máy trạng thái có ngõ vào là ngõ ra của máy trạng thái khác (hierarchy state machine)

Khi thiết kế máy trạng thái mô tả một hệ thống phức tạp, thông thường ta chia máy trạng thái này thành nhiều máy trạng thái, trong đó máy trạng thái này sẽ có ngõ vào (input) là ngõ ra (output) của máy trạng thái khác (hierarchy state machine).

Ví dụ ta muốn thiết kế một hệ thống điều khiển cửa tự động như sau.

  • Khi phát hiện chuyển động, hệ thống điều khiển cửa mở bằng cách đưa chân OPENDOOR lên 1.
  • Sau khi phát hiện không còn chuyển động 10s, đóng cửa bằng cách đưa chân OPENDOOR xuống 0.
  • Để báo hiệu, trong thời gian phát hiện ra chuyển động, hệ thống xuất xung với tần số 1Hz ra output LED. Khi không có chuyển động output này bằng 0.
  • Hệ thống xem là có chuyển động nếu tín hiệu từ cảm biến bằng 1 liên tục trong 50 ms, và xem là không có chuyển động nếu tín hiệu này bằng 0 liên tục trong 50 ms.

Nếu ta thiết kế hệ thống này chỉ bằng một máy trạng thái đơn lẻ, máy trạng thái này sẽ trở nên rất phức tạp vì các ngõ ra OPENDOOR và LED có hành vi rất khác nhau. Các chuyển trạng thái sẽ rất nhiều để đảm bảo cả hai ngõ ra này đều hoạt động đúng.

Để làm cho mô hình máy trạng thái trở nên đơn giản hơn, phương pháp ở đây là chia hệ thống thành 3 máy trạng thái.

  • Máy trạng thái thứ nhất làm nhiệm vụ phát hiện chuyển động, kết quả đưa một biến trung gian mnt.
  • Máy trạng thái thứ hai điều khiển output OPENDOOR
  • Máy trạng thái thứ ba điều khiển output LED sẽ hoạt động dựa vào kết quả của máy trạng thái thứ nhất.

Các máy trạng thái này được mô tả như trên Hình 10. Máy trạng thái MotionDetector dùng biển mnt để báo xem có chuyển động hay không. Hai máy trạng thái còn lại sử dụng biến mnt như input để điều khiển cửa và LED.

Hình 10: Máy trạng thái điều khiển cửa tự động

Hình 11 mô tả cách thực thi 3 máy trạng thái trên. Như đã nói ở trên, biến mnt được dùng như là một biến chung, được cập nhật giá trị bởi hàm motionStateMachineUpdate() và được sử dụng như là input của hai hàm doorStateMachineUpdate()ledStateMachineUpdate().

Trong hàm main(), ta khởi động ngắt System Tick với chu kỳ 1ms. vì vậy đơn vị thời gian trong chương trình được tính bằng ms.

#include "state.h"

static motionDetector_t mnt; //shared variable for motion status
static motionState_t motionState;
static unsigned int motionTimerCount;
static doorState_t doorState;
static unsigned int doorTimerCount;
static ledState_t ledState; 
static unsigned int ledTimerCount;

void motionStateMachineUpdate(void)
{
  switch(motionState)
  {
    case S_NOMOTION: 
      if(motionSensor() == MOTION)
      {
        motionState = S_NOMOTION_WAIT;
        DBG("state changed from S_NOMOTION to S_NOMOTION_WAIT \n");
        motionTimerCount = 50;
      }
      break;
    case S_NOMOTION_WAIT:
      if(motionSensor() == NOMOTION)
     (
        motionState = S_NOMOTION;
        DBG("state changed from S_NOMOTION_WAIT to S_NOMOTION \n");
      }
      else if(motionTimerCount == 0)
      {  
        motionState = S_MOTION;
        DBG("state changed from S_NOMOTION_WAIT to S_MOTION \n");
      }
      break;
    case S_MOTION:
      if(motionSensor() == NOMOTION)
      {
        motionState = S_MOTION_WAIT:
        DBG("state changed from S_MOTION to S_MOTION_WAIT \n");
        motionTimerCount = 50;
      }
      break;
    case  S_MOTION_WAIT: 
      if(motionSensor() == MOTION)
      {
        motionState = S_MOTION;
        BG("state changed from S_MOTION_WAIT to S_MOTION \n");
      }
      else if(motionTimerCount == 0)
      {
        motionState = S_NOMOTION
        DBG("statechanged from S_MOTION_WAIT to S_NOMOTION \n");
      }
      break;
  }

  switch(motionState)
  {
    case S_NOMOTION:
    case S_NOMOTION_WAIT:
      mnt = D_NOMOTION;
      break;
    case S_MOTION:
    case S_MOTION_WAIT:
      mnt = D_MOTION;
      break;
  }
}

void doorStateMachineUpdate(void)
{
  switch(doorState) 
  {
    case S_CLOSE: 
      if(mnt == D_MOTION)
      {
        doorState = S_OPEN;
        DBG("state changed from S_CLOSE to S_OPEN \n");
      }
      break;
    case S_OPEN: 
      if(mnt == D_NOMOTION) 
      {
        doorState = S_OPENWAIT;
        doorTimerCount = 10000;
        DBG("state changed from S_OPEN to S_OPENWAIT \n");
      }
      break;
    case S_OPENWAIT: 
      if(mnt = D_MOTION) 
      {
        doorState = S_OPEN;
        DBG("state changed from S_OPENWAIT to S_OPEN \n");
      }
      else if(doorTimerCount = 0) 
      {
        doorState = S_CLOSE;
        DBG("state changed from S_OPENWAIT to S_CLOSE \n");
      } 
      break;
  }

  switch(doorState) 
  { 
    case S_CLOSE: 
      doorCtrl(CLOSEDOOR);
      break;
    case S_OPEN: 
    case S_OPENWAIT: 
      doorCtrl(OPENDOOR);
      break;
  }
}

void ledStateMachineUpdate() 
{
  switch(ledState) 
  {
    case S_LEDOFF:
      if((mnt == D_MOTION) && (ledTimerCount == 0)) 
      {
        ledState = S_LEDON;
        ledTimerCount = 500;
        DBG("state changed from S_LEDOFF to S_LEDON \n");
      }
      break;
    case S_LEDON: 
      if(ledTimerCount == 0) 
      {
        ledState = S_LEDOFF;
        ledTimerCount = 500;
        DBG("state changed from S_LEDON to S_LEDOFF \n");
      }
      break;
  }

  switch(ledState)
  {  
    case S_LEDOFF: 
      LEDOFF;
      break;
    case S_LEDON: 
      LEDON;
      break;
  }
}

void SysTickIntHandler(void) 
{
  // 
  // Update the Timer counter. 
  // 
  if(motionTimerCount != 0)
  {  
    motionTimerCount--;
  }
  if(doorTimerCount != 0) 
  { 
    doorTimerCount--;
  } 
  if(ledTimerCount != 0) 
  {
    ledTimerCount--;
  }
}

Hình 11: Thực thi các máy trạng thái trong ví dụ example_stateMachine_4

Các hàm cập nhật máy trạng thái trên được tuần tự gọi trong module main như Hình 12.

#include "state.h"

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 / 1000,
  // resulting in a period of 1 milisecond.
  // 
  SysTickPeriodSet(SysCtlClockGet()/1000);
  IntMasterEnable(); 
  SysTickIntEnable(); 
  SysTickEnable();  

  SensorsInit(); 
  doorCtrlInit(); 
  ledinit ; 
  initmotionState(); 
  initdoorState ;
  initLedState ; 
  while(1) 
  {
    motionStateMachineUpdate(); 
    doorStateMachineUpdate(); 
    ledStateMachineUpdate();
  }
}

Hình 12: hàm main của ví dụ example_stateltlachine_4

Trong chương trình có thực hiện các dòng gở rối (debug) bằng cách xuất thông tin ra serial port. Sensor được mô phỏng bằng SW1, output OPENDOOR được mô phỏng bằng cách LED RED và output LED được mô phỏng bằng LED BLUE (xem sơ đồ board Tiva-C launchpad)

Khi nhấn SW1, ta sẽ thấy LED RED sáng, nghĩa là chân OPENDOOR được mở lên 1 đồng thời LED BLUE nhấp nháy. Sau khi nhả SW1, LED BLUE sẽ tắt, đồng thời LED RED sáng 10 giây sau thì tắt. Hình 13 mô tả các chyển trạng thái trong quá trình chương trình thực hiện trên màn hình terminal.

Hình 13: Kết quả thực thi ví dụ example_stateMachine_4

7. Thời gian đáp ứng của chương trình

Trong các ví dụ trên, các máy trạng thái được gọi liên tục trong hàm main. Trừ khi CPU xử lý chương trình phục vụ ngắt, các hàm cập nhật máy trạng thái chiếm toàn bộ thời gian còn lại của CPU,

Với cách này, thời gian đáp ứng của CPU đối với các tín hiệu vào là nhanh nhất có thể. Tuy nhiên, một hệ thống như vậy sẽ tiêu tồn năng lượng lớn vì CPU luôn phải chạy với toàn bộ công suất,

Trong thực tế, một hệ thống được coi là “thời gian thực” hay còn gọi là “real time” khi thời gian đáp ứng của nó đối với sự kiện bên ngoài luôn nhỏ hơn một thời gian định trước, chứ không cần phải làm cho hệ thống đáp ứng quá nhanh.

Ví dụ trong trường hợp bộ điều khiển cửa đã nói ở trên, với cách thực thi như trên thì CPU hỏi liên tục giá trị input của sensor và đáp ứng ngay lập tức. Trong thực tế, chỉ cần hệ thống đáp ứng với sự thay đổi của sensor sau một khoảng thời gian nhỏ hơn 100 giây hoạt động của hệ thống thì được xem là chấp nhận được.

Vì lý do đó, các ngõ vào của hệ thống không cần phải được lấy mẫu liên tục mà chỉ cần lấy mẫu tuần hoàn các khoảng thời gian cố định hợp lý được. Thông thường, thời gian này được cho bởi thời gian của ngắt Systick và chương trình thường được viết theo cấu trúc như sau:

//state.c
int synFlag = 0;

void SysTickIntHandler(void)
{ 
  // 
  // Update the Timer counter. 
  // 
  //code goes here 
  synFlag = 1; 
}

//main.c
extern int synFlag;

int main(void) 
{
  // initialize system 
  while(1) 
  {
    while(!synFlag);
    synFlag = 0;
    updateStateMachine(); 
  }
}

Hình 14: Máy trạng thái đồng bộ theo ngắt Systick

Như trên Hình 14, máy trạng thái chỉ cập nhật sau khi ngắt Systick xảy ra. Để tiết kiệm năng lượng trong các ứng dụng đòi hỏi công suất thấp, trong thời gian chờ ngặt xảy ra chương trình sẽ đi vào trạng thái công suất thấp, ví dụ như trạng thái Sleep. Cấu trúc chương trình như sau:

//state.c

int synFlag = 0; 
void SysTickIntHandler(void)
{ 
  // 
  // Update the Timer counter. 
  // 
  //code goes here 
  synFlag = 1;
}

//main.c
extern int synFlag;

int main(void) 
{
  // initialize system 
  while(1)
  {
    while(!synFlag) gotoSleep(); 
    synFlag = 0;
    updateStateMachine();
    gotoSleep();
  }
}

Hình 15: Thêm khả năng Sleep dành cho Máy trạng thái dùng cho các ứng dụng tiết kiệm năng lượng

Bình thường, CPU sẽ đi vào trạng thái tiết kiệm năng lượng. Khi có một ngắt xảy ra, nó sẽ được “wake up”, nếu như ngắt đó không phải ngắt Systick thi CPU lại đi vào trạng thái Sleep. Khi ngắt Systick xảy ra, CPU sẽ thực hiện cập nhật máy trạng thái và lại đi vào trạng thái sleep để tiết kiệm năng lượng. Khoảng thời gian để thực hiện hàm updateStateMachine là rất ngăn so với thời gian giữa hai lần ngắt Systick xảy ra, do đó phần lớn thời gian CPU ở trạng thái sleep, giúp tiết kiệm năng lượng cho hệ thống.

Icons made by Freepik from www.flaticon.com