Multitarea en microcontroladores PIC


17

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.hpero 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?


Los subprocesos solo se pueden usar en procesadores que ejecutan un sistema operativo, porque los subprocesos son parte del proceso y los procesos solo se usan en sistemas operativos.
TicTacToe

@Zola sí, tienes razón. ¿Pero qué pasa con los controladores?
Avión


1
¿Puede explicar por qué necesita una verdadera multitarea y no puede implementar razonablemente su software en base a un enfoque de tarea de turno rotativo o un ciclo select () o similar?
whatsisname

2
Bueno, como ya dije, estoy enviando y recibiendo datos a uart y al mismo tiempo enviando y recibiendo datos a ethernet. Además de esto, también necesito guardar datos en la tarjeta SD junto con el tiempo, por lo que sí DS1307 RTC está involucrado y EEPROM también está involucrado. Hasta ahora solo tengo 1 UART, pero es posible que después de unos días envíe y reciba datos de 3 módulos UART. El sitio web también recibirá datos de 5 sistemas diferentes instalados en un lugar remoto. Todo esto tiene que ser paralelo, pero no es paralelo, pero con un retraso de unos segundos. !
Avión del

Respuestas:


20

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.


3
Una ventaja de la multitarea cooperativa es que, en la mayoría de los casos, no es necesario usar bloqueos para coordinar el acceso a los recursos. Será suficiente para garantizar que las tareas siempre dejen recursos en un estado que se pueda compartir siempre que renuncien al control. La multitarea preventiva es mucho más complicada si una tarea se puede cambiar mientras mantiene bloqueado un recurso que otra tarea necesita. En algunos casos, la segunda tarea puede terminar siendo bloqueada por más tiempo de lo que hubiera sido bajo un sistema cooperativo, ya que la tarea de mantener el bloqueo habría dedicado el sistema ...
supercat

1
... recursos completos para terminar la acción que (en el sistema preventivo) habría requerido el bloqueo, haciendo que el objeto protegido esté disponible para la segunda tarea.
supercat

1
Si bien las multitareas cooperativas requieren disciplina, garantizar que se cumplan los requisitos de tiempo a veces puede ser más fácil con una multitarea cooperativa que con una preventiva. Dado que se necesitarán muy pocas cerraduras a través de un interruptor de tareas, un sistema de interruptor de tareas round-robin de cinco tareas donde se requiere que las tareas no pasen más de 10 ms sin ceder, combinado con una pequeña lógica que dice "Si la tarea X es urgente necesita ejecutarse, ejecutarlo a continuación ", asegurará que la tarea X nunca tenga que esperar más de 10 ms una vez que se indique antes de que pueda ejecutarse. Por el contrario, si una tarea requiere un bloqueo, qué tarea X ...
supercat

1
... va a necesitar pero se cambia por un conmutador preventivo antes de liberarlo, es posible que X no pueda hacer nada útil hasta que el programador de la CPU ejecute la primera tarea. A menos que el planificador incluya lógica para reconocer y manejar la inversión de prioridad, puede pasar un tiempo antes de que permita que la primera tarea finalice su actividad y libere el bloqueo. Tales problemas no son irresolubles, pero resolverlos requiere una gran complejidad que podría haberse evitado en un sistema cooperativo. Los sistemas cooperativos funcionan muy bien, excepto por un problema: ...
supercat

3
no necesita múltiples pilas en cooperativo si codifica en continuaciones. En esencia, su código se divide en funciones, 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.
monstruo de trinquete

16

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:

  • El clásico bucle de votación principal. Su función principal tiene un tiempo (1) que realiza la tarea 1 y luego realiza la tarea 2 ...
  • Main loop + ISR flags: tiene un ISR que realiza la función de tiempo crítico y luego alerta al bucle principal a través de una variable de flag que la tarea necesita servicio. Quizás el ISR coloca un nuevo carácter en un búfer circular y luego le dice al bucle principal que maneje los datos cuando esté listo para hacerlo.
  • Todo 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 debe reservarse solo para restricciones de tiempo críticas.
  • RTOS: un núcleo RTOS (facilitado por un ISR de temporizador) puede permitir cambiar entre múltiples hilos de ejecución. Mencionaste FreeRTOS.

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.


3
Esta es una respuesta muy útil porque aborda la raíz del problema (cómo realizar múltiples tareas en un pequeño sistema integrado, en lugar de hilos como solución). Un párrafo sobre cómo podría aplicarse a la pregunta original sería excelente, quizás incluyendo los pros y los contras de cada uno para el escenario.
David

8

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.


2
gracias, DMA suena interesante. ¡Definitivamente lo buscaré!
Aviones

No todas las series de PIC tienen DMA.
Matt Young

1
Estoy usando PIC32;)
Aeronave

6

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.


¿desde dónde puedo descargar su código fuente para windows? ¡Creo que solo está disponible para Linux!
Aeronave

@CZAbhinav Debe ser independiente del sistema operativo y puede obtener la última descarga aquí .
erebos

Estoy en Windows ahora y estoy usando MplabX, no creo que sea útil aquí. Gracias de todos modos.!
Aeronave

No he oído hablar de protothreads, suena como una técnica interesante.
Arsenal

@CZAbhinav ¿De qué estás hablando? Es código C y no tiene nada que ver con su sistema operativo.
Matt Young

3

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:

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 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.


1
Realmente no llamaría a eso 'multitarea cooperativa', ya que eso no es sustancialmente diferente de cualquier otro programa de microcontroladores que tenga que hacer varias cosas.
whatsisname

2

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:

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 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:

  1. 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).

  2. 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.


2
Gracias por esta buena explicación, estoy usando la biblioteca TCP / IP proporcionada por microchip y es un código complejo muy grande. De alguna manera logré dividirlo en partes y hacerlo utilizable según mis requisitos. Definitivamente voy a probar uno de sus enfoques.
Avión del

Diviértete :) Usar un RTOS definitivamente hace la vida más fácil en muchas situaciones. En mi opinión, el uso de un hilo (tarea) hace que el esfuerzo de programación sea mucho más fácil en un sentido, ya que puede evitar tener que dividir su tarea en una máquina de estado. En cambio, solo escribe el código de la tarea como lo haría en sus programas de C #, con su código de tarea creado como si fuera lo único que existe. Es esencial explorar ambos enfoques, y a medida que realiza una programación más integrada, comienza a tener una idea de cuál es el mejor enfoque en cada situación.
Trevor Page

También prefiero usar la opción de subprocesos. :)
Aeronave
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.