个人资料
归档
正文

Arduino上的多任务

(2022-12-02 21:12:19) 下一个

1.SCoop 

用户指南 V1.2 (2013/01/10)

Arduino® ARM和AVR的简单协作式调度器Scoop https://code.google.com/p/arduino?scoop?cooperative?scheduler?arm?avr/

示例:

#include “SCoop.h”    // 创建调度器实例,名为mySCoop
defineTask(Task1)         // 标准任务定义方式,含setup和loop
volatile long count;      // 易改变量强制内存读写,确保主循环读取最新值
void Task1::setup() { count=0; };
void Task1::loop() { sleepSync(1000); count++; };    //task1计数

defineTaskLoop(Task2) {   // 快捷任务定义方式,无setup
  digitalWrite(13, HIGH); sleep(100);       //任务中必须用sleep(ms),以便调度器介入
  digitalWrite(13,LOW); sleep(100);
}

void setup() {
  Serial.begin(115200);   //串口初始化为115.2Kbps
  mySCoop.start();        //启动任务调度器
}

void loop() {
  long oldcount=-1;
  yield();               //切换至任务调度器即运行task1/2
  if (oldcount!=count) {
Serial.print(“seconds spent :”); Serial.println(count); //串口输出计数值
oldcount=count;
  }
}

Cooperative multitasking即协作式多任务中,任务的优先级相同,没有抢占打断机制,适合Arduino大多数库?接受重入调用的现实。yield()是 Arduino due Scheduler 库的一部分,在版本>1.5 的标准 Arduino 库中引入。基本上,当任务有空闲时调用yield()就会转到调度程序,后者切换到下一个任务。每个任务的末尾都隐含yield()函数以便交回控制权。只要微处?器有足够的资源将它们保存在内存中并在您可接受的时间内执?它们,您就可以根据需要创建任意数?的任务。

1.1.任务及其堆栈

创建任务使用defineTask或defineTaskRun宏,可选第二个参数为此对象创建堆栈(字节数组),默认值AVR为 150,ARM为256。如果需要更多的局部变?,或者调用需要更多空间的函数,那么您必须分配更多的堆栈。示例: defineTask(Task2,200)将为任务提供 200 字节的专用本地堆栈;或者在程序中用调用init(堆栈地址、堆栈的??、要使?的函数)来初始化代码和堆栈,注意ARM程序需要8字节对齐:

SCoopTask myTask;
defineStack(mystack,128)
void mycode() { count+++; }
void setup() { myTask.init(&myStack,sizeof(mystack),&mycode); … }
对init()方法的调用也可以像这样与对象声明组合在一起:

defineStack(mystack,128)
Void mycode() { count+++; }
SCoopTask myTask(&myStack,sizeof(mystack),&mycode); // implicit call to init()
Void setup() { … }

没有简单的方法来计算所需的最大堆栈大小,但是该库包含一个名为stackLeft()的方法,返回堆栈中尚未使用的内存?。

一旦声明了任务,就会创建一个对象,Arduino 环境会在进入主setup()或loop()之前在程序开头自动调用对象“构造函数”。该对象会自动注册在项目列表中,调度程序稍后将使用这些项目来逐个启动和启动每个任务(或事件或计时器)。

每个用defineTask宏定义的任务都应该有自己的setup()方法。调度程序将使用命令mySCoop.start()启动列表中注册的所有任务的所有setup ()。注册的最新任务将首先启动(与您的程序中的声明相反的顺序)。此命令应放在主 setup() 中。它还将初始化调度程序并重置用于计算时间的变?。此后,每个任务都被认为是“RUNNABLE”,以安全地进入其特定的loop()方法。

每个用defineTask或defineTaskLoop宏定义的任务也应该有自己的loop()方法。调度程序将定期启动此方法,作为整个调度过程的一部分。如果需要,此loop()中的任务代码可以永远阻塞,只要 yield()(或 sleep)方法在某个时间某处被调用。

进入和退出任务的机制由调度程序使用其yield()函数实现。从任务中调用yield()很可能会切换到列表中的下一个任务,并在到达最后一个任务时返回到调度程序。然后 Scheduler 将启动所有挂起的计时器或事件(请参阅文档后面的内容)并将重新启动一个接一个的任务。一旦执?了所有其他潜在任务(以及计时器或事件),它会将控制权交还给原始任务,就好像它是从对yield() 方法的调用的简单返回一样。执?只是在之后继续。

建议使用mySCoop.yield()从主Arduino sketch loop()系统地调用 yield(),这将强制循环在执?自己的 loop() 代码之前首先启动所有任务和未决事件/计时器。在SCoop.h文件开头将其SCoopYIELDCYCLE值设为0可更改此?为,以便调度程序始终控制切换机制。使用这种方法,主循环()将被视为所有其他代码段的控制塔,其中的代码将具有更好的执?优先级,因为它将在每次任务切换或事件/定时器启动时执?。

