PIC Microcontroller specific Multitasking Methods

contents:

Note that task switching is very different from the switch statement used in the C language.

multitasking in general

See Multitasking methods@ .

This page mainly talks about cooperative multitasking. For small systems where the programmer has complete control of every task, cooperative multitasking can be superior to pre-emptive multitasking.

In other situations, pre-emptive multitasking can be superior .. but unfortunately, I'm pretty sure it's impossible to implement pre-emptive multitasking on 12x and 16x series PIC chips. On 18x series, apparently it is possible.

"Assuming this is on a PIC ... then cooperative is almost certainly the way to go. Most of the time multi-tasking isn't needed for a PIC project. When it is, it would be extremely rare indeed if cooperative multi-tasking couldn't solve the same problem with less overhead than preemptive. I've done somewhere between 50 and 100 PIC projects. Out of those maybe a dozen or so had at least one "task" in that there was code that looked like it was an infinite loop, but actually returned execution to the rest of the system thru what looked like a call or macro to the task. Only 3 have been true multi-tasking systems where everything was in a task, and every task called something like TASK_YIELD to let all the other tasks run for a while. And all of those were cooperative. I have yet to run into a PIC project where preemptive multi-tasking would have been a good solution."

-- Olin Lathrop

http://piclist.com/techref/postbot.asp?by=time&id=piclist\1997\06\02\031933a&tgt=post has a good, simple overview of cooperative task switching in PIC assembly code.

There's a nice discussion at Stack Overflow: "Multithreading using C on PIC18" .

state machines on the PIC

Even the smallest PIC can handle several ``state machines''.

[FIXME: isn't there another page on massmind somewhere that deals with state machines ?]

cooperative multitasking on the PIC

(I probably should have called this macro "yield" rather than "switch" -- is it too late to rename it?)

POSIT is a very simple cooperative multitasking system. The main loop of each task is always run in sequence:

; main runtime task scheduler when POSIT is set up for 4 tasks:
core
    call t0_run ; main loop of task 0
    call t1_run ; main loop of task 1
    call t2_run ; main loop of task 2
    call t3_run ; main loop of task 3
    goto core

I'm pretty sure the Calvin RTOS works the same way.

More complicated cooperative multitasking routines allow a task to return to the task scheduler *before* the end of it's main loop, and (once the task scheduler has run a few other routines) resumes executing right where it left off in that main loop.

Rich Leggitt says:

...I thought of a way to cut 'switch' to one instruction, it ain't gonna get any smaller than that :)
switch  macro
        retlw $+1               ; return 'return'
        endm

context equ 0x20

        movlw task1             ; note task 1 will run first
        movwf context
        movlw task2

        ; task switcher
        call $+2                ; make a place for 'switch' to return to
        goto $-1                ; (i.e. here!)
        xorwf context,f         ; then exchange w and context
        xorwf context,w
        xorwf context,f
        movwf pcl               ; and jmp to w

task1   blah
        switch
        etc
        switch
        stuff
        goto task1

task2   asdf
        switch
        zxcz
        goto task2

to which Scott Dattalo commented:

Cool! Now that you write this, I recall someone else (I believe it was Payson) doing something similar. Now that you've got the 'context switching' all confined to one section, you've opened up the possibilities of extending its functionality (without having duplicate code snippets scattered throughout).
  1. ) You could easily add additional tasks. Cycling through each can be done in a 'round-robin' fashion. For example, after task A then run task B, C,..., N, and back to A.
  2. ) You could add priorities to the tasks.
  3. ) With an extra instruction in the 'switch' macro, you could handle multiple pages:
    switch   macro
             movlw  HIGH($+2)
    ;[ed: I think we need to save W to somewhere at this point to avoid over-writing it. For example:
    	movwf switchpage
    ; where switchpage is a temp register.]
             retlw  $+1
             endm
             
    

If you're using the 12bit core, then you might consider populating the stack with the address of the context switcher. You could do the same with the 14bit core - but chances are you're more likely to need the stack for making calls. (on the 12bit core, the stack is only 2 levels ) Once the stack is filled, then the retlw's will take advantage of stack roll overs.

         movlw  task1
         movwf  context
         movlw  task2

         setc
         clrf   first_time

         goto   l2
l1       rlf    first_time,f
         btfss  first_time,1
