La multitarea es importante en estos días. Me pregunto cómo podemos lograrlo en microcontroladores y programación integrada. Estoy diseñando un sistema basado en un microcontrolador PIC. Diseñé su firmware en MplabX IDE usando C y luego diseñé una aplicación para él en Visual Studio usando C #.
Ya que me he acostumbrado a usar hilos en la programación de C # en el escritorio para implementar tareas paralelas, ¿hay alguna manera de hacer lo mismo en mi código de microcontrolador? MplabX IDE proporciona pthreads.h
pero es solo un trozo sin implementación. Sé que hay soporte para FreeRTOS, pero usarlo hace que su código sea más complejo. Algunos foros dicen que las interrupciones también se pueden usar como tareas múltiples, pero no creo que las interrupciones sean equivalentes a hilos.
Estoy diseñando un sistema que envía algunos datos a un UART y, al mismo tiempo, necesita enviar datos a un sitio web a través de Ethernet (por cable). Un usuario puede controlar la salida a través del sitio web, pero la salida se ENCIENDE / APAGA con un retraso de 2-3 segundos. Entonces ese es el problema que estoy enfrentando. ¿Hay alguna solución para la multitarea en microcontroladores?
fuente
Respuestas:
Hay dos tipos principales de sistemas operativos multitarea, preventivos y cooperativos. Ambos permiten que se definan múltiples tareas en el sistema, la diferencia es cómo funciona el cambio de tareas. Por supuesto, con un único procesador central solo se ejecuta una tarea a la vez.
Ambos tipos de sistemas operativos multitarea requieren una pila separada para cada tarea. Esto implica dos cosas: primero, que el procesador permite que las pilas se coloquen en cualquier lugar de la RAM y, por lo tanto, tiene instrucciones para mover el puntero de pila (SP), es decir, no hay una pila de hardware de propósito especial como la que hay en el extremo inferior PIC's. Esto deja de lado las series PIC10, 12 y 16.
Puede escribir un sistema operativo casi por completo en C, pero el conmutador de tareas, donde se mueve el SP, debe estar ensamblado. En varias ocasiones he escrito conmutadores de tareas para PIC24, PIC32, 8051 y 80x86. Las agallas son muy diferentes dependiendo de la arquitectura del procesador.
El segundo requisito es que haya suficiente RAM para proporcionar múltiples pilas. Por lo general, uno quisiera al menos un par de cientos de bytes para una pila; pero incluso con solo 128 bytes por tarea, ocho pilas requerirán 1K bytes de RAM; sin embargo, no tiene que asignar el mismo tamaño de pila para cada tarea. Recuerde que necesita suficiente pila para manejar la tarea actual, y cualquier llamada a sus subrutinas anidadas, pero también apila espacio para una llamada de interrupción ya que nunca sabe cuándo ocurrirá una.
Existen métodos bastante simples para determinar la cantidad de pila que está utilizando para cada tarea; por ejemplo, puede inicializar todas las pilas a un valor particular, digamos 0x55, y ejecutar el sistema durante un tiempo y luego detener y examinar la memoria.
No dices qué tipo de PIC quieres usar. La mayoría de los PIC24 y PIC32 tendrán mucho espacio para ejecutar un sistema operativo multitarea; El PIC18 (el único PIC de 8 bits que tiene pilas en RAM) tiene un tamaño de RAM máximo de 4K. Eso es bastante dudoso.
Con la multitarea cooperativa (la más simple de las dos), el cambio de tareas solo se realiza cuando la tarea "cede" su control al sistema operativo. Esto sucede cuando la tarea necesita llamar a una rutina del sistema operativo para realizar alguna función que esperará, como una solicitud de E / S o una llamada del temporizador. Esto facilita que el sistema operativo cambie las pilas, ya que no es necesario guardar todos los registros y la información de estado, el SP solo puede cambiarse a otra tarea (si no hay otras tareas listas para ejecutarse, una pila inactiva es control dado). Si la tarea actual no necesita hacer una llamada al sistema operativo pero se ha estado ejecutando durante un tiempo, debe ceder el control voluntariamente para mantener el sistema receptivo.
El problema con la multitarea cooperativa es que si la tarea nunca cede el control, puede acaparar el sistema. Solo él y cualquier rutina de interrupción a la que se le dé control pueden ejecutarse, por lo que el sistema operativo parecerá bloquearse. Este es el aspecto "cooperativo" de estos sistemas. Si se implementa un temporizador de vigilancia que solo se restablece cuando se realiza un cambio de tarea, entonces es posible detectar estas tareas errantes.
Windows 3.1 y versiones anteriores eran sistemas operativos cooperativos, lo que explica en parte por qué su rendimiento no era tan bueno.
La multitarea preventiva es más difícil de implementar. Aquí, no se requiere que las tareas cedan el control manualmente, sino que a cada tarea se le puede dar una cantidad máxima de tiempo para ejecutarse (digamos 10 ms), y luego se realiza un cambio de tarea a la siguiente tarea ejecutable si hay una. Esto requiere detener arbitrariamente una tarea, guardar toda la información de estado y luego cambiar el SP a otra tarea e iniciarla. Esto hace que el conmutador de tareas sea más complicado, requiere más pila y ralentiza un poco el sistema.
Tanto para la multitarea cooperativa como preventiva, pueden producirse interrupciones en cualquier momento, lo que anticipará temporalmente la tarea en ejecución.
Como señala supercat en un comentario, una ventaja de la multitarea cooperativa es que es más fácil compartir recursos (por ejemplo, hardware como un ADC multicanal o software como modificar una lista vinculada). A veces, dos tareas desean acceder al mismo recurso al mismo tiempo. Con la programación preventiva, el sistema operativo podría cambiar las tareas en el medio de una tarea utilizando un recurso. Por lo tanto, los bloqueos son necesarios para evitar que otra tarea ingrese y acceda al mismo recurso. Con la multitarea cooperativa, esto no es necesario porque la tarea controla cuándo se volverá a liberar al sistema operativo.
fuente
void foo(void* context)
la lógica del controlador (núcleo) extrae un puntero y un par de punteros de función de la cola y lo llama uno a la vez. Esa función utiliza el contexto para almacenar sus variables y demás, y luego puede agregar enviar una continuación a la cola. Esas funciones deben regresar rápidamente para permitir que otras tareas tengan su momento en la CPU. Este es un método basado en eventos que solo requiere una sola pila.El enhebrado lo proporciona un sistema operativo. En el mundo incrustado, generalmente no tenemos un sistema operativo ("bare metal"). Entonces esto deja las siguientes opciones:
Le aconsejaría que utilice el más simple de los esquemas anteriores que funcionarán para su aplicación. Por lo que describe, tendría el bucle principal generando paquetes y colocándolos en buffers circulares. Luego, tenga un controlador basado en UART ISR que se active cada vez que se envíe el byte anterior hasta que se envíe el búfer, luego espera más contenido del búfer. Enfoque similar para la ethernet.
fuente
Como en cualquier procesador de un solo núcleo, no es posible realizar múltiples tareas de software real. Por lo tanto, debe tener cuidado de cambiar entre múltiples tareas de una manera. Los diferentes RTOS se encargan de eso. Tienen un programador y, en función de un tic del sistema, cambiarán entre diferentes tareas para darle una capacidad de multitarea.
Los conceptos involucrados en hacerlo (guardar y restaurar el contexto) son bastante complicados, por lo que hacerlo manualmente probablemente será difícil y hará que su código sea más complejo y, como nunca lo ha hecho antes, habrá errores en él. Mi consejo aquí sería usar un RTOS probado como FreeRTOS.
Usted mencionó que las interrupciones proporcionan un nivel de multitarea. Esto es algo cierto. La interrupción interrumpirá su programa actual en cualquier momento y ejecutará el código allí, es comparable a un sistema de dos tareas donde tiene 1 tarea con baja prioridad y otra con alta prioridad que termina dentro de un segmento de tiempo del programador.
Por lo tanto, podría escribir un controlador de interrupciones para un temporizador recurrente que enviará algunos paquetes a través del UART, luego ejecute el resto de su programa durante unos milisegundos y envíe los siguientes bytes. De esa manera, obtienes una capacidad limitada de multitarea. Pero también tendrá una interrupción bastante larga que podría ser algo malo.
La única forma real de realizar múltiples tareas al mismo tiempo en un MCU de un solo núcleo es usar el DMA y los periféricos, ya que funcionan independientemente del núcleo (DMA y MCU comparten el mismo bus, por lo que funcionan un poco más lento cuando ambos están activos). Entonces, mientras el DMA baraja los bytes al UART, su núcleo es libre de enviar las cosas a Ethernet.
fuente
Las otras respuestas ya describen las opciones más utilizadas (bucle principal, ISR, RTOS). Aquí hay otra opción como compromiso: Protothreads . Básicamente es una lib muy ligera para subprocesos, que utiliza el bucle principal y algunas macros C para "emular" un RTOS. Por supuesto, no es un sistema operativo completo, pero para hilos "simples" puede ser útil.
fuente
Mi diseño básico para un RTOS de tiempo mínimo no ha cambiado mucho en varias micro familias. Básicamente es una interrupción del temporizador que conduce una máquina de estado. La rutina de servicio de interrupción es el kernel del sistema operativo, mientras que la declaración de cambio en el bucle principal son las tareas del usuario. Los controladores de dispositivo son rutinas de servicio de interrupción para interrupciones de E / S.
La estructura básica es la siguiente:
Esto es básicamente un sistema cooperativo multitarea. Las tareas se escriben para que nunca entren en un bucle infinito, pero no nos importa porque las tareas se ejecutan dentro de un bucle de eventos, por lo que el bucle infinito está implícito. Este es un estilo similar de programación para lenguajes orientados a eventos / sin bloqueo como javascript o go.
Puede ver un ejemplo de este estilo de arquitectura en mi software de transmisor RC (sí, en realidad lo uso para volar aviones RC, por lo que es algo crítico para la seguridad evitar que estrelle mis aviones y mate a personas): https://github.com / slebetman / pic-txmod . Básicamente tiene 3 tareas: 2 tareas en tiempo real implementadas como controladores de dispositivo con estado (consulte las cosas de ppmio) y 1 tarea en segundo plano que implementa la lógica de mezcla. Básicamente, es similar a su servidor web en que tiene 2 hilos de E / S.
fuente
Si bien aprecio que la pregunta se refiere específicamente al uso de un RTOS integrado, se me ocurre que la pregunta más amplia que se hace es "cómo lograr la multitarea en una plataforma integrada".
Le recomiendo encarecidamente que se olvide de usar un RTOS incorporado al menos por el momento. Aconsejo esto porque creo que es esencial aprender primero sobre cómo lograr la 'concurrencia' de tareas mediante técnicas de programación extremadamente simples que consisten en planificadores de tareas simples y máquinas de estado.
Para explicar de manera extremadamente breve el concepto, cada módulo de trabajo que debe realizarse (es decir, cada 'tarea') tiene una función particular que debe llamarse ('marcar') periódicamente para que ese módulo haga algunas cosas. El módulo conserva su propio estado actual. Luego tiene un bucle infinito principal (el planificador) que llama a las funciones del módulo.
Ilustración cruda:
La estructura de programación de un solo subproceso como esta mediante la cual periódicamente llama a las funciones principales de la máquina de estado desde un bucle principal del planificador es omnipresente en la programación integrada, y es por eso que recomiendo encarecidamente que el OP se familiarice y se sienta cómodo con él primero, antes de sumergirse directamente en el uso Tareas / hilos RTOS.
Trabajo en un tipo de dispositivo integrado que tiene una interfaz LCD de hardware, un servidor web interno, un cliente de correo electrónico, un cliente DDNS, VOIP y muchas otras funciones. Aunque usamos un RTOS (Keil RTX), el número de subprocesos (tareas) individuales utilizados es muy pequeño y la mayor parte de la 'multitarea' se logra como se describe anteriormente.
Para dar un par de ejemplos de bibliotecas que demuestran este concepto:
La biblioteca de redes Keil. Toda la pila TCP / IP puede ejecutarse con un solo subproceso; periódicamente llama a main_TcpNet (), que itera la pila TCP / IP y cualquier otra opción de red que haya compilado desde la biblioteca (por ejemplo, el servidor web). Consulte http://www.keil.com/support/man/docs/rlarm/rlarm_main_tcpnet.htm . Es cierto que, en algunas situaciones (posiblemente fuera del alcance de esta respuesta), llega a un punto en el que comienza a ser beneficioso o necesario usar hilos (particularmente si se usan tomas BSD de bloqueo). (Nota posterior: el nuevo V5 MDK-ARM en realidad genera un hilo Ethernet dedicado, pero solo estoy tratando de proporcionar una ilustración).
La biblioteca de VOIP de Linphone. La biblioteca de linphone en sí es de un solo hilo. Llama a la
iterate()
función en un intervalo suficiente. Ver http://www.linphone.org/docs/liblinphone-javadoc/org/linphone/core/LinphoneCore.html#iterate () . (Un poco de mal ejemplo porque lo usé en una plataforma Linux integrada y las bibliotecas de dependencias de linphone indudablemente generan hilos, pero de nuevo es para ilustrar un punto).Volviendo al problema específico descrito por el OP, el problema parece ser el hecho de que la comunicación UART debe tener lugar al mismo tiempo que algunas redes (transmisión de paquetes a través de TCP / IP). No sé qué biblioteca de red está utilizando realmente, pero supongo que tiene una función principal que debe llamarse con frecuencia. Debería escribir su código que se ocupa de la transmisión / recepción de datos UART para estructurarse de manera similar, como una máquina de estado que se puede iterar mediante llamadas periódicas a una función principal.
fuente