Real-Time Systems and Concurrency: A Developer’s Guide

When it comes to embedded systems, especially in fields like industrial control or medical devices, getting your code to run efficiently, predictably, and safely is paramount. You’ve likely put in the hard work to design detailed, robust systems, but without a solid grasp of real-time concepts and concurrency, your efforts might not achieve the desired reliability. This is where understanding Real-Time Operating Systems (RTOS) like Zephyr comes into play.

At Emtech, we build a lot of embedded systems, so I’d like to share and explore why real-time systems and concurrency are crucial for embedded developers and how they can help you build better, more reliable products.

Understanding the Basics: What is a Real-Time System?

A real-time system isn't necessarily a fast system; it's a system that guarantees a response within a predictable timeframe. This predictability is what separates a real-time system from a general-purpose one. There are two main categories:

  1. Hard Real-Time Systems: In these systems, missing a deadline is a critical failure. Think of a car's airbag system, a pacemaker, or an industrial motor controller. If these systems act even a millisecond too late, the consequences can be disastrous. A task must be serviced by its deadline; service after the deadline is equivalent to no service at all.
  2. Soft Real-Time Systems: Here, missing an occasional deadline is acceptable, though not ideal. Examples include audio playback, weather stations, and home automation applications. A slight delay in these systems won't cause a catastrophe, but it might degrade the user experience.

It’s Not All Multitasking! Understanding Concurrency

Concurrency is the ability of a system to manage multiple tasks that appear to run simultaneously. This is a key concept because in a microcontroller with a single CPU, you aren't achieving true parallelism like you would on a multi-core desktop processor. Instead, the system is rapidly switching between tasks, creating the illusion of parallel execution. For example, you might have one task reading a sensor every 10 milliseconds, another detecting a button press, and a third transmitting status information. These tasks are concurrent, not parallel.

Concurrency is typically implemented through:

  • Tasks and Threads: The basic units that contain the logic for a specific function.
  • Interrupts: Often triggered by peripherals like timers or GPIO pins.
  • Scheduler: The core of the RTOS that decides which task to execute at any given moment.
  • Synchronization Mechanisms: Tools that manage shared resources and communication between tasks.

However, sharing resources introduces risks like race conditions, data inconsistencies, and priority inversion. To manage these risks, we use synchronization tools like semaphores, mutexes, message queues, and events.

Key Strategies for Real-Time Scheduling

The scheduler is the heart of the RTOS, organizing tasks and managing the CPU. The fundamental criterion for scheduling is priority: each task has a numerical priority, and the scheduler always executes the highest-priority ready task. In Zephyr, lower numbers represent higher priority (whereas in other RTOS, such as FreeRTOS, the convention may be the opposite).

But what happens when tasks have the same priority?

  • FIFO (First-In, First-Out): Also known as scheduling by arrival order or FCFS (First Come, First Served), tasks are executed in the order they are created.
  • Round Robin: Similar to FIFO, but it uses time-slicing. Each task gets a "turn" or a quantum of CPU time (e.g., 50ms) before the scheduler moves to the next task.
Figure 1: Graphical representation of FIFO (or FCFS) and Round-Robin

We also distinguish between two main scheduling modes:

  1. Non-preemptive (Cooperative) Scheduling: In this model, a running task cannot be interrupted by another. The task itself must voluntarily yield control. This is suitable for applications like a motion sensor where a slight delay is not critical. In the provided example, cooperative scheduling resulted in a task designed to run every 2 seconds actually running every 3 seconds due to a long-running, lower-priority task. An example is the FIFO scheduling algorithm
  2. Preemptive Scheduling: The scheduler can interrupt a running task to allow a higher-priority one to execute. This is essential in hard real-time systems such as a pacemaker, which must respond immediately. For example, a low-priority task may be interrupted periodically every 2 seconds so that a critical task can run on time. Algorithms such as priority-based preemptive or round robin are commonly applied in this model.

Zephyr RTOS: Small but Powerful

Zephyr is a lightweight, modular, and real-time operating system optimized for embedded systems. We've been increasingly porting our projects to this platform at Emtech because of its practicality and robust features.

