Upload Corrected: Simple Spin2 Multitasking Demo (needs PNut_v47)
I wrote a very simple multitasking demo in Spin2 that runs a timer in the main loop while using a couple of tasks to blink LEDs on the Eval or Edge.
The screenshot shows my system. I used Spin Tools IDE (thanks @macca!) to edit, but the internal compiler doesn't support v47 features yet, so I used the external tools capability to compile and load RAM with PNut_v47, then another external tool to launch PST (my preferred terminal).
The thing to remember is that a task -- if you want it to keep running -- must be in its own loop (just like a method launched into its own cog). Just be careful that what the task does is quick and doesn't block too long which could affect the operation of other tasks.
Note: The file I originally uploaded doesn't work -- even if you remove the comments around the task features. The reason became clear during the online meeting because my tasks, which I wanted to keep running, were not in their own repeat loops. The code attached here is the correct code (archive dated 2024.12.14).
Comments
Is all of the task switching commented out on purpose?
Also, Thanks for the demo!
Darn it -- in my rush to get the file uploaded during the Zoom meeting I selected the wrong file. The correct version (up now) has "jm_" in the name which the other doesn't.
I'm very sorry about that.
For those wondering about using PNut with Spin Tools IDE, this is what I do.
1. Put a copy of PNut into a unified libraries folder (PNut doesn't have a libraries path)
2. Use the Preferences dialog to define the path to PNut and the program arguments (compile and upload to RAM shown)
Note: Use quotes as I've done for files and paths that may have spaces.
To compile with PNut the file must be saved -- this the same case as using PNut, so I set my tool to auto-save.
Your task_waitms method is very neat. Not only for timing events within tasks as you've shown here, but also for throttling tasks back to a defined rate.
An example would be the polling of pushbuttons. There's little point in polling pushbutton state more than 3-4 times a second, so a task_wait(300) is used to slow the task down to a sensible rep rate. The latest state of the pushbuttons is saved in a variable and it's that variable that the rest of the tasks read when they need pushbutton state. There will be good reasons to throttle back execution rate of other tasks - e.g. Changing a value on a screen more than 5 times a second isn't readable by humans, updating battery voltage display etc.
By slowing down tasks where practical, more time is left in our program for compute intensive work. If we're lucky, the program runs faster than one large main() method, as was the case before multitasking, where a programmer might not have thought about slowing anything down.
To be fair, that's a derivative of what Chip has in his original demo.
I think this is where pollct() can be helpful for getting tasks to run at a specific rate. I used it in another project (pre-tasks) when I've got to modify a strip of LEDs in a given time window while also checking and processing IR and radio inputs to the device, and it seemed to make sense here.
I've become very accustomed to implementing state machines in my Spin code. It's going to take a minute to wrap my head around tasks with these new features.
I favour state machines quite often when programming in LabView, where multitasking is inherent in the language. I had a little go at a state machine task. NB run in P/Nut debug mode or similar
Question: How do you know how much stack to give each task? (Give them enough and the program won't crash any more?)
That's always a question, isn't it?
Yesterday I employed and old-school trick to find out. I filled the task buffer with a known pattern and then displayed the task buffer in my clock routine.
We'll probably need to get some insight from Chip on estimating stack size.
My insight on (interpreted) Spin stack usage:
- (obviously) all local variables/parameters going up the call stack add up
- each function call anchor takes some space - 4 longs or so(?)
- Each value in the expression stack takes a long. A line like
x := (a*b)+(c*d)
will use 2 levels of expression stack (right before the add). If a function is called inside an expression likex := (a*b) + something()
, then the function is entered with thea*b
subexpression still on the stackSo perhaps a good approximation (better than just using 128): Find worst case call stack -> Add up all local variables in play and add some 8 longs per level of function call.
Flexspin's PASM backend is harder to reason about, so perhaps just add an extra heaping by gut feel if you're trying to write generic code.
Printing to screen is slow -- this is probably a more practical approach to determine how much of the stack is used.
Prop Tool throws an error with more that 64kB of local variables. So, guess that's one upper limit...
Maybe one could have a task that makes sure all the ends of task stacks aren't written to?
That would be simple to do, @Rayman. Or initialise all the stacks with all zeros and the stack monitor could then gauge how deep the stacks were used, give or take a long, like @JonnyMac was doing manually
The current release of Propeller Tool is wildly out-of-date and won't work with tasks, anyway. Chip commented on having Jeff update it with the v47 compiler, but Jeff is swamped with other issues so I'm not sure we should expect it soon. Thankfully, Marco enabled external tools in Spin Tools IDE so I'm using it as my editor and PNut as my final compiler (until Spin Tools and PNut are in alignment).