Skip to content

Commit 295546b

Browse files
committed
Added the documentation about coroutines.
1 parent 437b53a commit 295546b

File tree

3 files changed

+194
-12
lines changed

3 files changed

+194
-12
lines changed

README.md

Lines changed: 182 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ As many Arduino boards are powered by AVR micro-controllers, this library can be
1515

1616
# Application Programming Interface
1717

18-
## Data Types
18+
## Context Switching
19+
20+
### Data Types
1921

2022
```
2123
avr_context_t
@@ -24,7 +26,7 @@ avr_context_t
2426
The `avr_context_t` represents a machine context. It should be treated as an opaque data type.
2527

2628

27-
## Functions
29+
### Functions
2830

2931
The four functions `avr_getcontext()`, `avr_setcontext()`, `avr_swapcontext()`, and `avr_makecontext()` provide facility for context switching between multiple threads of execution.
3032

@@ -63,7 +65,7 @@ The function `avr_makecontext()` modifies the context obtained by a call to `avr
6365

6466
Before invoking the `avr_makecontext()`, the caller must allocate a new stack for the modifiable context and pass pointer to it (`stackp`) and the size of the memory region (`stack_size`).
6567

66-
## Macros
68+
### Macros
6769

6870
```
6971
#define AVR_SAVE_CONTEXT(presave_code, load_address_to_Z_code)
@@ -113,6 +115,75 @@ avr_context_t *volatile avr_current_ctx;
113115

114116
As these macros implemented on top of the `AVR_SAVE_CONTEXT` and `AVR_RESTORE_CONTEXT`, please make sure that you understand how they work.
115117

118+
## Coroutines
119+
120+
There are four functions that implement the asymmetric stackful coroutine facility: `avr_coro_init()`, `avr_coro_resume()`, `avr_coro_yield()`, `avr_coro_state()`. They are implemented on top of the context switching facility.
121+
122+
A coroutine is represented by the `avr_coro_t` opaque data type. A coroutine can be in one of the following states.
123+
124+
1. Suspended
125+
2. Running
126+
3. Dead
127+
128+
All of the functions return `0` on success or `1` on failure with one notable exception: `avr_coro_state()` returns either a state of a coroutine (represented as a member of the `avr_coro_state_t` data type), or `AVR_CORO_ILLEGAL` status on failure.
129+
130+
Failure status usually means that a wrong argument has been passed to the function.
131+
132+
### Data Types
133+
134+
```
135+
avr_coro_t
136+
```
137+
138+
An opaque data type which represents a coroutine.
139+
140+
### Functions
141+
142+
```
143+
int avr_coro_init(avr_coro_t *coro,
144+
void *stackp, const size_t stack_size,
145+
avr_coro_func_t func);
146+
```
147+
The function `avr_coro_init()` initialises a coroutine, represented by a structure pointed at by `coro`. The coroutine gets initialised in the suspended state. Upon resumption, the function `func` gets called with two arguments applied: a pointer to the `avr_coro_t` structure itself (`self`), an argument passed on at first resumption.
148+
149+
150+
```
151+
int avr_coro_resume(avr_coro_t *coro, void **data);
152+
int avr_coro_yield(avr_coro_t *self, void **data);
153+
```
154+
The function `avr_coro_resume()` resumes execution of a coroutine, represented by a structure pointed at by `coro.` The coroutine might return control to the invoker by calling the function `avr_coro_yield()`. It accepts a currently running coroutine represented by a structure pointed at by `self`.
155+
156+
Both of the functions accept pointer to a pointer as the last argument (`data`). The coroutine and its invoker can exchange data using this last argument. When the coroutine gets resumed using `avr_coro_resume()` for the first time, a value of the pointer, to which `data` points, gets passed as the second argument to the coroutine function.
157+
158+
After the call to the avr_coro_resume() returns, the pointer to which `data` points during the call, gets initialised to the value, correspondingly passed as the last argument of `avr_coro_yield()`.
159+
160+
Symmetrically, after the call to the `avr_coro_yield()` returns, the pointer to which `data` points during the call, gets initialised to the value correspondingly passed as the last argument of `avr_coro_resume()`.
161+
162+
When the coroutine's function returns (and, thus, becomes dead), the returned value initialises the pointer to which `data` points during the corresponding `avr_coro_resume()` call.
163+
164+
If there is no need to exchange data between the coroutine and its invoker, one can pass the `NULL` value as the `data` argument to `avr_coro_resume()`/`avr_coro_yield()`.
165+
166+
167+
```
168+
typedef enum avr_coro_state_t_ {
169+
AVR_CORO_SUSPENDED = 0,
170+
AVR_CORO_RUNNING,
171+
AVR_CORO_DEAD,
172+
AVR_CORO_ILLEGAL,
173+
} avr_coro_state_t;
174+
175+
avr_coro_state_t avr_coro_state(const avr_coro_t *coro);
176+
177+
```
178+
179+
The function `avr_coro_state()` returns the current state of a coroutine:
180+
181+
1. If the coroutine is suspended, the function returns `AVR_CORO_SUSPENDED`.
182+
2. If the coroutine is running, the function returns `AVR_CORO_RUNNING`. This value can be obtained only from within the context of the currently running coroutine.
183+
3. If the coroutine is dead, the function returns `AVR_CORO_DEAD`.
184+
185+
The value `AVR_CORO_ILLEGAL` gets returned in the case of error (e.g. the `NULL` value was passed instead of a pointer to a coroutine).
186+
116187
# Usage
117188

118189
## Arduino
@@ -167,7 +238,9 @@ Every example below is a complete Arduino sketch. It was decided to use Arduino
167238

168239
Nevertheless, this library can be used on its own on AVR controllers: it does not contain any Arduino-specific code. Hopefully, the examples are easy to adapt to any AVR based hardware.
169240

170-
## [GOTO via Context Switching](./examples/Context_Switching/01.GOTO_via_Context_Switching/01.GOTO_via_Context_Switching.ino)
241+
## Context Switching
242+
243+
### [GOTO via Context Switching](./examples/Context_Switching/01.GOTO_via_Context_Switching/01.GOTO_via_Context_Switching.ino)
171244

172245
This example demonstrates how `avr_getcontext()` and `avr_setcontext()` could be used to emulate the `GOTO` operator.
173246

@@ -234,6 +307,111 @@ Please keep in mind that our intention here is to show how task switching can be
234307

235308
Nevertheless, if you are brave enough to write your own RTOS, this tiny sketch might be a good start.
236309

310+
## Coroutines
311+
312+
### [Basic_Generator](./examples/Coroutines/01.Basic_Generator/01.Basic_Generator.ino)
313+
314+
This example demonstrates how an asymmetric coroutine might be used as a generator. Every time the coroutine gets invoked it generates the next number in sequence (`0...N`) and returns it back to the invoker.
315+
316+
One of the important points of this example is to demonstrate how a value from the coroutine can be passed back to the invoker.
317+
318+
When being uploaded to an Arduino board, this sketch print numbers in increasing order via serial port every one second, e.g.:
319+
320+
321+
```
322+
0
323+
1
324+
2
325+
3
326+
4
327+
5
328+
...
329+
330+
```
331+
332+
### [Symmetric_Coroutines_via_Asymmetric_Ones](./examples/Coroutines/02.Symmetric_Coroutines_via_Asymmetric_Ones/02.Symmetric_Coroutines_via_Asymmetric_Ones.ino)
333+
334+
This example demonstrates how one could implement symmetric coroutines on top of the asymmetric ones.
335+
336+
The most important difference between the symmetric and asymmetric coroutines is that the asymmetric ones cannot pass control to the other coroutine directly: they can return control only back to the coroutine invoker. Nevertheless, they are no less powerful.
337+
338+
To bypass the above-mentioned limitation, a coroutine, which yields control, passes the information about the coroutine which it wants to activate to a dispatching loop. The loop, in turn, activates the next coroutine.
339+
340+
It is a version of the other example, which was implemented directly on top of `avr_makecontext()`, `avr_swapcontext()`.
341+
342+
When being uploaded to an Arduino board, this sketch produces the following (or very similar) output via serial port every two seconds:
343+
344+
```
345+
Starting coroutines...
346+
347+
Coroutine 0 counts i=0 (&i=0x18A)
348+
Coroutine 1 counts i=0 (&i=0x20A)
349+
Coroutine 0 counts i=1 (&i=0x18A)
350+
Coroutine 1 counts i=1 (&i=0x20A)
351+
Coroutine 0 counts i=2 (&i=0x18A)
352+
Coroutine 1 counts i=2 (&i=0x20A)
353+
Coroutine 0 counts i=3 (&i=0x18A)
354+
Coroutine 1 counts i=3 (&i=0x20A)
355+
Coroutine 0 counts i=4 (&i=0x18A)
356+
Coroutine 1 counts i=4 (&i=0x20A)
357+
Done.
358+
359+
```
360+
361+
### [Producer-Consumer](./examples/Coroutines/03.Producer-Consumer/03.Producer-Consumer.ino)
362+
363+
This example demonstrates how asymmetric coroutines can be used to solve a well known "producer-consumer" problem.
364+
365+
Coroutines communicate using the `send()` and `receive()` functions. `send()` passes a value to the consumer and resumes it, `receive()` gets value from the producer and returns control back to it. These actions are repeated forever.
366+
367+
It is a consumer-driven design because the program starts by calling the consumer. When the consumer needs an item, it resumes the producer, which runs until it has an item to give to the consume, and then pauses until the consumer restarts it again.
368+
369+
When being uploaded to an Arduino board, this sketch produces a pair of messages via serial port every one second in the following format:
370+
371+
```
372+
Produced: 0
373+
Consumed: 0
374+
Produced: 1
375+
Consumed: 1
376+
Produced: 2
377+
Consumed: 2
378+
Produced: 3
379+
Consumed: 3
380+
Produced: 4
381+
Produced: 4
382+
...
383+
```
384+
### [Producer-Filter-Consumer](./examples/Coroutines/04.Producer-Filter-Consumer/04.Producer-Filter-Consumer.ino)
385+
386+
This example demonstrates how asymmetric coroutines can be used to solve the "producer-filter-consumer" problem (one can also call the filter as "mediator").
387+
388+
Coroutines communicate using the `send()` and `receive()` functions. The function `send()` passes a value to the receiver and returns control back to the invoker, the function `receive()` gets value from the sender and resumes it.
389+
390+
It is a consumer-driven design because the program starts by calling a consumer. The consumer receives a value from a filter and thus resumes it. Then the filter receives the value from a producer (again, by resuming it) and doubles it. After that, the filter sends the doubled value to the consumer. By doing so it resumes the consumer. Then the whole process repeats.
391+
392+
When being uploaded to an Arduino board, this sketch produces a triplet of messages via serial port every one second in the following format.
393+
394+
```
395+
Produced: 0
396+
Filtered: 0
397+
Produced: 0
398+
Produced: 1
399+
Filtered: 2
400+
Produced: 2
401+
Filtered: 4
402+
Consumed: 4
403+
Produced: 3
404+
Filtered: 6
405+
Consumed: 6
406+
Produced: 4
407+
Filtered: 8
408+
Consumed: 8
409+
Produced: 5
410+
Filtered: 10
411+
Consumed: 10
412+
...
413+
```
414+
237415
# Copyright
238416

239417
Copyright (c) 2020 [Artem Boldariev](https://chaoticlab.io/).

avrcoro.h

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ typedef void *(*avr_coro_func_t)(avr_coro_t *, void *);
3434
extern "C" {
3535
#endif /*__cplusplus */
3636
/*
37+
There are four functions that implement the asymmetric stackful
38+
coroutine facility: avr_coro_init(), avr_coro_resume(),
39+
avr_coro_yield(), avr_coro_state().
40+
41+
A coroutine is represented by the "avr_coro_t"
42+
opaque data type. A coroutine can be in one of the following states.
43+
44+
1. Suspended
45+
2. Running
46+
3. Dead
47+
3748
All of the functions return 0 on success or 1 on failure with one
3849
notable exception: avr_coro_state() returns either a state of a
3950
coroutine (represented as a member of the "avr_coro_state_t" data
@@ -42,13 +53,6 @@ type), or "AVR_CORO_ILLEGAL" status on failure.
4253
Failure status usually means that a wrong argument has been passed to
4354
the function.
4455
45-
A coroutine is represented by the "avr_coro_t" opaque data type. A
46-
coroutine can be in one of the following states.
47-
48-
1. Suspended
49-
2. Running
50-
3. Dead
51-
5256
The function avr_coro_init() initialises a coroutine, represented by a
5357
structure pointed at by "coro." The coroutine gets initialised in the
5458
suspended state. Upon resumption, the function "func" gets called with

examples/Coroutines/03.Producer-Consumer/03.Producer-Consumer.ino

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ solve a well known "producer-consumer" problem.
55
Coroutines communicate using the send() and receive() functions.
66
send() passes a value to the consumer and resumes it, receive() gets
77
value from the producer and returns control back to it. These actions
8-
are repeated indefinitely.
8+
are repeated forever.
99
1010
It is a consumer-driven design because the program starts by calling
1111
the consumer. When the consumer needs an item, it resumes the

0 commit comments

Comments
 (0)