Skip to main content

How to Make a Shared Utility Function Safe for RTOS Tasks

 

A utility function is often created with one simple goal:

Write the code once and reuse it everywhere.

For example:

convert_temperature();

calculate_crc();

send_message();

update_statistics();

This works well in a simple program.

But an RTOS system is different.

Multiple tasks may call the same function.

For example:

Sensor Task

      ↓

Shared Utility Function

      ↑

Communication Task

      ↑

Control Task

If the utility function uses shared data internally, the tasks can interfere with each other.

The function may work correctly when only one task calls it.

But it may fail when two tasks call it at almost the same time.

This article explains how to make shared utility functions safer for RTOS-based embedded systems.


Quick Answer

A shared utility function is safest when it:

  • Uses local variables where possible

  • Receives input through parameters

  • Returns results directly

  • Avoids hidden global state

  • Protects truly shared resources

  • Uses the correct RTOS synchronization mechanism

  • Clearly defines who owns shared data

The best solution is not always "add a mutex."

Often, the best solution is to redesign the function so there is less shared state.


First Understand the Problem

Imagine two RTOS tasks.

Task A → Calculate CRC

Task B → Calculate CRC

Both call:

uint32_t calculate_crc(uint8_t *data, uint32_t length);

If the function only uses local variables, both tasks may be able to execute safely.

But now imagine the function uses:

static uint32_t crc_result;

Now both tasks share that variable.

The function has hidden shared state.

That is where concurrency problems can begin.


The Golden Rule: Reduce Shared State

The easiest shared resource to protect is the one you do not share.

Consider this design:

static uint8_t working_buffer[256];

Every caller uses the same buffer.

Now compare it with:

void process_data(uint8_t *input,

                  uint8_t *output,

                  size_t length);

The caller provides the data.

The function does not secretly depend on one global working buffer.

This design is usually easier to:

  • Test

  • Reuse

  • Debug

  • Run from multiple tasks


Step 1: Find Every Shared Resource

Before adding synchronization, inspect the function carefully.

Look for:

  • Global variables

  • Static variables

  • Global buffers

  • Peripheral registers

  • Shared queues

  • Shared counters

  • Shared memory

  • Dynamic memory allocators

  • Communication interfaces

Ask:

Can another task access this resource while this function is using it?

If the answer is yes, investigate further.


Example: Unsafe Shared Counter

uint32_t message_count = 0;


void record_message(void)

{

    message_count++;

}

If multiple tasks call this function, updates can be lost.

The correct solution depends on the system.

Possible approaches include:

  • Atomic operations

  • Critical sections

  • Mutex protection

  • A dedicated owner task

The right choice depends on:

  • Execution context

  • Hardware architecture

  • Performance requirements

  • RTOS behaviour

  • Whether interrupts also access the resource


Step 2: Decide Whether the Data Really Needs to Be Shared

This is an important design question.

Imagine:

static uint8_t temp_buffer[128];

Why is it shared?

Maybe the programmer used a static buffer simply to avoid passing a buffer as an argument.

If every task can provide its own buffer, the design may become simpler.

For example:

void format_message(const uint8_t *input,

                    uint8_t *output,

                    size_t length);

Now each task can use separate memory.

No lock may be required.

This is often better than adding synchronization to unnecessary shared state.


Step 3: Use a Mutex for Shared Task-Level Resources

A mutex is useful when multiple RTOS tasks must access one shared resource.

Example resources:

  • UART

  • Shared configuration data

  • Shared file system

  • Shared external memory

  • Shared device driver

Conceptually:

Task A → Request mutex

Task A → Use resource

Task A → Release mutex


Task B → Wait

Task B → Use resource after release

This prevents simultaneous uncontrolled access.

However, keep the protected section as small as reasonably possible.

Do not lock a resource and then perform unrelated work for a long time.


A Simple Mutex Pattern

Conceptually:

lock();


access_shared_resource();


unlock();

The exact API depends on your RTOS.

The important idea is:

Acquire protection before accessing shared state and release it reliably afterward.

Also consider error paths.

A common programming mistake is:

Lock resource

    ↓

Error occurs

    ↓

Function returns

    ↓

Resource never unlocked

This can cause other tasks to wait indefinitely.


Step 4: Do Not Use One Lock for Everything

A common beginner mistake is creating one giant mutex for the entire application.

For example:

Global Application Mutex

Every task waits for it.

This may reduce concurrency so much that the system becomes slow.

Instead, think about resource ownership.

For example:

UART Mutex

