When and why should we create multiple tasks instead of single task in RTOS?

I am not able to understand when we should create more than one task. Currently, I write code in a bare-metal style where there is a single loop, and I continuously perform all operations in sequence inside that loop. Whenever there is an urgent event, like receiving data or performing something at a specific time, I use interrupts.

I feel that the same approach can also be followed in RTOS, and there should not be any problem with it. But then, what is the main purpose of creating multiple tasks? How do you decide when more than one task is needed?

If ALL you need to do is gather data and put it somewhere, that is a good use for interrupts.

But, often the task you want to do when something happens is more involved, and may even need to do actions that also want to use interrupts. The problem with code in interrupts is that it can’t “defer” to let something else happen while it waits.

The problem with the “big loop” form of program is that you can only switch operations when an operation returns back to that loop (or you do the operation totally in an interrupt). This means that EVERY operation done in the big loop, no matter how important or time sensitive, might be delayed by the longest ANY task might take. You effectively put all your “tasks” at a similar level of priority, which is normally not correct.

Normally, I make a task for every distinct asynchronous event that can come in, plus tasks for “periodic” operations (not every periodic necessarily needs its own task).

What you are explaining can be better understood if we apply it to a simple system. For example, we have an LED that should blink at different rates: 250 ms, 500 ms, and 1 second, based on commands received from UART (high, middle, low). When a character is received, we handle it using an interrupt, and then we process it outside the interrupt to determine whether the command is low, middle, or high.

Also, there is a timing requirement (deadline). For example, once a command is received, the system should update the LED blinking rate within, say, 2–5 ms. After that, the LED must continue blinking accurately at the selected interval (250 ms, 500 ms, or 1 second) without missing the timing. So the system should not miss this response time or disturb the blinking pattern.

In this kind of system, how many tasks would you typically create?

How about something like the following:

TickType_t gPeriod = pdMS_TO_TICKS( 250 );
QueueHandle_t gCommandQueue;
TaskHandle_t gBlinkyTaskHandle;

typedef enum
{
    CMD_PERIOD_LOW,
    CMD_PERIOD_MED,
    CMD_PERIOD_HIGH
} Command_t;

void BlinkyTask( void * param )
{
    TickType_t localPeriod  = pdMS_TO_TICKS( 250 );

    ( void ) param;

    for( ;; )
    {
        ToggleLED();

        // Assuming that TickType_t read is not atomic, it needs to be read
        // in a critical section.
        taskENTER_CRITICAL();
        {
            localPeriod = gPeriod;
        }
        taskEXIT_CRITICAL();

        vTaskDelay( localPeriod );
    }
}

void UartCommandhandlerTask( void * param )
{
    Command_t receivedCommand;
    TickType_t updatedPeriod  = pdMS_TO_TICKS( 250 );

    ( void ) param;

    for( ;; )
    {
        xQueueReceive( gCommandQueue, &( receivedCommand ), portMAX_DELAY );

        if( receivedCommand == CMD_PERIOD_LOW )
        {
            updatedPeriod = pdMS_TO_TICKS( 1000 );
        }
        else if( receivedCommand == CMD_PERIOD_MED )
        {
            updatedPeriod = pdMS_TO_TICKS( 500 );
        }
        else
        {
            updatedPeriod = pdMS_TO_TICKS( 250 );
        }

        // Assuming that TickType_t write is not atomic, it needs to be written
        // in a critical section.
        taskENTER_CRITICAL();
        {
            gPeriod = updatedPeriod;
        }
        taskEXIT_CRITICAL();

        // If the blinky task is in blocked in vTaskDelay, abort it, so that
        // the period is updated on the next run.
        xTaskAbortDelay( gBlinkyTaskHandle );
    }
}

First, simple examples like this tend not to be a good basis to understand the design methodology. They can be to learn the implementation methodology. If this is the whole system, I would say that the use of an RTOS is overkill, which is why it doesn’t make a good learning platform for design methodology.

Next, “Blinking lights”, by their nature tends not to be critical in timing. 10 millisecond timing shift will not be noticable by a person.

