Embedded Expertise

Task Priorities in FreeRTOS – Part 1: The Essential Three

Many FreeRTOS beginners grapple with a practical question: how many task priorities are “enough”? Documentation and tutorials often focus on technical details, leaving this crucial good-practice consideration unanswered. This series tackles this very question, guiding you from the essential minimum to strategic priority use.

A Message From Max

QuoteDear Embedded Experts –

I’m developing an embedded application using FreeRTOS and I’m currently defining task priorities. I’ve noticed a wide range in the number of priorities used in various examples and reference libraries. Some suggest 3-5 priorities, while others showcase applications with over 20.

I understand FreeRTOS itself doesn’t enforce a specific number, but I’m unsure about the best approach for my application. Considering factors like responsiveness, code complexity, and future maintainability, could you advise on how to determine a suitable number of task priorities for my project?

For reference, my application is basically a data pump: it reads from one protocol, extracts the payload, and forwards to another protocol. There are also a few ancillary tasks such as tracing and logging the operations.

Max, you’re absolutely right! Many developers, especially those new to FreeRTOS, find task priorities a bit of a head-scratcher. The good news is that you’re proactive in figuring out the right number for your application before diving in. That’s a good first step!

While we won’t be providing a specific recommendation for your data pump application just yet, this series will equip you with a framework of thought. We’ll explore best practices that help you balance responsiveness, code clarity, and future maintainability. This framework will guide you towards determining the ideal number of priorities for your specific needs – reading from one protocol, extracting data, and forwarding to another, with some logging on the side. Let’s dive in!

Short Answer For The Impatient

  • 3: A reasonable minimum
  • 5-6: Typical, perfect for most applications
  • 10+: A recipe for a maintenance disaster

 

Curious to know a bit more? Read on.

How Scheduler and Priorities Work Together

The priority of a task is assigned during creation and determines which task gets to run next when the scheduler makes its decision. So, task priorities are indeed evaluated by the scheduler. But when is the scheduler run?

Task scheduling in FreeRTOS primarily occurs at tick time. FreeRTOS utilizes a hardware timer to generate periodic interrupts at a configurable frequency, often referred to as “ticks.” While the default tick rate is 1 millisecond (ms), you can adjust it based on your application’s needs.

During each tick interrupt, the FreeRTOS scheduler wakes up and performs the following actions:

  1. Checks for Ready Tasks: It examines all tasks and identifies those that are currently in the “Ready” state, meaning they are available to run on the CPU.
  2. Selects Highest Priority Ready Task: Based on the pre-defined priorities, the scheduler chooses the task with the highest priority from the pool of ready tasks. It’s important to note that tasks with the same priority level are typically scheduled in a round-robin fashion (this can be configured though).
  3. Schedules Selected Task: The chosen task is then switched into the “Running” state, allowing it to execute on the CPU.

 

It’s important to note that tick interrupts are not the only way the scheduler can be triggered. A task can also voluntarily relinquish control of the CPU and request a reschedule by calling the taskYIELD() function. This can be useful in situations where a task doesn’t need to utilize the entire allocated time slice or wants to give higher priority tasks a chance to run.

Therefore, the tick interrupt acts as a primary trigger for the scheduler to evaluate task priorities and determine which task should be running next. Additionally, tasks themselves can influence scheduling through taskYIELD(). Understanding this core functionality will be crucial as we delve into the optimal number of task priorities in future articles.

Task Trio: Classifying Tasks and Setting Priorities Effectively

Now that we understand how FreeRTOS uses task priorities, let’s revisit Max’s initial question: how many priorities do we actually need?

There isn’t a one-size-fits-all answer, but most applications can be categorized into three main task types based on their real-time requirements:

  1. Hard Real-Time Tasks: These tasks have strict deadlines that must be met under all circumstances. Missing a deadline could have critical consequences for your system’s functionality. Examples include timestamping an event or maintaining communication timeouts. These tasks typically require the highest priority in FreeRTOS to ensure they are preempted only bysystem events (interrupts, inc. the tick).

  2. Background Tasks: On the other end of the urgency spectrum, these lowest priority tasks can tolerate some delays without impacting functionality. They often handle background activities like housekeeping chores, power management or watchdog operations. The idle task provided by FreeRTOS falls into this category. It’s the lowest priority task that gets executed only when no other tasks are ready to run.

  3. Normal Application Tasks: All the rest: These tasks represent the bulk of your application logic and typically don’t have strict real-time constraints. They might handle user interface updates, data processing, communication protocols, or other application-specific functionalities. In many cases, these tasks can share a common mid-range priority. The FreeRTOS scheduler will ensure a fair share of CPU time for all tasks at this level, balancing responsiveness with efficient resource utilization.