l2       call   l1
l3       xorlw context,f         ; then exchange w and context
         xorlw context,w
         xorlw context,f
         movwf pcl               ; and jmp to w

for the 14bit core you could fill the stack:

         movlw  task1
         movwf  context
         movlw  task2

         setc
         clrf   first_time

         goto   l2
l1       rlf    first_time,f
         btfss  first_time,7
l2       call   l1
l3       xorlw context,f         ; then exchange w and context
         xorlw context,w
         xorlw context,f
         movwf pcl               ; and jmp to w

I haven't tried this, but once you get the stack 'primed', you can save 4 execution cycles.

(perhaps this "switch" macro would be better named "yield" ?)

Extending ``switch'' to handle many tasks

    ; warning: untested code.
    ; Is there a better way ?
    ; 2006-04-18:DAV: David Cary extended to handle many tasks.
    ; ???: Original ideas from Rich Leggitt.
switch  macro
        retlw $+1               ; return 'return'
        endm

task1_context res 1
task2_context res 1
task3_context res 1
task4_context res 1
task5_context res 1

        movlw task1             ; note task 1 will run first
        movwf task1_context
        movlw task2
        movwf task2_context
        movlw task3
        movwf task3_context
        movlw task4
        movwf task4_context
        movlw task5
        movwf task5_context

task_switcher:
        movf task1_context,w
        call switch_to_task ; make a place for 'switch' to return to
        movwf task1_context
        movf task2_context,w
        call switch_to_task
        movwf task2_context
        movf task3_context,w
        call switch_to_task
        movwf task3_context
        movf task4_context,w
        call switch_to_task
        movwf task4_context
        movf task5_context,w
        call switch_to_task
        movwf task5_context
        goto task_switcher

switch_to_task:
        movwf pcl

task1   blah
        switch
        etc
task1_main_loop
        switch
        stuff
        goto task1_main_loop

task2   asdf
        switch
        zxcz
        goto task2

task3   asdf
        switch
        zxcz
        goto task3

task4   asdf
task4_loop1
        call do_something
        switch
        skpz
            goto task4_loop1
task4_loop2
        switch
        call do_something_else
        switch
        call that_other_thing
        skpz
            goto task4_loop2
        call oh_yeah_one_last_thing
        switch
        goto task4

task5   asdf
task5_main_loop:
        switch
        zxcz
        goto task5_main_loop

Extending ``switch'' to handle multiple pages

See the answers at "What is the reason my PIC16 multitasking RTOS kernel doesn't work?" for a cooperative multitasking system for the PIC16 that apparently works even when the tasks are located on multiple pages.

An earlier attempt:

The felix.althaus asked for more details on how to extend it to properly handle multiple pages.

<felix.althaus at swissonline.ch> asks: " Hi You said, you could handle paging with the instruction 'movlw HIGH($+2)'. But how do you store it back?"

