7. Versión 4: integración final y modos de bajo consumo¶
7.1 Modos de bajo consumo¶
Ya tenemos todos los elementos del sistema para que sea funcional. Quizás quisiéramos —si se hace el diseño correspondiente— diseñar una PCB para desplegarlo en algún sitio. Si hiciésemos esto, muy seguramente alimentaríamos el dispositivo con una batería. Si midiésemos el consumo con un amperímetro, podríamos calcular la autonomía de nuestro sistema. Te habrás fijado que en los dispositivos comerciales como relojes inteligentes, mandos de TV, dispositivos IoT…la autonomía puede superar de largo varios meses con un uso normal del mismo. Para conseguir esto contamos con los modos de bajo consumo. Buena parte de los microcontroladores que hoy en día se precien cuentan con distintos modos de bajo consumo. Lea la sección “Modos de bajo consumo” del libro de Fundamentos Teóricos 1.
En esta versión, antes de hacer la integración final, vamos a implementar unas pocas funciones para gestionar el modo sleep de bajo consumo en nuestro sistema. Esto se destaca en 2 estados de la FSM de Simone que veremos más adelante. Estos estados comprueban si alguna de las FSM de los elementos está, o no, activa, y en caso de que todas estén inactivas, se va a dormir. El sistema se despertará ante alguna interrupción de un timer o interrupción externa (pulsación de botón o teclado).
Antes de empezar a implementar las funciones de bajo consumo vamos a partir de una serie de consideraciones de la FSM. En la siguiente sección se detallan mucho más los estados, pero por ahora, nos fijamos en lo relativo al bajo consumo:
-
En bajo consumo desactivaremos el SysTick para que no despierte al sistema cada \(1 ms\). Así pues, el contador del sistema no aumenta mientras se está dormido.
-
Las ISR que generan interrupciones externas —botón y teclado matricial— son las encargadas de reactivar el SysTick.
-
La FSM del botón está inactiva en el estado
BUTTON_RELEASED. -
La FSM del teclado matricial está inactiva en el estado
KEYBOARD_RELEASED_WAIT_ROW. -
La FSM del RGB light está activa si el status indica que está funcionando, y no está ocioso (idle).
-
Las autotransiciones de los estados de la FSM Simone (
SLEEP_WHILE_IDLEySLEEP_WHILE_PLAYBACK) están pensadas para cuando esté trabajando en depuración. El depurador genera interrupciones en la ejecución del código que despiertan a nuestro sistema. Como no se trata de interrupciones de nuestros elementos, no pasaremos a los estados, pero debemos dormirnos mientras no se detenga el depurador de nuevo en otro breakpoint. Este es el cometido de dichas autotransiciones.
Procedamos. Como siempre, tenga abierta la página web de la API https://sdg2dieupm.github.io/simone/, ahí están todos los detalles de implementación. Ahora vamos a tocar varios ficheros pero no crearemos ninguno nuevo.
Vamos a añadir las funciones de comprobación específicas de cada máquina de estados.
-
En
fsm_button.c: Añade la funciónfsm_button_check_activity()y su prototipo y documentación del código enfsm_button.h. -
En
fsm_keyboard.cañade la funciónfsm_keyboard_check_activity()y su prototipo y documentación del código enfsm_keyboard.h. -
En
fsm_rgb_light.cañade la funciónfsm_rgb_light_check_activity()y su prototipo y documentación enfsm_rgb_light.h.
Para terminar, vamos a añadir las funciones HW específicas de manejo del modo stop y sleep en nuestro STM32F446RE. Primero añadiremos algunas funciones generales del sistema en stm32f4_system.c; luego las modificaciones necesarias para restablecer el reloj de sistema SysTick tras una interrupción del botón o del teclado matricial, o de temporizador.
En stm32f4_system.c:
-
Copia el código de
port_system_power_stop()yport_system_power_sleep()de la API. Por tener un orden, puedes hacerlo en una parte dedicada a POWER RELATED FUNCTIONS. -
Copia el código de
port_system_systick_suspend()de la API. Por tener un orden, puedes hacerlo en la parte dedicada a TIMER RELATED FUNCTIONS. -
Copia el código de
port_system_systick_resume()de la API. Por tener un orden, puedes hacerlo también en la parte dedicada a TIMER RELATED FUNCTIONS. -
Implementa la función
port_system_sleep()como indica la API. Por tener un orden, puedes hacerlo junto con las anteriores en la parte dedicada a POWER RELATED FUNCTIONS.
En port_system.h:
- Añade los prototipos de las funciones anteriores y su documentación.
En el fichero en el que se encuentran nuestras ISR, interr.c, añade al principio de todas las ISR de todos las GPIO la llamada a port_system_systick_resume() para reactivar el contador del sistema SysTick inmediatamente tras la interrupción de pulsación de cualquier tecla o del botón de usuario: EXTI15_10_IRQHandler(), EXTI4_IRQHandler(), y EXTI9_5_IRQHandler().
¡Ya tenemos un sistema eficiente energéticamente! En el futuro ten siempre en consideración la importancia de estos modos de bajo consumo en cualquier sistema embebido que se alimente con baterías. Vamos a unir todas las piezas.
7.2 Integración final¶
Ya tenemos todos los módulos de las versiones V1-V3 de Simone desarrollados y probados: botón, teclado matricial y RGB light (LED) RGB. Ahora vamos a integrarlos en el sistema central, y rellenar el main.c del programa. Procedamos:
La máquina de estados del sistema Simone involucra a todos los elementos del mismo y la gestión del bajo consumo. Será una implementación principalmente de la lógica de control del juego en fsm_simone.c y fsm_simone.h. La parte dependiente del hardware (PORT) relacionada con la temporización del juego (port_simone) se os proporciona parcialmente implementada para facilitar la integración.
7.3 Mecánica del juego y reglas¶
El sistema debe gestionar la lógica del juego, tiempos de espera, niveles de dificultad y la interacción con los drivers de hardware (botón, teclado matricial y LED RGB).
7.3.1 Inicio¶
El sistema arranca en reposo en el estado IDLE. Al pulsar el botón de usuario, el sistema arranca pansando al estado que gestiona las secuencias de colores.
El juego tiene 3 niveles de dificultad predefinidos: fácil, medio y difícil. Al arrancar el sistema, empieza en modo fácil por defecto.
Los niveles predefinidos son: LEVEL_EASY, LEVEL_MEDIUM y LEVEL_HARD.
Consejo
Utiliza un enum para definir los niveles y #defines para definir las teclas de cad nivel. La FSM tendrá un campo level en su estructura para guardar el nivel del juego.
7.3.2 Generación de secuencia de colores y teclas asociadas¶
El juego básico usa 6 colores para mostrar al usuario de manera aleatoria en una secuencia, y se define también el color apagado para parpadear entre colores. Cada color estará asociado a una tecla. Los colores están definidos en rgb_colors.h como estructuras de tipo rgb_color_t. Las asociaciones son las siguientes:
| Tecla | Color |
|---|---|
| '0' | color_white |
| '1' | color_red |
| '2' | color_green |
| '3' | color_blue |
| '5' | color_yellow |
| '8' | color_turquoise |
El número de colores se debe definir en el fichero de cabecera con la etiqueta NUMBER_OF_COLORS_GAME.
Cada color de la secuencia se mostrará a una velocidad (tiempo que tarda en apagarse), y a una intensidad lumínica. La dificultad rige estos dos parámetros elegidos al inicio del juego.
| Nivel | Velocidad por color | Intensidad mínima |
|---|---|---|
LEVEL_EASY |
SIMONE_TIME_ON_LEVEL_EASY_MS: 3000 ms |
LEVEL_EASY_MIN_INTENSITY: 80% |
LEVEL_MEDIUM |
SIMONE_TIME_ON_LEVEL_MEDIUM_MS: 2000 ms |
LEVEL_MEDIUM_MIN_INTENSITY: 50% |
LEVEL_HARD |
SIMONE_TIME_ON_LEVEL_HARD_MS: 1000 ms |
LEVEL_HARD_MIN_INTENSITY: 20% |
En todos los casos el tiempo de apagado entre colores es fijo: SIMONE_TIME_OFF_BETWEEN_COLORS_MS: 300 ms, que se debe definir en el fichero de cabecera. De igual modo se debe definir el tiempo de espera máximo para la entrada del usuario entre pulsaciones: SIMONE_TIME_WAIT_INPUT_MS: 5000 ms, y el timpo de feedback visual al usuario tras cada pulsación: SIMONE_TIME_VISUAL_FEEDBACK_MS: 300 ms.
La longitud de la secuencia máxima es fija para todos los niveles SEQUENCE_LENGTH: 5 colores. Cuando el jugador complete la secuencia máxima en un nivel, el sistema subirá automáticamente al siguiente nivel (si no está ya en el máximo) y reiniciará la secuencia.
Consejo
Declara los #define de velocidad e intensidad en fsm_simone.h para mayor legibilidad.
El juego debe comportarse de forma determinista siguiendo las siguientes reglas:
7.3.3 Flujo del juego¶
-
Ronda:
- El sistema añade un color aleatorio a la secuencia.
- El sistema reproduce la secuencia completa usando el LED (respetando la velocidad del nivel actual). Nota: Debe haber un breve instante de apagado entre colores consecutivos para distinguirlos si son el mismo.
- El sistema espera a que el usuario repita la secuencia.
-
Turno del jugador:
- El usuario debe pulsar las teclas en el orden correcto.
- Timeout de usuario: Si el usuario tarda más de
SIMONE_TIME_WAIT_INPUT_MSmilisegundos en pulsar una tecla entre paso y paso, pierde la partida. Se ha establcido que este valor sea de 5000 ms. - Si se pulsa una tecla incorrecta, pierde la partida.
- Si se pulsa una tecla correcta, se reinicia el temporizador y el jugador tiene otros
SIMONE_TIME_WAIT_INPUT_MSmilisegundos para pulsar la siguiente tecla.
-
Victoria de ronda y juego:
- Si el usuario completa la secuencia actual correctamente, el sistema añade un nuevo color y repite el proceso (Ronda + 1).
- Si el usuario completa la secuencia de máxima longitud
SEQUENCE_LENGTHcorrectamente, aumenta de nivel. - Si el usuario ya estaba en el nivel máximo y completa la secuencia, gana la partida y la FSM va al estado
IDLE.
-
Game over
- Si el usuario pierde (por error o por timeout), el sistema debe mostrar un mensaje de resultado y volver al estado de reposo para permitir empezar una nueva partida.
7.4 Instrucciones de implementación¶
Para el desarrollo de la FSM, se os proporcionan la parte portable PORT, algunos #define de fsm_simone.h, y algún código en fsm_simone.c, como dos funciones auxiliares que facilitan la conversión entre los tipos de datos:
_get_key_from_color(): devuelve el carácter asociado a un color (ej. '1' para Rojo)._get_color_from_key(): devuelve el color asociado a un carácter.
Debéis implementar el resto de la lógica siguiendo la tabla de transiciones que diseñéis basándoos en la especificación de la FSM. Debéis completar el fichero de cabecera con los prototipos de función y añadir cualquier #include, #define, o función auxiliar que consideréis necesaria para el correcto funcionamiento.
Descarga del repositorio de la asignatura los ficheros correspondientes a la parte PORT y COMMON de la librería de Simone correspondientes a la versión V4: https://github.com/sdg2DieUpm/simone/tree/simone_v4 y colócalos en las carpetas correspondientes de tu proyecto.
Comprobad los ficheros .h y .c de PORT y completad las funciones que no estén hechas. De igual modo, comprobad que estén todos los #include necesarios. En particular:
- las funciones
_timer_simone_setup()yport_simone_set_timer_timeout()deport_simone.cdebéis implementarlas, para lo que os podéis basar en el código del temporizador del teclado matricial. - La ISR
TIM3_IRQHandler()deinterr.cdebe gestionar la interrupción del temporizador de Simone para controlar los tiempos de reproducción y de espera del usuario.
7.5 FSM Simone. Especificación detallada¶
En esta ocasión no se proporciona la máquina de estados, ni funciones, ni API. Se dará el detalle de la lógica de control del juego y algún detalle de implementación más crítico, así como las restricciones a implementar.
La lógica del juego es más compleja que la de un simple periférico. El sistema debe ser capaz de generar secuencias aleatorias, reproducirlas respetando tiempos, esperar la entrada del usuario, validar dicha entrada en tiempo real y gestionar la victoria o la derrota. Para gobernar todo esto, utilizaremos una FSM central que orquestará el funcionamiento del juego.
Objetivo
El objetivo de esta versión es implementar la lógica de control del juego en fsm_simone.c y fsm_simone.h. La parte dependiente del hardware (PORT) relacionada con la temporización del juego (port_simone) se os proporciona parcialmente implementada para facilitar la integración.
7.5.1 Definición de la estructura de datos¶
Antes de dibujar estados y transiciones, es fundamental entender qué datos necesita manejar nuestra máquina para funcionar. La estructura de datos fsm_simone_t actúa como la memoria del juego. Aparte de los punteros a las otras FSM (teclado, botón, luces), necesitamos variables para gestionar la secuencia. Complétala con los detalles que se indican a continuación.
Hay que tener cuidado de no confundir los diferentes índices que gestionan el progreso del juego. Observa los campos definidos en la estructura:
- FSMs de los elementos:
f: estructura base de la FSM de Simone de tipo sm_t` y que ha de ser el primer elemento de la estructura.p_fsm_button: puntero a la FSM del botón de usuario.p_fsm_keyboard: puntero a la FSM del teclado matricial.-
p_fsm_rgb_light: puntero a la FSM del LED RGB. -
Secuencia de datos:
seq_colors: un array que almacena la lista de`SEQUENCE_LENGTHcolores (rgb_color_t) de la secuencia actual.seq_intensities: un array paralelo al anterior, también de longitudSEQUENCE_LENGTH, y que almacena la intensidad ([0-100]) de cada color como un entero.-
level: almacena como un entero el nivel de dificultad actual definido en un enumerado (fácil, medio, difícil). -
Índices de control (¡Cuidado aquí!):
seq_idx: indica la longitud actual de la secuencia que se debe jugar. Si estamos en la ronda 3, este índice valdrá 3. Determina hasta dónde tiene que llegar la máquina reproduciendo y hasta dónde tiene que llegar el jugador repitiendo.playback_idx: es el índice de Simone. Recorre la secuencia del array de colores y de intensidades. Indica qué color de la secuencia se está mostrando actualmente por los LED.-
player_idx: es el índice del jugador. Recorre también la secuencia, pero para comparar si el valor pulsado por el usuario es correcto. -
Flags y otros campos:
player_key: almacena el caracter de la tecla que acaba de pulsar el usuario para poder verificarla con la correspondiente del color que debería haber pulsado el usuarioplayback_over: es un booleano que usaremos para controlar el parpadeo de los LED (encendido/apagado) durante la reproducción de la secuencia de colores.level: almacena el nivel de dificultad actual del juego como un entero. Albergará los valores del enumerado que contiene los nivelesLEVEL_EASY,LEVEL_MEDIUMyLEVEL_HARD.on_off_press_time_ms: entero que almacena el tiempo que el botón de usuario ha estado presionado (para gestionar el encendido y apagado del sistema).
Completa la estructura y documéntala en fsm_simone.h.
Consejo sobre los índices
El juego consiste esencialmente en comparar índices.
- La máquina Simone reproduce desde
0hastaseq_idxusando su índiceplayback_idx. - El jugador repite desde
0hastaseq_idxusando su índiceplayer_idx. - Si
player_idxalcanza aseq_idxes que todo han sido aciertos, por tanto ¡ronda superada! Se incrementaseq_idxy vuelta a empezar.
Importante: Nombres de las constantes
Para que vuestro código pase los test automáticos de los profesores, debéis respetar escrupulosamente los nombres de los #define de tiempos y teclas, así como los nombres de los estados en el enum FSM_SIMONE definidos en el fichero de cabecera proporcionado.
7.5.2 Especificación de la máquina de estados¶
El juego consta de 7 estados: 5 de juego y 2 de gestión de bajo consumo. Mantén los nombres proporcionados de los estados. Tienes que completar y documentar todas las funciones de la tabla de transiciones y funciones auxiliares faltantes, así como la propia tabla de transiciones. Sigue los criterios que hemos usado en las tres versiones anteriores.
La lógica del juego se divide en los siguientes estados principales. Estudia detenidamente qué debe ocurrir en cada uno y, sobre todo, qué condiciones provocan las transiciones a los siguientes estados.
7.5.2.1 Estado IDLE¶
Es el estado de reposo. El sistema está dormido esperando a que el usuario quiera jugar.
| Entradas | Salidas |
|---|---|
| (1) al arrancar el sistema | (1) Por encendido del usuario |
(2) Por victoria (desde WAIT_KEY) |
(2) Por inactividad |
(3) Por derrota (desde WAIT_KEY) |
|
(4) Por apagado del usuario (desde WAIT_KEY) |
7.5.2.1.1 Transición (1): Por encendido del usuario¶
La función de comprobación check_on() debe detectar si el jugador ha pulsado el botón de usuario durante el tiempo definido en main.c (SIMONE_ON_OFF_PRESS_TIME_MS). Si esto sucede, pasará al estado ADD_COLOR para iniciar la partida.
La función de acción do_init_game() inicializa las variables del juego. Debe resetear la duración del botón de usuario, la tecla del teclado matricial, los índices de control (seq_idx, playback_idx, player_idx), y las variables playback_over, y player_key (esta última al carácter definido en KEY_NO_KEY_PRESSED).
El nivel de dificultad lo inicializa a LEVEL_EASY. Inicializa cada elemento del array de secuencia de colores al color color_off (ver colores en rgb_colors.c), y cada elemento del array de las intensidades a 0.
Llama a una función privada auxiliar (_add_color()) pasándole un puntero a la máquina de estados de Simone para añadir un color e intensidad aleatorios a la secuencia. Será el primer color de la ronda 1.
Para que el LED RGB muestre el color, esta función debe activar el status de la FSM RGB light llamando a la función apropiada de dicha FSM.
Por último, antes de salir, imprime un mensaje de inicio al usuario. Algo como:
printf("[SIMONE][%ld] Simone game INIT\n", port_system_get_millis());
Función auxiliar _add_color
Esta función privada que encapsula la generación aleatoria recibe un puntero a la FSM de Simone y debe:
-
Generar un índice aleatorio para seleccionar un color del array
p_colors_library. Este array debe colocarse al inicio defsm_simone.cy contiene direcciones los 6 colores usados en el juego.const rgb_color_t *p_colors_library[] = {&color_red, &color_green, &color_blue, &color_yellow, &color_turquoise, &color_white};Para generar el índice aleatorio usa la función
rand()de la<stdlib.h>, y el operador módulo%para acotar el valor al rango0aNUMBER_OF_COLORS_GAME. El valor aleatorio se generará gracias a la semillasrand()iniciada enfsm_simone_init(). -
Generar una intensidad aleatoria respetando los rangos definidos para el nivel actual (usando los define
LEVEL_X_MIN_INTENSITY).Como la función
rand()devuelve un valor entre0yRAND_MAX, puedes usar la siguiente fórmula para acotar el valor al rango deseado:random_num = (random_num % (max - min + 1)) + min;Donde los valores máximos y mínimos dependen del nivel actual
level. -
Si
seq_idxha alcanzado el valorSEQUENCE_LENGTH, reseteamosseq_idx. Si no, guardamos el color e intensidad generados en las posicionesseq_idxde los arraysseq_colorsyseq_intensities, respectivamente, y luego incrementamosseq_idxen 1.
7.5.2.1.2 Transición (2): Por inactividad¶
Si no hay actividad, el sistema puede dormirse pasando al estado SLEEP_WHILE_IDLE.
La función de comprobación check_no_activity() devuelve directamente el valor inverso al de su contraria check_activity(). Esta última, lo que hace es devolver true si alguna de las FSM de los elementos (botón, teclado, RGB light) está activa. Para ello, llama a las funciones de comprobación de actividad que hemos implementado en la sección de bajo consumo (fsm_xxx_check_activity()).
La función de acción do_sleep_idle() debe poner el sistema en un estado de bajo consumo. Para ello llama a la función de sleep del PORT del sistema que hemos implementado en la sección de bajo consumo.
7.5.2.2 Estado ADD_COLOR¶
Este es un estado de transición. El sistema no se detiene aquí esperando eventos externos, sino que realiza las operaciones lógicas necesarias para preparar la secuencia de la siguiente ronda antes de reproducirla.
| Entradas | Salidas |
|---|---|
(1) Al iniciar partida (desde IDLE) |
(1) Secuencia actualizada |
(2) Al completar ronda (desde WAIT_KEY) |
7.5.2.2.1 Transición (1): Secuencia actualizada¶
La función de comprobación check_color_added() verifica si la longitud de la secuencia (seq_idx) es diferente del índice del jugador (player_idx). Como la la función auxiliar _add_color() habrá añadido un nuevo color, esta condición se cumplirá inmediatamente, permitiendo el paso al estado de reproducción de la secuencia PLAYBACK.
La función de acción do_playback() es el core de la reproducción. Su objetivo es gestionar el parpadeo de los LED respetando los tiempos de cada nivel. Dado que esta función se llama repetidamente, utiliza la variable playback_over como un selector para alternar entre dos fases: mostrar color y pausa.

