Multitarea en microcontroladores PIC

16

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 que se basa en un microcontrolador PIC. He diseñado su firmware en MplabX IDE usando C y luego diseñé una aplicación para él en Visual Studio usando C #.

Desde que me acostumbré a usar subprocesos en la programación de C # en el escritorio para implementar tareas paralelas, ¿hay alguna forma de hacer lo mismo en mi código de microcontrolador? El IDE de MplabX proporciona pthreads.h pero es solo un apéndice sin implementación. Sé que hay soporte de FreeRTOS pero el uso hace que su código sea más complejo. Algunos foros dicen que las interrupciones también se pueden usar como multitarea, pero no creo que las interrupciones sean equivalentes a las hebras.

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 activa / desactiva con un retraso de 2-3 segundos. Así que ese es el problema que estoy enfrentando. ¿Hay alguna solución para la multitarea en microcontroladores?

    
pregunta Aircraft

6 respuestas

19

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 solo procesador central solo se ejecuta una tarea a la vez.

Ambos tipos de sistemas operativos multitarea requieren una pila separada para cada tarea. Por lo tanto, esto implica dos cosas: primero, que el procesador permita colocar pilas en cualquier lugar de la RAM y, por lo tanto, tenga instrucciones para mover el puntero de la 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 fuera las series PIC10, 12 y 16.

Puede escribir un sistema operativo casi por completo en C, pero el conmutador de tareas, donde se desplaza el SP tiene que estar en el ensamblaje. 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 varias pilas. Por lo general, a uno le gustaría 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, aunque no es necesario asignar el mismo tamaño de pila para cada tarea. Recuerde que necesita suficiente pila para manejar la tarea actual, y todas las llamadas a sus subrutinas anidadas, pero también espacio de pila para una llamada de interrupción, ya que nunca se sabe cuándo ocurrirá una.

Hay métodos bastante simples para determinar la cantidad de pila que estás usando para cada tarea; por ejemplo, puede inicializar todas las pilas a un valor particular, digamos 0x55, y ejecutar el sistema por 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 máximo de RAM de 4K. Así que 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 cada vez que 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 de temporizador. Esto hace que sea más fácil para el SO cambiar pilas, ya que no es necesario guardar todos los registros e información de estado, el SP solo se puede cambiar a otra tarea (si no hay otras tareas listas para ejecutarse, una pila inactiva es dado el control). Si la tarea actual no necesita realizar una llamada del sistema operativo, pero se ha estado ejecutando durante un tiempo, debe renunciar voluntariamente al control para mantener el sistema receptivo.

El problema con la multitarea cooperativa es que si la tarea nunca pierde el control, puede acaparar el sistema. Solo esto y cualquier rutina de interrupción que tenga el control dado puede ejecutarse, por lo que el sistema operativo parece 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 fue tan bueno.

La multitarea preferente es más difícil de implementar. Aquí, las tareas no son necesarias para renunciar al control manualmente, sino que a cada tarea se le puede otorgar un tiempo máximo de ejecución (por ejemplo, 10 ms), y luego se realiza un cambio de tarea a la siguiente tarea ejecutable si existe 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 para la preventiva, las interrupciones pueden ocurrir en cualquier momento, lo que anticipará temporalmente la tarea en ejecución.

Como señala supercat en un comentario, una de las ventajas de la multitarea cooperativa es que es más fácil compartir recursos (por ejemplo, hardware como un ADC multicanal o software como la modificación de una lista vinculada). A veces, dos tareas quieren acceder al mismo recurso al mismo tiempo. Con la programación preventiva, sería posible que el sistema operativo cambie de tareas en medio de una tarea utilizando un recurso. Por lo tanto, bloqueos son necesarios para evitar que otra tarea entre y acceda al mismo recurso. Con la multitarea cooperativa, esto no es necesario porque la tarea controla cuándo se liberará automáticamente al sistema operativo.

    
respondido por el tcrosley
16