For your case, taking this timing into account, I would have ONE task as the UART command receiver, and use the provide timer task to trigger a timer callback initiated and configured by the command task. This is reasonable, as the procedure to turn on and off the LED are quick and proper for such a callback, and the timer task, typically having top priority will have low jitter.

If you want VERY low jitter, I would move the LED blinking to an ISR triggered by a timer and that would drop the jiter to and order of maybe a few dozen instructions or so (depending on lengths of critical sections).

If you want EXTRAMELY low jitter (sub processor clock), I would design it to use a hardware timer to perform the blinking directly, and the command task manipulates the timer control registers.

If we want to be able to expand the complexity of the blinking, it might make sense to not use timer callbacks, and use an actual task to do that blinking, or if the LED control is outside the processor, but needs some I/O (like an I2C bus) to manipulate.

Your concern about being “disturb” has the problem that your system has nothing to actually disturb your LED operation. Even if we build a single task that both processes the commands and blinks the LED, the command processing operation is enherently quick enough to not be able to “disturb” the blinking to human perception unless you are using some form of pathological encoding.

Yes, I totally agree with you. Simple examples help with implementation but not with understanding system design.

For learning the design approach, what specific project would you recommend where the system must react to an input within a strict deadline, and missing that timing is not acceptable?

If we finalize one such project, we can then discuss it further to explore different aspects of system design in depth.

That is the problem. There really is a specific single project that is really idea, but it depends on the knowledge base of the learner and the teacher, and in their interest. You also want something that can actually be implemented, at least at scale, though one option is a dual computer setup where one machine simulates the environment that the first one is trying to control.

Robotics can be one field to work in, although it does tend to lead to more looking at being “smart” rather than being fast and responsive.

“Control Systems” can be another, but normally only if something more that a simple control-law system, which more needs just fast enough processing, but not the prioritization provided by an RTOS. There is a somewhat different problem of dealing with a single system trying to handle multiple control laws at different rates with differing computation loads. That is more of doing the design work to make sure you have enough resources to get the jobs done in time.

I understand that an RTOS is not strictly required for real‑time behavior it is mainly used to manage complexity. The decision to use or avoid an RTOS really depends on the system’s complexity and requirements.

I understand that a single loop with interrupts can handle real‑time requirements as long as we can guarantee timing. The complexity starts when we have multiple activities running at different rates

Would something like a drone flight controller make sense as a use case for this?

What I’m trying to do is identify a project that helps in understanding this design approach, not just implementation details.

I’m not planning to implement something like this fully (I know it’s not an easy system), but I want to use it as a reference to understand how complex systems are structured especially how different tasks, timing requirements, and resource limits are handled together.

There are numerous books out there that exclusively deal with concurrent software design on RTOS. May be worth investing in one of those before you reinvent the wheel.

How did you learn system designing ?

Did you first learn how to use the RTOS APIs with simple examples (like LED or basic tasks), and then later learn actual system design while working on more complex systems in a company?

My background is that I learned basic theory in university that provided basic ground rules. True understand came by applying that to actual problems.

I you want to try to develop some understanding of the principles, start by learning the guidelines, and then create “toy” applications where you use those guidelines, even if you think you could do better for this problem with something different. Then you can stop and think about what complications might have made the guidelines apply.

You comments so far have shown that you seem to think that the methods you have developed for those simple application are still “good”, and looking for someone to show you a SIMPLE case where they aren’t. The problem is the guidelines don’t necessarily give “wins” in the simple cases, and many of them really don’t need an RTOS, so they are not applicable.
Instead, think about applying the normal guidelines, even on the simple cases where they don’t show the big wins, but then try to imagine a much harder case (that you aren’t going to try to actually implement) and see how your simple non-RTOS ideas fail, but the guidelines provide a safety net.

Multiple tasks allow the software designer to decouple different processes in software systems.

A “superloop” design that uses a single thread of execution plus interrupts can work for even fairly complex software, but every action the software takes at thread level is coupled by the outer loop.

Separate tasks or threads allows each process to be largely self-contained, and is very good for event driven execution. Semaphores, Signals, Task Notifications, Queues, and software Timers, along with interrupts and exceptions allow a task to only be unblocked when it has something to do, reducing power use and improving responsiveness. This is especially valuable for asynchronous full-duplex communication and real-world interactions.