One could use the above switch code as long as you pick one page for page switching, and *only* use the ``switch'' macro on that page. Other pages can be used for subroutines, as long as you are very careful not to use the ``switch'' macro in those subroutines. (You can't use any of the switch macros on this page inside a subroutine in any case).

Another method would be to add a few instructions to the ``switch'' macro and the context switcher so ``switch'' can be called from any page.

    ; warning: untested code.
    ; Is there a better way ?
    ; 2001-10-19:DAV: David Cary extended to handle multiple pages.
    ; ???: more ideas from Scott Dattalo.
    ; ???: Original ideas from Rich Leggitt.

; RAM to bookmark current location in each task.
context_lo equ 0x20
context_hi equ 0x21

temp equ 0x77
; FIXME: ``temp'' is a temp register; ``banked'' location OK as long
; as that location is reserved in *every* bank as a temp location.

; ``switch'' overwrites ``temp'' and the W register.
switch   macro
        movlw  LOW($+3)
	movwf temp
        retlw  HIGH($+1)               ; return 'return'
        endm

;...

power_on_setup:
        ; ...
        movlw LOW(task1)             ; note task 1 will run first
        movwf context_lo
        movlw HIGH(task1)
        movwf context_hi
        movlw LOW(task2)
        movwf temp
        movlw HIGH(task2)

        ; task switcher
        call $+2                ; make a place for 'switch' to return to
        goto $-1                ; (i.e. here!)
        ; Bookmark the task we came *from*
        ; (currently in w:temp)
        ; in context_hi:context_lo.
        ; Resume the task we want to go *to*
        ; (currently bookmarked in context_hi:context_lo) into PCLATH:PCL.
        ; Swapping code optimized for 2 tasks.
        ; Code to handle 3 or more tasks
        ; would probably be much easier to understand.
          ; handle Hi part (the page) first
          xorwf context_hi,f
          xorwf context_hi,w
          xorwf context_hi,f
          movwwf PCLATH
          ; then handle Lo part of the address last
          movfw temp
          xorwf context_lo, f
          xorwf context_lo, w
          xorwf context_lo, f
          ; end Strange hack.
        movwf pcl               ; and jmp to w

task1   blah
        switch
        fcall input_stuff
        movwf temp ; OK to use temp as long as we're done before the next switch
        fcall get_mask
        andwf temp
        fcall output_stuff
        etc
        switch
        stuff
        goto task1

task2   asdf
        switch
        zxcz
        goto task2

Scott Dattalo 2000-02-10 came up with ideas ``it might be simpler to combine a state-machine and a task switcher'' to make switching between several tasks that are not in the same bank quicker than the above code:

    ; warning: untested code.
    ; Is there a better way ?
    ; 2002-03-04:DAV: copied ideas from Scott Dattalo to make
    ; faster and shorter (?).
    ; 2001-10-19:DAV: David Cary extended to handle multiple pages.
    ; ???: more ideas from Scott Dattalo.
    ; ???: Original ideas from Rich Leggitt.

; RAM to bookmark current location in each task.
context equ 0x20

; ``switch'' overwrites the W register.
switch   macro continuation_lable
        retlw ((continuation_lable)-task_table_start) ; return pointer to where to continue task
        endm

;...

power_on_setup:
        ; ...
        movlw (t1_1 - task_table_start) ; note task1_offset1 will run first
        movwf context
        movlw (t2_1 - task_table_start)

        ; task switcher
        call $+2                ; make a place for 'switch' to return to
        goto $-1                ; (i.e. here!)

        ;context_switcher:
        ; Swapping code optimized for 2 tasks.
        ; Code to handle 3 or more tasks
        ; would probably be much easier to understand.
        xorwf  context,w
        xorwf  context,f
        xorwf  context,w
        addwf  pcl,f
task_table_start:
t1_1 goto   task1_offset1
t1_2 goto   task1_offset2
t1_3 goto   task1_offset3

t2_1 goto   task2_offset1
t2_2 goto   task2_offset2
t2_3 goto   task2_offset3

task1_offset1
        ...
        normal code
        blah
        ...
        switch task1_offset2
task1_offset2
        fcall input_stuff
        movwf temp
        fcall get_mask
        andwf temp
        fcall output_stuff
        etc
        switch task1_offset2
task1_offset2
        stuff
        more_stuff
        ; select the address at which the task
        ; will next begin executing.
        ; this is very tricky.
        btfss condition,condition_bit
          switch(task1_offset2)
        switch(task1_offset3)
task1_offset3
        blah
        blah
        blah
        goto task1


task2
        asdf
        switch task2_offset2
task2_offset2
        zxcz
        goto task2


Warning: none of these ``switch'' macros work right when used inside a subroutine.

John Payson 1997-06-02 http://piclist.com/techref/postbot.asp?by=time&id=piclist\1997\06\02\021004a&tgt=post writes about a variation that allows one task to have regular subroutines nested as deep as the stack will go, and that task can Yield() at any and every level. All other tasks, however, have the same limitation as the switch() / Yield() on this page -- switch() can only be called from the main loop of the task, not from any subroutine.

co-routines

[FIXME: is there a good, simple explanation somewhere of exactly what a co-routine is ? http://c2.com/cgi/wiki?CoRoutine left me confused. ]

18C specific co-routines

Preemptive real-time multitasking

18C specific

See also

Operating systems on PCs ../oss.htm

unsorted

Software Stacks sstack.htm and http://piclist.com/techref/postbot.asp?by=time&id=piclist\1997\07\18\090751a&tgt=post has some ideas about simulating a stack in software, to allow something closer to true pre-emptive multitasking (and also allowing *much* deeper stack nesting).

See http://www.piclist.com/techref/microchip/pages.htm for more details on paging and PCLATH.

one main loop

Hi,

In the cooperative multitasking section of this page, there are comments and code regarding "cooperative multitasking routines [that] allow a task to return to the task scheduler *before* the end of it's main loop, and (once the task scheduler has run a few other routines) resumes executing right where it left off in that main loop".

At first I thought this was some really clever stuff, but after some examination, I'm not so sure this is different than things I achieve in a different way. It's quite conceivable that I'm missing some crucial nuance here, and I'd really like to learn what that is! ;-)

I would say there are actually 5 tasks in the example, not just "task1" and "task2". Let's call them "blah", "asdf", "etc", "stuff", and "zxcz". Please explain how the code below is any different than that proposed by Rich Leggitt

	; ---- My cooperative multitasking scheme ----
	; by Karl Liebau
	call	blah
	call	asdf

main:

	call	etc
	call	zxcz
	call	stuff
	call	zxcz
	goto	main

blah:
	nop
	return

asdf:
	nop
	return

etc:
	nop
	return

stuff:
	nop
	goto blah

zxcz:
	nop
	goto asdf

	; ---- End proposed equivalent code ----

I believe this code to be functionally equivalent to the original example. This code doesn't need W or a temp register to work. It is extensible by merely adding CALL statements in the main loop as-needed. It looks easier to me, but many of the contributors here are much better at this than I am. So, I offer my humble thanks to anyone who can explain the underlying difference between the two examples.

-- Karl Liebau

Nifty. You are right -- this proposed code *is* functionally equivalent to the original example -- running the "zxcz" code twice as often as the other bits of code. And it's simpler and faster. So this is a useful addition to the bag of tricks.

But how would this proposed task-switcher handle code like this?:

(there exists a clever method of loading the data byte with "1", then when that shifts into the carry bit, it signals we have a full 8 bits. Instead of trying to be clever, here we use a seperate "bit_count" counter. I'm attempting to write Screechingly Obvious Code .)

        ; ...
        ; using the Rich Leggitt task switcher
        ; ...
        ; the task switcher itself and the other task(s)
        ; omitted for clarity
        ; ...

clock_line EQU PORTA,3
data_line EQU PORTA,5
data_byte res 1
bit_count res 1

task1:
        movlw 8
        movwf bit_count

        ; wait for bit on data line (without blocking other tasks)
wait_for_hi:
        switch
        btfss clock_line
            goto wait_for_hi
wait_for_lo:
        switch
        btfsc clock_line
            goto wait_for_lo
        ; grab the bit at this falling edge
        rrl data_byte,f
        ; copy bit on data_line into lsb
        ; But first, get rid of random Carry flag that rrl shifted in
        bcf data_byte,0
        btfsc data_line
            bsf data_byte,0
        decfsz bit_count
            goto wait_for_hi
        switch
        movfw data_byte
        call write_byte
        nop

        goto task1

Is it possible to rewrite this task so the above "proposed" task switcher will work? (Maybe there is a way, but I don't see it just now). Do you see what I'm trying to say here? -- DavidCary

The "round robin" style (is there a better name?) is better than the "switch" style (is there a better name?) when each task starts from its beginning and runs through to its end every time. But for a complicated tasks (ones with several long-running loops), this round robin code forces me to move some of the flow-control code to the task scheduler.

For those more complicated tasks, I think the "switch" macro helps keep all the related code together. (When I suspect the latency of a series consecutive subroutine calls in one task is "too long", I can slip a "switch" statement in the middle -- or between each and every one -- without any changes to the task scheduler). -- DavidCary

alternatives to multitasking

If you have multiple ``things'' that need to be done, consider using multiple processors. Is anyone using multiple PICs for anything interesting ? Or even (heterogenous multiprocessing) a PIC and some other CPU/MCU ? Athough Mike Watson points out: http://piclist.com/techref/postbot.asp?by=time&id=piclist\1997\06\05\035449a&tgt=post ``Synergy does not apply to multi-processor systems. You always end up with less than the sum of the parts because of the overhead of sharing data.''

DAV mostly agrees if you're going to put all those CPUs in one box. However, if you want to control lots of differently widely-seperated things, often it's cheaper to (a) put a PIC in each box and connect all the boxes with a single fat power pair (shared power bus) + a single thin serial communication pair (daisy chain ? shared bus ?) than to (b) run several fat power lines to each thing.

DAV is currently working on a project that he expects will network dozens, perhaps hundreds of small microcontrollers in a "data collection grid".

Questions:

Interested:

See also: