Есть ли способ, которым я могу запустить несколько частей программы без выполнения нескольких операций в одном блоке кода?
Один поток ожидает внешнего устройства, а в другом потоке мигает светодиод.
Есть ли способ, которым я могу запустить несколько частей программы без выполнения нескольких операций в одном блоке кода?
Один поток ожидает внешнего устройства, а в другом потоке мигает светодиод.
Ответы:
На Arduino нет многопроцессорной и многопоточной поддержки. Вы можете сделать что-то похожее на несколько потоков с помощью некоторого программного обеспечения.
Вы хотите посмотреть на Protothreads :
Протопотоки - это чрезвычайно легкие потоки без стеков, предназначенные для систем с ограниченным объемом памяти, таких как небольшие встроенные системы или узлы беспроводной сенсорной сети. Протопотоки обеспечивают линейное выполнение кода для управляемых событиями систем, реализованных в C. Протопотоки могут использоваться с базовой операционной системой или без нее для обеспечения блокирующих обработчиков событий. Протопотоки обеспечивают последовательный поток управления без сложных конечных автоматов или полной многопоточности.
Конечно, есть Arduino пример здесь с примерами кода . Этот ТАК вопрос тоже может быть полезен.
ArduinoThread тоже хороший.
Arduino на базе AVR не поддерживает (аппаратную) многопоточность, я не знаком с Arduino на основе ARM. Одним из способов обойти это ограничение является использование прерываний, особенно прерываний по времени. Вы можете запрограммировать таймер так, чтобы он прерывал основную процедуру каждые много микросекунд, чтобы запустить определенную другую процедуру.
На Uno можно выполнять многопоточность на стороне программного обеспечения. Аппаратный уровень потоков не поддерживается.
Для достижения многопоточности потребуется реализация базового планировщика и ведение списка процессов или задач для отслеживания различных задач, которые необходимо выполнить.
Структура очень простого планировщика без вытеснения будет выглядеть так:
//Pseudocode
void loop()
{
for(i=o; i<n; i++)
run(tasklist[i] for timelimit):
}
Здесь tasklist
может быть массив указателей на функции.
tasklist [] = {function1, function2, function3, ...}
С каждой функцией формы:
int function1(long time_available)
{
top:
//Do short task
if (run_time<time_available)
goto top;
}
Каждая функция может выполнять отдельную задачу, такую как function1
выполнение манипуляций со светодиодами и function2
выполнение вычислений с плавающей запятой. Каждое задание (функция) будет нести ответственность за соблюдение выделенного ему времени.
Надеюсь, этого будет достаточно, чтобы вы начали.
Согласно описанию ваших требований:
Кажется, вы могли бы использовать одно прерывание Arduino для первого «потока» (на самом деле я бы скорее назвал его «заданием»).
Прерывания Arduino могут вызывать одну функцию (ваш код) на основе внешнего события (уровня напряжения или изменения уровня на выводе цифрового входа), которое немедленно активирует вашу функцию.
Тем не менее, один важный момент, который следует иметь в виду при обработке прерываний, заключается в том, что вызываемая функция должна быть максимально быстрой (обычно не должно быть ни delay()
вызова, ни какого-либо другого API, от которого зависит delay()
).
Если у вас есть длинная задача для активации по внешнему триггеру, вы можете использовать совместный планировщик и добавить в него новую задачу из функции прерывания.
Вторым важным моментом в отношении прерываний является то, что их количество ограничено (например, только 2 в UNO). Поэтому, если вы начнете иметь больше внешних событий, вам нужно будет реализовать своего рода мультиплексирование всех входов в один, и ваша функция прерывания определит, какой мультиплексированный вход был фактическим триггером.
Простое решение - использовать планировщик . Есть несколько реализаций. Это вкратце описывает тот, который доступен для плат на базе AVR и SAM. По сути, один вызов запускает задачу; "эскиз внутри эскиза".
#include <Scheduler.h>
....
void setup()
{
...
Scheduler.start(taskSetup, taskLoop);
}
Scheduler.start () добавит новую задачу, которая будет запускать taskSetup один раз, а затем повторно вызывать taskLoop точно так же, как работает эскиз Arduino. Задача имеет свой собственный стек. Размер стека является необязательным параметром. Размер стека по умолчанию составляет 128 байт.
Чтобы разрешить переключение контекста, необходимо вызвать yield () или delay () . Существует также макрос поддержки для ожидания условия.
await(Serial.available());
Макрос является синтаксическим сахаром для следующего:
while (!(Serial.available())) yield();
Await также может быть использован для синхронизации задач. Ниже приведен пример фрагмента:
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);
}
Для дальнейших деталей смотрите примеры . Есть примеры от многократного мигания светодиода до кнопки отмены и простой оболочки с неблокирующим чтением командной строки. Шаблоны и пространства имен могут использоваться, чтобы помочь структурировать и уменьшить исходный код. Ниже эскиз показывает , как использовать шаблон функцию для мульти-мерцания. Для стека достаточно 64 байта.
#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();
}
Существует также эталон, который дает некоторое представление о производительности, то есть время запуска задачи, переключение контекста и т. Д.
Наконец, есть несколько классов поддержки для синхронизации и связи на уровне задач; Очередь и семафор .
Из предыдущего заклинания этого форума следующий вопрос / ответ был перенесен в электротехнику. Он имеет пример кода Arduino для мигания светодиода с использованием прерывания по таймеру при использовании основного цикла для выполнения последовательного ввода-вывода.
Repost:
Прерывания - это обычный способ сделать что-то, пока что-то происходит. В приведенном ниже примере светодиод мигает без использования delay()
. Всякий раз Timer1
, когда срабатывает, isrBlinker()
вызывается подпрограмма обработки прерывания (ISR) . Включает / выключает светодиод.
Чтобы показать, что одновременно могут происходить другие вещи, loop()
несколько раз записывает foo / bar в последовательный порт независимо от мигания светодиода.
#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);
}
Это очень простая демонстрация. ISR могут быть намного более сложными и могут быть вызваны таймерами и внешними событиями (булавками). Многие из общих библиотек реализованы с использованием ISR.
Я также пришел к этой теме при реализации матричного светодиодного дисплея.
Одним словом, вы можете построить планировщик опроса, используя функцию millis () и прерывание по таймеру в Arduino.
Я предлагаю следующие статьи от Билла Эрла:
https://learn.adafruit.com/multi-tasking-the-arduino-part-1/overview
https://learn.adafruit.com/multi-tasking-the-arduino-part-2/overview
https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview
Вы также можете попробовать мою библиотеку ThreadHandler
https://bitbucket.org/adamb3_14/threadhandler/src/master/
Он использует планировщик прерываний, чтобы разрешить переключение контекста без ретрансляции на yield () или delay ().
Я создал библиотеку, потому что мне нужно было три потока, и мне нужно было два из них для запуска в точное время независимо от того, что делали другие. Первый поток обрабатывал последовательную связь. Вторым был запуск фильтра Калмана с использованием умножения матрицы с плавающей точкой на библиотеку Eigen. И третьим был поток с быстрым токовым контуром управления, который должен был прервать вычисления матрицы.
Каждый циклический поток имеет приоритет и период. Если поток с более высоким приоритетом, чем текущий исполняющий поток, достигает следующего времени выполнения, планировщик приостанавливает текущий поток и переключается на более высокий приоритет. Как только поток с высоким приоритетом завершает свое выполнение, планировщик переключается обратно на предыдущий поток.
Схема планирования библиотеки ThreadHandler выглядит следующим образом:
Потоки могут быть созданы с помощью наследования c ++
class MyThread : public Thread
{
public:
MyThread() : Thread(priority, period, offset){}
virtual ~MyThread(){}
virtual void run()
{
//code to run
}
};
MyThread* threadObj = new MyThread();
Или через createThread и лямбда-функцию
Thread* myThread = createThread(priority, period, offset,
[]()
{
//code to run
});
Объекты потоков автоматически подключаются к ThreadHandler при их создании.
Чтобы начать выполнение созданных объектов потока, вызовите:
ThreadHandler::getInstance()->enableThreadExecution();
И вот еще одна микропроцессорная кооперативная многозадачная библиотека - PQRST: приоритетная очередь для выполнения простых задач.
В этой модели поток реализован как подкласс a Task
, который запланирован на некоторое время в будущем (и, возможно, перенесен на регулярные промежутки времени, если, как это обычно бывает, LoopTask
вместо него подклассы ). run()
Метод объекта вызывается , когда задача становится из - за. run()
Метод делает некоторые должную работу, а затем возвращает (это кооперативное бит); обычно он поддерживает какой-то конечный автомат для управления своими действиями при последовательных вызовах (тривиальный пример - это light_on_p_
переменная в примере ниже). Это требует небольшого переосмысления того, как вы организуете свой код, но оказалось очень гибким и надежным при довольно интенсивном использовании.
Он не зависит от единиц времени, поэтому он так же рад работать в единицах, millis()
как micros()
и любой другой удобный тик.
Вот программа blink, реализованная с использованием этой библиотеки. Это показывает, что выполняется только одна задача: обычно создаются и запускаются другие задачи 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());
}
run()
метод вызван, он не прерывается, поэтому он обязан быстро завершить работу. Как правило, однако, он выполнит свою работу, а затем перенесет себя (возможно, автоматически, в случае подкласса LoopTask
) на некоторое время в будущем. Обычным шаблоном для задачи является поддержание некоторого внутреннего конечного автомата (тривиальным примером является light_on_p_
состояние выше), чтобы он вел себя соответствующим образом, когда наступит следующий срок.
run()
. Это отличается от кооперативных потоков, которые могут выдавать ЦП, например, путем вызова yield()
или delay()
. Или приоритетные потоки, которые могут быть запланированы в любое время. Я чувствую, что различие важно, так как я видел, что многие люди, которые приходят сюда в поисках потоков, делают это, потому что они предпочитают писать блокирующий код, а не конечные автоматы. Блокировка реальных потоков, которые дают процессор, в порядке. Блокировки задач RtC нет.