Key features include:

  • Modular and Scalable Architecture: You only compile the modules you need, which significantly reduces the memory footprint.
  • Multi-platform Support: Develop code for one microcontroller (e.g., ST) and easily migrate it to another (e.g., ESP32, NXP) with minimal code changes. This is a major advantage over systems like ChibiOS, which are often vendor-specific.
  • Preemptive Multitasking: Supports multiple threads with different priorities.
  • Rich Set of Drivers and Libraries: Includes support for GPIO, I2C, SPI, USB, Bluetooth, and protocols like MQTT, LoraWAN, and TCP/IP.
  • Linux-like Ecosystem: Uses Kconfig and CMake for configuration and building, which is familiar to Linux developers.

A Zephyr application is structured by combining hardware descriptions, system configuration, and multithreaded code.

1. DeviceTree: Describes the hardware, including the board (Figure 2), pins, peripherals (LEDs, UARTs), and any connected sensors. It allows you to describe your hardware in a standardized way, making the code portable across different boards. At compile time, a devicetree_generated.h file is created with macros that your C code can use.

Figure 2: Some of the supported boards and brands by ZephyrOS
Figure 3: defining an output on the device tree
Figure 4

2. Kconfig: Activates or deactivates RTOS functions, drivers, and other features. Since Zephyr is modular, this lets you enable only what you need, like UART or I2C support.

3. CMake/West: This is the build system. west is a tool that compiles, flashes, and administers the project.

Figure 5: compiling a project with the STM32 nucleo G474re as target

4. main() / Threads: This is the entry point for your program, where you can either use the main() function or create your own tasks.

Figure 6: blinking led example

A Practical Example: Producer-Consumer with Message Queues

To demonstrate synchronization, let's look at a classic producer-consumer problem.

  • A producer task generates data at its own pace.
  • A consumer task waits for data to become available and then processes it.

This can be easily implemented in Zephyr using a message queue (k_msgq) which acts as a buffer. The producer generates data and places it in the queue, while the consumer blocks until data is available for processing. This is a clean and efficient way to handle data transfer between tasks.

The code is straightforward: you create two threads (producer and consumer) and a message queue of a specific size. The producer loop generates data and sends it to the queue, while the consumer loop waits to receive data before processing it. This ensures that the tasks are synchronized and data is not lost.

Best Practices for Concurrent Systems

To wrap up, here are some best practices to keep in mind when designing concurrent embedded systems:

  1. Keep Tasks Short and Specific: Avoid "do-it-all" tasks. Break down functionality into smaller, single-responsibility threads. For instance, instead of one task to read, process, and send sensor data, use three separate tasks. This makes the system more scalable and responsive.
  2. Avoid Prolonged Blocking: Don’t use while(1) loops for busy waiting. Use RTOS synchronization primitives like k_sleep, semaphores, or queues to yield the CPU and allow other tasks to run.
  3. Minimize Shared Global Variables: Sharing data introduces risks. Use communication structures like message queues (k_msgq), FIFOs (k_fifo), or semaphores (k_sem) for synchronization.
  4. Use Interrupts Responsibly: Keep Interrupt Service Routines (ISRs) as short as possible. Never call blocking functions from within an interrupt. A common mistake for beginners is to wait for multiple bytes in a UART ISR; instead, use a flag and let a task handle buffering.

Final Thoughts: Building Better Embedded Systems

In the world of embedded development, mastering real-time and concurrency concepts is not just an option—it's a necessity for building efficient and reliable systems. By leveraging a powerful RTOS like Zephyr and following best practices, you can manage complexity and create robust applications that meet even the strictest timing requirements. As one of our engineers noted, the ease of switching between different hardware platforms with Zephyr is excellent, making development much more flexible. It truly simplifies the process of building sophisticated embedded solutions.

Bibliography

  • Operating Systems Internals and Design Principles. W. Stallings 7th Ed

Written by Alejandro Casanova

Edited by Adrián Evaraldo

For further inquiries, contact us: info@emtech.com.ar