Aunque el diagrama resume el flujo lógico, la implementación correcta de do_playback() requiere prestar atención a varios detalles técnicos para mantener la estabilidad del sistema:
- La naturaleza no bloqueante del temporizador: Cuando el diagrama indica "Set timer de Simone", debes llamar a
port_simone_set_timer_timeout(). Hay que entender que esta función no detiene la ejecución del código (no es undelay), sino que programa una interrupción futura. El microcontrolador se dormirá en el estadoSLEEP_WHILE_PLAYBACKhasta que ese tiempo expire, interrumpa y vuelva a comprobarse la tabla de transiciones. - Protección contra entradas espurias: Una de las primeras acciones es detener el escaneo del teclado. Si no se hace, el usuario podría pulsar teclas mientras se muestran las luces; esas pulsaciones se quedarían guardadas en el struct HW del teclado y se procesarían erróneamente en cuanto el juego pasara al estado de espera, provocando que se detecte como una tecla mal pulsada al inicio de la ronda siguiente.
- Acceso a la memoria de la secuencia: En la fase de encendido, debes recuperar la información almacenada previamente. Usa la variable
playback_idxpara acceder a los arrays paralelosseq_colorsyseq_intensities. Recuerda que la función de encendido del LED (fsm_rgb_light_set_color_intensity) requiere ambos parámetros. - Sincronización de índices: Fíjate bien en la comparación final. Comparamos
playback_idx(lo que estamos mostrando ahora) conseq_idx(la longitud total de la secuencia actual).- Si
seq_idxes 3, significa que hay colores en las posiciones 0, 1 y 2. - Cuando terminamos de mostrar el color 2 y su pausa, incrementamos
playback_idxa 3. - Como 3 es mayor o igual que 3, sabemos que hemos terminado.
- Si
El marcador de fin de playback
Para marcar el final de playback y poder comprobar al inicio si ha acabado o no, podemos usar varios mecanismos. Puedes usar, por ejemplo, una variable global, o puedes usar un valor inválido (que nunca vaya a ocurrir en el índice playback_idx). Cualquiera que uses, ten en cuenta que este marcador será la forma de comunicar a la función de comprobación check_playback_over() que la tarea de reproducción ha concluido.
7.5.2.3 Estado PLAYBACK¶
En este estado el sistema ha tomado el control para mostrar la secuencia de colores al jugador como se ha mostrado en el flujograma de do_playback.
| Entradas | Salidas |
|---|---|
(1) Desde ADD_COLOR |
(1) Apagado manual |
(2) Desde SLEEP_WHILE_PLAYBACK |
(2) Turno del jugador |
| (3) Por inactividad |
7.5.2.3.1 Transición (1): Apagado manual¶
La función de comprobación check_off() verifica si el botón de la placa se ha mantenido pulsado el tiempo suficiente. Es idéntica a la función check_on(). Si se cumple, el sistema debe volver al estado de reposo IDLE.
La función de acción do_stop_simone(): resetea la duración del botón de usuario, desactiva el estado del LED, resetea el nivel de dificultad a LEVEL_EASY, e imprime un mensaje de despedida indicando que el juego ha terminado y que puede presionar el botón para iniciar una nueva partida.
7.5.2.3.2 Transición (2): Turno del jugador¶
La función de comprobación check_playback_over() determina si la máquina ha terminado de reproducir toda la secuencia y, además, ha terminado el tiempo de espera del último apagado. Debe devolver true solo si se cumplen dos condiciones simultáneamente:
- El marcador de fin de reproducción está activado (establecido en
do_playback()). - El temporizador ha expirado (
port_simone_get_timeout_status()).
La función de acción do_start_player_sequence() prepara el sistema para escuchar al usuario. La función debe:
- Reiniciar flag
playback_overy reiniciar el índice del jugadorplayer_idxpara empezar a comprobar desde el principio. - Apagar LED con el color
color_offllamando a la función correspondiente de la FSM del LED. - Set timeout del temporizador con el tiempo máximo que tiene el usuario para reaccionar (
SIMONE_TIME_WAIT_INPUT_MS). - Iniciar el escaneo del teclado llamando a la función correspondiente de la FSM del teclado, ya que se desactivó durante la reproducción.
- Imprimir un mensaje por consola informando al usuario de que es su turno y cuántos segundos tiene para responder entre pulsaciones.
7.5.2.3.3 Transición (3): Por inactividad¶
Si no ha expirado el temporizador mientras mostramos un color o una pausa, el sistema puede dormirse para ahorrar energía.
La función de comprobación check_no_activity() devuelve el valor inverso al de su contraria check_activity().
La función de acción do_sleep_playback() debe poner el sistema en un estado de bajo consumo. Para ello llama a la función de sleep del PORT del sistema que hemos implementado en la sección de bajo consumo.
7.5.2.4 Estado WAIT_KEY¶
Es el turno del jugador. El sistema espera cualquier reacción por parte del usuario, ya sea para apagar el juego, introducir una tecla de la secuencia, o porque se ha agotado el tiempo.
| Entradas | Salidas |
|---|---|
(1) Desde PLAYBACK |
(1) Apagado manual |
(2) Desde VERIFY_INPUT |
(2) Victoria final |
| (3) Derrota por tiempo | |
| (4) Fin de ronda | |
| (5) Pulsación de una tecla |
7.5.2.4.1 Transición (1): Apagado manual¶
La función de comprobación check_off() verifica si el botón de la placa se ha mantenido pulsado el tiempo suficiente. Es idéntica a la función check_on(). Si se cumple, el sistema debe volver al estado de reposo IDLE.
La función de acción do_stop_simone(): resetea la duración del botón de usuario, desactiva el estado del LED, resetea el nivel de dificultad a LEVEL_EASY, e imprime un mensaje de despedida indicando que el juego ha terminado y que puede presionar el botón para iniciar una nueva partida.
7.5.2.4.2 Transición (2): Victoria final¶
La función de comprobación check_winner() es la más estricta. Deben cumplirse 3 condiciones: se cumple solo si (1) el jugador ha terminado la secuencia (debes jugar con los índices del jugador y la secuencia), (2) el índice que recorre el array de la secuencia ha llegado o superado la longitud máxima (SEQUENCE_LENGTH) y, además, (3) estamos en el nivel level de dificultad más alto (LEVEL_HARD).
Si se cumplen estas 3 condiciones, el jugador ha ganado la partida y pasa al estado de reposo IDLE.
La función de acción do_winner() detiene el temporizador (port_simone_stop_timer()), y muestra un mensaje de felicitación por consola indicando cuántos colores ha conseguido recordar el jugador.
7.5.2.4.3 Transición (3): Derrota por tiempo¶
La función de comprobación check_player_key_timeout() verifica si el temporizador de espera de usuario ha expirado (port_simone_get_timeout_status()). Si el jugador tarda demasiado en pensar (SIMONE_TIME_WAIT_INPUT_MS), la condición se cumple y pasa al estado de reposo IDLE.
La función de acción do_game_over_timeout() gestiona el fin de la partida por tiempo. Detiene el temporizador (port_simone_stop_timer()), reinicia todos los índices y elementos de la estructura de Simone (seq_idx, player_idx, etc.), detiene el escaneo del teclado antes de volver al reposo. Por último imprime un mensaje de Game Over, indicando alguna estadística relevante (por ejemplo, cuántos colores ha conseguido recordar el jugador).
7.5.2.4.4 Transición (4): Fin de ronda¶
La función de comprobación check_player_round_end() verifica si (1) el jugador ha reproducido con éxito toda la secuencia actual (comparando los índices player_idx y seq_idx), pero (2) aún no ha cumplido las condiciones de victoria total; esto es, no ha alcanzado la longitud máxima de la secuencia o no está en el nivel más alto.
Si se cumplen estas dos condiciones, el jugador ha superado la ronda y pasa al estado ADD_COLOR para preparar la siguiente ronda.
La función de acción do_add_color() prepara el sistema para el siguiente nivel o secuencia. Puedes ver su lógica detallada en el flujograma.