El enhebrado es proporcionado por un sistema operativo. En el mundo integrado, normalmente no tenemos un sistema operativo ("bare metal"). Así que esto deja las siguientes opciones:

  • El clásico bucle de sondeo principal. Su función principal tiene un tiempo (1) que hace la tarea 1 y luego la tarea 2 ...
  • Indicadores ISR del bucle principal: tiene un ISR que cumple la función de tiempo crítico y luego alerta al bucle principal a través de una variable de indicador de que la tarea necesita servicio. Quizás el ISR coloca un nuevo carácter en un búfer circular y luego le dice al ciclo principal que maneje los datos cuando esté listo para hacerlo.
  • All ISR: Gran parte de la lógica aquí se ejecuta desde el ISR. En un controlador moderno como un ARM que tiene múltiples niveles de prioridad. Esto puede proporcionar un poderoso esquema "similar a un hilo", pero también puede ser confuso para depurar, por lo que debería reservarse solo para restricciones de tiempo críticas.
  • RTOS: Un kernel RTOS (facilitado por un temporizador ISR) puede permitir el cambio entre múltiples hilos de ejecución. Has mencionado FreeRTOS.

Le aconsejaría que utilice el más simple de los esquemas anteriores que funcionará 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 dispare cada vez que el byte anterior termine de enviarse hasta que se envíe el búfer, luego espera más contenido del búfer. Enfoque similar para la ethernet.

    
respondido por el Houston Fortney
8

Como en cualquier procesador de un solo núcleo, no es posible realizar tareas múltiples de software real. Así que debes tener cuidado de cambiar entre múltiples tareas de una manera. Los diferentes RTOS se están encargando de eso. Tienen un programador y, en función de una marca del sistema, cambiarán entre diferentes tareas para ofrecerle la posibilidad de realizar múltiples tareas.

Los conceptos involucrados al hacerlo (guardar y restaurar el contexto) son bastante complicados, por lo que hacerlo de forma manual probablemente será difícil y hará que su código sea más complejo y, como nunca lo ha hecho antes, habrá errores. . Mi consejo aquí sería utilizar un RTOS probado como FreeRTOS.

Usted mencionó que las interrupciones proporcionan un nivel de multitarea. Esto es una especie de verdad. La interrupción interrumpirá su programa actual en cualquier punto y ejecutará el código allí, es comparable a un sistema de dos tareas donde tiene 1 tarea con prioridad baja y otra con prioridad alta que termina dentro de un intervalo 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 de UART, luego deje que el resto de su programa se ejecute durante unos pocos 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 una MCU de un solo núcleo es usar el DMA y los periféricos mientras trabajan independientemente del núcleo (DMA y MCU comparten el mismo bus, por lo que trabajan un un poco más lento cuando ambos están activos). Así que mientras el DMA está barajando los bytes al UART, tu núcleo es libre de enviar las cosas a Ethernet.

    
respondido por el Arsenal
6

Las otras respuestas ya describieron las opciones más utilizadas (bucle principal, ISR, RTOS). Aquí hay otra opción como compromiso: Protothreads . Básicamente es una lib muy liviana para subprocesos, que usa el bucle principal y algunas macros C, para "emular" un RTOS. Por supuesto, no es un sistema operativo completo, pero para subprocesos "simples" puede ser útil.

    
respondido por el erebos
3

Mi diseño básico para un RTOS reducido en intervalos de tiempo no ha cambiado mucho en varias micro familias. Es básicamente una interrupción temporizada conduciendo una máquina de estado. La rutina de servicio de interrupción es el núcleo del sistema operativo, mientras que la instrucció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:

unsigned char tick;

void interrupt HANDLER(void) {
    device_driver_A();
    device_driver_B();
    if(T0IF)
    {
        TMR0 = TICK_1MS;
        T0IF = 0;   // reset timer interrupt
        tick ++;
    }
}

void main(void)
{
    init();

    while (1) {
        // periodic tasks:
        if (tick % 10 == 0) { // roughly every 10 ms
            task_A();
            task_B();    
        }
        if (tick % 55 == 0) { // roughly every 55 ms
            task_C();
            task_D();    
        }

        // tasks that need to run every loop:
        task_E();
        task_F();
    }
}

Esto es básicamente un sistema multitarea cooperativo. 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.