There’s one thing that can’t be understated: By identifying a task as low urgency and assigning it a low priority, you actually improve the responsiveness of the rest of the system. Low priorities do matter!

By understanding these different task categories and their real-time requirements, we can start to define a suitable number of task priorities for your specific application. In the next section, we’ll explore different approaches to setting task priorities and avoiding common pitfalls.

From Theory to Code: Defining Task Priorities with C Macros

Based on the three task categories we have introduced before, let’s solidify our priority definitions with some concrete values. A convenient place to do so is in your FreeRTOSConfig.h file:

The last line defines the maximum number of priority levels that can be used within your FreeRTOS system. It’s probably already defined in your FreeRTOSConfig.h and should be adjusted there, i.e. not redefined. It’s important to consider that managing a large number of priorities can introduce overhead. The higher the number of  priorities, the longer the switching time. Additionally, a larger number of priorities might slightly increase memory usage.

The focus of this series is on using a minimal effective number of priorities for your specific application, rather than necessarily utilizing the entire configurable maximum. By following these guidelines, you can ensure efficient task management and a simple yet well-structured FreeRTOS system.

// Idle task has lowest priority  (0) and is already defined somewhere else
#define configPRIORITY_BACKGROUND (1) // Low priority tasks, lowest urgency
#define configPRIORITY_NORMAL (2) // Normal application tasks, mid-range prio
#define configPRIORITY_RT (3) // Hard real-time tasks, strictest deadlines
#define configMAX_PRIORITIES (4)

Remember, this is just a starting point. Purists may want to define the priorities in a more cryptic way with formulas like:

#define configPRIORITY_NORMAL (configMAX_PRIORITIES / 2)

This automatically defines the normal priority as midrange between the highest and lowest possible values.

Instead, I have decided to provide hardcoded values for the sake of clarity.

How to Use Defined Priority Levels

Now that we’ve established our priority levels, let’s see how to apply them when creating tasks.

Key point: Always use the defined macros (e.g., configPRIORITY_NORMAL) when setting task priorities. Avoid hardcoding values directly to improve code readability and maintainability. Calculations based on existing macros are also discouraged as they can lead to unexpected issues in the future.

Example: Creating a Task with Normal Priority

Here’s an example of creating a task with the configPRIORITY_NORMAL priority:

#include "FreeRTOS.h"
#include "task.h"


void myTask( void * pvParameters ) {
// Task code here
}

void createMyTask( void )
{
xTaskCreate( myTask, "MyTask",
configMINIMAL_STACK_SIZE,
NULL,
configPRIORITY_NORMAL,
NULL );
}

In this example:

  • myTask is the function that will be executed by the task.
  • configMINIMAL_STACK_SIZE is a FreeRTOS constant for the minimum stack size.
  • configPRIORITY_NORMAL is the macro we defined earlier.

 

By using configPRIORITY_NORMAL, we ensure that the task is created with the correct priority level with good readability.

Remember: Consistent use of priority macros enhances code clarity and simplifies future modifications. Avoid hardcoding values or using calculations based on existing macros.

Wrap-Up

By categorizing tasks into hard real-time, background, and normal application tasks, we’ve established a foundation for defining task priorities in FreeRTOS. This simple approach suggests using three distinct priority levels: high for real-time tasks, low for background tasks, and a middle ground for the majority of application logic.

This three-tier system offers a straightforward, efficient, and resource-friendly solution for many embedded systems. By leveraging the FreeRTOS scheduler, you can effectively manage task execution without the complexity of numerous priority levels.

While this approach is suitable for a wide range of applications, there may be scenarios where additional priority levels could be beneficial. In upcoming articles, we’ll explore these situations and discuss how to introduce extra levels carefully to avoid performance implications.

By adhering to these guidelines and consistently using defined priority macros, you’ll create well-structured, maintainable, and efficient FreeRTOS-based applications.