SPI Mutex

Configuration Mutex

Log Mutex

Only protect resources that actually require protection.

But do not create locks unnecessarily either.

Good concurrency design is about balance.


Step 5: Avoid Holding Locks During Slow Operations

Imagine:

Lock

   ↓

Read sensor

   ↓

Wait for communication

   ↓

Perform long calculation

   ↓

Unlock

Other tasks may be blocked for a long time.

A better design may be:

Lock

   ↓

Copy required shared data

   ↓

Unlock


Perform long processing without the lock

The less time spent holding a shared lock, the more responsive the system can be.


Step 6: Understand Deadlocks

A deadlock can happen when tasks wait for each other forever.

Imagine:

Task A holds Resource 1

Task A waits for Resource 2


Task B holds Resource 2

Task B waits for Resource 1

Neither task can continue.

This is a deadlock.

A common prevention strategy is defining a consistent locking order.

For example:

Always lock Resource A before Resource B

Everywhere in the program should follow the same order.


Step 7: Think About Priority

RTOS systems may run tasks with different priorities.

Imagine:

High Priority Task → Needs shared resource

Low Priority Task → Currently owns resource

The high-priority task may have to wait.

Some RTOS mutex implementations support mechanisms such as priority inheritance to help address priority-related blocking problems.

The exact behaviour depends on the RTOS.

This is why developers should understand the synchronization mechanism provided by their specific system rather than treating every lock as identical.


Step 8: Never Treat Task Code and ISR Code as the Same

This is extremely important.

A normal RTOS task may:

  • Wait

  • Sleep

  • Block

An interrupt handler has different requirements.

An ISR generally needs to finish quickly.

Do not automatically use a normal task-level synchronization API inside an ISR.

Many RTOS platforms provide separate mechanisms for interrupt-safe operations.

A common architecture is:

Interrupt

   ↓

Capture event quickly

   ↓

Notify RTOS task

   ↓

Task performs larger work

This improves responsiveness and reduces complexity.


Shared Utility Function Design Example

Poor Design

static uint8_t buffer[256];

static uint32_t result;


uint32_t process_data(uint8_t *data, uint32_t length)

{

    /* Uses shared buffer */

    /* Stores result globally */


    return result;

}

Problems:

  • Hidden shared buffer

  • Hidden shared result

  • Multiple callers may interfere


Better Design

uint32_t process_data(const uint8_t *data,

                      uint32_t length,

                      uint8_t *output)

{

    uint32_t result = 0;


    /* Process caller-provided data */


    return result;

}

Benefits:

  • Local result

  • Caller controls output memory

  • Fewer hidden dependencies

  • Easier concurrent use


Sometimes a Dedicated Task Is Better Than a Mutex

Suppose many tasks need to use one hardware peripheral.

Instead of allowing every task to access the hardware directly, create one owner task.

For example:

Sensor Task ──────┐

Control Task ─────┼──→ Queue ───→ Communication Task

Logging Task ─────┘                       ↓

                                       UART/SPI

Only one task owns the hardware.

Other tasks send requests.

This architecture can provide:

  • Clear ownership

  • Easier debugging

  • Fewer direct conflicts

  • Better separation of responsibilities


Choose the Correct Concurrency Tool

There is no single solution for every situation.

Use local data when:

The data does not need to be shared.

Use a mutex when:

Multiple tasks need controlled access to a shared resource.

Use an atomic operation when:

A small operation must be performed safely and atomically.

Use a queue when:

Tasks need to pass messages or data.

Use a semaphore or event mechanism when:

One execution context needs to signal another.

Use a dedicated owner task when:

A complex shared peripheral should have one clear controller.


A Practical Checklist Before Sharing a Function

Before allowing multiple RTOS tasks to use a function, ask:

1. Does the function modify global data?

If yes, determine whether concurrent access is possible.

2. Does it use static writable variables?

If yes, investigate whether calls can overlap.

3. Does it use a shared buffer?

If yes, define ownership.

4. Does it access hardware?

If yes, determine whether another task can access the same peripheral.

5. Can an ISR access related data?

If yes, task-level protection alone may not be enough.

6. Can the design avoid sharing?

If yes, redesigning may be simpler than locking.


Common RTOS Concurrency Mistakes

Using delays while holding a mutex

This unnecessarily blocks other tasks.

Locking too much code

Only protect the critical shared operation.

Forgetting error paths

Always ensure a protected resource is released correctly.

Calling blocking APIs from an ISR

ISRs have different execution rules.

