Search code examples
cstm32uartat-command

Designing a State Machine for a UART Module


I'm trying to work with a UART AT COMMAND 4G Module and I'm trying to design a work flow diagram as a state machine for it. I have a problem of

  1. processing the incoming messages and sending the commands
  2. How to move and change the states bewteen each other ?

Here is my initial States:

#define STATE_INIT 0 
#define STATE_CONNECTED 1 
#define STATE_DISCONNECTED 2 
#define STATE_RETRY_CONNECT 3 
#define STATE_FAILURE 4 
#define STATE_HTTP_POST 5
#define STATE_HTTP_GET 6 
#define STATE_HTTP_POST_RETRY 7
#define STATE_HTTP_POST_SUCCESS 8
#define STATE_HTTP_GET_RETRY 9
#define STATE_HTTP_GET_SUCCESS 10
#define STATE_CHECK_CONNECTIVITY 11
#define SIM_STATUS_ERROR 12 
#define SIM_STATUS_READY 13
#define SIM_STATUS_LOCKED 14
#define REG_STATUS_UNREGISTERED 15
#define REG_STATUS_SEARCHING 16
#define REG_STATUS_DENIED 17
#define REG_STATUS_OK 18
#define REG_STATUS_HOME 19
#define REG_STATUS_ROAMING 20
#define REG_STATUS_UNKNOWN 21
uint8_t current_state; 




void processMessage(char *msg) {

}

void sendCmd(char *cmd) {
    strcpy(UART_Out_Buffer, cmd);
    UART_Out_Cnt = strlen(cmd);
}

void Init_State(void) {

     current_state = STATE_INIT;
     sendCmd("AT+CGSOCKCONT=1,""\"IP""\",""\"A1.net""\"");
     sendCmd("AT+CSOCKAUTH=1,1,""\"ppp""\",""\"[email protected]""\"");
     sendCmd("AT+CHTTPSOPSE=""\"ipdb-eu1.com""\",443""\"");


}

Here when we send a command the response should be processed.

void process_uart(void)
{
    uint16_t uartBufPos = 0;
    char line[UART_BUFFER_SIZE];
    line[0] = '\0';
    uint16_t linePos = 0;

    while (UART_Buffer[uartBufPos] != '\0')
    {
        if (UART_Buffer[uartBufPos] == '\n')
        {
            line[linePos] = '\0';
            processMessage(line);
            linePos = 0;
        }
        else
        {
            line[linePos] = UART_Buffer[uartBufPos];
            linePos++;
            if (linePos == UART_BUFFER_SIZE)
            {
                linePos = 0;
            }
        }
        uartBufPos++;
        if (uartBufPos == UART_BUFFER_SIZE)
        {
            uartBufPos = 0;
        }
    }

    if (UART_Out_Cnt > 0)
    {
        HAL_UART_Transmit(&huart2, (uint8_t *)UART_Out_Buffer, UART_Out_Cnt, 100);
        UART_Out_Cnt = 0;
    }
}

Following up with the answer: I have done this:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM2)
  {
      start_processing = true;
      isSynced = true;

      if (waitreply > 1)
      {
        waitreply--;

        HAL_UART_Receive_DMA(&huart2, DMA_RX_Buffer, DMA_RX_BUFFER_SIZE);

        uint16_t uartBufPos = 0;
        uint16_t linePos = 0;

        while (DMA_RX_Buffer[uartBufPos] != '\0')
        {
          if (DMA_RX_Buffer[uartBufPos] == '\n')
          {
            wait_reply = -1;
          }
          else
          {
            if (uartBufPos == DMA_RX_BUFFER_SIZE)
            {
              uartBufPos = 0;
            }

            uartBufPos++;
          }
        }
      }
  }
}

process state machine:

