Uses the protothreads approach to enable imperative synchronous programming (as promoted by Blech) in C or C++.
With this header-only library you can simplify your embedded programming projects by keeping a delay
based approach but still enable multiple things to happen at once in a structured, modular and deterministic way.
/* This blinks an LED on every other tick. */
pa_activity (FastBlinker, pa_ctx(pa_defer_res), int pin) {
pa_defer {
setLED(pin, BLACK);
};
while (true) {
setLED(pin, RED);
pa_pause;
setLED(pin, BLACK);
pa_pause;
}
} pa_end
/* This blinks an LED on a custom schedule. */
pa_activity (SlowBlinker, pa_ctx_tm(pa_defer_res), int pin, unsigned on_ticks, unsigned off_ticks) {
pa_defer {
setLED(pin, BLACK);
};
while (true) {
setLED(pin, RED);
pa_delay (on_ticks);
setLED(pin, BLACK);
pa_delay (off_ticks);
}
} pa_end
/* An activity which delays for a given number of ticks. */
pa_activity (Delay, pa_ctx_tm(), unsigned ticks) {
pa_delay (ticks);
} pa_end
/* This drives blinking LEDs and preempts them after 3 and 10 ticks. */
pa_activity (Main, pa_ctx_tm(pa_co_res(3); pa_use(Delay); pa_use(FastBlinker); pa_use(SlowBlinker))) {
printf("Begin\n");
/* Blink Fast LED for 3 ticks */
pa_after_abort (3, FastBlinker, 0);
/* Blink both LED for 10 ticks */
pa_co(3) {
pa_with (Delay, 10);
pa_with_weak (FastBlinker, 0);
pa_with_weak (SlowBlinker, 1, 3, 2);
} pa_co_end
printf("Done\n");
} pa_end
In this example, a fast led is blinked for 3 ticks and then both the fast and a slow led are blinked concurrently for 10 ticks.
To trigger the Main activity you need to declare its usage and then tick it until done - either at a specific frequency - or in a free-running style - by repeatedly calling pa_tick
:
int main(int argc, char* argv[]) {
pa_use(Main);
while (pa_tick(Main) == PA_RC_WAIT) {
std::this_thread::sleep_for(std::chrono::milliseconds(20)); // Will result in a 50Hz tick frequency.
}
return 0;
}
As can be seen in the example above, an activity is defined by the pa_activity
macro which takes the
name of the activity as first parameter.
This is followed by what is called a context (pa_ctx(...)
) and which stores the
state which should outlive a single tick. Also sub-activities used in the activity are declared here with the pa_use(<SomeActivity>)
macro. Separate context elements need to be separated by a semicolon (;
).
To use delays within an activity, use pa_ctx_tm
instead of pa_ctx
, which holds an implicit time variable.
After the context, place the input and output parameters of the activity.
At the end of an activity, use pa_activity_end
or just pa_end
to close it off.
Within an activity you can place normal C control structures and the following synchronous statements.
For a detailed description of the statements, please currently refer to the Blech documentation or look at the proto_activities
test and example programs.
pa_pause
: will pause processing of an activity and resume it at the next tickpa_halt
: will pause the activity foreverpa_await (cond)
: will pause the activity and resume it oncecond
becomes truepa_await_immediate (cond)
: likepa_await
but will not pause ifcond
is true in the current tickpa_delay (ticks)
: will pause the activity for the given number of tickspa_delay_ms (ms)
: will pause the activity for the given number of millisecondspa_run (activity, ...)
: runs the given sub-activity until it returnspa_return
: end an activity from within its body - otherwise returns implicitly at the endpa_co(n)
: starts a concurrent section withn
trails - reserve the number of trails withpa_co_res(num_trails)
in the activities context - end section withpa_co_end
pa_with (activity, ...)
: runs the given activity concurrently with the others of this section - only applicable withinpa_co
pa_with_weak (activity, ...)
: runs the given activity concurrently with the others of this section and can be preempted - only applicable withinpa_co
pa_when_abort (cond, activity, ...)
: runs the given activity untilcond
becomes true in a subsequent tick - unless it ends beforepa_when_reset (cond, activity, ...)
: runs the given activity and restarts it whencond
becomes true in a subsequent tickpa_when_suspend (cond, activity, ...)
: will suspend the given activity whilecond
is true and lets it continue whencond
is false againpa_after_abort (ticks, activity, ...)
: will abort the given activity after the specified number of tickspa_after_ms_abort (ms, activity, ...)
: will abort the given activity after the specified time in millisecondspa_did_abort (activity)
: reports whether an activity was aborted in a call beforepa_always
: will run code on every tick - end block withpa_always_end
pa_every (cond)
: will run code everytimecond
is true - end block withpa_every_end
pa_every_ms (ms)
: will run code now and everyms
milliseconds thereafter - end block withpa_every_end
. Note: Do not use any other construct which uses timing (likepa_delay_ms
) in the enclosed blockpa_whenever (cond, activity, ...)
: will run the given activity whenevercond
is true and abort it ifcond
turns false
When compiling wit C++ you could also define the following lifecycle callbacks:
pa_defer
: defines an instantaneous block of code to run when the activity ends by itself or gets aborted. Addpa_defer_res
annotation to the context to enable this feature.pa_enter
: defines an instantaneous block of code to run whenever the activity is entered - and initially when defined. Addpa_enter_res
annotation to the context to enable this feature.pa_suspend
: defines an instantaneous block of code to run when an activity gets suspended by the surroundingpa_when_suspend
. Addpa_susres_res
annotation to the context to enable this feature.pa_resume
: defines an instantaneous block of code to run when an activity gets resumed by the surroundingpa_when_suspend
. Addpa_susres_res
annotation to the context to enable this feature.
In C++ you can also use signals. Signals can be emitted and checked for presence within a tick. The presence is automatically retreated at the begining of the next tick.
Define a signal in a pa_ctx
with either pa_def_signal(sig)
or pa_def_val_signal(T, sig)
. The latter can be used to define signals carrying a value in addition to the presence flag. You also need to annotatate the activity defining signals with either pa_signal_res
or pa_enter_res
.
Emit a signal with either pa_emit(sig)
for pure signals or pa_emit_val(sig, val)
for valued signals and check for presence by operator bool
. Extract the value of a valued signal by sig.val()
. Note that the value will stay in the next ticks even if not emitted again. This can e.g. be used to model flow values which inform about their update by the presence flag.
- A medium article about proto_activities can be found here.
- Here is a little robot with
proto_activities
running on three ESP32 nodes. - See running proto_activities code in this online Wokwi simulator.
- Blech is a new programming language for the embedded domain which inspired
proto_activities
. - Pappe is a sibling project which uses an embedded DSL to allow Blech-style imperative synchronous programming in Swift.