Puedes 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 un tanto crítico de seguridad evitar que choque mis aviones y potencialmente matar a personas): "https://github.com/slebetman/pic-txmod"> enlace . Tiene básicamente 3 tareas: 2 tareas en tiempo real implementadas como controladores de dispositivo con estado (consulte el material de ppmio) y 1 tarea en segundo plano que implementa la lógica de mezcla. Básicamente, es similar a su servidor web, ya que tiene 2 subprocesos de E / S.

    
respondido por el slebetman
2

Aunque aprecio que la pregunta se refiera específicamente al uso de un RTOS incorporado, se me ocurre que la pregunta más amplia que se plantea es "cómo lograr la multitarea en una plataforma integrada".

Le recomendaría encarecidamente que se olvide de usar un RTOS incorporado al menos por el momento. Recomiendo esto porque creo que es esencial aprender primero sobre cómo lograr la "concurrencia" de tareas por medio de técnicas de programación extremadamente simples que consisten en programadores de tareas simples y máquinas de estado.

Para explicar brevemente el concepto, cada módulo de trabajo que debe realizarse (es decir, cada 'tarea') tiene una función particular que debe ser llamada ('marcada') periódicamente para que ese módulo haga algunas cosas. El módulo conserva su propio estado actual. Entonces tiene un bucle infinito principal (el programador) que llama a las funciones del módulo.

Ilustración cruda:

for(;;)
{
    main_lcd_ui_tick();
    networking_tick();
}


...

// In your LCD UI module:
void main_lcd_ui_tick(void)
{
    check_for_key_presses();
    update_lcd();
}

...

// In your networking module:
void networking_tick(void)
{
    //'Tick' the TCP/IP library. In this example, I'm periodically
    //calling the main function for Keil's TCP/IP library.
    main_TcpNet();
}

La estructura de programación de un solo subproceso como esta, por la que periódicamente se invocan las funciones de la máquina de estado principal desde un bucle del programador principal, es omnipresente en la programación integrada, y es por eso que le recomiendo al OP que esté familiarizado y cómodo antes de bucear directamente en el uso de tareas / subprocesos RTOS.

Trabajo en un tipo de dispositivo integrado que tiene una interfaz LCD de hardware, servidor web interno, cliente de correo electrónico, cliente DDNS, VOIP y muchas otras características. Aunque usamos un RTOS (Keil RTX), el número de subprocesos individuales (tareas) que se utilizan es muy pequeño y la mayoría de las "tareas múltiples" se logran como se describe anteriormente.

Para dar un par de ejemplos de bibliotecas que demuestran este concepto:

  1. La biblioteca de redes Keil. Toda la pila TCP / IP se puede ejecutar de un solo hilo; 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 enlace . 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 para usar hilos (especialmente si se usan sockets de bloqueo BSD). (Nota adicional: el nuevo MDK-ARM V5 en realidad genera un subproceso Ethernet dedicado, pero solo estoy tratando de proporcionar una ilustración).

  2. La biblioteca VOIP de Linphone. La biblioteca de linphone en sí es de un solo hilo. Llamas a la función iterate() en un intervalo suficiente. Consulte enlace . (Un poco de un mal ejemplo, porque lo usé en una plataforma Linux incrustada y las bibliotecas de dependencias de Linphone sin duda generan hilos, pero nuevamente 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 (transmitir paquetes a través de TCP / IP). No sé qué biblioteca de red está utilizando en realidad, pero supongo que tiene una función principal que debe llamarse con frecuencia. Necesitaría escribir su código que se ocupa de la transmisión / recepción de datos de UART para que se estructure de manera similar, como una máquina de estado que puede repetirse mediante llamadas periódicas a una función principal.

    
respondido por el Trevor Page

Lea otras preguntas en las etiquetas

Comentarios Recientes

no es suficiente. La Figura 7A muestra que el uso del rendimiento del rendimiento del límite superior definido por software, el rendimiento del patrón más reciente y la frecuencia máxima es diferente del rendimiento del terminal. El cese de la creación de perfiles en cascada es útil para los subsistemas de cableado. Si bien no se eliminan por completo las trampas, los procedimientos revisados aquí reducen las posibles trampas, lo que proporciona una ruta clara para probar la estabilidad del control secundario,... Lees verder