备注:SCoop库在用户的任务loop()结束时强制调用yield()。如果您实现自己的循环机制(例如,在任务 loop() 内的 while(1){ .. }),则程序必须在循环中的某处?时调用yield()或sleep()。SCoop库用mySCoop.yield()覆盖了原始的 Arduino yield()函数,所以可以在你的程序或包含的文件中的任何地方使用简单的词 yield();它也提供了标准 Arduino delay()的挂钩机制,后者现在调用yield()函数以确保调度器能始终运行。

因为库为堆栈分配了一定?的静态内存,任务一旦开始就?应该结束!由用户决定在任务loop()中编写停止或启动或等待事件的代码,最终使用pause()和resume()。(有关动态任务,请参阅“Android 调度程序”)

1.1.1 任务中可以调用由SCoopTask基础对象继承的几个方法

sleep(time) :与标准 delay() 函数相同的?为,但空闲时间用于立即将控制权交还给调度程序或执?其他挂起的任务。

sleepSync(time) :与睡眠功能相同,但睡眠时间与之前调用sleep()或sleepSync()函数同步,从而实现严格的周期性时间处理(“无抖动”)。

sleepUntil(Boolean) :只需等待布尔变?变为真(然后将其设置为假),同时将控制权交还给调度程序。必须为此标志使用volatile变?,因为状态变更将来自另一个任务或来自主loop()本身。

sleepUntil(Boolean,timeOut) :与sleepUntil相同,但在给定的 timeOut 时间段后仍会返回。它可以用作表达式中的函数,并在超时的情况下返回 false。

stackLeft() :返回自任务启动以来任务堆栈中从未使用过的字节数。这个函数也可以从Arduino sketch中的主循环()调用,在任务对象上下文之外,通过使用任务对象本身或全局指针引用它:

void printstack(){
  SCoopEvent* ptr=SCoopFirstTask; // 全局库类型和变?
  while (ptr) {
Serial.println(reinterpret_cast(ptr)?>stackLeft()); ptr=ptr?>pNext;
  }
}

Void loop() { printstack(); … or … Serial.println(Task1.stackLeft()); }

1.1.2 跨任务对话和变量类型

编译器优化经常将变?设为局部变?或使用临时寄存器。为了跨任务传递信息或在多线程程序中使用变?(例如,一个任务正在写入,另一个任务正在读取),我们必须将公共变?声明为volatile,这会强制编译器每次都通过读写内存来访问它。

为了简化变?声明,SCoop 库为int8,16,32、uint8,16,32和Boolean预定义了一些类型,在原始 Arduino 类型名称前面加上 “v”并删除“_t”,例如:

vui16 mycount; // exact same as volatile uint16_t mycount
vbool OneSecond = false; exact same as volatile Boolean OneSecond
备注:末尾的“_t”已被自愿删除,但被认为是对传统类型命名约定的偏离......


1.2.定时器

该库提供了一个互补的SCoopTimer对象,可用于创建将由调度程序编排的周期性操作。您可以根据需要实例化任意多个计时器,甚至可以将它们临时声明为函数或任务中的本地对象。当您在单个原子yield()操作中进入和离开计时器时,计时器?需要堆栈上下文;他们使用Arduino的正常堆栈。计时器必须很快,因为它们在任何情况下都无法将控制权交还给调度程序(本地 yield() 方法已禁用)。定时器看起来像一个 MCU 定时器中断,但可以从中调用任何现有函数或库,而没有系统崩溃的风险??

1.2.1 定时器的定义

使用宏defineTimerRun(),参数1是定时器对象的名称,可选的参数2是周期,实例:

defineTimerRun(myTimer,1000){ ticSecond=true; countSecond++ }

此计时器的代码隐式附加到myTimer::run()方法。计时器注册在与任务相同的列表中,在调用yield()期间由调度程序触发。一旦自上次调用以来经过的时间达到定义的时间段,将执?一次run()方法,它必须快速且无阻塞。

如果计时器需要时间来完成(比如几毫秒),建议改用任务,在loop()方法的最开始使用sleepSync()函数,并在阻塞或缓慢的部分调用yield()或yield(0)。以下代码将给出与 timer(1000) 定义完全相同的?为:

defineTaskLoop(myTimer,100) { sleepSync(1000); ticSecond=true; countSecond++ }

备注:在这个例子中,我们通过强制第二个参数的值为100来减少默认堆栈大小,因为任务loop()?需要那么多,它?调用任何其他函数并且没有局部变?。

1.2.2 启动和监控定时器

