¿Máquina de estados finitos que maneja los temporizadores con gracia?

He estado trabajando principalmente con MCU de 8 bits, donde la mayoría de los RTOS tienen demasiada sobrecarga.

La mayoría de las aplicaciones en las que he trabajado solo han sido una interrupción periódica con cadenas if/else para toda la lógica de procesamiento, y luego la MCU vuelve a dormir.

Esto ha funcionado bien para muchas cosas y tiene una sobrecarga realmente mínima. Pero para un sistema, estoy llegando al punto en que hay tantas banderas de control que estoy listo para llamar a mi propio sistema "espagueti". Sería horrible que alguien nuevo tomara este sistema e implementara alguna funcionalidad nueva.

(Tengo un LED de dos colores, que debe tener como 8 estados diferentes y patrones de parpadeo dependientes del tiempo según el estado en el que se encuentre el resto del sistema. Es un ejercicio horrible, por lo que debería ser tan simple...)

Estaba viendo tal vez hacer una máquina de estado finito y tratar de eliminar tantas banderas de control.

Un problema conceptual que veo es el uso de temporizadores en una máquina de estado. Actualmente, tengo un temporizador de hardware y luego un montón de contadores de temporizadores definidos por variables que aumentan / disminuyen, una variable de indicador de control va a 0/1, y así pasamos por la cadena if / else.

En mi etapa de planificación para una máquina de estado más estricta, ¿usaría más temporizadores de hardware y activaría las interrupciones externas como eventos para volver a la máquina de estado?

Mi reacción instintiva (ya sea correcta o no) es usar tantas interrupciones externas como sea posible para la máquina de estado ¿eres tú? 1) presentar todo tipo de posibles problemas de prioridad de interrupción que traen su propio conjunto de problemas, donde actualmente el tiempo es muy determinista pero el la lógica de control es simplemente confusa y 2) está utilizando más corriente ejecutando un montón de temporizadores en lugar de simplemente manejar la lógica del temporizador como variables.

Veo cómo aún podría incrementar/decrementar los temporizadores variables en su máquina de estado, pero ¿no es eso antiético para el patrón de la máquina de estado?

Me siento bastante cómodo con el debate de los punteros de función frente a la declaración de cambio sobre cómo codifica la máquina de estado, o si desea usar una tabla de transición, etc.

Me pregunto específicamente cómo la gente ha manejado el aspecto de gestión del temporizador de sus máquinas de estado de una manera elegante.

+1 para una pregunta bien redactada, pero es un poco límite en la búsqueda de opiniones, por lo que si puede mejorarla, ¿podría obtener más respuestas?
¿Te importa editar? Literalmente no sé cómo hacer que sea menos opinión, es inherentemente una cuestión de diseño.
Cada máquina de estado tiene un código que determina el siguiente estado, ¿verdad? Cuando ingresa a un estado que tiene un tiempo de espera, toma una instantánea del temporizador del sistema y lo almacena. Luego, al decidir sobre el siguiente estado, obtiene la hora actual, resta la instantánea y compara con el valor de tiempo de espera. No parece muy complicado. Supongo que podría usar un poco de almacenamiento que podría ser escaso. Si desea salvar la ética de la máquina de estado, puede dedicar un estado para almacenar la hora actual con el fin de calcular el tiempo transcurrido más adelante.
@mkeith: sí, las funciones de la máquina de estado (estoy pensando en funciones de puntero, enfoque a la máquina de estado) apuntarían al siguiente estado. Me gustaré ese enfoque.
Si el temporizador es de 16 bits sin signo, la resta del límite de desbordamiento seguirá dando el tiempo transcurrido correcto.
"Sería terrible que alguien nuevo tomara este sistema e implementara alguna funcionalidad nueva".-> Implemente su máquina de estado usando una herramienta como IBM Rational Rhapsody. Genera código basado en su diagrama de máquina de estado y, por lo tanto, también está documentado en paralelo.

Respuestas (3)

Una forma común de hacer esto sería establecer un tiempo de ejecución máximo para cada estado y luego comparar cada uno de ellos (con una cobertura de código del 100 %) y asegurarse de que nunca excedan el tiempo de ejecución máximo. Con un poco de suerte, incluso puede usar el perro guardián en el chip para garantizar esto, si puede ejecutarse con tiempos de espera lo suficientemente bajos.

Ahora lo que probablemente esté buscando no es una, sino varias máquinas de estado. Es decir, puede tener una máquina de estado universal como

STATE_MACHINE[state++](); 
if(state == STATES_N) 
{ /* reset state machine */ 
} 

que no hace más que pasar por los distintos módulos de software, dándoles a cada uno una "fracción de tiempo". Puede ejecutar todos los controladores de hardware de una sola vez y volver a dormir, o puede elegir ejecutar solo uno de ellos. Por supuesto, esto depende de los requisitos en tiempo real.

Uno de esos estados podría ser led_execute(), que sería la rutina de LED que realiza un seguimiento de lo que está sucediendo en los LED en este momento. Esta rutina reside dentro del controlador de LED y, a su vez, puede realizar un seguimiento de cada estado de LED, de modo que se vea así:

typedef enum
{
  LED_OFF,
  LED_RED_LIT, // whatever names make sense
  LED_RED_BLUE_LIT,
  ...
  LED_DONE,
  LED_N
} led_state_t;
...    
static led_state_t led_state = LED_OFF;
...

void led_execute (void)
{
  led_state = LED_STATE_MACHINE[led_state]();
}

Si los estados dependen de una entrada externa, entonces tal vez omita la parte del estado de retorno y haga que el estado se actualice solo a través de setters/getters.

Esto debería eliminar por completo la necesidad de banderas, en particular banderas no relacionadas ubicadas en el mismo ámbito, lo que puede ser una pesadilla. La parte más importante aquí es no mezclar la complejidad del LED con la complejidad de algún otro hardware.

Digamos que simultáneamente estás eliminando el rebote de un botón. Digamos que debe terminar de eliminar el rebote antes de que se enciendan los LED; eso no significa que los botones tengan que saber sobre los LED o que los LED tengan que saber sobre los botones. El código de la persona que llama debe realizar un seguimiento de estas cosas. Lo que significa que es posible que necesite alguna capa de abstracción entre la máquina de estado más externa y los propios controladores. Si el controlador LED solo recibe una entrada "¡haz esto!" de la persona que llama, entonces no podría importarle menos las razones detrás de esto.

Lundin, totalmente de acuerdo. Estaba acostado en la cama imaginando múltiples máquinas de estado anoche pensando en esto. Si el LED tenía una máquina de estado separada, tomaba estados de otra máquina. En su opinión, si toma esa ruta, ¿reduce la complejidad lo suficiente como para que valga la pena? En mi rutina if/else, estoy llegando al punto en el que hay 6 indicadores si la mayoría de los IF controlan el control lógico de los casos de límite de estados. Estoy totalmente de acuerdo en que el LED está fundamentalmente separado de todo lo demás, pero las banderas se ensucian por todas partes para mantener el barco a flote.
Entonces, cuando dice comparar cada estado, ¿básicamente quiere decir que cada estado termina y tiene que patear al perro antes de que se acabe el tiempo? Esa es una gran idea. En mi caso, creo que mi perro guardián no puede correr tan rápido como lo necesitaría, pero en otros casos funcionaría muy bien.
@ Leroy105 Cuando tuve que lidiar con bases de código desordenadas en forma de "flaghetti" en el pasado, usé exactamente este diseño para desenredarlos. Tengo un ejemplo particular en el que un programa sufrió errores intermitentes, pero cuando las banderas se reemplazaron con máquinas de estado, todos los errores simplemente desaparecieron, aunque no había tocado la lógica de la aplicación real. Entonces, sí, sé por experiencia que esta es una buena manera de hacerlo.
@ Leroy105 En cuanto a la evaluación comparativa, deberá asegurarse de que algún estado no se vuelva loco y arruine todo el rendimiento en tiempo real. Lo anterior es una especie de forma cruda de un RTOS, si se implementa correctamente. También puede cronometrar cada estado desde el código de la persona que llama y registrarlos; eso es algo que uso en más código de misión crítica, pero luego también junto con algún tipo de perro guardián. También en particular, patear el wdog desde el interior de un ISR no es una idea brillante. Entonces, si todo esto es un gran ISR, tal vez busque un diseño alternativo.
"flaghetti" -- bingo.
Lundin, ¿has cambiado de opinión sobre el uso de punteros de función frente a interruptores? No soy religioso acerca de los argumentos de código de operación compilados (es decir, declaraciones de casos para saltar tablas, etc.). Lo que más me preocupa son los errores tipográficos y la capacidad de mantenimiento. No estoy codificando según los estándares MISRA, pero definitivamente he creado errores al no verificar los límites de la matriz en mi vida. ¡Incluso el uso de matrices me da un poco de pausa! Puede ser difícil detectar un error de matriz, etc. Me gusta la naturaleza limpia de los punteros. Parece un lavado, probablemente en cuanto a la ejecución. Agradecemos tu opinión, ya que has visto estos sistemas de principio a fin.
Sabes lo que realmente no me gusta, es que he visto máquinas de estado donde los estados no se devuelven en las funciones. Toda la lógica utiliza una matriz de estado tipificada. A mi cerebro no parece gustarle ese enfoque. ¿Parece difícil de depurar? En cualquier caso, haré algunas comprobaciones de límites en la máquina de estado. Lo aprendí de la manera difícil (es decir, ¿por qué un bit se alterna de alguna manera extraña, pero el resto del sistema está funcionando y solo se alterna en el tercer ciclo del ISR? Tuve que llamar a un temporizador antiguo en mi suite ofimática, para enseñarme sobre arreglos en C... ¡ya no estamos en el mundo de Java!).
Me estoy riendo de mí mismo, cuatro horas más tarde, cuando detecto un grave error de flaghetti dependiente del tiempo que podría dejar un LED de color fijo, pero pasar a una rutina parpadeante. Depende cuando el tiempo de transición. Caramba, qué cubo de mierda he codificado aquí sin una máquina de estado. ;)
@ Leroy105 La parte más importante de las máquinas de estado es que queda perfectamente claro dónde tienen lugar los cambios de estado. Más comúnmente al final de cada función de estado. Pero también es posible tener un "programador" externo que le dé a cada pieza de hardware un intervalo de tiempo para ejecutarse. Eso es lo que tenía en mente cuando escribí esta respuesta, pero supongo que tales diseños no deberían llamarse máquinas de estado sino programadores.
Es una especie de enfoque híbrido. Vi a un profesor universitario mantener un contador variable estático en cada función de estado, que registra un ISR. O crea algunas funciones setter/getter para modificar algunos contadores globales definidos. Sin embargo, creo que tengo un control, es dividirme en múltiples máquinas de estado como dices. Si alguna vez siente la necesidad de crear una bandera global, debe hacer que esa lógica sea otro estado definido y hacer que su máquina de estado vuelva al siguiente estado, etc. Me gustó este artículo: isa.uniovi.es/docencia/ redes/…

Si desea una "multitarea" de tecnología súper baja, su interrupción del temporizador puede verse así:

timer_isr()
{
    process1();
    process2();
    process3();
}

Entonces, si su temporizador dispara, digamos, 100x por segundo, entonces cada vez que se llama a cada función process(). Estas funciones son FSM que implementan sus diferentes tareas "multitareas". Si son todos independientes, entonces es fácil.

Ahora, si sus tareas son dependientes, puede hacer algo como:

timer_isr()
{
    check_buttons();
    blink_red();
    blink_blue();
}

En este caso, las tareas se comunicarán a través de feas variables globales (no vamos a hacer cuadros de mensajes y FIFO en 8 bits, ¿verdad?). Por ejemplo, check_buttons() rebotaría, etc., y establecería algunas banderas y/o influiría directamente en el estado de los otros dos FSM que hacen parpadear los LED.

