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
Post a Comment