Introduction

“A real-time operating system (RTOS) is an operating system (OS) intended to serve real-time application process data as it comes in, typically without buffering delays.” (wikipedia.org)

Key factors in an RTOS are minimal interrupt latency and minimal thread switching latency. An RTOS is valued more for how quickly or how predictably it can respond than for the amount of work it can perform in a given period of time.

For embedded devices, the general rule is that an RTOS is used when the application needs to do more than a few simple actions. An RTOS allows an application to be structured in a manner that scales as more application/system features are added (e.g. communication stacks, power management, etc.).

A RTOS has the following goals

  • Small latency: It is real-time after all!
  • Determinism: Again, it is real-time. You need to know how long things take to process to make sure deadlines are met.
  • Structured Software: With an RTOS, you are able to divide and conquer in a structured manner. It's straight-forward to add additional components into the application.
  • Scalability: An RTOS must be able to scale from a simple application to a complex one with stacks, drivers, file systems, etc.
  • Offload development: An RTOS manages many aspects of the system which allows a developer to focus on their application. For example an RTOS, along with scheduling, generally handles power management, interrupt table management, memory management, exception handling, etc.

In this workshop we'll cover general RTOS topics. The SimpleLink™ software development kit (SDK) supports both TI-RTOS and FreeRTOS. It also supports POSIX APIs on top of either RTOS. We'll use POSIX APIs below as a concrete example of an RTOS but the concepts are applicable for both RTOS offerings.

Here's what we'll learn:

  • Basic understanding of RTOS terms
  • Basic understanding of an RTOS scheduler

Terminology

Let's first standardize on some key definitions. It's assumed that you have a basic understanding of embedded processing in regards to knowing what an interrupt is, what a stack is, etc.

Standard types of threads

For this workshop, we are going to use the term thread as a generic term for any execution block. Here are the typical threads in every RTOS-based application.

  • Interrupt Service Routine (ISR): Thread initiated by a hardware interrupt. An ISR runs to completion. ISRs all share the same stack.
  • Tasks: Thread that can block while waiting for an event to occur. Tasks are traditionally long-living threads (as opposed to ISRs which run to completion). Each task has it's own stack which allows it to be long-living.
  • Idle: Lowest priority thread that only runs when no other thread is ready to execute. Generally Idle is just a special task with the lowest possible priority.

Schedulers

Every RTOS has a scheduler at its core. The scheduler is responsible for managing the execution of threads in the system. There are two main ways a scheduler manages this:

  • Preemptive Scheduling: This is the most common type of RTOS scheduler. With a preemptive scheduler, a running thread continues until it either

    • finishes (e.g. an ISR completes)
    • a higher priority thread becomes ready (in this case the higher priority thread preempts the lower priority thread)
    • the thread gives up the processor while waiting for a resource (e.g. a task calls sleep()).

    Both TI-RTOS and FreeRTOS have preemptive schedulers. This workshop will focus on preemptive schedulers.

  • Time-slice Scheduling: This type of scheduling guarantees that each thread is given a slot to execute. This type of scheduling is generally not conducive to real-time application. The TI-RTOS kernel supports time-slicing scheduling with Tasks if desired.

Other key terms

  • Thread-safe: A piece of code is thread-safe if it manipulates shared data structures in a manner that guarantees correct access (reading/writing) by multiple threads at the same time. Please note, thread-safety is not just an RTOS issue (e.g. interrupts modifying the same memory must be careful).
  • Blocked: A task is blocked if it is waiting on a resource and not consuming any of the CPU. For example, if a task calls Task_sleep() or Semaphore_pend() (with a non-zero timeout and the semaphore is not available), the task is blocked and another thread is allowed to run. Note: spinning on a register in a tight loop is not blocking…that’s polling.
  • Bare-metal: Common name for an application that does not use an RTOS.

Bare-Metal vs. RTOS

Let's look a little more closely at a typical bare-metal application. These applications can typically be broken down into three key pieces

  • Initialization: Initializing the hardware and software components in main().
  • Super-loop state machine: Code to manage the application. The actions are based on the results of interrupts (e.g. a SPI packet was received or a timer expired) or polling.
  • ISRs: Code executed by interrupts for peripherals (e.g. UART), timers or other device-specific items (e.g. exceptions or multi-core communication).

Here's a pictoral view of these key pieces.

Bare-metal applications have their place. They are generally small, fast, and relatively easy to understand with a simple application. Once more complicated logic is required, an RTOS starts to shine.

