Skip to content

Commit 7572262

Browse files
authored
Merge pull request #10 from bxparks/develop
0.3 - implement COROUTINE_DELAY_MICROS(); update COROUTINE_DELAY_SECONDS()
2 parents 2b4eebe + 106bfef commit 7572262

File tree

89 files changed

+1593
-749
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1593
-749
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
# Changelog
22

33
* Unreleased
4+
* 0.3 (2019-08-26)
5+
* Update `AutoBenchmark/README.md` benchmark numbers.
6+
* Use a `do-while` loop `COROUTINE_AWAIT()` so that it is guaranteed to call
7+
`COROUTINE_YIELD()` at least once. Previously, if the `condition` of the
8+
await was already (or always) true, the `while-loop` caused the coroutine
9+
to hog the control flow without yielding.
10+
* Use a `do-while` loop in `COROUTINE_DELAY()` so that `COROUTINE_YIELD()`
11+
is guaranteed to be called at least once, even if the delay is 0.
12+
* Add `COROUTINE_DELAY_MICROS(delayMicros)` which is similar to the
13+
existing `COROUTINE_DELAY(delayMillis)` macro. The actual delay time may
14+
be inaccurate on slow processors (e.g. 16 MHz AVR processors) and become
15+
more accurate for faster processors (e.g. ESP32). (#9)
16+
* **Breaking**: The `COROUTINE_DELAY_SECONDS(delaySeconds)` macro now takes
17+
only one parmeter instead of 2 parameters. An external `loopCounter`
18+
variable no longer needs to be provided by the caller, which simplifies
19+
the API.
20+
* Add `examples/Delay/Delay.ino` program to validate the various
21+
`COROUTINE_DELAY*()` macros.
22+
* The `sizeof(Coroutine)` increases from 14 bytes to 15 bytes on an 8-bit
23+
processor. No change on 32-bit (still 28 bytes).
424
* 0.2.2 (2019-07-31)
525
* Add `SHIFT_ARGC_ARGV()` macro for easy token shifting,
626
and `isArgEqual()` method for easy comparison against flash string

README.md

Lines changed: 127 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@
33
A low-memory, fast-switching, cooperative multitasking library using
44
stackless coroutines on Arduino platforms.
55

6-
Version: 0.2.2 (2019-07-31)
7-
8-
This library is currently in "beta" status. I'm releasing it through the Arduino
9-
Library Manager to solicit feedback from interested users. Send me an email or
10-
create a GitHub ticket.
11-
12-
[![AUniter Jenkins Badge](https://us-central1-xparks2018.cloudfunctions.net/badge?project=AceRoutine)](https://github.com/bxparks/AUniter)
13-
14-
## Summary
15-
166
This library is an implementation of the
177
[ProtoThreads](http://dunkels.com/adam/pt) library for the
188
Arduino platform. It emulates a stackless coroutine that can suspend execution
@@ -38,9 +28,10 @@ their life cycle:
3828
* `COROUTINE_AWAIT(condition)`: yield until `condition` becomes `true`
3929
* `COROUTINE_DELAY(millis)`: yields back execution for `millis`. The `millis`
4030
parameter is defined as a `uint16_t`.
41-
* `COROUTINE_DELAY_SECONDS(loopCounter, seconds)`: yields back execution for
42-
`seconds`. The maximum value of `seconds` is determined by `loopCounter`
43-
which can be of any integer type.
31+
* `COROUTINE_DELAY_MICROS(micros)`: yields back execution for `micros`. The
32+
`micros` parameter is defined as a `uint16_t`.
33+
* `COROUTINE_DELAY_SECONDS(seconds)`: yields back execution for
34+
`seconds`. The `seconds` parameter is defined as a `uint16_t`.
4435
* `COROUTINE_LOOP()`: convenience macro that loops forever
4536
* `COROUTINE_CHANNEL_WRITE(channel, value)`: writes a value to a `Channel`
4637
* `COROUTINE_CHANNEL_READ(channel, value)`: reads a value from a `Channel`
@@ -54,9 +45,10 @@ others (in my opinion of course):
5445
no matter how many coroutines are active
5546
* extremely fast context switching
5647
* ~6 microseconds on a 16 MHz ATmega328P
57-
* 1.1-2.0 microseconds on Teensy 3.2 (depending on compiler settings)
58-
* ~1.7 microseconds on a ESP8266
59-
* ~0.5 microseconds on a ESP32
48+
* ~2.9 microseconds on a 48 MHz SAMD21
49+
* ~1.7 microseconds on a 80 MHz ESP8266
50+
* ~0.4 microseconds on a 240 MHz ESP32
51+
* 0.7-1.1 microseconds on 96 MHz Teensy 3.2 (depending on compiler settings)
6052
* uses the [computed goto](https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html)
6153
feature of the GCC compiler (also supported by Clang) to avoid the
6254
[Duff's Device](https://en.wikipedia.org/wiki/Duff%27s_device) hack
@@ -87,7 +79,14 @@ AceRoutine is a self-contained library that works on any platform supporting the
8779
Arduino API (AVR, Teensy, ESP8266, ESP32, etc), and it provides a handful of
8880
additional macros that can reduce boilerplate code.
8981

90-
### HelloCoroutine
82+
Version: 0.3 (2019-08-26)
83+
84+
Status: In "beta". API has been relatively stable since 0.2. Breaking change
85+
made to `COROUTINE_DELAY_SECONDS()` in 0.3.
86+
87+
[![AUniter Jenkins Badge](https://us-central1-xparks2018.cloudfunctions.net/badge?project=AceRoutine)](https://github.com/bxparks/AUniter)
88+
89+
## HelloCoroutine
9190

9291
This is the [HelloCoroutine.ino](examples/HelloCoroutine) sample sketch.
9392

@@ -221,6 +220,9 @@ The following example sketches are provided:
221220
primitive "shell". The shell is non-blocking and uses coroutines so that other
222221
coroutines continue to run while the board waits for commands to be typed on
223222
the serial port.
223+
* [Delay.ino](examples/Delay): validate the various delay macros
224+
(`COROUTINE_DELAY()`, `COROUTINE_DELAY_MICROS()` and
225+
`COROUTINE_DELAY_SECONDS()`)
224226
* [Pipe.ino](examples/Pipe): uses a `Channel` to allow a Writer to send
225227
messages to a Reader
226228
* [ChannelBenchmark.ino](examples/ChannelBenchmark): determines the amount of
@@ -253,9 +255,10 @@ The following macros are available to hide a lot of boilerplate code:
253255
* `COROUTINE_AWAIT(condition)`: yield until `condition` become `true`
254256
* `COROUTINE_DELAY(millis)`: yields back execution for `millis`. The maximum
255257
allowable delay is 32767 milliseconds.
256-
* `COROUTINE_DELAY_SECONDS(loopCounter, seconds)`: yields back execution for
257-
`seconds`. The maximum allowable delay is the maximum value of the integer
258-
type of `loopCounter` which can be of any integer type.
258+
* `COROUTINE_DELAY_MICROS(micros)`: yields back execution for `micros`. The
259+
maximum allowable delay is 32767 microseconds.
260+
* `COROUTINE_DELAY_SECONDS(seconds)`: yields back execution for `seconds`. The
261+
maximum allowable delay is 32767 seconds.
259262
* `COROUTINE_LOOP()`: convenience macro that loops forever, replaces
260263
`COROUTINE_BEGIN()` and `COROUTINE_END()`
261264
* `COROUTINE_CHANNEL_WRITE()`: writes a message to a `Channel`
@@ -357,81 +360,130 @@ while (!condition) COROUTINE_YIELD();
357360

358361
### Delay
359362

360-
The `COROUTINE_DELAY(millis)` macro delays the return of control until `millis`
361-
milliseconds have elapsed. The `millis` argument is a `uint16_t`, a 16-bit
362-
unsigned integer, which reduces the size of each coroutine instance by 4 bytes.
363-
However, the actual maximum delay is limited to 32767 milliseconds to avoid
364-
overflow situations if the other coroutines in the system take too much time for
365-
their work before returning control to the waiting coroutine. With this limit,
366-
the other coroutines have as much as 32767 milliseconds to complete their work,
367-
which should be more than enough time for any conceivable situation. In
368-
practice, coroutines should complete their work within several milliseconds and
369-
yield control to the other coroutines as soon as possible.
370-
371-
To delay for longer, an explicit loop can be used. For example, to delay
372-
for 1000 seconds, we can do this:
363+
The `COROUTINE_DELAY(millis)` macro yields back control to other coroutines
364+
until `millis` milliseconds have elapsed. The following waits for 100
365+
milliseconds:
366+
373367
```C++
374-
COROUTINE(waitThousandSeconds) {
368+
COROUTINE(waitMillis) {
375369
COROUTINE_BEGIN();
376-
static uint16_t i;
377-
for (i = 0; i < 1000; i++) {
378-
COROUTINE_DELAY(1000);
379-
}
370+
...
371+
COROUTINE_DELAY(100);
380372
...
381373
COROUTINE_END();
382374
}
383375
```
384-
See **For Loop** section below for a description of the for-loop construct.
385376
386-
This for-loop construct happens often enough that it seemed worthwhile to
387-
provide the `COROUTINE_DELAY_SECONDS(loopCounter, seconds)` convenience macro.
388-
It replaces the above for-loop, like this:
377+
The `millis` argument is a `uint16_t`, a 16-bit unsigned integer, which reduces
378+
the size of each coroutine instance by 4 bytes (8-bit processors) or 8 bytes
379+
(32-bits processors). However, the actual maximum delay is limited to 32767
380+
milliseconds to avoid overflow situations if the other coroutines in the system
381+
take too much time for their work before returning control to the waiting
382+
coroutine. With this limit, the other coroutines have as much as 32767
383+
milliseconds before it must yield, which should be more than enough time for any
384+
conceivable situation. In practice, coroutines should complete their work within
385+
several milliseconds and yield control to the other coroutines as soon as
386+
possible.
387+
388+
To delay for longer period of time, we can use the
389+
`COROUTINE_DELAY_SECONDS(seconds)` convenience macro. The following example
390+
waits for 200 seconds:
389391
```C++
390-
COROUTINE(waitThousandSeconds) {
392+
COROUTINE(waitSeconds) {
391393
COROUTINE_BEGIN();
392-
static uint16_t loopCounter;
393-
COROUTINE_DELAY_SECONDS(loopCounter, 1000);
394+
...
395+
COROUTINE_DELAY_SECONDS(200);
394396
...
395397
COROUTINE_END();
396398
}
397399
```
398-
The `static uint16_t loopCounter` variable is still required for the
399-
`COROUTINE_DELAY_SECONDS()` macro because it needs a loop counter that must
400-
preserve its value across multiple invocation of the coroutine. The
401-
`loopCounter` may be any integer type, and the maximum number of seconds is the
402-
maximum value of that particular integer type. For example, if the `loopCounter`
403-
was a `uint8_t`, the maximum delay would be 255 seconds. If the `loopCounter`
404-
was a `uint32_t`, the maximum delay would be about 4 billion seconds.
405-
406-
The `loopCounter` may also be a member variable of the `Coroutine` subclass,
407-
when you use custom or manual coroutines (see the sections on *Custom
408-
Coroutines* or *Manual Coroutines* below for details). In other words, you can
409-
define a coroutine with a `mDelayCounter` member variable, and use the
410-
`COROUTINE_DELAY_SECONDS()` macro like this:
400+
The maximum number of seconds is 32767 seconds.
401+
402+
On faster microcontrollers, it might be useful to yield for microseconds using
403+
the `COROUTINE_DELAY_MICROS(delayMicros)`. The following example waits for 300
404+
microseconds:
405+
411406
```C++
412-
class MyCoroutine: public Coroutine {
413-
public:
414-
int runCoroutine() override {
415-
...
416-
COROUTINE_DELAY_SECONDS(mDelayCounter, 1000);
417-
...
418-
}
407+
COROUTINE(waitMicros) {
408+
COROUTINE_BEGIN();
409+
...
410+
COROUTINE_DELAY(300);
411+
...
412+
COROUTINE_END();
413+
}
414+
```
415+
This macro has a number constraints:
419416
420-
private:
421-
uint16_t mDelayCounter;
422-
};
417+
* The maximum delay is 32767 micros.
418+
* All other coroutines in the program *must* yield within 32767 microsecond,
419+
otherwise the internal timing variable will overflow and an incorrect delay
420+
will occur.
421+
* The accuracy of `COROUTINE_DELAY_MICROS()` is not guaranteed because the
422+
overhead of context switching and checking the delay's expiration may
423+
consume a significant portion of the requested delay in microseconds.
424+
425+
If the above convenience macros are not sufficient, you can choose to write an
426+
explicit for-loop. For example, to delay for 100,000 seconds, instead of using
427+
the `COROUTINE_DELAY_SECONDS()`, we can do this:
428+
429+
```C++
430+
COROUTINE(waitThousandSeconds) {
431+
COROUTINE_BEGIN();
432+
static uint32_t i;
433+
for (i = 0; i < 100000; i++) {
434+
COROUTINE_DELAY(1000);
435+
}
436+
...
437+
COROUTINE_END();
438+
}
423439
```
424440

441+
See **For Loop** section below for a description of the for-loop construct.
442+
425443
### Stackless Coroutines
426444

427445
Each coroutine is stackless. More accurately, the stack of the coroutine
428446
is destroyed and recreated on every invocation of the coroutine. Therefore,
429447
any local variable created on the stack in the coroutine will not preserve
430448
its value after a `COROUTINE_YIELD()` or a `COROUTINE_DELAY()`.
431449

432-
The easiest way to get around ths problem is to use `static` variables inside
433-
a `COROUTINE()`. Static variables are initialized once and preserve their value
434-
through multiple calls to the function, which is exactly what is needed.
450+
The problem is worse for local *objects* (with non-trivial destructors). If the
451+
lifetime of the object straddles a continuation point of the Coroutine
452+
(`COROUTINE_YIELD()`, `COROUTINE_DELAY()`, `COROUTINE_END()`), the destructor of
453+
the object will be called incorrectly when the coroutine is resumed, and will
454+
probably crash the program. In other words, do **not** do this:
455+
456+
```C++
457+
COROUTINE(doSomething) {
458+
COROUTINE_BEGIN();
459+
String s = "hello world"; // ***crashes when doSomething() is resumed***
460+
Serial.println(s);
461+
COROUTINE_DELAY(1000);
462+
...
463+
COROUTINE_END();
464+
}
465+
```
466+
467+
Instead, place any local variable or object completely inside a `{ }` block
468+
before the `COROUTINE_YIELD()` or `COROUTINE_DELAY()`, like this:
469+
470+
```C++
471+
COROUTINE(doSomething) {
472+
COROUTINE_BEGIN();
473+
{
474+
String s = "hello world"; // ok, because String is properly destroyed
475+
Serial.println(s);
476+
}
477+
COROUTINE_DELAY(1000);
478+
...
479+
COROUTINE_END();
480+
}
481+
```
482+
483+
The easiest way to get around these problems is to avoid local variables
484+
and just use `static` variables inside a `COROUTINE()`. Static variables are
485+
initialized once and preserve their value through multiple calls to the
486+
function, which is exactly what is needed.
435487

436488
### Conditional If-Else
437489

@@ -1260,7 +1312,7 @@ advantages:
12601312
All objects are statically allocated (i.e. not heap or stack).
12611313

12621314
* 8-bit processors (AVR Nano, UNO, etc):
1263-
* `sizeof(Coroutine)`: 14
1315+
* `sizeof(Coroutine)`: 15
12641316
* `sizeof(CoroutineScheduler)`: 2
12651317
* `sizeof(Channel<int>)`: 5
12661318
* 32-bit processors (e.g. Teensy ARM, ESP8266, ESP32)
@@ -1311,19 +1363,19 @@ MacOS 10.14.5.
13111363

13121364
### Hardware
13131365

1314-
The library is extensively tested on the following boards:
1366+
The library has been extensively tested on the following boards:
13151367

13161368
* Arduino Nano clone (16 MHz ATmega328P)
1369+
* Arduino Pro Mini clone (16 MHz ATmega328P)
13171370
* Arduino Pro Micro clone (16 MHz ATmega32U4)
1371+
* SAMD21 M0 Mini (48 MHz ARM Cortex-M0+) (compatible with Arduino Zero)
13181372
* NodeMCU 1.0 clone (ESP-12E module, 80 MHz ESP8266)
13191373
* ESP32 dev board (ESP-WROOM-32 module, 240 MHz dual core Tensilica LX6)
1320-
* SAMD21 M0 Mini (48 MHz ARM Cortex-M0+) (compatible with Arduino Zero)
13211374

13221375
I will occasionally test on the following hardware as a sanity check:
13231376

13241377
* Teensy 3.2 (72 MHz ARM Cortex-M4)
13251378
* Teensy LC (48 MHz ARM Cortex-M0+)
1326-
* Arduino Pro Mini clone (16 MHz ATmega328P)
13271379
* Mini Mega 2560 (Arduino Mega 2560 compatible, 16 MHz ATmega2560)
13281380

13291381
## Changelog

docs/doxygen.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ PROJECT_NAME = "AceRoutine"
3838
# could be handy for archiving the generated documentation or if some version
3939
# control system is used.
4040

41-
PROJECT_NUMBER = 0.2.2
41+
PROJECT_NUMBER = 0.3
4242

4343
# Using the PROJECT_BRIEF tag one can provide an optional one line description
4444
# for a project that appears at the top of each page and should give viewer a

0 commit comments

Comments
 (0)