¿Cómo puedo crear múltiples hilos en ejecución?

60

¿Hay alguna manera de que varias partes del programa se ejecuten juntas sin hacer varias cosas en el mismo bloque de código?

Un hilo esperando un dispositivo externo mientras parpadea un LED en otro hilo.

Bja
fuente
3
Probablemente primero deberías preguntarte si realmente necesitas hilos. Los temporizadores pueden estar bien para sus necesidades ya y son compatibles de forma nativa en Arduino.
jfpoilpret
1
Es posible que también desee visitar Uzebox. Es una consola de videojuegos homebrew de dos chips. Entonces, aunque no es exactamente un Arduino, todo el sistema se basa en interrupciones. Por lo tanto, el audio, el video, los controles, etc., son impulsados ​​por interrupciones, mientras que el programa principal no tiene que preocuparse por nada de eso. Puede ser una buena referencia.
cbmeeks

Respuestas:

50

No hay soporte multiproceso, ni multiproceso, en el Arduino. Sin embargo, puede hacer algo cercano a múltiples hilos con algún software.

Quiere ver Protothreads :

Los Protothreads son hilos sin pila extremadamente ligeros diseñados para sistemas con limitaciones severas de memoria, como pequeños sistemas integrados o nodos de red de sensores inalámbricos. Los Protothreads proporcionan ejecución de código lineal para sistemas controlados por eventos implementados en C. Los Protothreads se pueden usar con o sin un sistema operativo subyacente para proporcionar controladores de eventos de bloqueo. Los Protothreads proporcionan un flujo de control secuencial sin máquinas de estado complejas o multihilo completo.

Por supuesto, hay un ejemplo de Arduino aquí con código de ejemplo . Esta pregunta SO también podría ser útil.

ArduinoThread también es bueno.

Sachleen
fuente
Tenga en cuenta que el Arduino DUE tiene una excepción a esto, con múltiples bucles de control: arduino.cc/en/Tutorial/MultipleBlinks
tuskiomi
18

Los Arduino basados ​​en AVR no admiten subprocesos (hardware), no estoy familiarizado con los Arduino basados ​​en ARM. Una forma de evitar esta limitación es el uso de interrupciones, especialmente interrupciones temporizadas. Puede programar un temporizador para interrumpir la rutina principal cada tantos microsegundos, para ejecutar otra rutina específica.

http://arduino.cc/en/Reference/Interrupts

jippie
fuente
15

Es posible realizar subprocesos múltiples del lado del software en el Uno. El subproceso de nivel de hardware no es compatible.

Para lograr el subprocesamiento múltiple, requerirá la implementación de un planificador básico y mantener un proceso o una lista de tareas para rastrear las diferentes tareas que deben ejecutarse.

La estructura de un planificador no preventivo muy simple sería como:

//Pseudocode
void loop()
{

for(i=o; i<n; i++) 
run(tasklist[i] for timelimit):

}

Aquí, tasklistpuede haber una matriz de punteros de función.

tasklist [] = {function1, function2, function3, ...}

Con cada función de la forma:

int function1(long time_available)
{
   top:
   //Do short task
   if (run_time<time_available)
   goto top;
}

Cada función puede realizar una tarea separada, como function1realizar manipulaciones de LED y function2hacer cálculos flotantes. Será responsabilidad de cada tarea (función) cumplir con el tiempo asignado.

Con suerte, esto debería ser suficiente para comenzar.

Asheeshr
fuente
2
No estoy seguro de hablar de "hilos" cuando uso un programador no preventivo. Por cierto, dicho planificador ya existe como una biblioteca arduino: arduino.cc/en/Reference/Scheduler
jfpoilpret
55
@jfpoilpret - El multihilo cooperativo es algo real.
Connor Wolf
¡Sí tienes razón! Mi error; hacía tanto tiempo que no me había enfrentado al multihilo cooperativo que, en mi opinión, el multihilo tenía que ser preventivo.
jfpoilpret
9