Let's look at a comparison of a bare-metal application to a minimal RTOS application (and then to a more traditional RTOS application). As you can see, the three key pieces we talked about before (init, super-loop, and ISRs) are basically the same between a bare-metal application and a minimal RTOS application. However, with the minimal RTOS application, you now have the stepping off point to a much more complex application that allows multiple developers to add their content without dealing with a potentially fragile super-loop.

RTOS Components

Here are some of the main components of an RTOS. The SimpleLink SDK offers all of these features for both TI-RTOS and FreeRTOS.

  • Scheduler: Preemptive scheduler that guarantees the highest priority thread is running.
  • Communication Mechanism: Semaphores, Message Queues, Queues, etc.
  • Critical Region Mechanisms: Mutexes, Gates, Locks, etc.
  • Timing Services: Clocks, Timers, etc.
  • Power Management: For low power devices, power management is generally part of the RTOS since it knows the state of the device.
  • Memory Management: Variable-size heaps, fixed-size heaps, etc.
  • Peripheral Drivers: UART, SPI, I2C, etc.
  • Protocol stacks: BLE, WiFi, etc.
  • File System: FatFs, etc.
  • Device Management: Exception Handling, Boot, etc.

POSIX Support in SimpleLink SDK

POSIX is an IEEE industry API standard for OS compatibility. The SimpleLink SDK has both TI-RTOS and FreeRTOS support; however it also offers POSIX support on top of either of these RTOSes. This allows applications to be independent of the underlying RTOS.

The POSIX APIs in the SimpleLink SDK are a small shim on top of the underlying RTOS. When a POSIX Pthread is created, an underlying TI-RTOS (or FreeRTOS) Task is created. Similarly, when a POSIX Pthread semaphore is created, an underlying TI-RTOS (or FreeRTOS) semaphore is created.

A nice feature of POSIX support is the ability to grab POSIX-based code from the web and quickly get it to work.

For a more detailed description of the POSIX support in SimpleLink SDKs, please refer to the POSIX Overview Workshop.

POSIX is not an RTOS.

It is a OS compatibility layer to allow an application to port easily between operating systems.

RTOS Threads Communication

All RTOSes offer standard communication mechanisms like semaphores, mutexes, message queues, linked lists, etc. Let's look a little closer at a couple of these...

Semaphore

A semaphore allows resource management. A task can block on a sem_wait() until a resource becomes available (via a sem_post()). A common use case is for a Hwi to receive data and post a semaphore so a task can process it. This is desirable because it minimizes the duration of the interrupt.

Most RTOS's supports both binary and counting semaphores.

Message Queue

Message Queues are useful for sending data between threads. Message Queues can be configured to send/receive user-defined messages of any size. Here, a task is sending a message to another task:

Message Queues are useful when you want to centralize a specific functionality into a single task. All other threads can send messages to the centralized task for processing. The message queue handles the messages in a thread-safe manner.

Please note that message queues in the POSIX support layer of the SimpleLink SDK are built on top of Mailboxes in TI-RTOS and Queues in FreeRTOS.

Execution

Let's see a preemptive scheduler in action. Let's assume the following threads were created in main():

  • ISRX: An interrupt service routine
  • MidA: created first in main()with priority 4
  • MidB: created second in main() with priority 4
  • High: created last in main() with priority 8

Let's look at the following execution graph and talk about what is happening.

Once the kernel's scheduler starts (in this case BIOS_start() in main()), all the tasks are ready to run, however it's the highest task (High) that runs first since it has the highest priority and is ready to run. Here's a description of some of the key transition points noted in the above graph.

  1. ISRX asserts, so it preempts all tasks. High is now in a preempted state.
  2. Once ISRX finished, High starts to run again until it blocks on a Task_sleep() (or some blocking API). Now MidA can run.
  3. MidA runs until it hits a blocking call (say Semaphore_pend()). Now MidB can run.
  4. MidB runs until High unblocks (say the Task_sleep() expired). MidB is now preempted.
  5. High runs until ISRX is asserted and preempts High. Note: there are two tasks preempted now.
  6. MidA becomes ready (say ISRX posted the semaphore it was blocked on). MidA does not run since there is a higher priority thread running.
  7. ISRX finishes, so High runs again and then blocks again, so MidB runs again until it blocks. Now MidA can run since there are no higher priority tasks running. Note: that MidA had to wait until after MidB finished since MidB was running when MidA became ready.
  8. MidA blocks and now there are no threads running or ready to run, so Idle finally runs until...
  9. MidB unblocks and runs.

All of the above context switching is managed by the scheduler in the RTOS.

Additional Resources

Additional training and reference material for RTOS is available in the following places:

  • Refer to SimpleLink_SDK_Install_Dir\docs\Documentation_Overview.html
Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.