void process_state_machine()
{

    uint8_t timeout = 0;
    switch (current_state)
    {
    case STATE_INIT:
        if (wait_reply == 0)
        {
            // just entered this state, send command
            HAL_UART_Transmit(&huart2, "AT+CGSOCKCONT=1,"
                                       "\"IP"
                                       "\","
                                       "\"A1.net"
                                       "\"\r\n",
                              strlen("AT+CGSOCKCONT=1,"
                                     "\"IP"
                                     "\","
                                     "\"A1.net"
                                     "\"\r\n"),
                              100);
            wait_reply = 2;
        }
        else
        {
            // reply, or timeout
            if (wait_reply == 1)
            {
                // timeout, retry
                timeout = 0; // this will re-enter this state
            }
            if (wait_reply == -1)
            {
                // analyze reply, may be change state
                wait_reply = 0;
                timeout = 1;
                current_state = STATE_CONNECTED;
            }
        }
        break;

Solution

  • It is not easy, but you can do it with a state machine; but that state machine must have at least two levels. This is because when you send a command to the modem, it takes time; still more time is needed to wait for the response. It can be done with a single timer (a simple variable), provided that you have two circular buffers, or at least one, for the modem communication.

    The state machine is called in a timely manner, say every 1/100 of second. The timer variable is called waitreply. Pseudo code is something like this:

    statemachine:
      if (waitreply > 1): 
        waitreply--;
      read characters from modem (from circular buffer)
      is the read message complete? (ends with CR-LF?)
        no:
          (fall to the rest of the routine)
    
        yes:
          is this out-of-band data?
            yes:
              put it aside and ignore
            no:
              waitreply = -1;
    

    The remaining part is a switch statement, one case for every state. Every state is divided in two:

    case SEND_AT:
      if (waitreply == 0) {
        // just entered this state, send command
        send command
        waitreply = some_timeout
      } else {
        // reply, or timeout
        if (waitreply == 1) {
          // timeout, retry
          waitreply = 0;  // this will re-enter this state
        }
        if (waitreply == -1) {
          // analyze reply, may be change state
          waitreply = 0;
          STATE = SEND_ATI;
        }
      }
      break;
    

    This is just an idea, hope it helps in some way.

    ===== EDIT after comments =====

    As seen in the code above, the waitreply variable implements a second-level state machine. If waitreply==0, no transaction is in progress: a command can be sent; if ==-1, then a reply from the modem is ready to be read by the current "state"; otherwise, the state machine is just waiting. Thus, the test for (waitreply > 0) could be moved to the beginning of the function and, if satisfied, simply exit the function prematurely. But this doesn't seem a big improvement.

    About the questions from the OP:

    1. Yes, a timer calls the state machine every 1/100 of second. The variable waitreply is initialized to zero.
    2. There is no "thread", this is a simple routine called from the main

    A C program skeleton is as follow:

    int waitreply;
    enum blahblah state;
    void statemachine(void);
    
    void main(void) {
      waitreply = 0;    // already zeroed by C runtime
      state=ST_SENDAT;  // see if the modem is alive
      do {
        if (timer_expired) {
          // 1/100 sec elapsed
          statemachine();
          start_timer();
        }
      }
    }
    
    
    void statemachine(void) {
      // prologue... modem_replay will contain the reply from modem
      switch (state) {
        case ST_SENDAT:
          if (waitreply == 0) {
            // just entered this state, send command
            send_to_modem("AT" CR LF);
            waitreply = 200;   // 2 seconds
          } else {
            // reply, or timeout
            if (waitreply == 1) {
              // timeout, retry
              waitreply = 0;  // this will re-enter this state
            }
            if (waitreply == -1) {
              // analyze reply, may be change state
              waitreply = 0;
              if (0 == strcmp(modem_reply, "OK"))
                  STATE = SEND_ATI;   // another state, to get info from modem
                  // else will re-enter this same state, for ever
            }
          }
          break;
      }  // end of switch
    } // func statemachine
    

    I repeat, this is just an idea which grants fine control, but nothing more.