Shop OBEX P1 Docs P2 Docs Learn Events
Upload Corrected: Simple Spin2 Multitasking Demo (needs PNut_v47) — Parallax Forums

Upload Corrected: Simple Spin2 Multitasking Demo (needs PNut_v47)

JonnyMacJonnyMac Posts: 9,159
edited 2024-12-14 15:36 in Propeller 2

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

  • ke4pjwke4pjw Posts: 1,169

    Is all of the task switching commented out on purpose?

  • ke4pjwke4pjw Posts: 1,169

    Also, Thanks for the demo!

  • JonnyMacJonnyMac Posts: 9,159

    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.

  • JonnyMacJonnyMac Posts: 9,159
    edited 2024-12-13 18:35

    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.

  • JonnyMacJonnyMac Posts: 9,159

    Your task_waitms method is very neat.

    To be fair, that's a derivative of what Chip has in his original demo.

    ...but also for throttling tasks back to a defined rate.

    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.

  • bob_g4bbybob_g4bby Posts: 440
    edited 2024-12-15 15:46

    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

    '' File .......... state machine.spin2
    '' Version........ 1
    '' Purpose........ demo state machine working in a multitasking setting
    '' Author......... Bob Edwards
    '' Email.......... 
    '' Started........ 15/12/2024
    '' Latest update.. / /
    
    ''=============================================================
    
    {SPIN2_V47}
    
    CON { timing }
    
      CLK_FREQ = 200_000_000                                        ' system freq as a constant
      MS_001   = CLK_FREQ / 1_000                                   ' ticks in 1ms
      '_xinfreq = 20_000_000                    ' oscillator input for P2-EVAL
      _xtlfreq = 20_000_000                     ' crystal input for P2-EDGE 
    
      _clkfreq = CLK_FREQ
    
    CON {Enumerated constants}
    
    #1, state1, state2, state3
    
    
    VAR {Variable Declarations}
    
        long task1_nextstate
        long task2_counter
        long  stack1[32]                    ' task stacks
        long  stack2[32]
    
    
    {Public / Private methods}
    
    pub main()
        taskspin(1,task1(),@stack1)
        taskspin(2,task2(),@stack2)
        repeat
            pintoggle(56 addpins 0)
            task_waitms(200)
    
    '================================== State machine works well as a task ===================================
    '================================== Especially if each state is brief  ===================================
    pub task1()
        task1_nextstate := state1
        repeat
            debug(udec(task1_nextstate))
            case task1_nextstate
                state1 : 
                    task1_nextstate := state3   ' + do more stuff in a real program     
                state2 : 
                    task1_nextstate := state1   ' + do more stuff in a real program
                state3 : 
                    task1_nextstate := state2   ' + do more stuff in a real program
            task_waitms(400)                ' between states, let the others have a go
    
    '==========================================================================================================
    
    pub task2()
        repeat
            task2_counter++
            debug(udec(task2_counter))
            task_waitms(550)
    
    pub task_waitms(ms) | t
      t := getct() + (ms * MS_001)                                  ' target value for pollct()
      repeat while (pollct(t) == false)                             ' wait here until delay done
        tasknext()                                                  ' - let other tasks run
    
    

    Question: How do you know how much stack to give each task? (Give them enough and the program won't crash any more?)

  • JonnyMacJonnyMac Posts: 9,159

    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.

  • @JonnyMac said:
    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 like x := (a*b) + something(), then the function is entered with the a*b subexpression still on the stack

    So 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.

  • JonnyMacJonnyMac Posts: 9,159
    edited 2024-12-15 17:03

    Printing to screen is slow -- this is probably a more practical approach to determine how much of the stack is used.

    pub stack_usage(p_stack, maxsize) : ss
    
    '' Return longs used on stack (at p_stack)
    
      repeat maxsize with ss
        if (long[p_stack][ss] == MARKER)
          return
    

  • RaymanRayman Posts: 14,752

    Prop Tool throws an error with more that 64kB of local variables. So, guess that's one upper limit...

  • RaymanRayman Posts: 14,752

    Maybe one could have a task that makes sure all the ends of task stacks aren't written to?

  • bob_g4bbybob_g4bby Posts: 440
    edited 2024-12-15 18:48

    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

  • JonnyMacJonnyMac Posts: 9,159

    Prop Tool throws an error with more that 64kB of local variables. So, guess that's one upper limit...

    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).

Sign In or Register to comment.