14
\$\begingroup\$

For a bared metal MCU, Comparing to the homemade code with background loop plus timer interrupt architecture, what are the benefits of a non-preemptive OS? What among these benefits are attractive enough for a project to adopt a non-preemptive OS, rather than to use homemade code with background loop architecture?
.

Explanation to the Question:

I really appreciate all those have replied my question. I feel the answer has been almost there. I add this explanation to my question here which shows my own consideration and may help to narrow the question or make it more precise.

What I'm trying to do is to understand how to choose the most appropriate RTOS for a project in general.
To achieve this, better understanding of the basic concepts and the most attractive benefits from different kinds of RTOS and the corresponding price will help, since there is no best RTOS for all applications.
I read books about OS a few years ago but I don't have them with me anymore. I searched on the internet before I posted my question here and found this information was most helpful: http://www.ustudy.in/node/5456.
There are a lot other helpful information like the introductions in the website of different RTOS, articles comparing pre-emptive scheduling and non-preemptive scheduling, and etc.
But I didn't find any topic mentioned when to choose a non-preemptive RTOS and when is better just write your own code using timer interrupt and background loop.
I have certain my own answers but I'm not satisfied enough with them.
I really would like to know the answer or view from more expeirenced people, especially in industry practice.

My understanding so far is:
no matter use or not use an OS, certain kind of scheduling codes are always necessary, even it's in the form of code like:

    in the timer interrupt which occurs every 10ms  
    if(it's 10ms)  
    {  
      call function A / execute task A;  
    }  
    if(it's 50ms)  
    {  
      call function B / execute task B;  
    }  

Benefit 1:
A non-preemptive OS designates the way / programming style for the scheduling code, so that engineers can share the same view even if they were not in the same project before. Then with the same view about concept task, engineers can work on different tasks and test them, profile them independently as much as possible.
But how much are we really able to gain from this? If engineers are working in the same project, they can find way share the same view well without using a non-preemptive OS.
If one engineer is from another project or company, he will gain the benefit if he knew the OS before. But if he didn't, then again, it seems not to make big difference for him to learn a new OS or a new piece of code.

Benefit 2:
If the OS code has been well tested, so it saves the time from debugging. This is really a good benefit.
But if the application has only about 5 tasks, I think it's not really messy to write your own code using timer interrupt and background loop.

A non-preemptive OS here is referred to a commercial / free / legacy OS with a non-preemptive scheduler.
When I posted this question, I mainly think of certain OSes like:
(1) KISS Kernel (A Small NonPreemptive RTOS - claimed by its website)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (lightweight RTOS - claimed by its website)
(3) FreeRTOS (It's a preemptive RTOS, but as I understand, it can be configured as a non-preemptive RTOS as well)
(4) uC/OS (similar as FreeRTOS)
(5) legacy OS / scheduler code in some companies (usually made and maintained by the company internally)
(Can't add more links because limitation from new StackOverflow account)

As I understand, a non-preemptive OS is a collection of these codes:
(1) a scheduler using non-preemptive strategy.
(2) facilities for inter-task communication, mutex, synchronization and time control.
(3) memory management.
(4) other helpful facilities / libraries like File System, network stack, GUI and etc. (FreeRTOS and uC/OS provides these, but I'm not sure if they still work when the scheduler is configured as non-preemptive)
Some of them are not always there. But the scheduler is a must.

\$\endgroup\$
1
  • \$\begingroup\$ That's pretty much it in a nutshell. If you have a workload that needs to be multithreaded and you can afford the overhead, use a threading OS. Otherwise a simple time or task-based "scheduler" suffices for most cases. And to figure out whether preemptive or cooperative multitasking is best... I guess it comes down to overhead and how much control you want to have over the multitasking you need to do. \$\endgroup\$
    – akohlsmith
    Commented Aug 11, 2013 at 21:16

3 Answers 3

13
\$\begingroup\$

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

:-)

\$\endgroup\$
6
  • \$\begingroup\$ Thank you very much for your reply with detailed examples, akohlsmith. However, I can't conclude from your reply why you choose simple timer and background loop architecture rather than the cooperative multitasking. Don't get me wrong. I really appreciate your reply, which provides a lot of useful information about different scheduling. I just haven't got the point. \$\endgroup\$
    – hailang
    Commented Aug 11, 2013 at 17:52
  • \$\begingroup\$ Could you please work on this a bit more? \$\endgroup\$
    – hailang
    Commented Aug 11, 2013 at 18:03
  • \$\begingroup\$ Thanks, akohlsmith. I like the sentence you put at the end. It took me for a while to recognize it:) Back to the point of your answer, you almost always code to the loop+timer implementation. Then, in the cases you gave up this implementation and turn to non-preemptive OS, what made you do so? \$\endgroup\$
    – hailang
    Commented Aug 11, 2013 at 19:55
  • \$\begingroup\$ I've gone with both cooperative and preemptive multitasking systems when I was running someone else's OS. Either Linux, ThreadX, ucOS-ii or QNX. Even in some of those situations, I've used the simple and effective timer+event loop (poll() comes immediately to mind). \$\endgroup\$
    – akohlsmith
    Commented Aug 11, 2013 at 21:13
  • \$\begingroup\$ I am not a fan of threading or multitasking in embedded, but I know that for complex systems it's the only sane option. Canned micro-operating systems give you a quick way to get things up and running and oftentimes provide device drivers as well. \$\endgroup\$
    – akohlsmith
    Commented Aug 11, 2013 at 21:18
6
\$\begingroup\$

Multi-tasking can be a useful abstraction in a lot of microcontroller projects, although a true pre-emptive scheduler would be too heavyweight and unnecessary in most cases. I have done well over 100 microcontroller projects. I have used cooperative tasking a number of times, but pre-emptive task switching with its associated baggage has so far not been appropriate.

The problems with pre-emptive tasking as apposed to cooperative tasking are:

  1. Much more heavyweight. Pre-emptive task schedulers are more complicated, take more code space, and take more cycles. They also require at least one interrupt. That is often a unacceptable burden on the application.

  2. Mutexes are required around structures that might be accessed concurrently. In a cooperative system, you just don't call TASK_YIELD in the middle of what should be a atomic operation. This effects queues, shared global state, and creaps into a lot of places.

In general, dedicating a task to a particular job makes sense when the CPU can support this and the job is complicated enough with enough history-dependent operation that breaking it into a few separate individual events would be cumbersome. This is generally the case when handling a communications input stream. Such things are usually heavily state driven depending on some previous input. For example, there may be opcode bytes followed by data bytes unique for each opcode. Then there is the issue of these bytes come at you when something else feels like sending them. With a separate task handling the input stream, you can make it appear in the task code as if you are going out and getting the next byte.

Overall, tasks are useful when there is a lot of state context. Tasks are basically state machines with the PC being the state variable.

Many things a micro has to do can be expressed as responding to a set of events. As a result, I usually have a main event loop. This checks each possible event in sequence, then jumps back to the top and does it all again. When handling a event takes more than just a few cycles, I usually jump back to the start of the event loop after handling the event. This in effect means events have a implied priority based on where they are checked in the list. On many simple systems, this is good enough.

Sometimes you get a little more complicated tasks. These can often be broken down into a sequence of a small number of separate things to do. You can use internal flags as events in those cases. I have done this sort of thing many times on low end PICs.

If you have the basic event structure as above but also have to respond to a command stream over the UART, for example, then it's useful to have a separate task handle the received UART stream. Some microcontrollers have limited hardware resources for multi-tasking, like a PIC 16 which can't read or write its own call stack. In such cases, I use a what I call a pseudo-task for the UART command processor. The main event loop still handles everything else, but one of its events to handle is that a new bytes was received by the UART. In that case it jumps to a routine which runs this pseudo-task. The UART command module contains the task code, and the execution address and a few register values of the task are saved in RAM in that module. The code jumped to by the event loop saves the current registers, loads the saved task registers, and jumps to the task restart address. The task code invokes a YIELD macro that does the reverse, which then eventually jumps back to the start of the main event loop. In some cases the main event loop runs the pseudo-task once per pass, usually at the bottom to make it a low priority event.

On a PIC 18 and higher, I use a true cooperative tasking system since the call stack is readable and writeable by firmware. On these systems, the restart address, a few other pieces of state, and the data stack pointer are kept in a memory buffer for each task. To let all other tasks run once, a task calls TASK_YIELD. This saves the current task state, looks thru the list for the next available task, loads its state, then runs it.

In this architecture, the main event loop is just another task, with a call to TASK_YIELD at the top of the loop.

All my multi-tasking code for PICs is available for free. To see it, install the PIC Development Tools release at http://www.embedinc.com/pic/dload.htm. Look for files with "task" in their names in the SOURCE > PIC directory for the 8 bit PICs, and the SOURCE > DSPIC directory for the 16 bit PICs.

\$\endgroup\$
12
  • \$\begingroup\$ mutexes still may be necessary in cooperatively multitasked systems, although it is rare. The typical example is an ISR that needs access to a critical section. This can almost always be avoided through better design or choosing an appropriate data container for the critical data. \$\endgroup\$
    – akohlsmith
    Commented Aug 9, 2013 at 13:42
  • \$\begingroup\$ @akoh: Yes, I have used mutexes on a few occasions to handle a shared resource, like access to the SPI bus. My point was that mutexes aren't inherently required to the extent they are in a pre-emptive system. I didn't mean to say they are never needed or never used in a cooperative system. Also, a mutex in a cooperative system can be as simple as spinning in a TASK_YIELD loop checking a single bit. In a pre-emptive system they generally need to be built into the kernel. \$\endgroup\$ Commented Aug 9, 2013 at 13:56
  • \$\begingroup\$ @OlinLathrop: I think the most significant advantage of non-preemptive systems when it comes to mutexes is that they are only required either when interacting directly with interrupts (which by nature are preemptive) or when either the time one needs to hold a guarded resource exceeds the time one wants to spent between "yield" calls, or one wants to hold a guarded resource around a call which "might" yield (e.g. "write data to a file"). On some occasions when having a yield within a "write data" call would have been a problem, I've included... \$\endgroup\$
    – supercat
    Commented Aug 9, 2013 at 15:27
  • \$\begingroup\$ ...a method to check how much data can be written immediately, and a method (which may likely yield) to ensure that some quantity is available (expediting the reclamation of dirty flash blocks, and waiting until a suitable number have been reclaimed). \$\endgroup\$
    – supercat
    Commented Aug 9, 2013 at 15:32
  • \$\begingroup\$ Hi Olin, I like your reply so much. Its information is far beyond my questions. It includes a lot of practical experiences. \$\endgroup\$
    – hailang
    Commented Aug 11, 2013 at 19:13
1
\$\begingroup\$

Edit: (I'll leave my earlier post below; maybe it will help someone someday.)

Multitasking OSes of any kind and Interrupt Service Routines aren't - or shouldn't be - competing system architectures. They're meant for different jobs at different levels of the system. Interrupts are really intended for brief code sequences to handle immediate chores like restarting a device, possibly polling non-interrupting devices, timekeeping in software, etc. It is usually assumed that the background will do any further processing that is no longer time critical after the immediate needs have been met. If all you need to do is restart a timer and toggle an LED or pulse another device, the ISR can usually do it all in the foreground safely. Otherwise it needs to inform the background (by setting a flag, or queuing a message) that something needs doing, and release the processor.

I have seen very simple program structures whose background loop is just an idle loop: for(;;){ ; }. All of the work was done in the timer ISR. This can work when the program needs to repeat some constant operation that's guaranteed to finish in less than a timer period; certain limited kinds of signal processing come to mind.

Personally, I write ISRs that clean up an get out, and let the background take over anything else that needs doing, even if that's as simple as a multiply and add that could be done in a fraction of a timer period. Why? Someday, I'll get the bright idea to add another "simple" function to my program, and "heck, it'll just take a short ISR to do it" and suddenly my previously simple architecture grows some interactions I hadn't planned on and happen inconsistently. Those aren't much fun to debug.


(Previously posted comparison of two kinds of multi-tasking)

Task switching: Pre-emptive MT takes care of task switching for you including ensuring no thread gets CPU-starved, and that high priority threads get to run as soon as they are ready. Cooperative MT requires the programmer to make sure no thread keeps the processor for too long at a time. You'll also have to decide how long is too long. That also means means that whenever you modify the code, you'll need to be aware of whether any code segment now exceeds that time-quantum.

Protecting non-atomic operations: With a PMT, you'll have to make sure thread swaps don't occur in the middle of operations that must not be divided. Reading/writing certain device-register pairs that must be handled in a particular order or within a maximum amount of time, for instance. With CMT it's pretty easy - just don't yield the processor in the middle of such an operation.

Debugging: Generally easier with CMT, since you plan when/where thread switches will occur. Race conditions between threads and bugs related to non thread-safe operations with a PMT are particularly hard to debug because thread changes are probabilistic, so not repeatable.

Understanding the code: Threads written for a PMT are pretty much written as if they could stand alone. Threads written for a CMT are written as segments and depending on the program structure you choose, may be harder for a reader to follow.

Using non thread-safe library code: You'll need to verify that each library function you call under a PMT thread-safe. printf() and scanf() and their variants are almost always not thread-safe. With a CMT, you'll know that no thread change will occur except when you specifically yield the processor.

A finite state machine-driven system to control a mechanical device and/or track external events are often good candidates for CMT, since at each event, there's not much to do - start or stop a motor, set a flag, choose the next state, etc. Thus, state-change functions are inherently brief.

A hybrid approach can work really well in these kinds of systems: CMT to manage the state machine (and therefore, most of the hardware) running as one thread, and one or two more threads to do any longer running computations kicked off by a state change.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your reply, JRobert. But it's not tailored to my question. It compares preemptive OS vs. non-preemptive OS, but it doesn't compares non-preemptive OS vs. non-OS. \$\endgroup\$
    – hailang
    Commented Aug 11, 2013 at 19:31
  • \$\begingroup\$ Right - sorry. My edit should address your question better. \$\endgroup\$
    – JRobert
    Commented Aug 12, 2013 at 15:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.