Multitasking on a PIC18: Proof Of Concept

Tips, Tricks and methods for programming, learn ways of making your programming life easier, and share your knowledge with others.

Moderators: Benj, Mods

Post Reply
Spanish_dude
Posts: 594
Joined: Thu Sep 17, 2009 7:52 am
Location: Belgium
Has thanked: 63 times
Been thanked: 102 times
Contact:

Multitasking on a PIC18: Proof Of Concept

Post by Spanish_dude »

Hi everyone,

After a week of programming we (dridri and myself) are proud to show you a proof of concept (P.O.C.) of multithreading, a.k.a. multitasking on a PIC18F2455.
It is programmed in C, with a some ASM lines.
We worked with the mikroC IDE and it's debugger. The debugger is really an awesome tool to have.
We could quickly and easily spot an error in the program / variables, even though it sometimes said the program runned Ok but "in real life" it didn't.

I'll try and make a Flowcode version of it.

Multithreading ?
Multithreading is a way of using multiple threads.
These threads are "tasks" or "programs" that are executed by the microcontroller.

These tasks that the microcontroller needs to execute are a bit special.
They are running at almost the same time.

The microcontroller will switch from one task to another at a very high speed, even though the executing task isn't done yet.

A task could be anything you want, from a blinking LED to printing a text on a LCD screen.
As I mentioned it, multithreading enables you to execute multiple tasks at almost the same time.
So you could have a thread that will print text on a LCD screen on PORTB.
Another one could sample some ADC signal on PORTA.
Another one could turn on and off a LED each second.
And so on ...


To use multithreading we needed "something" that would enable us to jump from anywhere in the microcontroller's program to some piece of code that would handle the different tasks.
This piece of code is probably, if not, the most important part of the program. This is where the microcontroller will choose which task it will execute next.
This is know as a threadhandler.

That "something" we needed, was an interrupt. The TMR0 interrupt was perfect for this job.

Once we had a working program that used the interrupt, we needed to find a way to tell the microcontroller to jump into the different tasks when the interrupt was done executing.
This is done by controlling the STACK and the stackpointer (STKPTR).

STACK :
A stack is a little "last in first out" (LIFO) memory (32 times 20 bits) that is used by the microcontroller when a function or macro is called from somewhere in your program.
How does the microcontroller know where he needs to go back to when the function is done ?
That's where the STACK kick's in.
When a function is called, the address of the next instruction is saved in the stack. The stackpointer is then increased by 1.
Whenever the function is done and executes the RETURN instruction, the previously saved address will be written into the Program Counter (PC) and then cleared from the STACK. At this point the stackpointer will decrease by 1.
The Program Counter specifies the address of the instruction to fetch for execution.

Here is a little animated gif I made that will help you understand how a STACK works.
Image

Note1: The ASM code in the gif isn't 100% correct. This is just a "pseudo" example.
Note2: The value of the STACK at stackpointer 0 is always 000000h on this microcontroller.

TOSU, TOSH and TOSL are three registers used by the RETURN operation.
Those three registers together is like a variable containing the last written address of the STACK.

The STKPTR indicates where you are in the STACK.

Let's follow the red arrow.

Whenever a program is executed after reset, it will always starts at PC = 0000h.
The first instruction is a CALL to a subroutine (function) called LED_BLINK.
The CALL instruction will push the address of the next instruction into the STACK and increase de stackpointer by 1.

The arrow then jumps into the LED_BLINK subroutine.
With the next instruction we will clear bit 0 of PORTB.
Then we will CALL another subroutine. This one is a little delay so we can see the LED blink.
When the CALL instruction is executed, the address of the next instruction is pushed into the STACK and the stackpointer will once again increase by 1.

The arrow jumps now into the DELAY function.
When a function is made, you always need to end it with a RETURN instruction.
When the RETURN instruction is executed you'll see the program taking or "popping" the last saved address, decreasing the stackpointer by 1 and then jump to that address by changing the Program Counter with this address.

The program is now back into the LED_BLINK function and is now setting bit 0 from PORTB to 1.

Another CALL instruction is executed for another little delay.
Here the same will happen as previously.
The address of the next instruction is pushed into the STACK. The stackpointer increases by 1.
When executing the RETURN instruction the program will pop the last saved address of the STACK, deceasing the stackpointer by 1 again and then jump to that address.

When the last delay is done, the program will execute the RETURN instruction of the LED_BLINK function and you will be back into the mainloop.
All the addresses of the STACK are gone and the stackpointer is back to it's original value.



For each thread in the program there are a couple of arrays.
These arrays are used to store registers that are used in those threads, such as the WREG (Accumulator), STATUS, BSR and R0 to R20 registers.
What we also are storing is the last address pushed in the STACK and the stackpointer, so we will know where to go back to.

The great thing about the interrupt is that when it is triggered, it will write the next instruction of where it has been triggered into the STACK.
We just need to read and store it into an array.