调用mySCoop.start()将初始化所有已注册的计时器,如果时间段作为defineTimerRun宏的第二个参数提供,则将启用它们。如果?是,则由程序稍后通过调用方法schedule(time)来初始化它。以下是修改或监视计时器的方法:

schedule(time) :启用计时器并准备每“time”毫秒启动一次
schedule(time, count) :与schedule(time)相同,增加了最大执?次数。
getPeriod() :返回为该计时器的定义周期。 setPeriod() :设置定时器周期。
getTimeToRun() :返回下一次执?计时器 run() 方法之前的时间(以毫秒为单位)。


1.2.3 定义计时器的其他方法

defineTimer(T1,optional period) 可以为这个对象声明一个 T1::setup() 和一个 T1::run() 方法,如果我们想在这个对象中添加一些设置代码,而?是将它们写在主 setup() 中,这很有用,但与V1.1不兼容,示例:

defineTimer(myTimer,1000) void myTimer::setup() { count=0; } void myTimer::run() { count++; }

defineTimer(myTimer,1000) void myTimer::setup() { count=0; } void myTimer::run()

另外2个宏是defineTimerBegin(event[,period]) 和defineTimerEnd(event)。请参阅库中的示例2。

1.3.事件 Event

该库提供了一个补充对象SCoopEvent ,可用于处?调度程序在外部事件或触发器上执?的代码。例如,可以从中断 (isr, signal) 触发事件,但相应的run()代码将仅由调度程序在中断上下文之外执?。这使得能够编写复杂的事件处理,调用库函数,而无需编码硬件中断所需的关键性。

与SCoopTimer对象一样,SCoopEvent没有堆栈上下文,并且应该尽可能快以保持调度流畅。如果事件需要很长时间才能完成,则应将其声明为永久任务,使用sleepUntil()方法等待易变标志。事件声明和使用示例:

defineEventRun(myevent){ Serial.println(“trigger received”); } isr(pin) { myevent.set(); } // or myevent=true;

事件对象只有一个公共方法来设置触发标志:

set() :将触发标志设置为真。该事件将由调度程序通过调用 yield() 启动。

set(value) :设置触发值,如果为false,则?么也?会发生。如果为真则相当于 set()

事件触发标志也可以通过赋值直接设置,如“myevent=true”,因为库为该对象重载了标准“=”运算符。

defineEvent(event)可用于定义event::setup()和event::run(),就像定时器一样。与V1.1?兼容!

另外2个宏是:defineEventBegin(event)和defineEventEnd(event)。请参阅库中的示例2。

1.4.先进先出缓冲区 Fifo

SCoop库的补充对象SCoopFifo和defineFifo宏用于管理先进先出缓冲区,适合于字节、整数、长整型或任何类型的 256字节以下的结构数据。这对于使用生产者?消费者或发送者?接收者模型以同步方式在任务、事件或计时器与另一个任务之间交换数据非常有用(任务之间?需要同步)。

下面是一个示例,定义了一个包含 100 个整数的 fifo 缓冲区,并在执?模拟采样的计时器和使用它们监视变化的任务之间使用它:

defineFifo(analogBuf,int16_t,100)

int16_t A,B;

defineTimer(anaRead,10)           // 每10ms运行一次anaRead()

void anaRead::run() { A=analogRead(1); analogBuf.put(&A); }   //每次读取一个采样值并存入Fifo

defineTaskLoop(task1) {

  while(analogBuf<32) yield(0);    // 等待Fifo缓冲区中有32个数据

  int16_t avg=0;

  for (int i=0; i<32; i++){

analogBuf.get(&B); avg += B; // 读取Fifo中的32个数据并取其和

  }

  avg /= 32; yield();             // 算得32个数据的平均值

  Serial.print “average analog value for 32 samples = “); Serial.println(avg);

}

SCoopFifo还提供了以下方法:

put(&var) :将变?的值添加到缓冲区。如果结果成功则返回 true,如果缓冲区已满则返回 false。

putChar(val)、putInt(val)、putLong(val)可用于直接将给定值添加到缓冲区中,而?需要中间变?。

get(&var) :从缓冲区中取出旧值并将其存储在传递的变?中。如果成功则返回 true,如果缓冲区为空则返回 false。

flush() :清空缓冲区并根据声明返回缓冲区的大小(项目数)。

flushNoAtomic():与flush相同,但?涉及中断。?应与中断服务程序一起用。

count() :返回缓冲区中可用的样本数?。也可在整数表达式中使用对象的名称,该表达式将返回与方法count() 相同的值。

1.5.Virtual timer/delay: SCoopDelay和SCoopDelayus

类SCoopDelay用于延迟或超时测?,有点像 TimerDown (容后叙)。自动重新加载是选项,如果使用它则定时/延迟会自动启动。典型用法:

SCoopDelay time; time = 10;

while (time) yield();     // this launch yield() during 10ms

