Gestión de tareas en el firmware

Me gustaría obtener un consejo sobre el manejo de tareas en un firmware.

Tengo 3 tareas principales que hacer:

  1. Escanear si se está presionando un interruptor o no.

  2. Transmisión de datos vía SPI - A EEPROM

  3. Transmisión de datos vía USB - A PC

Logré hacer una interrupción del temporizador cada 1 ms para ver si se presionó el interruptor o no. Pero la transmisión SPI ocurre solo cada 6 horas [tarda 50 ms en completarse] y la transmisión USB puede ocurrir en cualquier momento.

Mi pregunta es ¿cómo gestiono las tareas?

¿Debo deshabilitar la verificación del interruptor una vez que ingresa al estado 2/estado 3 y habilitarla después de que sale de los estados?

¿Cómo se supone que debo manejar tales situaciones?

Ninguno de los anteriores parece muy buen candidato para interrupciones. Si está escaneando los interruptores, entonces esto, por definición, no está impulsado por interrupciones: está escaneando para encontrar un interruptor que puede haberse cerrado desde la última vez que escaneó. Aparte de cualquier otra cosa, si tiene muchos interruptores (dispuestos en una matriz), entonces la exploración en segundo plano es lo más sensato en muchas ocasiones. La transmisión de datos está dictada por la necesidad de transmitir datos; por lo general, es la recepción de datos lo que la gente considera importante para que trabaje un controlador de interrupciones.
solo tengo un solo interruptor @Andyaka
Sugeriría sondear ese interruptor de forma regular en lugar de que genere una interrupción; piense en el rebote del interruptor y lo que puede hacer si no maneja la interrupción correctamente. Si tiene un cronómetro regular, encuéntrelo en el tic, diría yo.
Supongo que f/w = FirmWare?
¿Podemos suponer que el SPI y el USB son mensajes SALIENTES y no ingresan a su sistema?
si.ha sido bien editado
¿Cómo se activa la transmisión USB a la PC? ¿Se hace periódicamente dentro de algún intervalo de tiempo?
sí. Así es @Avin. cada 50 ms de verificación cruzada

Respuestas (1)

La mayoría de las veces es bueno manejar las diversas cosas que el procesador necesita hacer con una combinación de interrupciones, tareas y eventos. En este caso, usaría una interrupción periódica de 1 ms para verificar y eliminar el rebote del interruptor, una tarea separada para manejar la transmisión USB del host y eventos u otra tarea para administrar la comunicación SPI. USB requiere cierto soporte continuo, incluso cuando en realidad no está enviando ni recibiendo nada en la capa de aplicación. Lo convertiría en una tarea separada o llamaría a la rutina de procesamiento en segundo plano USB regularmente desde el bucle de eventos principal.

