I understand that resource sharing is important for ensuring that multiple tasks can safely access and use shared resources, such as CPU time or shared variables. FreeRTOS offers tools like mutexes, semaphores, and queues to manage access to these resources, preventing conflicts and data corruption in real-time embedded systems.
Regarding hardware peripherals on the microcontroller where FreeRTOS runs, resources like Timers, UART, SPI, and I2C
Do you considered them shared resources?
Do you protect them when multiple tasks need to interact with these peripherals concurrently?
You MUST protect any hardware resource that is desired to be used by multiple tasks, as I know of no peripheral that has some multi-threaded operation at the fundamental access level.
Now, if the hardware resource is going to be dedicated to a single task, then it doesnât need protection. In my experience âHardware Timersâ tend to not need protection, as you tend to allocate a given hardware timer to a particular operation, but if you do make one shared, you will need to have a system to control how it it is shared.
My low-level generic UART, SPI, and I2C assume that they will be shared so include a mutex per physical channel.
Iâve recently came across a concept thatâs left me with some questions regarding resource sharing and data protection.
Iâve observed some of example where global variables are used for resource sharing without the use of synchronization tools like mutex or semaphore. Iâm trying to understand when itâs necessary to protect resources and when it might not be required.
Could you share your thoughts that might help me clarify when resource protection is essential and when it might be not necessary?
Any practical use case would be helpful for better understanding the difference between the two cases.
When the COMPLETE access to a global variable will be simple enough that the processor will automatically do the operation atomically, then protection might not be needed. For writes, not only does the write need to be atomic, but all reads that use that data.
Sometimes you can make the access become atomic with a critical section (which disables interrupts) to make the protection simpler.
So, reading the value of a word sized memory location that is set by something else would be atomic, and not need protection. If you need to get multiple locations, and they need to be âconsistentâ with each other, that would not be, and would need some form of protection.
Could you please provide some real-world application examples for better understanding the difference between when resource protection is essential and when it might not be necessary?
This would help to clarify the practical applications of these concepts
One example is within FreeRTOS. On a 16 bit processor, if the TickCount is only being kept to 16 bits, then you can get the value of the tick counter atomically. But if the TickCount is being kept to 32 bits, then we need a critical section to read the counter, so we canât get 16 bits from before the increment and 16 bits after it.
Another example would be if you have an array of buffers which have a flag in them to indicate they need servicing. A task could quickly scan the list without needing exclusive access to each one to check that flag. When if finds one that needs servicing, it could then aquire the Mutex for that buffer, recheck that it does need servicing, and then do the operation, knowing it has full control over the buffer.
Another example is setting up e.g. a UART controller for a transfer, which usually requires multiple operations (write to UART HW registers).
If right in the middle of this setup sequence the current task gets preempted by the scheduler and the other now running task also sets up a UART transfer, the previous HW setup is overwritten. After the scheduler switches back to the previously preempted task it continues with the remaining part of the UART HW setup sequence but the HW registers are partially corrupted because they were overwritten by the other task which was meanwhile running.
Hence setting up the UART HW for a transfer and waiting for its completion must be (mutex) protected because the UART HW is exclusively in use during that time.
Alternatively and as already discussed you can design your application that e.g. the UART HW is exclusively handled by 1 UART handler task and other tasks send messages/data requests to this handler task. Then you donât have concurrent access to the UART HW and wonât need mutex protection for it. If the handler task uses a e.g. FreeRTOS queue or message buffer youâre fine because FreeRTOS queues/message buffers take care about concurrent access by multiple tasks, of course.
In a 16-bit processor and a 32-bit shared variable âcounterâ that two threads are trying to increment concurrently. I want to understand how data can be corrupted.
I believe critical resources is processor time. Whatâs the priority of both tasks in this case , and in which situations would each task go from running to a blocked state and then back to running?
The issue is that if the counter is at a value of, say, 0000 FFFF, and the reading task first reads 0000 as the upper byte of the counter, then a task switch occurs, and then the second task (or interrupt) increments the counter to 0001 0000, and then later the first task gets to run again, it will now see 0000 in the bottom word, thinking the counter had a value of 0000 0000, and thus it got an incorrect value.
The âresourceâ here is the counter value, not processor time, though the sharing of the processor time is one cause of the issue.
For this to happen, the second task/ISR needs a priority at least a high as the first task (ISR are ALWAYS higher in priority to any task). The solution is to do something that keeps the second task from altering the variable in the middle of the read. This is an ideal case for a critical section, instead of a Mutex, as it is much lighter weight, and by temporarily blocking the interrupt for a few cycles, the second task canât get switched in, as ALL task switches are either cause by an interrupt (perhaps just the tick), or a task calling an API function.
Imagine In a system with multiple tasks that share a UART for communication:
The UART hardware is a critical resource since it can only handle one task at a time.
Consider Task 1 (Temperature Sensor):
Task 1 starts configuring the UART to send temperature data, setting the UART parameters.
And also consider Task 2 (LED Control):
Task 2 starts configuring the UART for LED status updates.
I think Data may become corrupt in the following case:
While Task 1 is configuring the UART for temperature data transmission, itâs interrupted by the scheduler, and Task 2 begins configuring the UART for LED status updates. In this situation, Task 2âs configuration can overwrite the settings that Task 1 was in the process of defining, potentially leading to incorrect or corrupted data transmission for both tasks.
Yes, a resource line the UART, because its usage by a task is not naturally atomic, needs protection.
Note, the SETUP for the uart is very likely not going to be changed, because the UART will almost certainly be physically connected to a line that will have a single defined protocol (baud rate, character length, etc). The protocol might have sequences that negotiate a change in that, but tasks will not be change that just by connecting.
The MESSAGES being sent over the link, may well need protection, so they go out as a whole, and nothing can interfere until that communication is completed.
Right. Thatâs what I tried to say with setting up the transfer. For instance when using DMA there a usually a few steps needed to setup and start the DMA with the provided buffer.
The basic HW setup for the protocol (baud rate, start/stop bits, parity in case of a UART) is usually done once during init and wonât need protection if this initialization is not done concurrently by application design as it wouldnât make much sense to do so.
I see that mutex and semaphore are typically used to protect resources, and message queues are commonly used to exchange data between tasks. My confusion arises from observing code where data exchange occurs without the use of a message queue.
I would like to understand when itâs appropriate to skip using a message queue and when itâs necessary to apply one.
The message queue is a SIMPLE way to pass a stream of data between tasks (between a task and an ISR), but there is nothing âmagicalâ about how it works that make it necessary. I would say that ALL uses of a Queue / Stream Buffer could be replaced with other methods.
Sometimes the access pattern just doesnât match that of a Queue, so you canât use them. The counter example is one case here. A task wants the current value of some counter to use, not that it want to receive a list of counter values, it want the CURRENT value. While you could âabuseâ a queue to provide that (make a length 1 queue that you overwrite the new value into the queue, and peek the queue to see what that value is) that is just adding a lot of overhead to do what could be more simply done.
By treating the block of memory with the data as a resource, and using some form of access exclusion with it, it becomes an effective form of communication. In the case of something like the counter, due to the atomic nature of some of the actions, critical sections can form a very efficient form of access control. In some cases you might want to use a Mutex, and in some cases you can even establish an âownershipâ of the blocks of memory, and a task that owns a block has free access to it.
I believe a log file is another example of resource sharing where multiple tasks can concurrently access the same file for reading, writing, or making modifications. However, when multiple tasks attempt to write to the same log file simultaneously without proper synchronization, it can lead to data corruption. Therefore, synchronization mechanisms are essential to protect the file.
Imagine a scenario where two threads are responsible for controlling a DC motor. Does this situation indeed involves the need for thread synchronization?
Having 2 independent tasks trying to control the same thing might cause surprising behavior.
Or is there a master control task controlling those motor control tasks ?
Which I turn leads to the question why 2 motor control tasks are needed at all ?