Assuming a variable update is automatically safe

A statement that looks simple in C may involve multiple memory operations.

Sharing one global buffer everywhere

This creates hidden coupling between unrelated modules.


How to Test Shared Utility Functions

A concurrency test should try to create overlap.

For example:

  • Call the same function from multiple tasks

  • Use different priorities

  • Run tasks repeatedly

  • Generate interrupts during processing

  • Test for long durations

  • Check counters and buffers for corruption

You can also use:

  • Static analysis

  • RTOS tracing

  • Debugger watchpoints

  • Assertions

  • Code review

Testing should answer:

What happens when timing is different from normal testing?


FAQs

How can I make a function safe for multiple RTOS tasks?

First reduce unnecessary shared state. Then identify resources that truly need sharing and use an appropriate synchronization or ownership mechanism.

Should every shared function use a mutex?

No. A mutex is useful for some shared resources, but redesigning the function to avoid shared state may be simpler and safer.

Are global variables unsafe in an RTOS?

Not automatically. However, global variables accessed by multiple execution contexts require careful design and appropriate protection where necessary.

Can two RTOS tasks call the same function?

Yes, if the function and all resources it uses are designed to handle concurrent access safely.

What is the safest design for a shared hardware peripheral?

In many cases, assigning one dedicated task as the owner of the peripheral and sending requests through a queue can simplify concurrency management.


Final Thoughts

Code reuse is valuable.

But shared code must be designed for the environment where it runs.

In an RTOS system, the question is not only:

“Does this function produce the correct result?”

You must also ask:

“What happens if two tasks call it at nearly the same time?”

The strongest designs usually have:

  • Clear ownership

  • Minimal shared state

  • Clear function inputs

  • Clear outputs

  • Appropriate synchronization

  • Short critical sections

  • Separate handling for task and interrupt contexts

When a utility function is designed this way, it becomes easier to reuse, easier to test, and much more reliable in real embedded systems.


Comments

Popular posts from this blog

MicroPython Advantages and Disadvantages

  MicroPython Advantages and Disadvantages (2025) MicroPython continues to reshape embedded development in 2025, especially across regions with strong IoT adoption such as India, Southeast Asia, the Middle East, and Europe. With faster development cycles, Python-friendly syntax, and unmatched flexibility for low-power devices, it stands out as one of the most future-ready technologies for IoT and embedded engineers. Introduction — Why MicroPython Is Growing Fast in 2025 MicroPython has become one of the fastest-growing technologies in the embedded and IoT world in 2025. As industries continue to shift toward smart automation, robotics, wearables, home automation, and edge computing , developers need a language that is both powerful and easy to learn — and that’s exactly where MicroPython shines. The rise of IoT across India, Southeast Asia, Europe, and the Middle East has pushed companies to adopt solutions that allow rapid prototyping, faster development, and low-power perform...

Types of Memory in Embedded Systems

  Types of Memory in Embedded Systems (2025 Guide): Architecture, Functions & Real-World Examples Embedded systems —whether inside your smartwatch, car ECU , smart home hub, or industrial controller—are designed to operate with tight memory constraints . That means the type of memory you choose impacts: Speed Power consumption Reliability Cost Real-time performance In simple words: memory shapes how an embedded device thinks, stores, and operates. This 2025 expert guide covers every type of memory used in embedded systems , explains how they work , and shows clear examples from microcontrollers like ARM Cortex-M, ESP32, STM32, AVR, and PIC. Let’s dive deep. 1. Introduction — Why Memory Matters in Embedded Systems Embedded systems are resource-constrained devices . They must work: With limited power Under strict real-time constraints Using minimal storage While remaining cost-effective Unlike laptops or smartphones, an embedded device cannot a...

Skills Required for Embedded Systems Jobs

  Introduction to Embedded Systems Careers Embedded systems are everywhere. From the alarm clock that wakes you up in the morning to the engine control unit in your car and the smartwatch on your wrist, embedded systems quietly power the modern world. As industries move toward automation, smart devices, and connected ecosystems, the demand for skilled embedded systems engineers continues to grow steadily. If you’re considering a career in embedded systems or preparing for embedded systems jobs, understanding the right skill set is crucial. Unlike general software development, embedded engineering sits at the intersection of hardware and software. It requires precision, logical thinking, and the ability to work close to the machine. You are not just writing code—you are writing code that directly interacts with physical components. Companies hiring for embedded roles look for engineers who can design, develop, test, and debug systems that operate under strict performance, memory,...