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_endIn 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 oncecondbecomes truepa_await_immediate (cond): likepa_awaitbut will not pause ifcondis 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 withntrails - reserve the number of trails withpa_co_res(num_trails)in the activities context - end section withpa_co_endpa_with (activity, ...): runs the given activity concurrently with the others of this section - only applicable withinpa_copa_with_weak (activity, ...): runs the given activity concurrently with the others of this section and can be preempted - only applicable withinpa_copa_when_abort (cond, activity, ...): runs the given activity untilcondbecomes true in a subsequent tick - unless it ends beforepa_when_reset (cond, activity, ...): runs the given activity and restarts it whencondbecomes true in a subsequent tickpa_when_suspend (cond, activity, ...): will suspend the given activity whilecondis true and lets it continue whencondis 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_endpa_every (cond): will run code everytimecondis true - end block withpa_every_endpa_every_ms (ms): will run code now and everymsmilliseconds 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 whenevercondis true and abort it ifcondturns 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_resannotation 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_resannotation 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_resannotation 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_resannotation 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_activitiesrunning 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.