[Back to Home Page]

www.RomanBlack.com

PIC-thread
Simple multi-thread systems to make a PIC do parallel tasks - 2nd Jan 2010 - updated 13th Jan 2010.


What is it?

This page covers some ideas and systems to make any cheap microcontroller (like PIC or AVR) run simultaneous "threads" to do a number of tasks at the same time.

This page was inspired by some of the new multi-thread or parallel processors like the XMOS and Parallax Propeller that do multi-thread tasks at high speed. PIC micros are not designed to do this so they won't do what these processors do.

Although my simple systems here are nothing like "proper" multi-thread processors, they do provide a crude multitasking ability;

PIC-thread goals...
  • Will run many threads, each is independent
  • Processor time can be divided between threads
  • Threads are fast sequenced, and for many purposes "run at the same time"
  • Threads can be easily added without affecting application performance
  • RAM variables and special timers can be shared or dedicated to one thread
  • Threads can be prioritised

    These multi-threading are simple and will not be the best system for every application. But it is still interesting to explore simple minimalist multi-tasking systems that may turn out to be quite useful. :)




    My PIC-thread automatic macros! - 13th Jan 2010

    On the 11th Jan I had an idea on how to take simple PIC multi-thread systems to a new level, with automatic macros that insulate the user from the workings of the multi-thread engine and make it VERY easy to convert any code into multi-thread.

    These macros are convenient "one-liners" so they can interspersed through the source code with very little formatting effort, and are designed so the 3 simple macros actually work as an entire multi-thread engine!

    If you want to see how I came up with the idea for these simple automatic macros and my innovative way of implementing them you can see it in this forum thread; Here-some-pic-multi-tasking-systems.




    Using PIC-thread automatic macros

    Lets say you have existing PIC source code that performs a lot of tasks in a loop;
    
    //-----------------------------------------------------------------------------
    void main()
    {
      // main run loop
      while(1)
      {
        check_uart_receive();
        check_uart_send();
        read_adc(0);
        read_adc(1);
        read_adc(2);
        read_adc(3);
        calc_adc_temperature();
        calc_adc_battery_voltage();
        adc3int = (adc3 * 51) / 1024;
        format_adc_to_transmit();
        check_menu_buttons();
        display_data_on_lcd();
      }
    }
    //-----------------------------------------------------------------------------
    

    It doesn't matter if the tasks are in functions (shown above for clarity) or just use inline code, it is a very simple job to insert the automatic macros so this same project now becomes a multi-thread project where the tasks can be performed "in parallel" to each other.

    In this case the ADC functions are a bit slow because they need sampling time for each ADC input, likewise some of the calcs and display functions are slow. So these tasks are separated into 2 "threads" so that only one of the tasks is executed in each loop, and the high priority tasks like the UART send/receive are executed in every loop;
    
    //-----------------------------------------------------------------------------
    // Romans PIC-thread automatic multi-thread macros,
    // example of converting a project to multi-thread!
    // open source - 13th Jan 2010 - www.RomanBlack.com/PICthread.htm
    
    // My PIC-thread macros and vars below;
    unsigned char seq[10];  // will handle 10 threads
    unsigned char thseq;
    unsigned char thseqtemp;
    #define THREAD_START(A) thseq=seq[A]; thseqtemp=0; if(thseq==thseqtemp) {
    #define THREAD_BREAK } thseqtemp++; if(thseq==thseqtemp) {
    #define THREAD_END(B) } seq[B]++; if(seq[B]>thseqtemp) seq[B]=0;
    
    //-----------------------------------------------------------------------------
    void main()
    {
      // main run loop
      while(1)
      {
        //-----------------------------------------------------
        // high priority, run these every loop;
        check_uart_receive();
        check_uart_send();
    
        //-----------------------------------------------------
        // do the adc tests in a thread
        THREAD_START(0)
        read_adc(0);
        THREAD_BREAK
        read_adc(1);
        THREAD_BREAK
        read_adc(2);
        THREAD_BREAK
        read_adc(3);
        THREAD_END(0)
    
        //-----------------------------------------------------
        // do the calcs, display etc in a thread
        THREAD_START(1)
        calc_adc_temperature();
        THREAD_BREAK
        calc_adc_battery_voltage();
        THREAD_BREAK
        adc3int = (adc3 * 51) / 1024;
        THREAD_BREAK
        format_data_to_transmit();
        THREAD_BREAK
        process_menu_buttons();
        THREAD_BREAK
        display_data_on_lcd();
        THREAD_END(1)
        //-----------------------------------------------------
      }
    }
    //-----------------------------------------------------------------------------
    

    That's all there is to it! Thread0 runs the ADC sampling, and every loop ONE of the ADC functions will be run. In Thread1, it handles the calcs and other tasks, and again just ONE of these tasks is run every loop.

    The two UART tasks both run EVERY loop, so even though they are not marked as "threads" each UART functon really performs as its own thread.

    The PIC-thread automatic macros provide a high degree on insulation, unlike cruder multithread systems the user does not need to work with the sequencing variables or change the indenting of code. Also, the last macro THREAD_END() is automatic and self-adjusts for any number of THREAD_BREAKs, so the user can add or remove THREAD_BREAKs at will without any issues.

    This system is innovative and extremely user friendly. It is a very workable solution for larger projects because THREAD_BREAKs can be added or removed at will, threads broken or split into multiple threads, or code between THREAD_BREAKs can be expanded or changed with no issues. Even with the 3 simple macros as shown, it will automatically handle up to 256 threads and up to 255 THREAD_BREAKs in each thread, although performance will be better with smaller number of THREAD_BREAKs, generally under 10.


    How do the automatic macros work?

    THREAD_START(x)
  • x indicates the number of this thread
  • loads the current position in this thread; seq[x]
  • sets the temp position to zero; thseqtemp = 0
  • performs the next code IF the seq == thseqtemp

    THREAD_BREAK
  • increments the temp position; thseqtemp++
  • performs the next code IF the seq == thseqtemp

    THREAD_END(x)
  • increments the current position (ready for next time!); seq[x]++
  • clears seq[x] to zero (IF it just ran the last seq in the thread)

    To explain the operation as simply as possible; The code jumps from macro to macro, and ONLY when the seq number matches, it will then execute the code BETWEEN 2 macros.


    Versatility!

    The macro system has huge versatility. Besides being able to easily turn a segment of code into a "thread" (or easily turn it back again!) the thread numbering allows additional power.

    Threads can use any thread number, and be in any sequence. This allows versatility of moving code around without needing to re-number it, which works nicely with the above feature of being able to turn code into a thread or back again as needed, as a thread can be added or "disappear" with no issues.

    Likewise THREAD_BREAKs can be added (or removed) at any time, so if some code is changed and has become too long, a THREAD_BREAK can just be inserted into that code to split it for the multi-tasking.


    Multi-tasking overhead (wasted time)

    This has some obvious overhead costs (calculated for PIC 16F);
  • THREAD_START()   = 7-8 PIC instructions
  • THREAD_BREAK   = 5-6 PIC instructions
  • THREAD_END()   = 6 PIC instructions

    
        THREAD_START(0)
        read_adc(0);
        THREAD_BREAK
        read_adc(1);
        THREAD_BREAK
        read_adc(2);
        THREAD_BREAK
        read_adc(3);
        THREAD_END(0)
    

    So in the above thread the overhead is 8+6+5+6+6 or a total of 31 PIC instructions for one pass through the thread to run the read_adc(2) function. This is not an unreasonable amount of overhead for a multi-tasking system, especially if the functions like read_adc() contain any sampling delays or 16bit math that might easily be 100 PIC instructions or more.

    This overhead is less on the PIC 18F series that have an improved instruction set and of course with much faster PICs like 18F, dsp24 and PIC32 etc these PIC-thread automatic macros become more and more attractive and viable.

    There may be some way of reducing the overhead and making the macros jump directly to the correct place in the code, but for the moment these macros are working and form a very elegant and easy to use system that forms a powerful multi-tasking engine using just 3 simple macros that can be added to or removed from code at will!




    NOTE! Section below is my original PIC-thread work from 2nd Jan 2010
    (The first examples are very simple, the more usable code is near the page bottom.)



    The simplest example? - 2nd Jan 2010

    In this example an automatic sequencing engine is used to select a new thread number after a predetermined period. Each thread is responsible to end its task when it's number is no longer selected. Then the next thread gets a time period to do it's task.

    This way the available time can be divided up between threads, either equally or unequally (with some type of prioritising).

    Here is the most simple "PIC-thread" multitasking system I could come up with;
    
    void main()
    {
      // setup PIC 12F675, 4MHz xtal
      CMCON = 0x07;         // comparators OFF
      T1CON = 0b00100001;   // TMR1 on, 1:4 prescale
      TRISIO = 0b00000010;   // 
    
      // loop here and do each thread for 1024 instructions (1mS)
      while(1)
      {
        // THREAD 0 ===========================================
        while(TMR1H == 0)
        {
          if(!GPIO.F1) GPIO.F0 = 1;  // LED on if button pressed
        }
        // THREAD 1 ===========================================
        while(TMR1H == 1)
        {
          if(GPIO.F1) GPIO.F0 = 0;   // LED off if button not pressed
        }
        // THREAD 2 ===========================================
        while(TMR1H == 2)
        {
    
        }
        // THREAD END =========================================
        TMR1L = 0;
        TMR1H = 0;
      }
    }
    

    The automatic sequencing engine is done by the PIC timer; TMR1H, which allocates a timeslice of 1024 instructions (1mS) to each of the 3 threads. There could be as many as 255 threads.

    Each of the 3 threads operates in turn, and takes 1/3 of the total time. Thread 0 checks a button, and lights a LED. Thread 1 is responsible to turn the LED off again. And thread 2 does nothing but use up 1/3 of the timeslice, but of course it could do another simultaneous task.

    This system is very crude but does satisfy the requirement of sharing timeslice between tasks, and all tasks run "at the same time" provided that a few mS delay between tasks will not be noticed by the application.

    It is also capable of allocating uneven timeslice;
    
        // THREAD 0 ===========================================
        while(TMR1H == 0)
        {
          if(!GPIO.F1) GPIO.F0 = 1;  // LED on if button pressed
        }
        // THREAD 1 ===========================================
        while(TMR1H <= 3)    // gets 1, 2 and 3
        {
          if(GPIO.F1) GPIO.F0 = 0;   // LED off if button not pressed
        }
        // THREAD 4 ===========================================
        while(TMR1H == 4)
        {
    
        }
        // THREAD END =========================================
    
    See above that thread 1 has been given 3 timeslice units, so it gets 3mS and the other 2 threads still get 1mS each. Total timeslice is now 5mS.




    Adding an interrupt

    If a timed interrupt is used as the thread sequencing engine the system becomes a lot more powerful and useful. The interrupt engine can now generate a precise timed period, and automatically sequence the threads based on that exact timeslice.

    Imagine if the thread sequencing engine sets a "low_priority" flag every 10 loops;
    
        // THREAD 0 ===========================================
        while(thread == 0)
        {
          if(!GPIO.F1) GPIO.F0 = 1;  // LED on if button pressed
        }
        // THREAD 1 ===========================================
        while(thread == 1)   
        {
          if(GPIO.F1) GPIO.F0 = 0;   // LED off if button not pressed
        }
        // THREAD 2 ===========================================
        while(thread == 2)
        {
    
        }
        // THREAD 3 ===========================================
        while(thread == 3 && low_priority)
        {
    
        }
        // THREAD END =========================================
    

    Any threads at the bottom of the sequence can now be controlled by the low_priority flag, so they will only be given timeslice when the low_priority flag is set.

    An obvious extension of this would be to allow priority ranking using multiple x_priority flags, with the lowest ranked priority threads at the bottom of the sequence. The interrupt engine could easily control how often each priority rank is enabled, and each rank could apply to any number of threads.


    Interrupt engine example

    Another BIG advantage is that the interrupt engine can also update a number of independent timers, which can be shared by threads or allocated exclusively to threads.

    This is a sequencing engine using the TMR0 interrupt;
    
    void interrupt(void)
    {
      // TMR0 interrupt, gets here every 200 instructions (100uS)
      // this sequences the threads, and also sets a flag every second.
      TMR0 += ((256-200)+3);   // adjust TMR0 so it rolls every 200 ticks
      intcount++;
      if(intcount >= 10)  // if 1mS
      {
        intcount = 0;
        thread++;       // sequence new thread every 1000uS (1mS)
        mscount++;
        if(mscount >= 1000)
        {
          mscount = 0;
          newsecond = 1;   // set this flag every second
        }
        thread2timer++;    // counts up in mS
      }
      INTCON.T0IF = 0;  // clear int flag
    }
    

    TMR0 is used so the other PIC timers are left available. TMR0 is set to make an interrupt every 200 instructions which is exactly 100uS so it can be used for multiple timing tasks.

    The variable mscount is incremented exactly every mS, and exactly every second the flag newsecond is set. One of the threads can be controlled by this flag to operate as a real time clock thread.

    The variable thread2timer is also incremented automatically every 1mS, and it is exclusively used by thread 2 which can now do its own fully independant tasks. Any of the threads can have their own timers.




    A real PIC-thread application!

    The application below was tested in hardware and the TMR0 int occurs exactly every 100uS, and the clock keeps perfect time;
    
    /******************************************************************************
      multi-thread3.c    Example of PIC multi-thread - making a 12:00:00 clock
      Open-source       22nd Dec 2009 - www.RomanBlack.com
      (PIC16F887 8MHz EasyPIC6 2x16 LCD; HSOSC WDTOFF LVPOFF)
      
      TMR0 interrupt is the thread control engine and timer generator.
      Uses 4 threads; each is 1mS duration.
    ******************************************************************************/
    unsigned int mscount;       // mS counters
    unsigned int thread2timer;
    
    unsigned char newsecond;
    unsigned char intcount;
    unsigned char thread;
    unsigned char refresh_display;
    
    unsigned char hours;        // clock vars
    unsigned char mins;
    unsigned char secs;
    unsigned char txt[4];
    
    #include "RomanLCD.c"       // my library to drive 2x16 LCD 
    
    //-----------------------------------------------------------------------------
    void interrupt(void)
    {
      // TMR0 interrupt, gets here every 200 instructions (100uS)
      // this sequences the threads, and also sets a flag every second.
      TMR0 += ((256-200)+3);   // adjust TMR0 so it rolls every 200 ticks
      intcount++;
      if(intcount >= 10)  // if 1mS
      {
        intcount = 0;
        thread++;       // sequence new thread every 1000uS (1mS)
        mscount++;
        if(mscount >= 1000)
        {
          mscount = 0;
          newsecond = 1;   // set this flag every second
        }
        thread2timer++;    // counts up in mS
      }
      INTCON.T0IF = 0;  // clear int flag
    }
    
    //-----------------------------------------------------------------------------
    void main()
    {
      // setup PIC 16F887, 8MHz xtal (EasyPIC6)
      ANSEL  = 0;           // Configure AN pins as digital
      ANSELH = 0;
      CM1CON0 = 0;
      CM2CON0 = 0;
      PORTB = 0b00111111;   // SET LCD pins HI
      TRISB = 0b00000000;   // PORTB all outs, drives LCD
      TRISC = 0b00000011;   // RC0,1 are buttons       
      OPTION_REG = 0b00001000;       // TMR0 1:1 prescale (2MHz)
    
      thread = 0;
      mscount = 0;
      thread2timer = 0;
      hours = 12;
      mins = 0;
      secs = 0;
    
      RomanLCD_Init();      // init 2x16 text LCD
      INTCON = 0b10100000;  // TMR0 interrupt on
    
      // loop here and do each thread for 2000 instructions (1mS)
      while(1)
      {
        // THREAD 0 ===========================================
        while(thread == 0)
        {
          // this thread looks for a new second and updates the seconds
          if(newsecond)
          {
            newsecond = 0;
            secs++;
            refresh_display = 1;  // tell other thread to redraw LCD
          }
        }
        // THREAD 1 ===========================================
        while(thread == 1)
        {
          // this thread updates the minutes and hours
          if(secs >= 60)
          {
            secs = 0;
            mins++;
          }
          if(mins >= 60)
          {
            mins = 0;
            hours++;
          }
          if(hours > 12) hours = 1;
        }
        // THREAD 2 ===========================================
        while(thread == 2)
        {
          // this thread checks the 2 buttons to set the clock
          // test buttons 4 times per sec
          if(thread2timer > 250)    // 250mS
          {
            thread2timer = 0;
            if(PORTC.F0)   // set mins button
            {
              mins++;
              mscount = 0;
              secs = 0;
              newsecond = 1;
            }
            if(PORTC.F1)   // set hours button
            {
              hours++;
              mscount = 0;
              secs = 0;
              newsecond = 1;
            }
          }
        }
        // THREAD 3 ===========================================
        while(thread == 3)
        {
          // this thread draws the clock on LCD if needed
          // this thread is last in case it runs >1mS
          if(refresh_display)
          {
            ByteToStr(secs,txt);    // format and display secs
            txt[0] = ':';
            if(txt[1] == ' ') txt[1] = '0';
            RomanLCD_Out(0,6,txt);
    
            ByteToStr(mins,txt);    // format and display mins
            txt[0] = ':';
            if(txt[1] == ' ') txt[1] = '0';
            RomanLCD_Out(0,3,txt);
    
            ByteToStr(hours,txt);    // format and display hours
            RomanLCD_Out(0,0,txt);
            refresh_display = 0;     // refresh is done
          }
        }
        // THREAD END =========================================
        thread = 0;    // double clear in case of interrupt
        thread = 0;
      }
    }
    

    This uses the same interrupt as the thread control engine. Also take a look at thread 2. Thread 2 tests the 2 buttons used to set the clock time. It has it's own independent 1mS timer variable; thread2timer. So thread 2 operates as an independent process that checks the buttons every 250mS regardless of what the other threads are doing.

    The threads communicate between themselves in a crude fashion. The newsecond flag is used by the interrupt engine to add a second to the clock. Thread 0 sets the refresh_display flag that tells thread 3 that the display must be updated.

    Thread 1 is the actual clock. It's only task is to check the hours, mins and secs variables, and if any variable rolls over it fixes the clock time.

    Thread 2 uses a bit of a cheat because when the user changes the time by pressing a button it cheats and sets the newsecond flag. This tells thread 0 to add a bogus second, so it then tells thread 3 to display the new time and before that happens thread 1 gets a chance to fix any time errors.

    Thread 3 is placed at the bottom of the sequence because writing to the LCD takes time, and it is the only thread that risks running overtime. If thread 3 does run >1mS then the next thread executed (thread 0) will get a timeslice that is less than 1mS. Which may or may not matter, in this clock application it won't matter.




    Polite threads?

    It would be nice to have the option of "polite threads" that can choose to give up their timeslice if they have nothing to do.

    This should be easy enough. The interrupt engine needs to separate the thread timing task from any real world timers. Then any thread can force its own timeslice to end prematurely which allows the next thread to take over.

    
    void interrupt(void)
    {
      // TMR0 interrupt, gets here every 200 instructions (100uS)
      // Sequence the threads first
      TMR0 += ((256-200)+3);   // adjust TMR0 so it rolls every 200 ticks
      intcount++;
      if(intcount >= 10)    // if 1mS
      {
        intcount = 0;
        thread++;       // sequence new thread every 1000uS (1mS)
      }
      // update real world timers
      timerintcount++;
      if(timeintrcount >= 10)  // if 1mS
      {
        timerintcount = 0;
        mscount++;
        if(mscount >= 1000) // if 1 second
        {
          mscount = 0;
          newsecond = 1;    // set this flag every second
        }
        thread2timer++;     // counts up in mS
      }
      INTCON.T0IF = 0;      // clear int flag
    }
    

    The new interrupt (see above) needed one more variable; timerintcount, but now it has completely separated the thread control from the timer control.

    Now a thread can politely give away it's timeslice. All that it needs to do is;
  • 1. Make sure the interrupt is not impending (so it won't interfere)
  • 2. increment thread (so next thread can start)
  • 3. set intcount to 255 (so the next thread gets a full timeslice)

    
        // THREAD 1 ===========================================
        while(thread == 1)
        {
          if(PORTB.F1) LED = 1;
          else         give_up_timeslice();
        }
        // THREAD 2 ===========================================
        while(thread == 2)
        {
    
        }
        // THREAD END =========================================
        give_up_timeslice();
        thread = 0;
      }
    }
    
    // this function can be called by any thread
    void give_up_timeslice(void)
    {
      while(TMR0 > 240);  // WAIT HERE until sure that int cant happen
      thread++;           // make next thread active
      intcount = 255;     // will roll to zero, so next thread gets full timeslice
    }
    

    Above you can see above, thread 1 politely decided to give up its timeslice when it discovered it had nothing left to do.

    The little function give_up_timeslice() can also be used to clean up after all the threads at the bottom of the sequence. So if a thread has gone into overtime the give_up_timeslice() function will re-sync so the next thread (which will be thread 0) will get a full timeslice.




    multi-threading by frequency

    The systems above are all timeslice multi-thread systems, they operate by allocating a percentage of the available time to each thread.

    This next system is a totally different approach. It is a "frequency multi-thread system" (for want of a better name). Threads are executed based on frequency so a thread might be executed on every loop, or every 2nd loop, or every 20th loop etc.

    This might be a better multi-threading system where there is a very high priority thread which can be called every loop and other threads called much less frequently. It may also be a good system if there are a large amount of very fast tasks, which are of varying priorities.

    
      while(1)
      {
        // LOOP SYNCHRONISE ===================================
        while(!loop_time_sync); // WAIT HERE until loop time
        loop_time_sync--;
        loopnum++;
    
        // THREAD 0 ===========================================
        // this is very high priority, it is done every loop
    
    
        // THREAD 1 ===========================================
        // this thread is also very high priority
    
    
        // THREAD 2 ===========================================
        if((loopnum % 2) == 0)    // do this thread every 2nd loop
        {
        }
        // THREAD 3 ===========================================
        if((loopnum % 20) == 0)   // do this thread every 20th loop
        {
        }
        // THREAD 4 ===========================================
        if(loopnum == 0)   // do this thread every 256th loop
        {
        }
        // THREAD 5 ===========================================
        if(loopnum == 1)   // do this thread every 256th loop
        {
        }
        // THREAD END =========================================
      }
    

    The variable loopnum increments for every loop done. It just rolls over so it is always 0-255 range. So threads can be executed on loop frequency like every X loops.

    This is a fairly common programming practice; to de-prioritise a section of code that does not need to be run as often as other tasks. But I think if this technique is refined to a new level it may qualify as a form of multi-threading.

    The obvious improvement would be to use a timer interrupt again. So any thread could use a shared real world timer or have its own dedicated timer. Also I have added a very simple but sophisticated loop synchronisation system. Now it is looking like a useful form of multi-threading.

    The loop sync system (see above) works in tandem with the interrupt timer engine. Imagine that the loop needs to be executed 100 times a second (10mS). The interrupt increments the variable loop_time_sync every 10mS. If a loop takes less than 10mS, the next loop will wait and sync (restart) when the variable is incremented to 1 by the interrupt.

    Now some loops may take longer than 10mS, even a lot longer. Or there may be a number of consecutive loops each one being longer than 10mS. The loop_time_sync system will increment for every 10mS, so it keeps perfect accumulated time. If there is a very long overtime loop, or many overtime loops, then the loop_time_sync variable just increments in value and keeps track of the lost time.

    Then when there are shorter loops (ie <10mS) they will decrement the loop_time_sync variable every loop until the loop is perfectly back in sync with the interrupt engine! It is a self-correcting self-synchronising loop timer that will allow quite a large total of overtime situations and then just fix itself up later.




    Loop frequency PIC-thread application!

    This is the same clock project with the same 4 threads. But this time it has been re-structured in a loop frequency multi-thread format and another "time waster" thread has been added to test it. Yes it has been tested in hardware and it keeps great time.
    
    /******************************************************************************
      multi-thread4.c    Example of PIC multi-thread - making a 12:00:00 clock
      Open-source       22nd Dec 2009 - www.RomanBlack.com
      (PIC16F887 8MHz EasyPIC6 2x16 LCD; HSOSC WDTOFF LVPOFF)
      
      This uses loop frequency multi-threading.
      TMR0 interrupt is the loop timing generator.
      Uses 5 threads; varying priorities.
    ******************************************************************************/
    
    unsigned char intcount;
    unsigned char loop_time_sync;
    unsigned char loopnum;
    unsigned char loop10ms;
    unsigned char refresh_display;
    unsigned char thread1timer;
    
    unsigned char hours;        // clock vars
    unsigned char mins;
    unsigned char secs;
    unsigned char txt[4];
    
    #include "RomanLCD.c"       // my library to drive 2x16 LCD 
    
    //-----------------------------------------------------------------------------
    void interrupt(void)
    {
      // TMR0 interrupt, gets here every 200 instructions (100uS)
      // this incs the loop sync flag every 10mS.
      TMR0 += ((256-200)+3);   // adjust TMR0 so it rolls every 200 ticks
      intcount++;
      if(intcount >= 100)   // if reached 10mS
      {
        intcount = 0;
        loop_time_sync++;
        thread2timer++;     // every 10mS
      }
      INTCON.T0IF = 0;      // clear int flag
    }
    
    //-----------------------------------------------------------------------------
    void main()
    {
      // setup PIC 16F887 (EasyPIC6)
      ANSEL  = 0;           // Configure AN pins as digital
      ANSELH = 0;
      CM1CON0 = 0;
      CM2CON0 = 0;
      PORTB = 0b00111111;   // SET LCD pins HI
      TRISB = 0b00000000;   // PORTB all outs, drives LCD
      TRISC = 0b00000011;   // RC0,1 are buttons       
      OPTION_REG = 0b00001000;       // TMR0 1:1 prescale (2MHz)
    
      loopnum = 0;
      hours = 12;
      mins = 0;
      secs = 0;
    
      RomanLCD_Init();      // init 2x16 text LCD
      INTCON = 0b10100000;  // TMR0 interrupt on
    
      // loop here and do the multi-threading loop.
      while(1)
      {
        // LOOP SYNCHONISE ====================================
        while(!loop_time_sync); // WAIT HERE until loop time (10mS)
        loop_time_sync--;
        loopnum++;
    
        // THREAD 0 ===========================================
        // HIGH PRIORITY! this thread generates the 1 second event
        loop10ms++;
        if(loop10ms >= 100)     // if reached 1 second!
        {
          loop10ms = 0;
          secs++;
          refresh_display = 1;  // tell other thread to redraw LCD
        }
    
        // THREAD 1 ===========================================
        // medium priority 
        if((loopnum % 5) == 0)    // do this thread every 5th loop
        {
          // this thread checks the 2 buttons to set the clock
          // test buttons 4 times per sec
          if(thread1timer > 25)    // 25x10mS = 250mS
          {
            thread1timer = 0;
            if(PORTC.F0)   // set mins button
            {
              mins++;
              secs = 0;
              refresh_display = 1;  // tell other thread to redraw LCD
            }
            if(PORTC.F1)   // set hours button
            {
              hours++;
              secs = 0;
              refresh_display = 1;  // tell other thread to redraw LCD
            }
          }
        }
    
        // THREAD 2 ===========================================
        // HIGH PRIORITY! this thread updates the minutes and hours
        if(secs >= 60)
        {
          secs = 0;
          mins++;
        }
        if(mins >= 60)
        {
          mins = 0;
          hours++;
        }
        if(hours > 12) hours = 1;
    
        // THREAD 3 ===========================================
        // medium priority 
        if((loopnum % 5) == 1)    // do this thread every 5th loop
        {
          // this thread draws the clock on LCD if needed
          // this thread is last in case it runs >1mS
          if(refresh_display)
          {
            ByteToStr(secs,txt);    // format and display secs
            txt[0] = ':';
            if(txt[1] == ' ') txt[1] = '0';
            RomanLCD_Out(0,6,txt);
    
            ByteToStr(mins,txt);    // format and display mins
            txt[0] = ':';
            if(txt[1] == ' ') txt[1] = '0';
            RomanLCD_Out(0,3,txt);
    
            ByteToStr(hours,txt);    // format and display hours
            RomanLCD_Out(0,0,txt);
            refresh_display = 0;     // refresh is done
          }
        }
    
        // THREAD 4 ===========================================
        // low priority 
        if((loopnum % 20) == 2)    // do this thread every 20th loop
        {
          // this thread is just a nasty big delay every 20 loops
          // to test the self correcting loop sync system.
          Delay_ms(60);
        }
        // THREAD END =========================================
      }
    }
    

    As the sync loop can be relied on to make exactly 100 loops per second I generated the 1 second event by counting loops, mainly to test the reliability of the loop sync. However it could have used a dedicated 1 second timer like the last project, which would have been a better choice. Thread 1 still uses a dedicated timer that is controlled in the interrupt to make the 250mS period for checking the buttons.

    The loop sync works fine, it swallows up the large 60mS "task" in thread 4 that occurs every 20 loops and resyncs within a few loops and there is no noticable difference on the display, and obviously it keeps perfect time.

    I'm definiely NOT an expert in multi-thread software! But having independant threads that can have independant resources (real world timers etc) and being able to just slot in a large clumsy task by simply adding a thread, all without any noticable effect on operation, seems like a decent definition. At least it's a starting point for working with multi-thread systems on a PIC.




    Where to go from here?

    The code examples on this page are all pretty crude. Even the 2 clock applications have no real NEED to be multi-threaded apps.

    But when I look back at some of the larger PIC projects I have made in the past (especially ones that grew until they became clumsy) I can see they would have been much better built with some type of multi-thread structure.

    For the future it would be good to explore systems for controlling the threads communication with each other. Using better variable names and a system for controlling whether data flows in or out of a thread, so threads can pass data and commands to each other with no conflict. Also to look at ways of making threads more independent, like ways of removing any need for threads to be sequenced in a particular order.

    And also to explore ways of making thread execution much more parallel and less sequential. As an example if a thread is writing graphics data to a GLCD (which can take a while) it might only write one byte every time that thread is executed, so the GLCD write occurs in parallel to other tasks. This could be done with intra-thread scheduling so each thread can queue up tasks and and perform a little piece of each task every time that thread is executed.




    A proper PIC-thread project; Baudrate converter 1

    This uses the PIC-thread loop frequency system and 4 threads to operate as a high speed asynchronous bidirectional baudrate converter.

    The 4 threads are displayed in colour;
    
        // THREAD 0 ===========================================
        // check if usart1 received a byte, put it in buffer
    
        // THREAD 1 ===========================================
        // check if usart2 received a byte, send it out to usart1
    
        // THREAD 2 ===========================================
        // send usart1 byte if any bytes are in buffer
        
        // THREAD 3 ===========================================
        // display the buffer1 load as a 7 LED bargraph on PORTB
    



    The loop frequency system gives high priority to threads 0, 1 and 2. These are checked and executed every loop to keep data transfer high. Thread 3 is a lower priority (because it just displays the bargraph) and is only executed every 16th loop.

    Here is the actual thread code;
      while(1)
      {
        // LOOP SYNCHONISE ====================================
        // don't sync, just run loop at full speed
        loopnum++;
        
        // THREAD 0 ===========================================
        // HIGH PRIORITY!
        // check if usart1 received a byte, put it in buffer
        if(PIR1.RC1IF)
        {
          buffer1[inpointer1] = RCREG1;   // store the byte
          inpointer1++;
          if(inpointer1 >= MAXBUFFER1) inpointer1 = 0;
        } 
        
        // THREAD 1 ===========================================
        // HIGH PRIORITY!
        // check if usart2 received a byte, send it out to usart1
        if(PIR3.RC2IF)
        {
          if(TXSTA1.TRMT) TXREG1 = RCREG2;  // if usart1 TX free, send byte
        } 
        
        // THREAD 2 ===========================================
        // HIGH PRIORITY!
        // send usart1 byte if any bytes are in buffer
        bufferload1 = (inpointer1 - outpointer1); 
        if(bufferload1)
        {
          if(bufferload1 >= MAXBUFFER1) bufferload1 += MAXBUFFER1; // fix buffer roll
          th3display = (bufferload1 >> 8);  // 1792->7 ready for bar display
          
          if(TXSTA2.TRMT)   // if usart2 free, send another byte
          {
            TXREG2 = buffer1[outpointer1];
            outpointer1++;
            if(outpointer1 >= MAXBUFFER1) outpointer1 = 0;  // fix buffer roll
          }
        }
        else  th3display = 0;     
            
        // THREAD 3 ===========================================
        // medium priority
        // display the buffer1 load as a 7 LED bargraph on PORTB
        if((loopnum & 0x0F) == 0)   // do this thread every 16th loop
        {
          LATB = 0;
          if(bufferload1)    LATB.F7 = 1;   // 7 LED bargraph
          if(th3display)     LATB.F6 = 1;
          if(th3display > 1) LATB.F5 = 1;
          if(th3display > 2) LATB.F4 = 1;
          if(th3display > 3) LATB.F3 = 1;
          if(th3display > 4) LATB.F2 = 1;
          if(th3display > 5) LATB.F1 = 1;
        } 
        // THREAD END =========================================
      }
    

    The 7 LED bargraph will light LED 7 if there are any bytes in the buffer and then LEDs 6 to 1 show the buffer usage in a linear fashion.

    This project is tested in hardware and can be seen complete on my BIGPIC6 Projects Page along with a more sophisticated PIC-thread project which is a Baudrate converter with GLCD display and a 62 bar bargraph to show the buffer usage.

    I think this project is a pretty fair demonstration of a multi-threading application, for these reasons;

  • Performance actually benefits from the multi-threading
  • The threads could be arranged in any order
  • Each thread is a process that operates independently
  • Each thread can be executed (or not) with no effect on the other threads
  • All threads can have different priority (frequency)
  • Communication from Thread2 to Thread3 is done through a specific Thread3 variable
  • Threads can be added later with no problems




    Check this page for more to come...

    - end -

    [Back to Home Page]