Incluso podemos usar esta tecnología de última generación llamada C++:

timer_isr()
{
    check_buttons();
    red.blink();
    blue.blink();
}

En este caso, check_buttons() llamaría a "red.setBlinkMode (algún valor)" cuando se presiona el botón apropiado, por ejemplo. "rojo" y "azul" son objetos globales. En este caso, esta pequeña parte de OO le permite implementar el mismo algoritmo para ambos sin tener que meterse con toneladas de globales o punteros a estructuras, etc.

Es bueno manejar sus botones en un solo lugar en su código. Especialmente si los botones controlan varias cosas, por ejemplo un botón para seleccionar el LED y otro botón para modificar el patrón de parpadeo del LED seleccionado.

El método .blink(), por ejemplo, incrementaría un contador específico de LED hasta que alcance el período de parpadeo, o ajustaría un PWM de forma dependiente del tiempo para que parpadee elegantemente, ese tipo de cosas.

Por ejemplo, si su tiempo se llama cada milisegundo, su método de parpadeo () podría ser:

LED::blink()
{
    if( led_on) {
        if( counter++ > period ) {
            counter=0;
            led_pin = !led_pin;
        }
    } else { led_pin = 0; counter=0; }
}

...algo como eso. Todos son llamados en el mismo período de tiempo, por lo que todas estas pequeñas máquinas de estado conocen el tiempo contando la cantidad de veces que son llamados. En este caso el estado del FSM es led_on y contador.

Sí, entiendo esto, pero ¿ves cómo introdujiste un montón de variables de control? Tengo el lío de if/else, pero también tengo montañas de lío de variables de control/lógica. Estoy tratando de ordenar ambas pilas. ;)
Intente dividirlo en pequeñas cajas independientes que se comuniquen a través de "canales" (en su caso, variables de señal simples), cuanto más pequeña sea cada caja/FSM, mejor.
@ Leroy105 Si necesita usar, digamos, 20 variables de bandera/señal para la comunicación entre sus módulos, entonces TENDRÁ que usar 20 variables para ellos, ya sea como máscaras de bits o como variables volátiles separadas o como una estructura de comunicación separada o incluso un módulo de comunicación separado con varias estructuras en el interior: depende de usted, según la complejidad. Solo necesita aislar sus variables/banderas de comunicación fuera de su lógica principal.
El estrecho acoplamiento entre un controlador de LED y un controlador de botón no es de ninguna manera un mejor diseño que el espagueti original. ¡Así no es como se hace OO correctamente! Tus clases no son autónomas, pero saben cosas que no deberían saber. Eso no es un diseño OO, ni en C ni en C++. Para un diseño adecuado, el código de nivel superior debe realizar un seguimiento de los botones y los LED.

En general, he descubierto que es mejor elegir un pequeño incremento de tiempo (tal vez un par de milisegundos hasta tal vez 250 useg para un micro de 8 bits) y usarlo para la mayor parte o la totalidad del tiempo. Esto es comparable a la granularidad en un RTOS.

Es más fácil si tiene un micro con una arquitectura decente que permita interrupciones anidadas.

Sphero, ¿quieres decir que activas la máquina de estado cada 250 segundos? ¿Qué pasa si su LED necesita parpadear cada 1000usecs, usaría un contador en su código de máquina de estado que? Cuando LedCounter = 1, el LED se enciende, etc. Estoy tratando de eliminar tantas variables de contador y banderas lógicas. El sistema actualmente es básicamente un temporizador de 500 us, y todo funciona con eso.
Sí, un contador. Imagine una serie de contadores, cada uno reducido a cero en el código de interrupción (declararlos como volátiles, obviamente).
Si dijera, está bien, vamos a tener un ISR de temporizador SysTick ahora, y luego agregaremos ISR de LedTimer en lugar de solo ejecutar variables de contador para LedTimer dentro del temporizador de SysTick: en general, la complejidad del sistema ha aumentado con dos ISR. ¿ser introducido? Ese es mi presentimiento es que es un mal enfoque.