有没有一种方法可以让程序的多个部分一起运行而无需在同一代码块中执行多项操作?
一个线程在等待外部设备,同时另一个线程中的LED闪烁。
有没有一种方法可以让程序的多个部分一起运行而无需在同一代码块中执行多项操作?
一个线程在等待外部设备,同时另一个线程中的LED闪烁。
Answers:
Arduino上没有多进程或多线程支持。不过,您可以使用某些软件在接近多个线程的位置进行操作。
您想看一下Protothreads:
Protothread是轻量级的无堆栈线程,专为内存受限的系统(例如小型嵌入式系统或无线传感器网络节点)而设计。Protothreads为用C实现的事件驱动系统提供线性代码执行。Protothreads可以与有或没有底层操作系统一起使用,以提供阻塞的事件处理程序。Protothreads提供顺序控制流,而无需复杂的状态机或完整的多线程。
当然,还有一个Arduino的例子在这里与示例代码。这样的问题可能也很有用。
ArduinoThread也是一个不错的选择。
基于AVR的Arduino不支持(硬件)线程,我不熟悉基于ARM的Arduino。解决此限制的一种方法是使用中断,尤其是定时中断。您可以对计时器进行编程,使其每隔这么多微秒中断一次主例程,以运行特定的其他例程。
可以在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
执行LED操作和function2
进行浮点计算。遵守分配给它的时间是每个任务(功能)的责任。
希望这足以让您入门。
根据您的要求的描述:
看来您可以在第一个“线程”中使用一个Arduino中断(实际上,我宁愿称其为“任务”)。
Arduino中断可以根据外部事件(数字输入引脚上的电压电平或电平变化)调用一个功能(您的代码),这将立即触发您的功能。
但是,对于中断要记住的重要一点是,被调用函数应尽可能快(通常,不应存在任何delay()
调用或依赖的任何其他API delay()
)。
如果您有一个很长的任务要在外部事件触发时激活,那么您可能会使用协作调度程序,并从中断函数向其添加新任务。
关于中断的第二个重要点是中断的数量是有限的(例如,UNO上只有2个)。因此,如果您开始有更多的外部事件,则需要实现将所有输入多路复用为一个的某种方式,并让中断功能确定实际触发了哪些多路复用输入。
一个简单的解决方案是使用Scheduler。有几种实现。简要介绍了一种适用于基于AVR和SAM的板。基本上,一个电话将启动一个任务。“在草图中素描”。
#include <Scheduler.h>
....
void setup()
{
...
Scheduler.start(taskSetup, taskLoop);
}
Scheduler.start()将添加一个新任务,该任务将运行一次taskSetup,然后在Arduino草图工作时重复调用taskLoop。该任务具有其自己的堆栈。堆栈的大小是可选参数。默认堆栈大小为128字节。
为了允许上下文切换,任务需要调用yield()或delay()。还有一个用于等待条件的支持宏。
await(Serial.available());
该宏是以下语法糖:
while (!(Serial.available())) yield();
等待也可以用于同步任务。以下是一个示例片段:
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);
}
有关更多详细信息,请参见示例。有多个LED闪烁到反跳按钮的示例,以及一个具有非阻塞命令行读取功能的简单外壳。模板和名称空间可用于帮助结构化和减少源代码。下图显示了如何使用模板功能进行多次闪烁。堆栈只有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代码,可在使用主循环执行串行IO时使用计时器中断使LED闪烁。
重新发布:
中断是在其他情况发生时完成工作的一种常用方法。在下面的示例中,不使用时,LED闪烁delay()
。每当Timer1
触发时,isrBlinker()
就会调用中断服务程序(ISR)。它打开/关闭LED。
为了表明其他事情可以同时发生,请loop()
反复将foo / bar写入串行端口,而与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);
}
这是一个非常简单的演示。ISR可能更为复杂,并且可以由计时器和外部事件(引脚)触发。许多通用库都是使用ISR实现的。
在实现矩阵LED显示屏时,我也谈到了这个主题。
一言以蔽之,您可以通过使用millis()函数和Arduino中的计时器中断来构建轮询调度程序。
我建议Bill Earl发表以下文章:
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库的浮点矩阵乘法来运行Kalman滤波器。第三个是快速电流控制回路线程,该线程必须能够中断矩阵计算。
每个循环线程都有一个优先级和一个周期。如果优先级比当前执行线程高的线程到达其下一个执行时间,调度程序将暂停当前线程并切换到优先级更高的线程。一旦高优先级线程完成其执行,调度程序就会切换回上一个线程。
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和lambda函数
Thread* myThread = createThread(priority, period, offset,
[]()
{
//code to run
});
创建线程对象时,它们会自动连接到ThreadHandler。
要开始执行创建的线程对象,请调用:
ThreadHandler::getInstance()->enableThreadExecution();
这是另一个微处理器协作式多任务库– PQRST:运行简单任务的优先级队列。
在此模型中,线程被实现为的子类Task
,该子类被安排在将来的某个时间进行(并且有可能按规则的时间间隔进行重新安排,如果通常LoopTask
改为子类)。run()
任务到期时将调用对象的方法。该run()
方法做了一些应做的工作,然后返回(这是协作位);它通常会维护某种状态机来管理其对连续调用的操作(一个简单的示例是light_on_p_
下面示例中的变量)。它需要重新考虑如何组织代码,但是事实证明,在相当密集的使用中,它非常灵活和健壮。
它与时间单位无关,因此millis()
以micros()
或任何其他方便的刻度为单位运行时就很高兴。
这是使用此库实现的“闪烁”程序。这仅显示了一个正在运行的任务:通常会创建其他任务,然后在中启动其他任务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()
或产生CPU delay()
。或抢占式线程,可以随时调度。我觉得区别很重要,因为我已经看到很多来这里搜索线程的人这样做是因为他们更喜欢编写阻塞代码而不是状态机。阻塞产生CPU的实际线程是可以的。否阻止RtC任务。