7.5.2.4.5 Transición (5): Pulsación de una tecla¶
La función de comprobación check_any_key_pressed() utiliza el driver del teclado para detectar si hay alguna tecla disponible en el buffer. Devuelve true si el usuario ha pulsado algo (valor de tecla leída es distinto de KEY_NO_KEY_PRESSED). En tal caso, el sistema transiciona al estado intermedio VERIFY_INPUT para validar la pulsación.
La función de acción do_capture_input() realiza de nuevo la lectura de la tecla pulsada y proporciona feedback visual inmediato al usuario del color de la tecla que haya pulsado (sea correcta, o no). Debe:
- Obtiene la tecla del teclado y la guarda en la variable
player_key. Luego, resetea el valor de la tecla del teclado para evitar lecturas repetidas usando la función correspondiente de la FSM del teclado. - Traduce la tecla a color usando la función auxiliar
_get_color_from_key(), y enciende el LED al máximo brillo con el color resultante. - Pone el temporizador de Simone con un tiempo breve de feedback visual (definido en
SIMONE_TIME_VISUAL_FEEDBACK_MS).
7.5.2.5 Estado VERIFY_INPUT¶
Este es un estado temporal de retención. El sistema entra aquí justo después de que el usuario pulse una tecla para mantener el LED encendido durante un breve instante (SIMONE_TIME_VISUAL_FEEDBACK_MS), permitiendo al jugador ver qué color ha seleccionado. En este estado se comprueba si la tecla leída es la esperada, o no.
| Entradas | Salidas |
|---|---|
(1) Desde WAIT_KEY |
(1) Tecla correcta |
| (2) Tecla incorrecta |
7.5.2.5.1 Transición (1): Tecla correcta¶
La función de comprobación check_input_valid() realiza una validación en dos pasos:
- Espera visual: Primero verifica si el temporizador de feedback visual ha expirado (
!port_simone_get_timeout_status()). Si el tiempo no ha pasado, devuelvefalsey sale de la función. - Validación lógica: Si el tiempo ha pasado, compara la tecla pulsada guardada en la estructura con la esperada según la secuencia (ayúdate de la función auxiliar
_get_key_from_color()). Devuelvetruesolo si coinciden. En este caso transicionará de vuelta al estadoWAIT_KEYpara esperar la siguiente pulsación.
La función de acción do_valid_key() consolida el progreso del jugador. Debe:
- Apagar el LED con el color
color_off. - Aumentar el índice del jugador.
- Reiniciar la tecla guardada en la estructura a
KEY_NO_KEY_PRESSED. - Reiniciar el temporizador (
port_simone_set_timer_timeout()) para dar tiempo al usuario a pulsar la siguiente tecla.
7.5.2.5.2 Transición (2): Tecla incorrecta¶
La función de comprobación check_input_invalid() sigue la misma lógica temporal que la anterior, pero devuelve true si la tecla pulsada es diferente a la esperada.
La función de acción do_game_over_invalid_key() gestiona la derrota:
- Apaga el LED para finalizar el feedback.
- Reinicia todos los elementos de la estructura de Simone (
seq_idx,player_idx,player_key, etc.) para que la próxima vez se empiece desde cero. - Reiniciar el temporizador de Simone (
port_simone_stop_timer()) y detiene el escaneo* del teclado usando la función correspondiente de la FSM del teclado. - Muestra por consola información sobre qué tecla se esperaba y cuál se pulsó realmente, junto con un mensaje de Game Over. Hace uso de la función auxiliar
_get_key_from_color().
7.5.2.6 Estado SLEEP_WHILE_IDLE¶
Es el estado de bajo consumo. El sistema entra aquí cuando está en reposo (IDLE) y no hay ninguna interacción por parte del usuario, permitiendo ahorrar energía mientras se espera a que se inicie una nueva partida.
| Entradas | Salidas |
|---|---|
(1) Desde IDLE |
(1) Detección de actividad |
(2) Desde SLEEP_WHILE_IDLE (autotransición) |
(2) Sin actividad |
7.5.2.6.1 Transición (1): Detección de actividad¶
La función de comprobación check_activity() verifica si algún periférico ha generado un evento (botón pulsado, tecla pulsada, etc.). Al despertar por una interrupción, esta condición se cumple y el sistema transiciona de vuelta a IDLE para procesar dicho evento.
En esta transición no hay función de acción asociada (es NULL), ya que la propia salida del estado de sueño es suficiente para reactivar la lógica principal.
7.5.2.6.2 Transición (2): Sin actividad¶
La función de comprobación check_no_activity() confirma que el sistema sigue inactivo. Esta autotransición sirve para gestionar el bajo consumo en modo depuración, cuando se despierta por un breakpoint o similar.
La función de acción do_sleep_idle() detendrá el reloj de la CPU hasta que ocurra la próxima interrupción.
7.5.2.7 Estado SLEEP_WHILE_PLAYBACK¶
Este estado gestiona el bajo consumo durante la reproducción. Mientras el LED está encendido mostrando un color o apagado durante una pausa, no es necesario que la CPU esté consumiendo ciclos. El sistema duerme aquí hasta que el temporizador interrumpe.
| Entradas | Salidas |
|---|---|
(1) Desde PLAYBACK |
(1) Timeout del color/pausa |
(2) Desde SLEEP_WHILE_PLAYBACK (autotransición) |
(2) Sin actividad |
7.5.2.7.1 Transición (1): Timeout del color/pausa¶
La función de comprobación check_playback_color_timeout() consulta al si el temporizador (port_simone_get_timeout_status()) configurado en el paso anterior ha expirado. Si es así, significa que es hora de cambiar el estado del LED.
La función de acción asociada es do_playback(). Esta transición devuelve al sistema al estado PLAYBACK, ejecutando inmediatamente la lógica de alternancia de luces (encender/apagar) descrita en el flujograma de dicho estado.
Funcionamiento cíclico
Observa que el sistema entra y sale constantemente entre PLAYBACK y SLEEP_WHILE_PLAYBACK.
PLAYBACKconfigura el LED y el temporizador, y salta a dormir.- Espera dormido en bajo consumo.
- Timer interrumpe y vuelve a
PLAYBACKpara cambiar el LED, y puede volver a dormir de nuevo.
7.5.2.7.2 Transición (2): Sin actividad¶
La función de comprobación check_no_activity() verifica que no hay eventos pendientes.
La función de acción do_sleep_playback() detendrá el reloj de la CPU hasta que ocurra la próxima interrupción de la misma forma que lo hace do_sleep_idle(), o do_sleep_playback().
Funciones de bajo consumo
Habrás notado que hay 2 funciones que hacen lo mismo: do_sleep_idle() y do_sleep_playback(). Esto es una buena práctica para poder saber dónde está y de dónde viene el sistema cuando se está depurando.
Bajo consumo durante la lectura del teclado
Nótese que no hay bajo consumo en el estado WAIT_KEY. Esto es intencionado, ya que el jugador debe poder interactuar en cualquier momento. Por la forma en la que se excitan y leen las filas y columnas, la gestión del bajo consumo aquí es posible, pero más complicada de manejar. Se deja como implementación a elegir en la Versión 5.
7.5.2.8 Inicialización de la FSM Simone¶
Ya hemos codificado las funciones de entrada y salida, ahora vamos a codificar las funciones privadas que nos quedan.
Codifica la función fsm_simone_init() de forma análoga a las anteriores máquinas de estados.
- Llama a la función
fsm_init()pasándole el puntero a la máquina de estados, y el array de transiciones. - Inicializa el HW asociado a la FSM de Simone llamando a
port_simone_init(). - Inicializa en la estructura todos los elementos que ser reciben: los punteros a las máquinas de estados de los elementos del sistema Simone, el tiempo de pulsación del botón para encender y apagar, y el nivel inicial.
- Inicializa la semilla aleatoria con la función
srand(time(NULL))para asegurar que los números aleatorios generados sean diferentes en cada ejecución. Deberás importar la cabecera<time.h>para usar la funcióntime(). - Imprime un mensaje por consola indicando al usuario que debe pulsar el botón para iniciar una nueva partida.
Codifica las funciones fsm_simone_fire() y fsm_simone_destroy() de forma análoga a las anteriores máquinas de estados.
Ya hemos terminado con al FSM, ahora vamos a integrar todas las FSM en el main.c y a probarlo.
7.6 Integración HW-SW de la FSM Simone¶
Ha llegado la hora de integrar la parte HW-SW del sistema, y depurar. Vamos a escribir las líneas de código necesarias en main.c para probar que funciona. Procedamos:
-
Abre el fichero
main.ce incluye las cabeceras necesarias. -
Define la macro
SIMONE_ON_OFF_PRESS_TIME_MScomo indica la API para definir una pulsación larga como aquella que supere \(1 s\). ¡Ojo, porque el tiempo hay que darlo en milisegundos! Este es el tiempo que se debe mantener pulsado el botón para encender y apagar el juego. -
Después de la inicialización del sistema con la llamada a la función
port_system_init(), crea la máquina de estados para el botón. Dale un nombre representativo (*e.g.*,p_fsm_button). Para ello llama a la funciónfsm_button_new()con los argumentos necesarios. A continuación, haz lo propio con la máquina de estados del teclado matricial, y con la del RGB light trasero. Dales nombres representativos. -
Crea la máquina de estados para el sistema Simone, puedes darle un nombre representativo (*e.g.*,
p_fsm_simone). ¡No pases los valores “a pincho”, usa los#defineque has creado! -
En el bucle
while, lanza constantemente la funciónfsm_xxx_fire(), para las máquinas de estados del botón, el teclado, el RGB light y Simone.Conviene que el sistema sea lo último porque depende de la actualización del estado de las FSM de los elementos anteriores.
-
Por último solo nos queda un aspecto meramente formal, casi académico. Cuando creamos las máquinas de estado con las funciones
fsm_xxx_new()estamos reservando memoria de forma dinámica (con la funciónmalloc()). Cuando las máquinas de estado dejan de usarse, esa memoria debe ser liberada para poder ser usada por otras partes del código. Esa liberación se hará con la llamada a la funciónfsm_destroy().Después del bucle
whilellama afsm_xxx_destroy()para cada una de las FSM pasándole su tipo concreto. Esto libera la memoria de cada una de las máquinas de estado creadas: botón, teclado matricial, RGB light, y Simone.Como se decía, esto es pura ortodoxia, porque el bucle
whiledelmaines infinito, y nunca saldrá de ahí, por lo que nuestras FSM nunca dejarán de usarse y las líneas que acabas de escribir confsm_xxx_destroy()nunca se ejecutarán. No obstante, conviene que sepas que así debería hacerse. -
Compila y comprueba que no tiene ningún error. Si tienes, corrígelos.
Puede ser buen momento ahora para documentar todo el código con Doxygen.
7.7 Test de integración de Simone¶
7.7.1 Test de funcionalidades¶
Ya has probado los test de ejemplo de los distintos elementos y que se os han sido proporcionado. Ahora vamos a probar el sistema Simone completo. No se os va a pedir hacer un test formal, pero con todo el trabajo que has hecho, es conveniente que compruebes que todo funciona correctamente. Monta el circuito como se muestra en la . Prueba todo el sistema como en el vídeo demostración Simone. Procedamos:

