This smells somewhat off-topic but I'll try to steer it back on track.
Pre-emptive multitasking means that the operating system or kernel can suspend the currently running thread and switch to another based on whatever scheduling heuristics it has in place. Most times the threads running have no concept that there are other things going on on the system, and what this means for your code is that you must be careful to design it so that if the kernel decides to suspend a thread in the middle of a multi-step operation (say changing a PWM output, selecting a new ADC channel, reading status from an I2C peripheral, etc.) and let another thread run for a while, that these two threads don't interfere with each other.
An arbitrary example: let's say you're new to multithreaded embedded systems and you have a little system with an I2C ADC, an SPI LCD and an I2C EEPROM. You decided that it would be a good idea to have two threads: one which reads from the ADC and writes samples to the EEPROM, and one which reads the last 10 samples, averages them and displays them on the SPI LCD. The inexperienced design would look something like this (grossly simplified):
char i2c_read(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_READ);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
char i2c_write(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_WRITE);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
adc_thread()
{
int value, sample_number;
sample_number = 0;
while (1) {
value = i2c_read(ADC_ADDR);
i2c_write(EE_ADDR, EE_ADDR_REG, sample_number);
i2c_write(EE_ADDR, EE_DATA_REG, value);
if (sample_number < 10) {
++sample_number;
} else {
sample_number = 0;
}
};
}
lcd_thread()
{
int i, avg, sample, hundreds, tens, ones;
while (1) {
avg = 0;
for (i=0; i<10; i++) {
i2c_write(EE_ADDR, EE_ADDR_REG, i);
sample = i2c_read(EE_ADDR, EE_DATA_REG);
avg += sample;
}
/* calculate average */
avg /= 10;
/* convert to numeric digits for display */
hundreds = avg / 100;
tens = (avg % 100) / 10;
ones = (avg % 10);
spi_write(CS_LCD, LCD_CLEAR);
spi_write(CS_LCD, '0' + hundreds);
spi_write(CS_LCD, '0' + tens);
spi_write(CS_LCD, '0' + ones);
}
}
This is a very crude and fast example. Don't code like this!
Now remember, the pre-emptive multitasking OS can suspend either of these threads at any line in the code (actually at any assembly instruction) and give the other thread time to run.
Think about that. Imagine what would happen if the OS decided to suspend adc_thread()
between the setting of the EE address to write and writing the actual data. lcd_thread()
would run, muck around with the I2C peripheral to read the data it needed, and when adc_thread()
got its turn to run again, the EEPROM would not be in the same state it was left. Things would not work very well at all. Worse, it might even work most of the time, but not all of the time, and you would go crazy trying to figure out why your code is not working when it LOOKS like it should!
That's a best-case example; the OS might decide to pre-empt i2c_write()
from adc_thread()
's context and start running it again from lcd_thread()
's context! Things can get really messy really fast.
When you're writing code to work in a pre-emptive multitasking environment you have to use locking mechanisms to make sure that if your code is suspended at an inopportune time that all hell doesn't break loose.
Cooperative multitasking, on the other hand, means that each thread is in control of when it gives up its execution time. The coding is simpler, but the code must be designed carefully to make sure all threads get enough time to run. Another contrived example:
char getch()
{
while (! (*uart_status & DATA_AVAILABLE)) {
/* do nothing */
}
return *uart_data_reg;
}
void putch(char data)
{
while (! (*uart_status & SHIFT_REG_EMPTY)) {
/* do nothing */
}
*uart_data_reg = data;
}
void echo_thread()
{
char data;
while (1) {
data = getch();
putch(data);
yield_cpu();
}
}
void seconds_counter()
{
int count = 0;
while (1) {
++count;
sleep_ms(1000);
yield_cpu();
}
}
That code won't work how you think, or even if it does seem to work, it won't work as the data rate of the echo thread increases. Again, let's take a minute to look at it.
echo_thread()
waits for a byte to appear at a UART and then gets it and waits until there's room to write it, then writes it. After that is done it gives other threads a turn to run. seconds_counter()
will increment a count, wait for 1000ms and then give the other threads a chance to run. If two bytes come in to the UART while that long sleep()
is happening, you could miss seeing them because our hypothetical UART has no FIFO to store characters while the CPU is busy doing other things.
The correct way to implement this very poor example would be to put yield_cpu()
where ever you have a busy loop. This will help things move along, but could cause other issues. e.g. if timing is critical and you yield the CPU to another thread that takes longer than you expect, you could have your timing thrown off. A pre-emptive multitasking OS would not have this issue because it forcibly suspends threads to make sure all threads are scheduled correctly.
Now what's this got to do with a timer and background loop? The timer and background loop are very similar to the cooperative multitasking example above:
void timer_isr(void)
{
++ticks;
if ((ticks % 10)) == 0) {
ten_ms_flag = TRUE;
}
if ((ticks % 100) == 0) {
onehundred_ms_flag = TRUE;
}
if ((ticks % 1000) == 0) {
one_second_flag = TRUE;
}
}
void main(void)
{
/* initialization of timer ISR, etc. */
while (1) {
if (ten_ms_flag) {
if (kbhit()) {
putch(getch());
}
ten_ms_flag = FALSE;
}
if (onehundred_ms_flag) {
get_adc_data();
onehundred_ms_flag = FALSE;
}
if (one_second_flag) {
++count;
update_lcd();
one_second_flag = FALSE;
}
};
}
This looks pretty close to the cooperative threading example; you have a timer that sets up events and a main loop that looks for them and acts on them in an atomic fashion. You don't have to worry about the ADC and LCD "threads" interfering with each other because one will never interrupt the other. You still have to worry about a "thread" taking too long; e.g. what happens if get_adc_data()
takes 30ms? you'll miss three opportunities to check for a character and echo it.
The loop+timer implementation is oftentimes a lot simpler to implement than a cooperatively multitasked microkernel since your code can be designed more specific to the task at hand. You aren't really multitasking so much as designing a fixed system where you give each subsystem some time to do its tasks in a very specific and predictable way. Even a cooperatively multitasked system has to have a generic task structure for each thread and the next thread to run is determined by a scheduling function that can become quite complex.
The locking mechanisms for all three systems are the same, but the overhead required for each is quite different.
Personally, I almost always code to this last standard, the loop+timer implementation. I find threading is something that should be used very sparingly. Not only is it more complex to write and debug, but it also requires more overhead (a preemptive multitasking microkernel is always going to be bigger than a stupidly simple timer and main loop event follower).
There's also a saying that anyone working on threads will come to appreciate:
if you have a problem and use threads to solve it, yoeu ndup man with y pemro.bls
:-)
Best Answer
The Microchip’s TCP/IP stack has an implementation of such a timer library called the Tick Module. It's written for portability and supports the PIC18, PIC24, dsPIC & PIC32 devices.
From the Microchip TCP/IP Stack Help:
You'll just need to find the Tick.h and Tick.c files in the TCP/IP stack (download link, it's free) and modify them a bit to make them work independent of the stack.