Según la descripción de sus requisitos:

  • un hilo esperando un dispositivo externo
  • un hilo parpadeando un LED

Parece que podría usar una interrupción de Arduino para el primer "hilo" (de hecho, preferiría llamarlo "tarea").

Las interrupciones de Arduino pueden llamar a una función (su código) en función de un evento externo (nivel de voltaje o cambio de nivel en un pin de entrada digital), que activará su función de inmediato.

Sin embargo, un punto importante a tener en cuenta con las interrupciones es que la función llamada debe ser lo más rápida posible (por lo general, no debe haber ninguna delay()llamada ni ninguna otra API de la que dependa delay()).

Si tiene una tarea larga para activar en el desencadenante de eventos externos, podría utilizar un programador cooperativo y agregarle una nueva tarea desde su función de interrupción.

Un segundo punto importante sobre las interrupciones es que su número es limitado (por ejemplo, solo 2 en UNO). Entonces, si comienza a tener más eventos externos, necesitará implementar algún tipo de multiplexación de todas las entradas en una sola, y que su función de interrupción determine qué entrada multiplexada fue el disparador real.

jfpoilpret
fuente
6

Una solución simple es usar un Programador . Hay varias implementaciones. Esto describe brevemente uno que está disponible para placas basadas en AVR y SAM. Básicamente, una sola llamada comenzará una tarea; "bosquejo dentro de un bosquejo".

#include <Scheduler.h>
....
void setup()
{
  ...
  Scheduler.start(taskSetup, taskLoop);
}

Scheduler.start () agregará una nueva tarea que ejecutará taskSetup una vez y luego llamará repetidamente taskLoop tal como funciona el boceto Arduino. La tarea tiene su propia pila. El tamaño de la pila es un parámetro opcional. El tamaño predeterminado de la pila es de 128 bytes.

Para permitir el cambio de contexto, las tareas deben llamar a yield () o delay () . También hay una macro de soporte para esperar una condición.

await(Serial.available());

La macro es azúcar sintáctica para lo siguiente:

while (!(Serial.available())) yield();

Await también se puede usar para sincronizar tareas. A continuación se muestra un fragmento de ejemplo:

volatile int taskEvent = 0;
#define signal(evt) do { await(taskEvent == 0); taskEvent = evt; } while (0)
...
void taskLoop()
{
  await(taskEvent);
  switch (taskEvent) {
  case 1: 
  ...
  }
  taskEvent = 0;
}
...
void loop()
{
  ...
  signal(1);
}

Para más detalles ver los ejemplos . Hay ejemplos de múltiples parpadeos de LED para eliminar el botón de rebote y un shell simple con lectura de línea de comando sin bloqueo. Se pueden usar plantillas y espacios de nombres para ayudar a estructurar y reducir el código fuente. El siguiente bosquejo muestra cómo usar las funciones de plantilla para el parpadeo múltiple. Es suficiente con 64 bytes para la pila.

#include <Scheduler.h>

template<int pin> void setupBlink()
{
  pinMode(pin, OUTPUT);
}

template<int pin, unsigned int ms> void loopBlink()
{
  digitalWrite(pin, HIGH);
  delay(ms);
  digitalWrite(pin, LOW);
  delay(ms);
}

void setup()
{
  Scheduler.start(setupBlink<11>, loopBlink<11,500>, 64);
  Scheduler.start(setupBlink<12>, loopBlink<12,250>, 64);
  Scheduler.start(setupBlink<13>, loopBlink<13,1000>, 64);
}

void loop()
{
  yield();
}

También hay un punto de referencia para dar una idea del rendimiento, es decir, el tiempo para comenzar la tarea, el cambio de contexto, etc.

Por último, hay algunas clases de soporte para la sincronización y comunicación a nivel de tarea; Cola y semáforo .

Mikael Patel
fuente
3

De un encantamiento anterior de este foro, la siguiente pregunta / respuesta fue trasladada a Ingeniería Eléctrica. Tiene un código arduino de muestra para parpadear un LED usando una interrupción del temporizador mientras usa el bucle principal para hacer IO en serie.