-
Conecta la placa Nucleo-STM32 al ordenador.
-
Compila y carga el programa
mainen la placa (
Clean and Debug). Comprueba que no tienes errores de compilación. -
Prueba que el botón enciende y apaga el juego Simone.
-
Prueba que el RGB light se enciende de manera acorde en la primera ronda y enciende un color y mostrando los mensajes por la terminal oportuna
-
Prueba que el teclado recoge correctamente las teclas, tanto cuando aciertos, como cuando fallas.
-
Prueba que el nivel de dificultad sube correctamente cada vez que se completa una secuencia de longitud
SEQUENCE_LENGTH, hasta un máximo de 3 niveles. -
Prueba que mientras está haciendo el playback no responde al teclado.
-
Prueba que puedes apagarlo y encenderlo en cualquier momento.
-
Prueba que, estando apagada, el juego Simone no responde al teclado ni muestra nada por el LED RGB.
-
Prueba, en general, el funcionamiento correcto como en el vídeo de demostración. Si encuentras algún error, corrígelo.
7.7.2 Comprobación de bajo consumo¶
Comprobaremos que el modo sleep de bajo consumo se gestiona correctamente. Compila y comprueba que no tienes errores de sintaxis o de código. Para comprobar que el sistema está dormido, podemos hacerlo de dos formas:
-
Lo más habitual —si no tenemos acceso a un depurador y si tenemos que caracterizar nuestro producto— sería hacerlo mediante la medición del consumo del microcontrolador (¡no de los elementos HW de nuestro sistema!). Para medir el consumo, ve el punto “6.6 JP6 (IDD)” del manual de la placa 2. Si vas a medirlo, también deberías desconectar los jumpers del
ST-LINKdel conectorCN2. Para ver el ahorro tendríamos que medirlo en ejecución sobre versión final. Esto puedes hacerlo si deseas como funcionalidad extra en la Versión 5, e incluir la información en la documentación del código (ficheroREADME.md). -
Lo que haremos para demostrar que el sistema alterna entre el modo despierto y dormido será depurando. Continúa con la depuración sin poner puntos de parada. Cuando el sistema esté inactivo, pausa la depuración y comprueba que se ha detenido en la línea de código tras la llamada a wait for interrupt (
__WFI()), similar a como se muestra en la . Esto querrá decir que, efectivamente, la ejecución estaba detenida esperando una interrupción, se ha despertado, y ha pasado a la siguiente línea de código.

