Retardo de no bloqueo para máquinas de estado

0

¿Cuál es la mejor manera de implementar un retardo de no bloqueo para una máquina de estado para cada estado? Hasta ahora, lo mejor que encontré es algo así:

static uint8_t state = STATE_ONE;
if (state == STATE_ONE)
{
   static uint64_t time_value_ms = (uint64_t)0;
   if (time_value_ms == (uint64_t)0)
   {
      time_value_ms = system_get_ms() + delay_ms;
   }
   else
   {
      if (system_get_ms() > time_value_ms)
      {
         time_value_ms = (uint64_t)0;
         state = STATE_TWO;
      }
   }
}
else if (state == STATE_TWO)
{
       static uint64_t time_value_ms = (uint64_t)0;
       if (time_value_ms == (uint64_t)0)
       {
          time_value_ms = system_get_ms() + delay_ms;
       }
       else
       {
          if (system_get_ms() > time_value_ms)
          {
             time_value_ms = (uint64_t)0;
             state = STATE_ONE;
          }
       }
}
.
.
.
else
{
   return;
}

Pero el problema en el código anterior es que cuando el estado cambia, la variable time_value_ms tiene que establecerse en 0 para estar disponible para contar una demora nuevamente cuando el estado regrese en el STATE_ONE después .

Por lo tanto, cuando necesita un retardo de no bloqueo en todos los estados de una máquina de estados grande con demasiados estados (por ejemplo, 10 estados), el código se vuelve complejo con una legibilidad muy mala.

El retraso se utiliza como temporizador de tiempo de espera para un interbloqueo o un temporizador de eventos.

    
pregunta MrBit

4 respuestas

2

Esto se puede resolver mediante el uso de punteros de función.

void (*FP)(void*, uint64_t);//FP = Function Pointer
//The void means that whatever function FP will point 
// to will always return nothing.
//The *FP means that FP is a pointer named FP.
//The void* means that whatever function FP will point 
// to, its first argument will be yet another pointer of type void.
//The unsigned long means that whatever function FP will point 
// to, its second argument will be an unsigned long variable.

uint64_t next_time_pulse=0;
//time variable that we compare system clock with

void state_0(void* FP(void*, uint64_t), uint64_t t){
    //custom code that should always happen if state_0 is active
    if(t<system_get_ms()){
        //custom code that should happen when state_0 transitions to state_1
        FP = &state_1;
        t = system_get_ms()+delay;
    }
    return;
}

void state_1(void* FP(void*, uint64_t), uint64_t t){
    //custom code that should always happen if state_1 is active
    if(t<system_get_ms()){
        //custom code that should happen when state_1 transitions to state_0
        FP = &state_0;
        t = system_get_ms()+delay;//this will update "next_time_pulse"
    }
    return;
}

void init(){//initiate your system, only called once
    FP=&state_0;//Set FP to state 0
    next_time_pulse = system_get_ms()+delay; 
    //when should the absolutely first clock happen?
}

void loop(){//your main loop of your system
    //some code that won't be blocked. 
    FP(&FP,&next_time_pulse);//This will call either state_1 or state_2
    //if you want to know which state FP is in
    //then write whatever that knowledge would give you, in the functions
}

Como puede ver, solo estoy usando punteros, por lo que si necesita que sucedan varias máquinas de estados finitos de manera independiente, puede crear más variables y, probablemente, almacenarlas en una matriz.

Si necesita más estados, simplemente agregue más funciones.

Otra forma de resolverlo sería con matrices simples, porque esa es la esencia de la solución anterior.

uint64_t machine[2];
uint8_t next_state[2]={1,0};
//state 0's next state is 1
//state 1's next state is 0
//=> {1,0}

void init(){//initiate your system, only called once
    state=0;
    //Set state to 0
    machine[0]=system_get_ms()+delay; 
    //when should the absolutely first clock happen?
}

void loop(){//your main loop of your system
    //some code that won't be blocked. 
    if(machine[state]<system_get_ms()){
        state=next_state[state];
        machine[state]=system_get_ms()+delay;
    }
}

De esta manera es más difícil hacer que las cosas personalizadas sucedan dependiendo de un estado (podría resolverse con una caja de interruptores ...), pero su ejemplo simple ahora es más fácil de entender con respecto al código.

Aunque, con más información sobre cómo va a utilizar sus máquinas de estados finitos, se podría crear un código más elegante / optimizado.

    
respondido por el Harry Svensson
1

La solución típica que se usa en algunos sistemas en tiempo real, es ejecutar el estado y luego "grabar" el tiempo restante después de eso, de modo que cada estado se ejecute siempre en intervalos de tiempo fijos, con un intervalo de tiempo dado. Si quieres, un "hombre pobre RTOS":