SCoopDelay t10seconds(10000);

If (T10seconds.reloaded()) { /* do something every 10 seconds */ }

SCoopDelay对象的几个方法:

set(time) :定义延迟的开始时间。然后延迟将开始倒计时到 0;

get() :返回延迟的值。如果延迟已过,则结果为 0。

add(time) : 给延迟增加一定的时间

sub(time) : 减去一定?的延迟时间

这4个方法也可以通过直接使用SCoopDelay对象,由运算符重载在x=delay或delay=x或delay+=x或delay?=x等表达式中透明的使用。

elapsed() :如果延迟结束则返回 true。

reloaded() :与 elapsed 相同,但如果已定义,则使用重新加载值自动重新启动延迟。

setReload(time) :预定义并附加一个重载值到对象。与在对象声明期间提供的一样。

getReload() :返回附加到对象的重新加载参数的值。

reload() :将重新加载值添加到 SCoopDelay 对象。

initReload() :设置 SCoopDelay 对象及其重载值。

所有值都是int32类型,如SCoop.h文件开头的SCDelay_t变?所定义。

通过扩展,该库还提供了一个SCoopDelayus对象,它是相同的,但使用的参数以微秒为单位。这些值对于AVR是 int16,对于ARM是int32,由 SCoop.h 文件开头的 micros_t 变?定义。

1.6.时间计数器:TimerUp和TimerDown

用于完全独立于调度程序来处?时间的递增和递减计数。可将 tic 计数的时基指定为 1 毫秒到 30 秒 (int16)。可以声明的定时器数?没有限制。

1.6.1 TimerUp递增计数器

从 0 到要在对象声明中定义的最大值(可以是 0)。一旦达到最大值,计数器将返回 0,并且可以使用 rollOver()方法监视翻转事件以启动操作。例子:

#include TimerUp

counterMs(0);            // ?么都?做,需要进一步初始化

TimerUp myMillis(1000);   // 每 1 毫秒从 0 计数到 999

TimerUp mySeconds(3600,1000); // 每 1000 毫秒从 0 计数到 3599

void loop() {

  if (myMillis.rollOver())

Serial.print(“又过了一秒”)

  if (mySeconds.rollOver())

Serial.print(“又过了一小时”)

}

该对象也可以直接用在表达式或赋值中:

TimerUp myTime(1000);

??