Realiza con con Paint, Drawio, o cualquier programa que elijas el diagrama de la FSM del sistema con todas sus transiciones. Añádela a tu README.md.
¡Ya tenemos el sistema Simone funcionando! No olvides documentarlo (vídeo "[MatrixMCU] Documentación de código con Doxygen”). En la siguiente versión podrás añadir más funcionalidades a tu elección. Puedes incluir más capturas o imágenes para enriquecer la documentación.
Guarda una copia de su proyecto como simone_v4 para tener un punto de partida para la siguiente versión, y una copia de seguridad por si algo falla. Esta copia súbela al buzón de entrega de la asignatura separada de la que hagas con la versión 5, que tiene otro buzón. ¡Ánimo!
-
Josué Pagán Ortiz, Pedro José Malagón Marzo, Román Cárdenas Rodríguez, and Juan José Gómez Valverde. Fundamentos teóricos de sistemas basados en microcontrolador STM32. Sistemas Digitales II, Sistemas Electrónicos. Josué Pagán Ortiz, Madrid, March 2025. URL: https://oa.upm.es/88460/. ↩
-
STMicroelectronics. Um1724 user manual. stm32 nucleo-64 boards. Technical Report, STMicroelectronics, 2020. URL: https://www.st.com/resource/en/user_manual/um1724-stm32-nucleo64-boards-mb1136-stmicroelectronics.pdf. ↩