Mi arquitectura habitual para tales cosas contiene las siguientes construcciones:

  • Interrupción periódica de 1 ms. 1 ms es un tiempo "largo" para la mayoría de los microcontroladores, por lo que solo requiere una pequeña fracción de la CPU. Aquí es donde se realiza la lógica antirrebote para los interruptores mecánicos. Una señal de interruptor debe verse en un estado durante 50 interrupciones consecutivas para que se declare oficialmente que está en ese estado. El resto del sistema solo ve los bits de bandera que representan el estado de rebote de cada entrada del interruptor.

    Esta interrupción también incrementa uno o más contadores de pulsos de reloj. A veces, las potencias de 10 son útiles, como 1 ms, 10 ms, 100 ms y segundos. La rutina de interrupción incrementa cada uno de estos según corresponda, y el resto del sistema los ve cambiar automáticamente.

  • Interrupciones para otras cosas que deben manejarse con baja latencia, como lecturas A/D. La rutina de interrupción A/D toma la nueva lectura del A/D, posiblemente aplica algún filtro de paso bajo, tal vez aplica escala y compensación, y deja los resultados finales en variables globales para que el resto del sistema los lea cuando quiera.

  • Una tarea separada para manejar la recepción de cada flujo de comunicaciones. La información en los flujos de comunicación en serie suele depender bastante del estado. Por lo tanto, tiene sentido procesar dichos flujos utilizando máquinas de estado. Una tarea es básicamente una máquina de estado donde la PC se usa como variable de estado, excepto que es más fácil de mantener, menos ofuscada y mucho más intuitiva que una máquina de estado tradicional implementada como un GOTO calculado en la variable de estado. Dado que el estado para procesar cada flujo de comunicación es independiente y asincrónico con respecto a otro estado del sistema, tiene sentido asignarle a cada flujo su propia tarea.

    Cada tarea se ejecuta como un ciclo infinito, excepto que llama a TASK_YIELD cuando no tiene nada que hacer de inmediato o después de una parte significativa del trabajo. Para recibir un flujo de bytes, es más conveniente escribir el código como si saliera y obtuviera el siguiente byte, aunque en realidad los bytes entran cuando entran. La rutina GET_BYTE generalmente comienza en un ciclo que llama a TASK_YIELD, verifica para que haya un nuevo byte disponible y, si no, vuelve al inicio del bucle. Cuando un byte está disponible, regresa con el nuevo byte. Esto permite que el código de la tarea principal llame a GET_BYTE en muchos lugares que dependen del contexto. Por ejemplo, al comienzo del bucle de la tarea principal, llama a GET_BYTE para obtener el código de operación del siguiente paquete. El código que se envía a la rutina para ese código de operación, que llama a GET_BYTE para obtener el primer byte de datos, etc.

  • Una tarea, normalmente la original para que no sea necesario hacer nada especial si no hay otras tareas, ejecuta el bucle de eventos principal. Esto verifica y maneja todas las pequeñas cosas que deben manejarse ocasionalmente. Llama a TASK_YIELD en la parte superior del ciclo, luego verifica secuencialmente los eventos que se van a manejar. Si un evento no está pendiente, salta al final de esa sección, que luego procede a verificar el próximo evento. Si se encuentra un evento pendiente, maneja ese evento y vuelve a la parte superior del bucle. Si no se encuentran eventos pendientes, llega al final del bucle, que simplemente vuelve al principio.

    Este mecanismo da prioridad a los eventos de manera efectiva y los marca en orden de prioridad alta a baja. Cada evento debe ser algo que sea relativamente simple de manejar y no requiera esperar nada más. Esto mantiene el ciclo de eventos principal llamando a TASK_YIELD regularmente. Si es necesario, divida los procedimientos más complicados en pasos independientes ejecutables inmediatamente, generalmente mediante el uso de una secuencia de indicadores de 1 bit para indicar que está listo para la siguiente fase de procesamiento. Por ejemplo, un evento podría iniciar una nueva lectura A/D. En lugar de esperar a que se haga eso, la rutina de interrupción A/D establece una bandera cuando hay una nueva lectura disponible. Ese es entonces un evento separado manejado por la rutina del evento principal.

    Algunos eventos se basan en el tiempo. En lugar de establecer banderas para ellos en la rutina de interrupción del reloj, generalmente hago que la rutina del evento principal verifique los contadores de tic del reloj global cerca del comienzo del ciclo para decidir si es necesario activar algún evento relevante basado en el tiempo. Esto tiene varias ventajas:

    • Mantiene limpia la rutina de interrupción del reloj. Es mejor para el mantenimiento no tener la lógica del evento principal oculta en otros módulos, como el módulo del reloj. Esto también permite utilizar más fácilmente un módulo de reloj genérico con una personalización mínima.

    • Mantiene la lógica local al módulo de eventos principal. Alguien que haga el mantenimiento más tarde puede ver toda la lógica en un solo lugar.

    • No pierde ticks en caso de que algún procesamiento tarde más de un intervalo de ticks. Mantengo el último valor de reloj conocido en la rutina del evento principal para cada evento cronometrado. Estos valores se comparan con los relojes actuales para determinar si hay un nuevo tick o no. Si es así, el último valor de reloj conocido solo se incrementa en uno y se gestiona el tick. Si por alguna razón el sistema se atrasa, se pondrá al día automáticamente con varios de estos eventos en sucesión cada vez que se verifiquen, lo que eventualmente hará que los últimos relojes conocidos vuelvan a sincronizarse con los relojes en vivo. Si se atrasa constantemente, entonces el sistema general estuvo mal diseñado y es necesario arreglar algo más.

Una gran cantidad de módulos de plantilla y algunos ejemplos están disponibles como parte de mi entorno de desarrollo de PIC . Esto incluye buenos administradores multitarea cooperativos para PIC 18 y PIC de 16 bits, plantillas para módulos de reloj, el bucle de eventos principal y mucho más.