Yes, unfortunately many current languages overdo with constant new language features.
But the explicit WaitGroup handling in Go seems inconvenient and error prone.
Yes, unfortunately many current languages overdo with constant new language features.
But the explicit WaitGroup handling in Go seems inconvenient and error prone.
Thanks for posting this! To me, your examples look pretty nice. I would personally rather use FreeRTOS and pay a slight performance penalty instead of explicitly wiring my code with your library, but that doesn't make your approach invalid. It's quite nifty.
Thanks for your feedback.
Performance might be better in some cases but its main advantage IMO is that programs get simpler and more deterministic. E.g. there is no need to manually synchronize access to data in different tasks which can lead to non-deterministic time dependent behaviors.
BTW, I put up this add-on library to debounce buttons and detect short, double and long presses as activities.
When I can interleave tasks written to not block in a loop that runs 10's of times per millisecond, technically only one runs at a time but effectively they run "at the same time".
Moreover I can code each separately and as long as none of them steps on another, put them in the same void loop() without regard to what's already there except for CPU load and They All Run Independently (and can cooperate and coordinate) as Tasks... I call that Running Together.
By having each Task run a short step at a time it is not hard to keep void loop() running 50KHz or faster while doing Several Things At A Time.
Programmers have been doing this albeit at lower speed since at least the 1970's that I know of, I learned how in the 80's when it was called Main Loop Programming.
Perhaps you missed that ship.
Thank you.
With delay(), it is not just that delay wastes 16000 cycles per ms, the Way That Delay gets used requires overhead to "fix".
Easier code... depends on how beginner you want to be.
Once you learn cooperative tasking the door opens to breaking down indented structures into multiple tasks.
Once you learn that, building a task based toolbox becomes a thing.
I read a basis like this in a 1977 Intel document aimed at EE's that got me to understand DOS TSR's before 85.
To me, these keep delay() work-around's are like Paint-By-Number "Art".
My OS and system used pre-emptive multi-tasking, it would never work using cooperative MT. It also was hardware fault tolerant, 'pair and spare' plus no software accommodation for the FT, each task was written as if it was the only thing running, and in fact it was. We created a supervisor that allowed dynamic creation and control of additional tasks if new lines need to be accommodated. The exchanges loved to surprise us at 8AM that 5 new lines from Chicago would be on-line at 9AM. The CPU's were detuned to (my memory is fuzzy here) 5Mhz to improve up-time. At busy times it was common for 100% CPU with no disk I/O as we kept most data in memory and flushed updates to disk when the scheduler algorithm allowed it.
Maybe now you can see I come at this from a different time (mid 80's) and different system requirements. We were greatly helped by the hardware architecture and OS (Multics influenced)
The nice thing about proto_activities is that under the hood it works as you propose using the millis and state-machine based cooperative approach, but to the user it abstracts away from that and allows a simplified more declarative programming model. With activities you can build a truly reusable toolbox.
I use a function as a code object to run a task.
It can have local data members, like for time keeping as well as task-specific data. The cases are member functions but how they are used may be complex due to constant exit and re-entry of the function.
It will have a single time structure at the start to return if the 1-shot timer is set and time is not up yet. Any case may set the timer to pause the state machine until timeout, allowing other functions run time.
It will have a switch-case where each case is one step like code lines between structure braces only each case has a break to allow interleaving with other functions in loop();
It is like regular code except that each task function needs to run repeatedly to work, does not finish in a single top to bottom pass and that is a fundamental difference with ordinary IT-type code that delay might get used in. That is a difference I think is important for the programmer to know well before approaching a code job with tasking in mind. It's about how you see, not just what.
The task functions are like wheels and void loop() is the driver.
That sounds very similar to how proto_activities work:
Each activity is also a function which is called by another activity up to a single top activity which is repetitively invoked by loop.
Activity local data which needs to survive multiple invocations is stored in a context structure passed in from the invoker - so no globals are needed besides the one of the top activity.
Like a coroutine, an activity function can return or restart midway. The pa_delay_ms statement for instance records the current millis and then creates a re-entry point before it checks the new current time against the delay. If the check fails, the activity is left right away. On the next loop invocation the activity function directly jumps to the re-enter point mentioned above.
#define pa_delay_ms(ms) \
pa_self._pa_time = millis(); \
pa_mark_and_continue; \
if (millis() - pa_self._pa_time < ms) { \
return PA_RC_WAIT; \
}
Besides pa_delay_ms there are several more constructs to control the partial processing in an activity:
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 once cond becomes truepa_await_immediate (cond): like pa_await but will not pause if cond 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 with n trails - reserve the number of trails with pa_co_res(num_trails) in the activities context - end section with pa_co_endpa_with (activity, ...): runs the given activity concurrently with the others of this section - only applicable within pa_copa_with_weak (activity, ...): runs the given activity concurrently with the others of this section and can be preempted - only applicable within pa_copa_when_abort (cond, activity, ...): runs the given activity until cond becomes true in a subsequent tick - unless it ends beforepa_when_reset (cond, activity, ...): runs the given activity and restarts it when cond becomes true in a subsequent tickpa_when_suspend (cond, activity, ...): will suspend the given activity while cond is true and lets it continue when cond 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 with pa_always_endpa_every (cond): will run code everytime cond is true - end block with pa_every_endpa_every_ms (ms): will run code now and every ms milliseconds thereafter - end block with pa_every_end. Note: Do not use any other construct which uses timing (like pa_delay_ms) in the enclosed blockpa_whenever (cond, activity, ...): will run the given activity whenever cond is true and abort it if cond turns falseBest
Marc
How many ticks are in a millisecond?
It seems a bit complicated but then you are trying to cover everything that can be done ahead of time and I'm not.
The number of ticks in a millisecond is dynamic and depends on the execution time of the activities in a tick.
Then how do you know how many ticks you'd want to delay an activity?
If you use the free-running-mode - i.e. run the next tick back-to-back with the previous one - a tick does not not give you a guarantee about elapsed time. In this case use pa_delay_ms, pa_every_ms etc.
Alternatively, you could run the next tick in a periodic fashion - say at 10 Hz, or 100 Hz - depending on the WCET of a tick and the needs of your problem at hand. A tick would have a defined timing and delaying can alternatively be done by waiting for n ticks.
I have a loop counter task that I use to tell what impact a change has made to my non-blocking sketch.
Most sketches are not high demand and lightweight demos I posted before 2020 ran with the older version LC hit 67KHz. I try to keep it over 50KHz, fast response and saw with small teeth at high RPM cuts fast. Those are my ticks.
Before 2000 I remember when there were 18.2 ticks per second set by the keyboard clock. My last real customer wanted things to within a second.. no problem but until I got into Arduino I never had millis and micros for timing.
This somewhat larger proto_activities project illustrates IMHO well the modularity benefits of its approach.
Each activity can be seen as a separate yet easily composable setup()/loop() in its own.
That function can be achieved with the tasking functions of the RTOS. Googling [arduino vTask] will get you a bunch of info, but it is fairly well summarised at HERE
If you like slower, bigger code.
He needs to walk before he runs.