https://electronics.stackexchange.com/questions/67089/how-can-i-control-things-without-using-delay/67091#67091

Volver a publicar:

Las interrupciones son una forma común de hacer las cosas mientras sucede algo más. En el ejemplo a continuación, el LED parpadea sin usar delay(). Siempre que se Timer1dispara, isrBlinker()se llama a la rutina de servicio de interrupción (ISR) . Enciende / apaga el LED.

Para mostrar que otras cosas pueden suceder simultáneamente, loop()escribe repetidamente foo / bar en el puerto serie independientemente del parpadeo del LED.

#include "TimerOne.h"

int led = 13;

void isrBlinker()
{
  static bool on = false;
  digitalWrite( led, on ? HIGH : LOW );
  on = !on;
}

void setup() {                
  Serial.begin(9600);
  Serial.flush();
  Serial.println("Serial initialized");

  pinMode(led, OUTPUT);

  // initialize the ISR blinker
  Timer1.initialize(1000000);
  Timer1.attachInterrupt( isrBlinker );
}

void loop() {
  Serial.println("foo");
  delay(1000);
  Serial.println("bar");
  delay(1000);
}

Esta es una demostración muy simple. Los ISR pueden ser mucho más complejos y pueden ser activados por temporizadores y eventos externos (pines). Muchas de las bibliotecas comunes se implementan mediante ISR.

walrii
fuente
2

También puedes probar mi biblioteca ThreadHandler

https://bitbucket.org/adamb3_14/threadhandler/src/master/

Utiliza un programador de interrupción para permitir el cambio de contexto sin transmitir en yield () o delay ().

Creé la biblioteca porque necesitaba tres subprocesos y dos para ejecutarlos en un momento preciso, sin importar lo que estuvieran haciendo los demás. El primer hilo manejó la comunicación en serie. El segundo estaba ejecutando un filtro de Kalman usando la multiplicación de matriz flotante con la biblioteca Eigen. Y el tercero era un hilo de bucle de control de corriente rápido que tenía que poder interrumpir los cálculos de la matriz.

Cómo funciona

Cada hilo cíclico tiene una prioridad y un punto. Si un subproceso, con mayor prioridad que el subproceso actual en ejecución, alcanza su próximo tiempo de ejecución, el programador pausará el subproceso actual y cambiará al de mayor prioridad. Una vez que el subproceso de alta prioridad completa su ejecución, el planificador vuelve al subproceso anterior.

Reglas de programación

El esquema de programación de la biblioteca ThreadHandler es el siguiente:

  1. Máxima prioridad primero.
  2. Si la prioridad es la misma, el subproceso con la fecha límite más temprana se ejecuta primero.
  3. Si dos hilos tienen la misma fecha límite, el primer hilo creado se ejecutará primero.
  4. Un hilo solo puede ser interrumpido por hilos con mayor prioridad.
  5. Una vez que se ejecuta un subproceso, bloqueará la ejecución de todos los subprocesos con menor prioridad hasta que regrese la función de ejecución.
  6. La función de bucle tiene prioridad -128 en comparación con los subprocesos ThreadHandler.

Cómo utilizar

Los hilos se pueden crear a través de la herencia de c ++

class MyThread : public Thread
{
public:
    MyThread() : Thread(priority, period, offset){}

    virtual ~MyThread(){}

    virtual void run()
    {
        //code to run
    }
};

MyThread* threadObj = new MyThread();

O a través de createThread y una función lambda

Thread* myThread = createThread(priority, period, offset,
    []()
    {
        //code to run
    });

Los objetos de subproceso se conectan automáticamente al ThreadHandler cuando se crean.

Para iniciar la ejecución de los objetos de hilo creados, llame a:

ThreadHandler::getInstance()->enableThreadExecution();
Adam Bäckström
fuente
1

Y aquí hay otra biblioteca multitarea cooperativa de microprocesador: PQRST: una cola prioritaria para ejecutar tareas simples.

En este modelo, un subproceso se implementa como una subclase de a Task, que está programado para algún tiempo futuro (y posiblemente reprogramado a intervalos regulares si, como es común, en su LoopTasklugar se subclasifica ). El run()método del objeto se llama cuando la tarea se vence. El run()método realiza el trabajo debido y luego regresa (este es el bit cooperativo); normalmente mantendrá algún tipo de máquina de estado para administrar sus acciones en invocaciones sucesivas (un ejemplo trivial es la light_on_p_variable en el ejemplo a continuación). Requiere un ligero replanteamiento de cómo organizar su código, pero ha demostrado ser muy flexible y robusto en un uso bastante intensivo.

Es agnóstico acerca de las unidades de tiempo, por lo que es tan feliz correr en unidades de millis()como micros(), o cualquier otro tic que sea conveniente.

Aquí está el programa 'blink' implementado usando esta biblioteca. Esto muestra solo una tarea en ejecución: otras tareas normalmente se crearían y comenzarían dentro setup().

#include "pqrst.h"

class BlinkTask : public LoopTask {
private:
    int my_pin_;
    bool light_on_p_;
public:
    BlinkTask(int pin, ms_t cadence);
    void run(ms_t) override;
};

BlinkTask::BlinkTask(int pin, ms_t cadence)
    : LoopTask(cadence),
      my_pin_(pin),
      light_on_p_(false)
{
    // empty
}
void BlinkTask::run(ms_t t)
{
    // toggle the LED state every time we are called
    light_on_p_ = !light_on_p_;
    digitalWrite(my_pin_, light_on_p_);
}

// flash the built-in LED at a 500ms cadence
BlinkTask flasher(LED_BUILTIN, 500);

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    flasher.start(2000);  // start after 2000ms (=2s)
}

void loop()
{
    Queue.run_ready(millis());
}
Gris normando
fuente
Estas son tareas de "ejecución hasta la finalización", ¿verdad?
Edgar Bonet
@EdgarBonet No estoy seguro de lo que quieres decir. Una vez run()que se llama al método, no se interrumpe, por lo que tiene la responsabilidad de finalizarlo razonablemente de inmediato. Normalmente, sin embargo, hará su trabajo y luego se reprogramará (posiblemente de forma automática, en el caso de una subclase de LoopTask) para algún tiempo futuro. Un patrón común es que la tarea mantenga alguna máquina de estado interna (un ejemplo trivial es el light_on_p_estado anterior) para que se comporte adecuadamente cuando sea el próximo vencimiento.
Norman Gray
Entonces, sí, esas son tareas de ejecución completa (RtC): ninguna tarea puede ejecutarse antes de que la actual complete su ejecución al regresar de run(). Esto está en contraste con los hilos cooperativos, que pueden producir la CPU, por ejemplo, llamando yield()o delay(). O subprocesos preventivos, que se pueden programar en cualquier momento. Siento que la distinción es importante, ya que he visto que muchas personas que vienen por aquí buscando hilos lo hacen porque prefieren escribir código de bloqueo en lugar de máquinas de estado. El bloqueo de hilos reales que producen la CPU está bien. Bloquear tareas RtC no lo es.
Edgar Bonet
@EdgarBonet Es una distinción útil, sí. Consideraría tanto este estilo como los hilos de estilo de rendimiento, como simplemente diferentes estilos de hilo cooperativo, en lugar de hilos preventivos, pero es cierto que requieren un enfoque diferente para codificarlos. Sería interesante ver una comparación reflexiva y profunda de los diversos enfoques mencionados aquí; Una buena biblioteca no mencionada anteriormente es protothreads . Encuentro cosas para criticar en ambos, pero también para alabar. Yo (por supuesto) prefiero mi enfoque, porque parece más explícito y no necesita pilas adicionales.
Norman Gray
(corrección: se mencionaron protothreads , en la respuesta de @ sachleen )
Norman Gray