start_timer(x); // x miliseconds
  state = STATE_MACHINE[state]();
while(timer_running) // blocking or non-blocking, busy-wait or preferably wake-up interrupt
{}

En cualquier caso, no debe mezclar ni la lógica de la máquina de estado ni los retrasos con la lógica de la aplicación. Hacerlo no tiene ningún sentido y conducirá a la ofuscación y al código inflado. Eso es mal diseño, punto.

    
respondido por el Lundin
1

No entiendo por qué está usando una instancia diferente de time_value_ms para cada estado. La máquina de estados está en un solo estado a la vez. Entonces solo use la misma instancia de time_value_ms para todos los estados. Y establece time_value_ms en cada transición de estado (aunque no se restablece a través de cero como si fuera algún tipo de indicador).

También hay un error en su implementación cuando system_get_ms() está dentro de delay_ms de reinvertir. Si se agrega delay_ms , el valor de time_value_ms se reiniciará. Luego, posteriormente, la comparación system_get_ms() > time_value_ms será verdadera al instante y no se producirá el retraso deseado. En su lugar, debe restar el start_time del current_time y comparar el resultado con delay_time.

En mi ejemplo, estoy usando el estado STATE_INIT simplemente para inicializar el valor de time_start_ms en la primera invocación. Podría regresar a STATE_ONE directamente desde otro estado y no tener que pasar por STATE_INIT.

Finalmente, prefiero la construcción de switch en lugar de if-else-if en esta situación.

static uint8_t state = STATE_INIT;
static uint64_t time_start_ms;

uint64_t current_ms = system_get_ms();

switch (state)
{
   case STATE_INIT:
      time_start_ms = current_ms;
      state = STATE_ONE:
      // break omitted intentionally to fall through into STATE_ONE.

   case STATE_ONE:
      if ((current_ms - time_start_ms) > delay_ms)
      {
          state = STATE_TWO;
          time_start_ms = current_ms;
      }
      break;

   case STATE_TWO:
      if ((current_ms - time_start_ms) > delay_ms)
      {
          state = STATE_THREE;
          time_start_ms = current_ms;
      }
      break;
   .
   .
   .
}
    
respondido por el kkrambo
1

Puedes usar un estado de transición.

Dentro de una función de estado, siempre vuelvo con el siguiente estado al que la máquina debe llegar y toma el anterior como argumento.
En lugar de esperar dentro de cada estado, que se volverá desordenado, puede saltar a un estado de espera. El estado de espera espera varias evaluaciones de la máquina de estado (o hasta la hora especificada del sistema), y el estado de espera regresa a su interlocutor.

Si confía en los argumentos de máquina de estado que contienen el estado anterior, puede implementar el estado de espera para que sea implícito. Y así, puede saltar o regresar a un estado sin que el estado sepa que vino de un estado de espera. Como yo.

A continuación he puesto algunos fragmentos de mi máquina de estado típica.

Mis reglas:
 - La máquina se evalúa a una velocidad constante. (por ejemplo: 1 KHz)
 - Hay un sistema temporizador de conteo de tics del sistema.  - Se deben evitar los punteros de funciones variables.

void evaluate(void){
    previous = current;
    current = next;

    switch(current){
    case WAIT_STATE:
    {
        int elapsed = systick() - wait_entry_time;
        if( previous != current){
            wait_previous = previous;
        }
        if( elapsed >= wait_time){
            next = wait_next;
            current = wait_previous;
        }
    }  
    break;
    case STATE_INIT:   next = init(previous); break;
    case STATE_ENTRY:  next = entry(previous); break;
    // .. other states ...

Prepararse para esperar o saltar a la siguiente con un retraso se puede hacer con una macro.

#define WAIT_PREP(s, w)   next = WAIT_STATE;           \
                          wait_next = s;               \
                          wait_entry_time = systick(); \
                          wait_time = w;

Esto se supone que está configurado para que cada estado sea una función, tome como argumento el estado anterior y devuelva el siguiente estado.

state_t init(state_t prev){
    state_t next = current;
    // Delay 2000 ms then jump to ENTRY
    WAIT_PREP(STATE_ENTRY, 2000);
    return next;
}

Necesitará algunas variables estáticas en la parte superior del módulo, como los argumentos wait_ . Y las variables de máquina de estado current , next y previous .

La disciplina adecuada le impide pasar por alto la operación prevista utilizando los indicadores globales.

    
respondido por el Jeroen3

Lea otras preguntas en las etiquetas