Like all RETURN instruction, the RETFIE (return from interrupt) will pop the last address in the STACK and write it into the PC, thus jumping to address taken from the STACK.

Aaahh. You're starting to understand what we are going to do, aren't you ?

We used a global variable that we increase each time the interrupt is triggered. This variable is a counter so the program will know which thread it needs to go next.
Once we know which thread we want to jump in, we change the value of the last address pushed into the STACK.
We do NOT increase or decrease the stackpointer as we haven't added or removed an address in the STACK.

Next thing we need to do is wait until we get to the interrupt's return instruction and it will jump right into another thread.

Isn't it a bit dangerous to mess with the STACK ?
You are right.
If the STACK isn't correctly managed, you'll get STACK overflows or underflows. These will (if the option is enabled) reset the whole microcontroller !
But that's not the only problem you'll have.
The microcontroller could jump anywhere in the program causing freezes (it gets stuck somewhere), strange behaviors, jumping where it isn't supposed to jump, ...
This is why you need to be very careful when you want to push, pop or change something in the STACK.


Our Proof Of Concept program :
Note: We added comments to the program so it will be easier to understand.
Let's talk about our example program. In this program we tried to get two LEDs blinking at different frequencies, one connected to RB0 and the other one to RB7.
The main function just initializes the chip by setting the timer interrupt and some registers, after that it enters an infinite loop to prevent the microcontroller to reset (or to stop).

As explained above, the chip will go into the interrupt function every 1ms.
To have the interrupt get triggered each ms, we did this little equation:
(((Fosc / 4) / prescaler) / (256 - TMR0_preset_value))
We ended up with a prescaler of 1:64 and a TMR0 preset value of 69 (decimal).

At the first execution of the interrupt, there are no running threads so the program will avoid the backup of user registers.
The interrupt will jump in the next thread by replacing the Return Address (saved in the STACK when the interrupt was called).
Once the RETFIE instruction is executed, it will change the value of the Program Counter (PC) and the microcontroller will jump into one of the two threads.

Everything we mentioned above had to be executed in a specific order. For example, we had to save the TOS address (return address) after saving the R registers because saving the TOS used some R registers and thus could erase some important values from the previously jumped thread.
This could lead to problems when executing that thread..

The most weird thing was the delay function used in mikroC.
Delay_ms is in fact a built in function using loops to make the microcontroller wait a certain amount of time.
But, by using 2 threads, the execution time is divided by two and the delay functions made the delays twice as long because it is executed only half the time.
Measuring the outputs with a scope, we measured delays of 1s instead of 500ms. (After this we changed the delays to 10ms and 5ms)

When a thread calls a delay function, it means that it doesn't have to work during a certain period of time.
For instance, if 'thread1' calls the delay function to pause for 10 ms, then the other threads will have all this time to work and the scheduler will bypass 'thread1'.
By supposing that the interrupt is called every milliseconds, we made a variable containing the time in ms that the thread will "pause".
This variable will decrease at each interrupt call. When it's equal to 0, this means that all the delay time is elapsed and that the scheduler can continue running that thread.

If all the threads are in "pause mode" the scheduler can't resume any of them. To get around this, we made a loop that decrements the waiting variables every ms.
When the "pause time" of one of the threads reaches 0, the microcontroller will jump into it when executing the RETFIE instruction.

When we tried this code, we could see that the delays are wrong. For example, we measured 8.2ms instead of a 10ms delay (-20%).
It is very inaccurate, but we found a way to improve the pause time by adding the execution time of the interrupt (about 167µs at 48MHz) in addition to the delays of 1ms.
By using this trick, the pause time is about 9.57ms instead of 10ms (-5%).

Note: The execution time of the interrupt has been measured by setting and clearing RB0 at the start and end of the interrupt macro.

I think that's pretty much it, I'll just show you a scope screen of the output signals.
The blue one is RB0 and the red one is RB7, each are executed in a different thread.

Image


This program learned us a lot about all the registers used in the microcontroller, the ASM instruction set and managing to correctly manipulate the STACK without the PIC going crazy.
We hope to have learned you something in this article and hope you enjoyed reading it.

Best regards,

Adrien A. & Nicolas L. F.
Attachments
Threads.hex
HEX file
(8.41 KiB) Downloaded 436 times
Threads.c
Proof Of Concept program
(6.42 KiB) Downloaded 601 times

dridri
Posts: 10
Joined: Sun Aug 21, 2011 2:23 am
Contact:

Re: Multitasking on a PIC18: Proof Of Concept

Post by dridri »

Hope you'll enjoy the article ! :)
France France France... :)

User avatar
Benj
Matrix Staff
Posts: 15312
Joined: Mon Oct 16, 2006 10:48 am
Location: Matrix TS Ltd
Has thanked: 4803 times
Been thanked: 4314 times
Contact:

Re: Multitasking on a PIC18: Proof Of Concept

Post by Benj »

Very nice project, sure this will come in very useful, thanks for sharing :D

Post Reply