void loop() {

  if (skip)&&(myTime > 500)

    mytime = 800; // 将读取并强制定时器

在定义rollover时的设定值可以在此后用setRollOver(timeMax)修改。

1.6.2 TimerDown递减计数器

从最大值递减到 0。一旦递减计数到 0,计数器保持 0 值,直到程序强制它为另一个值。示例:

#include

#include

TimerUp tic(100,10)       //每 10ms 从 0 递增到 99

TimerDown msLeft(0);      // 每 ms 倒计时一次

TimerDown secondsLeft(3600,1000); // 每 1000 毫秒从 3599 计数到 0

??

void loop() {

  while (secondsLefts) {

if (tic.rollOver())

  msLeft=1000;        // 重新启动这个计数器

Serial.print(secondsLeft); Serial.print(“ seconds and “);

Serial.print(msLeft); Serial.println(“ ms before end”);

  }

}

1.6.3 TimerUp和TimerDown对象的相关方法

Init(time, timeBase):用给定的 timemax 和给定的时基初始化计数器。例如,counter.init(5,1000) 将使用对应于5秒的值初始化计数器(向上或向下)。若是TimerUp将从0计数到4;TimerDown将从5计数到 0。

set(time) :在计数器中强制一个新值。这等于给counter=100这样的赋值;

reset() :只是强制计数器为 0。同时清除 TimerUp 对象的翻转标志。

get() :返回定时器的值。计时器也可以直接在整数表达式中读取,例如 x=counterMs;

pause(time) :计时器将保持其实际值直到恢复。

resume() :如果计时器暂停,则它会恢复并从该值开始重新计数。

1.7.库编译选项

7.1 数字输入输出和时间过滤

SCoop 库提供了一个独立的组件,通过使用对象的强大功能和运算符重载提供的可能性,以全面的方式定义、使用和过滤数字输入和输出。IOFilter.h库的提供主要是为了消除反弹或过滤和/或计算任何输入的转换,并具有使用调度程序编排和同步计时器的好处。但是像TimerUp和TimerDown一样,这个库也可以完全独立于SCoop调度器使用,然后时间过滤就变成异步的并且依赖于用户代码。使用示例:

#include

#include

Output led(13);

Input switch1(1);

InputFilter button2(2,150,100);    // 150ms before HIGH, 100ms before LOW

defineTimer(debounce,20)           // will check all the inputs registered every 20ms

void debounce::setup() { }

void debounce::loop() { IOFilter::checkAll(0); }

void setup() { led = LOW; }

void loop() {

  if (button1 || (button2.count==2)) {

button2.reset(); led=HIGH;

  }

}

可以在没有SCoop库的情况下使用相同的示例,只需删除“scoop.h”文件和“debounce”计时器定义。每次读取 button2输入时,过滤和计数序列将异步发生。用户代码可以通过在程序的某个地方调用方法checkAll()来强制定期同步检查过滤(参见下面的解释)。

1.7.2 IOFilter.h库中可用的方法和类

Input myInput(pin) : 定义 myInput 为 INPUT 并准备使用 digitalRead(pin)

get() :使用 digitalRead(pin) 读取引脚的即时值

myInput 可用于整数表达式 (x=myInput)

readAll() :V1.2此方法仍在开发中,但将用于强制库读取列表中声明和注册的所有输入对象,以?在特定时间同步输入值。其后使用get()或整数表达式读取引脚时将返回最近readAll()方法存储的值。这通常在工业过程中或在开发作为 PLC 循环工作的程序时很有用,从而实现类似于梯形图或FBD语言的编程风格。

Output myOut(pin) : 将myOut定义为OUTPUT并准备使用 digitalWrite(pin)

set(value) :基本上执? digitalWrite(pin, value)。存储该值以供进一步阅读。

get() :返回通过 set(value) 命令写入引脚的最后一个值。

myOut 也可用于整数表达式 (x=myOut) 或赋值 (myOut=1)

writeAll() :此方法是一个尚未实现的占位符,在此版本中仍处于开发阶段,但将用于强制库写入列表中声明和注册的所有输出对象,以?在特定时间同步输出?新。然后使用 set() 或通过在表达式中分配对象来写入输出,只会将新值写入内存,直到用户调用 writeAll() 为止。至于 readAll(),这通常在工业过程中或在开发作为 PLC 循环工作的程序时很有用,其中输入在循环开始时读取,输出在循环结束时写入一次。

InputFilter myInputF(pin, timeOn, timeOff) :将 myInputF 定义为 INPUT 并准备在输入升至高 (timeOn) 和输入降至低 (timeOff) 时使用带时间过滤的 digitalRead(pin)。只有当引脚在 timeOn 或 timeOff 定义的时间内保持此状态时,该值才会设置为高或低

       get() :一旦应用了过滤约束,就返回输入的值。

       check():启动一个过程以根据时间过滤约束验证输入。如果自上次调用 check() 以来引脚值已从低变为高,则还增加附加计数器。

       备注:check() 方法总是在用户代码调用 get() 方法或读取整数表达式中的输入时启动一次。如果?使用调度程序库,这保证了时间?改和计数事件的同步处?。

       count() :返回输入上的升起转换数,由 check() 方法检测到。

reset() :重置连接到输入的计数器。 Des ?影响时序检查约束。

checkAll() :为所有已声明并自动注册的对象(ptrInputFilter)启动 check() 方法。此方法将首先检查自上次调用以来花费的时间是否超过默认阈值 10 毫秒,以避免在?需要的地方浪费时间。该值可以在 IOFilter.h 文件的开头?改。

checkAll(time) :提供与 checkAll() 相同的?为,但例程开始时的时间检查器将使用给定的时间参数而?是默认时间参数。使用 0将强制执? checkAll();

       备注:checkAll() 和 checkAll(time) 被声明为静态的,因此可以由用户代码调用而无需引用正式对象,只需强制作用域 (IOFilter::checkAll())。

readAll() :占位符尚未实现。与 Input 对象的 readAll() ?为相同。

InputMem myBit(addr,bit) 通过从提供的内存地址中提取位,将 myBit 定义为返回 0 或 1 值(低/高)的输入。这有助于定义附加到内存中某个位的综合输入变?,例如,由 Modbus 协议返回并存储在 RAM 区域中给定内存地址的“线圈”。

       get() :返回内存中位的值。

该对象还可以在整数表达式 (x=myBit) 中使用,因为库重载了运算符。

OutputMem myBit(addr,bit) 将 myBit 定义为与提供的内存地址中的位相对应的输出。这有助于定义中间输出变?,或定义附加到存储在 RAM 区域中的内存地址的输出,如 Modbus 协议中使用的“线圈”对象。

       set(value) :根据传递的值强制该位为 0 或 1。

       get() :返回内存中位的值。

       该对象还可以用于整数表达式(x=myBit 或 myBit=y),因为库重载了运算符和赋值逻辑。

备注:打算在 SCoop 框架的?高版本中为模拟输入和 PWM 输出创建相同类型的库,名称为IOAnalog.h

1.8.更多技术性的东西

此图片表示对象类和一些实例,以及它如何与Arduino程序中的标准setup()和loop()函数一起工作。

1.8.1 时序测量

通过在SCoop.h文件开头定义的变?SCoopTIMEREPORT ,该库将提供额外的变?来控制任务的进出时间。

如果预定义变?的值N设置为 0,则这些变?都?可用,并且库的代码被优化以减少调度程序的开销时间和大小。如果值N设置为 1、2、 3 或 4(ARM 最多为 7),则以下4个变?将在 SCoop 和 SCoopTask 对象中可用:

SCoopTask.yieldMicros :给出调度程序最后N个周期中任务花费的总时间。

mySCoop.cycleMicros:整个周期的总时间,包括最后 N 个周期在主循环()中花费的时间。将这 2 个数字相除,就是cpu用于任务的比率。

cycleMicros:一旦除以N表示调度程序的平均响应能?。任何计时器或事件都将在此时间窗口内启动(除非任务在同一时刻需要?多时间)。

SCoopTask.maxYieldMicros:提供自调度程序启动以来 yieldMicros 达到的最大值。这可用于确定某个任务在某个时间点是否消耗了比预期更多的时间。该变?可在 R/W 中访问,并可由主程序重置。

mySCoop.maxCycleMicros:提供自调度程序启动以来cycleMicros达到的最大值。更频繁地调用yield()肯定会降低cycleMicros和maxCycleMicros之间存在大差异的风险。

备注:通过在SCoop.h文件的开头将SCoopTIMEREPORT的值从1更改为4,可以定义用于累积时间的周期数,从2(默认)、4、8到16。对于AVR程序,这些变?只有16位,可能会溢出。对于ARM,可以通过将这些值扩展到5、6或7来累积多达128个周期,而?会对性能产生任何影响。

1.8.2 Quantum 强制执?每个任务花费的时间或CPU百分比的方法

每个任务对象都提供一个名为quantumMicros的变?,用于定义调度程序在切换到另一个任务之前应该在此任务中花费的时间?。这保证了任务获得一定数?的 CPU 时间来继续,相对于整个循环时间。初始化对象时,此变?根据程序设置为默认值(SCoopDefaultQuantum):ARM为200us,AVR为400us。如果需要,可以在程序中动态?改此值(例如myTask.quantumMicros=700;),最好是在任务setup()方法中。这将影响在此任务中花费的默认时间,因此也会影响整个周期的长度。这是一种将一定数?的CPU时间(“百分比”)强制用于任务的聪明方法。但是因为模型是Cooperative,代码没有提交。

备注:当任务正在使用sleep()或sleepUntil()或sleepSync()时,如果?满足条件,它几乎会立即切换回调度程序。因此,任务花费的时间在yieldMicros变?中被视为 0,这并?完全正确,因为任务至少花费了检查其条件所需的时间。

用户程序还可以在微秒内将一个参数强制传递给SCoopTask::yield(x),该参数将仅用于此yield(x)调用,而?是 quantumMicros。例如,写入“yield(100)”将要求调度程序检查我们是否已经在该任务中花费了100uS。如果是,那么调度器将切换到下一个;如果没有,它将立即从yield(100)返回。类似的用法,写入“yield(0)”会强制调度程序立即切换到下一个任务。

mySCoop.start(cycleTime)也可用于将作为参数提供的cycleTime除以注册的任务数(主循环 +1)来覆盖默认任务quantumMicros。如果任务quantumMicros值为0,那么SCoop库正在优化代码,以便系统地切换到下一个任务,而?控制其花费的时间。这减少了切换时间,因为yield()?会调用micros()函数来与quantumMicros进?比较。这可以通过在主程序setup()中编写mySCoop.Start(0)或通过更改SCoop.h文件开头的预定义变? SCoopDefaultQuantum 来实现。

mySCoop.start(cycleTime,mainLoop)可以将特定时间(甚至0)分配给mainloop。备注:如果mainLoop?为 0,则调度程序将始终调整主循环所花费的时间(加或减)以尽?保证恒定的cycleTime!

1.8.3 原子代码

由于yield()函数现在嵌入到Arduino>1.5库的许多函数中,因此可能需要强制一段代码成为“原子的”并且?被调度程序中断。想象一个示例,您使用库写入一个芯片寄存器,并且在发送新寄存器之前需要延迟3毫秒等待3毫秒。在这种情况下,该库提供了一个名为mySCoop.Atomic的8位全局变?,您只需在敏感代码的开头递增 (mySCoop.Atomic++),然后在此代码部分的末尾递减 (mySCoop.Atomic??)。当mySCoop.Atomic变?包含非空值时,yield 函数就会返回。

从 V1.2 开始,可以使用预定义的宏SCoopATOMIC { ?}(与yieldATOMIC { ? } 相同)。这将在方括号内声明的块代码的开头和结尾插入正确的代码。

1.8.4 重入保护

我们想要保护一段代码的另一种情况是当一个函数调用 yield()(就像以太网库中的一些函数)但该函数?可重入并且必须防止来自其他任务的多次调用。为此,引入了一个特殊的宏调用SCoopPROTECT(),与yieldPROTECT() 相同。只需在函数的开头使用此宏来保护它,这将自动插入一些静态?失性标志和一些代码来检查该标志是否已由先前的调用设置。

如果是这样,代码将调用yield()直到标志被重置。标志重置自动插入到方括号中的bloc代码末尾,其中使用了 SCoopPROTECT()。可以在块代码结束之前使用SCoopUNPROTECT()来提前重置标志。

1.8.5 性能

对于抢占式操作系统,SCoop协作调度程序的性能取决于调度程序检查时间和切换任务上下文所花费的时间。SCoop使用优化的代码和例程,以在 yield()方法中花费尽可能少的时间。

如果自调度器启动以来,在任务中花费的时间还没有达到“quantumMicros”,那么yield()将在任务中尽快返回。如果时间结束(AVR默认为200us,ARM为400us),那么yield()将花费更多时间准备,然后切换到下一个任务。

SCoop使用标准的Arduino micros()函数来评估花费的时间。为了在Arduino UNO上获得更好的性能,已经为 AVR 重写了一个特定的16位微函数,利用了Teensy核心中使用的大多数想法。有关更多信息和版权,请参阅源代码。

协作式调度的另一个关键区别是用户代码必须非常频繁地且在有空闲时间或阻塞代码段就调用yield(),这是保证任务、计时器和事件之间平滑切换的唯一方法。因此,在 yield() 中花费的时间相对比抢占式操作系统更重要,后者的任务调度由周期性中断自上而下触发,比如每毫秒一次。而SCoop在某些情况下,如果任务正在做非常简单的事情(例如使用sleepUntil()),则yield()可能会非常频繁,大约每10us一次,由此花费在yield()上的总时间将是以上抢占式操作系统的100倍!但这并?意味着SCoop?好,只是您的任务?需要这么多的CPU能?,因此将转移到yield()。为此引入了时间片的概念,以确保我们在切换到另一个任务代码之前继续执?任务代码。这样一来,花在 yield() 上的相对时间就减少了,并且保证任务有一定数?的 CPU 资源,这对于需要像fft或采样计算的应用程序很重要。

为了评估整体性能,SCoop库提供了一个名为performance1的示例。它显示了在没有调度程序的情况下我们可以做多少“计数”,然后是3个具有大量度和0?度的任务。我们可以看到在yield()中花费的时间对10秒内完成的计数有多少影响。通过将此差异除以对yield()的调用次数(AVR为32或ARM为128),我们可以计算出每个yield() 花费的平均时间?

1.8.6 对象上下文与主要的Arduino程序

SCoop库强制用户将任务定义为具有相关setup()和loop()函数的对象,这看起来很酷,但是当程序变大时,这会带来一些约束和限制。为了可读性,任务可能分布在由task loop()调用的多个函数中:

void printMyStack() { … }

defineTaskRun(task1){ printMyStack(); x=x+1; sleep(100); }

在这种情况下,外部函数如printMyStack无法访问 SCoopTask对象中的方法,如sleep()或stackLeft()(它们未声明为静态)。yield()例外,因为该库定义了一个全局的yield(),它可以在任何地方使用。

为了解决这个问题,可以使用对象本身的名称来引用该方法,例如task1.sleep(10),但是如果该函数对多个任务

是通用的,那么代码必须使用一个全局指针mySCoop.Task,它始终包含一个指向正在执?的任务对象的指针。所以在这种情况下,函数printMyStack()应该包含以下类型的代码:

void printMyStack() { Serial.println(mySCoop.Task?>stackLeft());  mySCoop.Task?>sleep(100);  }

1.8.7 “Android Scheduler” V1.2 New –> SchedulerARMAVR.h

基于Atmel ARM芯片的Arduino DUE板的Arduino 1.5库提供了一个Scheduler.h库,这是一个非常基本的调度程序,通过全局 yield() 方法进?任务上下文切换。原始源代码来自 Android 团队/项目,这就是为?么我简单地将这个库称为“Android Scheduler”。所以呢?

Scheduler.h的升级版SchedulerARMAVR.h随SCoop库一起提供,该版本现在与ARM和AVR都兼容!我通过使用AVR堆栈切换扩展原始的Scheduler.h库将代码移植到 AVR,尽管功能?佳,但结果非常好。因为每个用户都会质疑使用哪种解决方案,所以我想透明地提供此文件作为您项目的可能替代方案。在本用户指南中包括它就像提供本指南的硬拷贝和镀金封面,我?会考虑现在的金价,因为我把它绿色化了。

这个调度程序非常快,但?会检查任何时间片或在任务中花费的时间,这可能会给您的应用程序带来非常糟糕的结果。此外,该库?为Fifo、时间测?或计时器和事件提供任何支持功能。没关系,SCoop可以弥补这些。

这个调度程序的特点是可以在主程序中随时启用动态任务,只需调用startLoop(myLoop,stackSize),如您在 multipleBlink 原始示例中所见。然后代码将使用标准的malloc和free函数从堆中分配任务对象及其堆栈。这种方法通常被认为对于内存?足的小型系统来说是有风险的,并且对于非专家用户来说非常敏感。该库还提供了任务终止的可能性。然后内存被动态释放,在堆heap中留下空?。

您可能会试用它并且会对这个库感到满意,所以将SCoop带到几乎相同的级别并使其兼容非常重要,这样您就可以现在或以后移植到它。当然性能上是有差别的。使用SCoop在AVR上的最佳上下文切换时间是15us,而使用移植的“Android Scheduler”则降至 9us。但是,SCoop最后可能会?快,因为如果?花费时间片,基本的yield()调用将只需要2.5us。

换句话说,我已经决定在 SCoop 库中添加 2 个方法,这两个方法提供了使用相同语法定义动态任务的完全相同的可能性:可以通过调用mySCoop.startLoop(myLoop, myStack)或Scheduler.startLoop(myLoop,myStack)并稍后通过调用SCoopTask::kill()终止此任务!为了保留指向此任务的指针,使用它的好方法是:

SCoopTask* ptrAndroid; ptrAndroid

ptrAndroid = Scheduler.startLoop(myLoop,256);

mySCoop.start(); // 或 Scheduler.start();

while (1) {

  yield();

  if (something) ptrAndroid->killMe();

}

从技术上讲,这是可?的而且您只能使用这种方式将任务添加到SCoop。但我个人的选择已经完成 。似乎也有可能(根据 multipleBlink 示例的成功)立即替换现有项目中Android Scheduler的使用,只需替换:

#include 为 #include ,当然这个还有待进一步体验。

一个先决条件是在主setup()结束时启动Scheduler.start(),否则SCoop将?会启动 (当 SCoopANDROIDMODE>=1时“Scheduler.”与 “mySCoop.”相同)。最后但同样重要的是,仅当您在SCoop.h头文件里将预定义变? SCoopANDROIDMODE更改为2时,kill()才会起作用。原因是我?想用kill()带来的可能约束,污染主要“SCoop::yield()”中的一些关键代码......

在阅读了 20 页长篇之后,是时候体验所有这一切并在 Arduino 论坛中报告任何反馈或错误了!多谢你们。

C:Program Files (x86)Arduinohardwarearduinoavrplatform.txt

2. FreeRTOS

https://blog.csdn.net/jiyotin/article/details/118494109 

3. TaskScheduler

https://blog.csdn.net/weixin_44035986/article/details/123412711 

https://github.com/arkhipenko/TaskScheduler https://www.electrosoftcloud.com/en/arduino-taskscheduler-no-more-millis-or-delay 推荐,支持常见的多种uP如Uno/Nano/Attiny85/ESP32/nRF52/STM32/MSP43x。支持分层式优先级如两级,每级中多个任务。

https://www.instructables.com/Simple-Multi-tasking-in-Arduino-on-Any-Board/ 

4. Blink Without Delay

https://docs.arduino.cc/built-in-examples/digital/BlinkWithoutDelay 

unsigned long previousMillis = 0;  // last time LED was updated
const long interval = 1000;  // interval ms at which to blink
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    // save the last time you blinked the LED
    previousMillis = currentMillis;
    // if the LED is off turn it on and vice-versa:
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }
    // set the LED with the ledState of the variable:
    digitalWrite(ledPin, ledState);
  }


void sos()                 
{ digitalWrite(LED, HIGH);   // Turn on the LED 
  for (int i=0; i<3; i++){
    tone(Buzzer, note, 100); // pin,freq,duration
    delay(200);
    noTone(Buzzer);
  }
  delay(200);
  digitalWrite(LED, LOW);    // Turn off the LED
  for (int i=0; i<3; i++){
    tone(Buzzer, note, 300);
    delay(400);
    noTone(Buzzer);
  }
  delay(200);
  digitalWrite(LED, HIGH);   // Turn on the LED 
  for (int i=0; i<3; i++){
    tone(Buzzer, note, 100);
    delay(200);
    noTone(Buzzer);
  }
  delay(600);
}

5. Simulator

https://docs.wokwi.com/parts/wokwi-attiny85 https://docs.wokwi.com/parts/wokwi-arduino-uno 

[ 打印 ]
阅读 ()评论 (0)
评论
目前还没有任何评论
登录后才可评论.