Esta es la tercera parte de la serie donde vuelvo a explorar los patrones embebidos más comunes. Esta vez le toca a la máquina de estados finitos (finite state machine). El código fuente completo y probado vive en el repositorio complementario: ileanmjr88/tetzontli. Cada patrón de esta serie es su propio módulo con una suite completa de GoogleTest, así que se puede clonar el repositorio y ejecutar las pruebas uno mismo. La suite también arma el controlador de motor completo que se usa a lo largo de este post, así que sirve además como ejemplo de uso. Concepto básico Una máquina de estados finitos es una de las formas más comunes de diseñar el comportamiento de un sistema, y en sistemas embebidos es especialmente importante. El comportamiento es lo que convierte el hardware en un producto: el sistema tiene que saber qué está haciendo en este momento, y qué hacer cuando algo pasa. Es como seguir una receta de pan. Se juntan los ingredientes, se mezclan los secos, se mezclan los húmedos, y luego se combinan ambos. Cada paso es un estado. Se está en un solo paso a la vez, y solo pasa al siguiente cuando ese paso termina. La receta también tiene notas como «si la masa está muy aguada, agregar harina; si está muy firme, agregar agua». Esas notas son la parte interesante. Algo pasa (se revisa la masa), una condición decide qué nota aplica (muy aguada o muy firme), y se toma una acción (agregar harina o agua) antes de continuar. Eso es todo lo que es una máquina de estados: estados, los eventos que llegan mientras se está en ellos, las condiciones (guardas) que deciden qué regla aplica y las acciones que se toman. Modelar el comportamiento así es lo que permite que un sistema se adapte. A todos nos gustarían condiciones perfectas e ideales, pero el mundo real es caótico, y el sistema tiene que manejarlo de todos modos. Casi todo sistema embebido es una máquina de estados: un antirrebote de botón, un driver de módem, un cargador de batería, un bootloader. La mayoría empiezan como un switch(state) con un switch(event) anidado en cada caso. Eso funciona para tres estados y cuatro eventos. Con diez estados es un muro de casos donde el comportamiento está disperso en cientos de líneas, la lógica de entrada y salida está copiada y pegada, y nadie puede responder «¿qué pasa si llega OVERTEMP mientras estamos en CHARGING?» sin leerlo todo. La idea La receta mostró que todos ya usamos máquinas de estados. Antes de ver una máquina de estados de ejemplo, fijemos el vocabulario, ya que cada representación que sigue usa las mismas cinco ideas. Estado: lo que el sistema está haciendo en este momento. En la receta, cada paso es un estado. El sistema siempre está exactamente en uno. Evento: algo que pasa y necesita una respuesta, como una entrada, un temporizador que se dispara o una lectura de un sensor. En la receta, revisar la masa. Guarda (guard): una condición que decide si una regla aplica. En la receta: ¿la masa está muy aguada? Acción: lo que el sistema hace en respuesta. En la receta: agregar harina. Transición: pasar de un estado a otro por un evento (y su guarda). En la receta: la masa está lista, así que se pasa al siguiente paso. Aquí está el controlador de motor de la suite de GoogleTest, escrito a la manera del switch. Tiene cuatro estados (IDLE, ARMED, RUNNING, FAULT) y eventos como EV_ARM, EV_START y EV_ESTOP. if (event == EV_ESTOP) { /* any state / motor_off(); state = FAULT; return; } switch (state) { case IDLE: if (event == EV_ARM) { state = ARMED; } break; case ARMED: switch (event) { case EV_DISARM: state = IDLE; break; case EV_START: if (battery_ok()) { state = RUNNING; } else { warn_battery_low(); / stay ARMED / } break; default: break; } break; case RUNNING: switch (event) { case EV_STOP: state = ARMED; break; case EV_SET_SPEED: set_speed(); / stay RUNNING / break; case EV_OVERCURRENT: motor_off(); state = FAULT; break; default: break; } break; case FAULT: if (event == EV_RESET && fault_cleared()) { state = IDLE; } break; } Funciona, pero veamos lo que ya está pasando con solo cuatro estados. El paro de emergencia tuvo que vivir fuera del switch, porque una regla que aplica a todos los estados no tiene un lugar natural. Para responder «¿qué hace EV_START cuando la batería está baja?» hay que rastrear ramas anidadas. Y cada caso necesita su break: a mi primer borrador de este ejemplo le faltaban varios, y en silencio se pasaba de IDLE a ARMED. Con diez estados y veinte eventos, esto se vuelve cientos de líneas donde nadie puede ver todo el comportamiento a la vez. Máquina de estados basada en tablas Aquí está el mismo comportamiento como tabla: static const sm_transition_t kMotorTable[] = { // from event guard action to {SM_ANY_STATE, EV_ESTOP, NULL, motor_off, FAULT}, {IDLE, EV_ARM, NULL, NULL, ARMED}, {ARMED, EV_DISARM, NULL, NULL, IDLE}, {ARMED, EV_START, battery_ok, NULL, RUNNING}, {ARMED, EV_START, NULL, warn_battery_low, ARMED}, {RUNNING, EV_STOP, NULL, NULL, ARMED}, {RUNNING, EV_SET_SPEED, NULL, set_speed, RUNNING}, {RUNNING, EV_OVERCURRENT, NULL, motor_off, FAULT}, {FAULT, EV_RESET, fault_cleared, NULL, IDLE}, }; Cada fila define una sola regla. El estado from y el event juntos actúan como la clave: cuando llega un evento, la máquina de estados busca una fila que coincida con su estado actual y con ese evento. La guard es una condición opcional que tiene que ser verdadera para que la regla aplique. Si lo es, la máquina ejecuta la action (si hay una) y pasa al estado to. Las reglas se revisan de arriba hacia abajo, y gana la primera regla que coincide. Esa sola convención hace mucho trabajo: La fila SM_ANY_STATE para EV_ESTOP está hasta arriba, así que un paro de emergencia gana desde cualquier estado. Por fin tiene un lugar natural. Las dos filas ARMED, EV_START dependen del orden: la regla con la guarda battery_ok se revisa primero, y la regla sin guarda que está debajo es el respaldo que avisa sobre la batería. Si se intercambian, la advertencia se dispara siempre. Si ninguna regla se dispara, como EV_RESET mientras la falla siga activa (la fila coincide, pero su guarda dice que no), no pasa nada y la máquina se queda donde está. Todo el comportamiento ahora cabe en una pantalla, y se lee como una especificación. Responder «¿qué hace EV_START cuando la batería está baja?» significa leer dos filas en lugar de rastrear ramas anidadas. Implementación Después de comparar una máquina de estados basada en switch con una basada en tablas, la pregunta natural es: ¿qué es lo que realmente dispara las guardas y las acciones? La respuesta corta es sm_dispatch, pero primero necesitamos las piezas con las que trabaja. El comportamiento vive en dos tablas que son propiedad de quien llama: la tabla de transiciones (sm_transition_t), que define cómo responde la máquina a los eventos, y una tabla de hooks opcional (sm_state_hooks_t), que define qué hace cada estado al entrar, mientras está activo y al salir. Structs de las tablas La tabla de transiciones. Cada fila se lee como una oración: en from, ante event, si guard pasa, ejecutar action e ir a to. // modules/state_machine/state_machine.h typedef struct { sm_state_t from; /< State, or SM_ANY_STATE. */ sm_event_t event; /< Triggering event. */ sm_guard_fn guard; /< NULL = always passes. */ sm_action_fn action; /< NULL = no action. */ sm_state_t to; /< Next state. */ } sm_transition_t; La tabla de hooks (opcional). Una entrada por estado, cada una con on_entry, on_exit y on_run. La lógica de entrada y salida vive en un solo lugar por estado, en lugar de repetirse en cada transición que lo toca. // modules/state_machine/state_machine.h typedef struct { sm_action_fn on_entry; /< NULL = none. */ sm_action_fn on_exit; /< NULL = none. */ sm_run_fn on_run; /< NULL = none. */ } sm_state_hooks_t; Para el motor, la tabla de hooks se ve así: static const sm_state_hooks_t kMotorHooks[STATE_COUNT] = { // on_entry on_exit on_run {idle_entry, NULL, NULL}, // IDLE {NULL, NULL, NULL}, // ARMED {running_entry, running_exit, running_run}, // RUNNING {fault_entry, NULL, NULL}, // FAULT }; Una entrada por estado, en el orden del enum. IDLE y FAULT activan el freno al entrar, ARMED no necesita nada, y RUNNING enciende la salida PWM en running_entry, la apaga en running_exit y sondea la corriente del motor en running_run. Struct de la máquina de estados Quien llama es dueño de todo: las tablas, el struct, el contexto. La biblioteca nunca reserva memoria, lo cual importa en sistemas donde malloc no es bienvenido (ver el post del pool de memoria). // modules/state_machine/state_machine.h typedef struct { const sm_transition_t *table; size_t count; const sm_state_hooks_t *hooks; sm_state_t state_count; sm_state_t current; bool started; void *ctx; } sm_t; Tipos Al revisar los structs que usamos para definir las tablas, puede que surjan dudas sobre algunos tipos de datos que no resultan familiares. Son tipos personalizados definidos por la biblioteca. // modules/state_machine/state_machine.h typedef uint8_t sm_state_t; /< 0 .. state_count - 1; 0xFF reserved. */ typedef uint8_t sm_event_t; /< Caller-defined; 0xFF reserved. */ #define SM_ANY_STATE ((sm_state_t)0xFFu) /< from wildcard: any state. */ #define SM_NO_EVENT ((sm_event_t)0xFFu) /< on_run: nothing to dispatch. */ typedef bool (*sm_guard_fn)(void *ctx); /< true = allow the row. */ typedef void (*sm_action_fn)(void *ctx); /< Action, entry or exit. */ typedef sm_event_t (*sm_run_fn)(void *ctx); /< Event or SM_NO_EVENT. */ sm_state_t y sm_event_t son uint8_t. La biblioteca no puede conocer los estados y eventos de cada quien, así que uno define su propio enum y la biblioteca solo guarda los números. Ocho bits mantienen chica cada fila de la tabla, a costa de un límite: 255 estados y eventos, con 0xFF reservado como centinela (SM_ANY_STATE para estados, SM_NO_EVENT para eventos). Para una máquina de estados de firmware, eso sobra. sm_guard_fn devuelve bool: ¿debería aplicar esta fila? Responde una pregunta y no cambia nada. sm_action_fn hace el trabajo. El mismo tipo sirve para las acciones de transición y para los hooks on_entry y on_exit, ya que todos tienen la misma forma: hacer algo, no devolver nada. sm_run_fn es el interesante: devuelve un evento. El on_run de un estado puede observar el mundo (sondear un sensor, revisar un timeout) y reportar lo que pasó, o devolver SM_NO_EVENT si no pasó nada. Así es como un estado genera sus propios eventos sin que quien llama tenga que hacerlo. Todo callback recibe void *ctx. La máquina pasa tal cual el puntero de contexto de quien llama, así que los callbacks nunca necesitan variables globales. Eso significa que se pueden ejecutar varias máquinas independientes, y en las pruebas se puede pasar un contexto falso y verificar exactamente qué pasó. Las funciones que manejan la máquina devuelven un sm_result_t, así que quien llama siempre sabe qué le pasó a un evento: // modules/state_machine/state_machine.h typedef enum { SM_HANDLED, /< A row fired, or sm_run() had nothing to do. */ SM_UNHANDLED, /< No row matches. */ SM_GUARD_REJECTED, /< Rows matched; every guard rejected. */ SM_ERROR /*< NULL, not started, or state out of range. */ } sm_result_t; SM_HANDLED: una regla coincidió y se disparó, o sm_run() no tenía nada que hacer. SM_UNHANDLED: no existe ninguna regla para este estado y evento. El evento se ignoró. SM_GUARD_REJECTED: existían reglas para este estado y evento, pero todas las guardas dijeron que no. El evento se entendió, pero se bloqueó. SM_ERROR: mal uso, no comportamiento. Cubre un argumento NULL, una máquina que no se ha arrancado o un estado fuera de rango. Función de inicialización Igual que el búfer circular y el pool de memoria, la máquina de estados necesita una función de inicialización. Quien llama es dueño del sm_t y de las tablas; sm_init las valida y las conecta. // modules/state_machine/state_machine.c bool sm_init(sm_t *sm, const sm_transition_t *table, size_t count, const sm_state_hooks_t *hooks, sm_state_t state_count, sm_state_t initial, void *ctx) { if (sm == NULL) { return false; } *sm = (sm_t){0}; if (table == NULL || count == 0u || state_count == 0u || initial >= state_count) { return false; } for (size_t i = 0u; i < count; i++) { if (table[i].from != SM_ANY_STATE && table[i].from >= state_count) { return false; } if (table[i].to >= state_count || table[i].event == SM_NO_EVENT) { return false; } } sm->table = table; sm->count = count; sm->hooks = hooks; sm->state_count = state_count; sm->ctx = ctx; sm->current = initial; return true; } Primero, la verificación de NULL sobre sm. Sin un lugar donde escribir, no hay nada más que hacer. *sm = (sm_t){0}; ocurre antes del resto de la validación, a propósito. Si cualquier verificación posterior falla, quien llama se queda con una máquina en cero: started es falso y table es NULL. sm_start la rechaza, y sm_dispatch y sm_run devuelven SM_ERROR en lugar de actuar sobre basura. Un init fallido deja la máquina en un estado seguro y conocido. Verificaciones básicas de argumentos: la tabla tiene que existir, count y state_count no pueden ser cero, e initial tiene que ser un estado válido. Cada fila se valida una sola vez, por adelantado: from tiene que ser un estado real o SM_ANY_STATE. to tiene que ser un estado real. Como SM_ANY_STATE es 0xFF, que siempre es >= state_count, «cualquier estado» queda rechazado automáticamente como destino. No se puede pasar a cualquier estado, y la verificación sale gratis. event no puede ser SM_NO_EVENT, ya que ese valor está reservado para significar «no pasó nada». Después se guardan los campos. Nótese lo que no se asigna: started sigue en falso. Funciones auxiliares Antes de arrancar la máquina, veamos dos pequeñas funciones auxiliares que mantienen los hooks opcionales en todos los niveles: // modules/state_machine/state_machine.c static void run_entry(const sm_t *sm, sm_state_t state) { if (sm->hooks != NULL && sm->hooks[state].on_entry != NULL) { sm->hooks[state].on_entry(sm->ctx); } } static void run_exit(const sm_t *sm, sm_state_t state) { if (sm->hooks != NULL && sm->hooks[state].on_exit != NULL) { sm->hooks[state].on_exit(sm->ctx); } } static: son privadas de state_machine.c. Quien llama nunca invoca los hooks directamente; la máquina decide cuándo se ejecutan. Dos verificaciones de NULL, dos niveles de «opcional»: toda la tabla de hooks puede ser NULL (una máquina sin ningún hook), y cualquier hook individual dentro de ella puede ser NULL (un estado que solo necesita on_entry, por ejemplo). En ambos casos, no se ejecuta nada. Indexadas por estado, sin verificación de límites aquí. Las funciones confían en state, porque todo estado que llega a ellas ya se validó: el estado inicial en sm_init, y cada estado to cuando se revisó su fila. Se valida una vez en los bordes, y el código interno se mantiene simple. Se pasa el ctx de quien llama, así que los hooks trabajan sobre los datos de quien llama, nunca sobre variables globales. Función de arranque sm_init conecta la máquina, pero no ejecuta nada. Arrancar es un paso aparte: sm_start entra al estado inicial, ejecutando su hook on_entry, y marca la máquina como activa. // modules/state_machine/state_machine.c bool sm_start(sm_t *sm) { if (sm == NULL || sm->table == NULL || sm->started) { return false; } run_entry(sm, sm->current); sm->started = true; return true; } sm->table == NULL atrapa un init fallido. Como sm_init pone el struct en cero antes de validar, una máquina que no se pudo inicializar tiene una tabla NULL, así que sm_start la rechaza. Esa es la recompensa de poner en cero primero. sm->started protege contra arrancar dos veces. Una segunda llamada ejecutaría otra vez el on_entry del estado actual: volver a habilitar hardware o reiniciar un temporizador que ya está en marcha. Devolver false hace visible el error en lugar de repetir efectos secundarios en silencio. run_entry(sm, sm->current) ejecuta el hook on_entry del estado inicial, si hay tabla de hooks y ese estado tiene uno, pasando el ctx de quien llama. Luego se asigna started y la función devuelve true. Función de despacho sm_dispatch hace el trabajo pesado: dado un evento, encuentra la primera regla que aplica al estado actual y la lleva a cabo. // modules/state_machine/state_machine.c sm_result_t sm_dispatch(sm_t *sm, sm_event_t event) { if (sm == NULL || !sm->started || sm->current >= sm->state_count) { return SM_ERROR; } bool matched = false; for (size_t i = 0u; i < sm->count; i++) { const sm_transition_t *row = &sm->table[i]; if ((row->from != sm->current && row->from != SM_ANY_STATE) || row->event != event) { continue; } matched = true; if (row->guard != NULL && !row->guard(sm->ctx)) { continue; } if (row->to != sm->current) { run_exit(sm, sm->current); } if (row->action != NULL) { row->action(sm->ctx); } if (row->to != sm->current) { sm->current = row->to; run_entry(sm, sm->current); } return SM_HANDLED; } return matched ? SM_GUARD_REJECTED : SM_UNHANDLED; } Verificaciones defensivas: una máquina NULL, una máquina que nunca se arrancó (incluida una cuyo init falló) o un estado actual fuera del rango válido devuelven SM_ERROR. sm_init ya validó la tabla, pero esta verificación es un seguro barato contra un struct corrupto. Un recorrido lineal, de arriba hacia abajo. Una fila coincide cuando su from es el estado actual (o SM_ANY_STATE) y su event coincide. Todo lo demás se salta. matched = true se asigna antes de revisar la guarda. Esa sola bandera es lo que separa SM_UNHANDLED (no existe ninguna regla) de SM_GUARD_REJECTED (existían reglas, pero todas las guardas dijeron que no). Una guarda fallida significa continue, no return. El recorrido sigue buscando, que es exactamente como funciona el respaldo: en ARMED, la fila de battery_ok falla, así que la siguiente fila de EV_START (la advertencia de batería) tiene su oportunidad. El orden de una transición es salida, luego acción, luego entrada. El estado viejo limpia, la transición hace su trabajo, y luego el estado nuevo se prepara. Gana la primera coincidencia: la función devuelve SM_HANDLED de inmediato, así que las filas posteriores nunca se ejecutan. Las autotransiciones no vuelven a ejecutar los hooks. Cuando to es igual al estado actual (como RUNNING ante EV_SET_SPEED), solo se ejecuta la acción. La salida y la entrada se omiten. Cambiar la velocidad no debería detener y volver a arrancar el motor, que es justo lo que running_exit y luego running_entry le harían a la salida PWM. Algunos frameworks vuelven a ejecutar los hooks en las autotransiciones; elegí no hacerlo, porque en código embebido la entrada y la salida suelen tocar hardware. Las guardas, las acciones y los hooks no deben llamar a sm_dispatch ni a sm_run. Durante la acción, el estado viejo ya salió, pero current todavía no se ha actualizado. Un despacho anidado vería un estado a medio camino de una transición. Una llamada anidada desde una guarda no es mejor: el recorrido externo todavía va a la mitad de la tabla, y seguiría comparando filas contra un current que pudo haber cambiado sin que se diera cuenta. Si una acción necesita disparar otro evento, hay que encolarlo y despacharlo después de que regrese el primero. (El búfer circular de la primera parte es una cola de eventos natural.) Función de ejecución sm_run es lo que se llama desde el bucle principal. Mientras que sm_dispatch reacciona a eventos que entrega alguien más, sm_run le da al estado actual la oportunidad de mirar el mundo y reportar los suyos. // modules/state_machine/state_machine.c sm_result_t sm_run(sm_t *sm) { if (sm == NULL || !sm->started || sm->current >= sm->state_count) { return SM_ERROR; } if (sm->hooks == NULL || sm->hooks[sm->current].on_run == NULL) { return SM_HANDLED; } sm_event_t ev = sm->hooks[sm->current].on_run(sm->ctx); if (ev == SM_NO_EVENT) { return SM_HANDLED; } return sm_dispatch(sm, ev); } Las mismas verificaciones defensivas que sm_dispatch. El mal uso devuelve SM_ERROR antes de que se ejecute cualquier código del usuario. Sin on_run, no hay nada que hacer. Si no hay tabla de hooks, o el estado actual no tiene on_run, devuelve SM_HANDLED. Eso hace que sea seguro llamar a sm_run en cada pasada del bucle, aun cuando el estado actual solo reacciona a eventos externos. El on_run del estado actual recibe el ctx de quien llama y devuelve un evento. Aquí es donde vive el sondeo (polling): leer un sensor, comparar un temporizador contra un timeout o revisar una bandera que activó una ISR. SM_NO_EVENT significa que no pasó nada, que es el caso común, así que devuelve SM_HANDLED. Por eso también sm_init rechaza cualquier fila con SM_NO_EVENT como evento: ese valor nunca se puede despachar. Cualquier otra cosa va directo a sm_dispatch, y su resultado se devuelve tal cual. Un evento reportado por on_run pasa por la misma tabla que uno de quien llama, así que todas las transiciones siguen el mismo camino. Por eso on_run devuelve un evento en lugar de llamar a sm_dispatch por su cuenta. Devolverlo permite que el hook termine antes de que empiece la transición, así que la máquina nunca despacha desde dentro de uno de sus propios hooks. Es la misma regla que para las acciones: los hooks reportan lo que pasó, y la máquina decide qué hacer al respecto. En el caso más simple, donde todos los eventos vienen del on_run de un estado, el bucle principal es una sola llamada: for (;;) { sm_run(&motor_sm); } La mayoría de las máquinas no son tan simples. En el ejemplo del motor, EV_ARM, EV_START y EV_ESTOP vienen de fuera, así que siguen pasando por sm_dispatch. Función de estado actual La última función es la más simple: sm_state devuelve el estado en el que está la máquina. // modules/state_machine/state_machine.c sm_state_t sm_state(const sm_t *sm) { return (sm == NULL) ? SM_ANY_STATE : sm->current; } Solo lectura. Recibe un const sm_t *, así que preguntar no puede cambiar nada. Los campos de sm_t se pueden leer, pero pasar por sm_state evita que quien llama meta mano dentro del struct. NULL devuelve SM_ANY_STATE. Como 0xFF nunca puede ser un estado real, sirve también como «sin respuesta». No hace falta devolver un bool y usar un parámetro de salida como lo hace ring_buffer_get. No verifica started. Antes de sm_start, devuelve el estado inicial de sm_init. Después de un init fallido devuelve 0, del struct en cero, que parece un estado real. Hay que revisar el valor de retorno de sm_init en lugar de confiar en que sm_state avise que algo salió mal. Sirve sobre todo para pruebas y logging. Una prueba típica despacha un evento y luego revisa sm_state para confirmar dónde terminó la máquina. Cierre Eso es una máquina de estados completa. El comportamiento vive en una tabla en lugar de un muro de casos de switch, así que toda la máquina cabe en una pantalla y se lee como una especificación. No hace reserva dinámica, valida su tabla de transiciones una sola vez en el init y manda cada evento por el mismo camino, ya venga de quien llama o del propio on_run de un estado. Las concesiones, todas deliberadas: No es ISR-safe ni thread-safe. sm_dispatch lee y escribe current sin ninguna protección. Los eventos de una ISR van a una cola y se despachan desde un solo contexto. Sin llamadas reentrantes. Las guardas, las acciones y los hooks no deben llamar a sm_dispatch ni a sm_run sobre la misma máquina. on_run devuelve su evento en su lugar, y todo lo demás se encola. Las guardas no deben tener efectos secundarios. Una guarda se puede ejecutar para una fila que nunca se dispara, así que responde una pregunta y no cambia nada. El orden de las filas es comportamiento. Gana la primera coincidencia, así que las filas de seguridad con SM_ANY_STATE van primero y las filas con guarda van arriba de sus respaldos. Reordenar la tabla cambia lo que hace la máquina. Las autotransiciones omiten la entrada y la salida. Solo se ejecuta la acción, así que cambiar la velocidad no detiene ni vuelve a arrancar el motor. Recorrido lineal. Cada despacho recorre la tabla de arriba hacia abajo, lo cual está bien para decenas de filas. Máquina plana. No hay estados jerárquicos, así que el comportamiento compartido entre estados va en filas SM_ANY_STATE. 255 estados y 255 eventos. uint8_t mantiene chica cada fila, y 0xFF está reservado para SM_ANY_STATE y SM_NO_EVENT. No se verifica el tamaño de la tabla de hooks. sm_init solo recibe un puntero, así que no puede saber si hooks tiene state_count entradas, y una tabla corta significa saltar a través de un puntero a función basura. Hay que declararla como hooks[STATE_COUNT], con STATE_COUNT como el último valor del enum de estados. El compilador marca las entradas de más (compilar con -Werror para que sea un error duro), y las que faltan se rellenan con ceros, es decir NULL, que significa sin hook. Los campos de sm_t son de solo lectura. Se pueden leer para depurar, pero escribirlos se salta la validación que hizo sm_init. Para saber dónde está la máquina, hay que preguntarle a sm_state. Llevamos tres patrones, y este es en el que los datos empiezan a significar algo. La mayoría de las aplicaciones embebidas se descomponen en una máquina de estados en cuanto se enumera lo que el sistema puede estar haciendo y lo que le puede pasar, y escribir esa lista como una tabla obliga a responder cada «¿y si...?» por adelantado. El búfer circular y el pool nos dieron almacenamiento al que no le importaba qué guardaba. La máquina de estados decide qué pasa después, y como sm_event_t es un solo byte, el búfer circular de la primera parte puede transportar sus eventos tal cual. Lo que sigue en la serie es el despachador de comandos. Lee su entrada directamente de un búfer circular y asigna cada comando a la función que lo maneja: la misma idea basada en tablas, aplicada a texto en lugar de eventos. Nos vemos ahí.
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)