Shared resource between task and ISR

Hi FreeRtos community,

So there is a global variable that needs to be modified by a task and ISR how can I handle this efficiently?

I know there are two ways for this.

  1. I can use enter and exit critical while accessing from task.

  2. I can use mutex.

apart from these two any other ways are there?

The critical section is an overkill because it freezes all OS activities and all threads of computation while in effect. If you have a way to disable only the isr that competes with your task, that can help enhance throughput and prevent system freezes that result from improper use of the critical section.

And no, you can not use muteces in isrs. Just query the forum, it has been discussed a bazillion times.

1 Like

Hi @RAc ,

Can you give me any conversation link related to why we should not use mutex in ISR?

use the forum search function and type “mutex isr” (no quotes) in the search field. It’ll jump right into your face.

The simplest is there is no FromISR access functions for a Mutex, and even if you cheat and use the FromISR version of semaphore operations (which aren’t promised to work on the semaphore) you run into the issue that the ISR can’t wait for the Mutex to be ready, as it has blocked the task that held it (unless you are using SMP)

The ONLY ways a task can protect access of something from an ISR is to use a critical section, which needs to be kept very short as it globally disables all interrupts, disable the specific interrupt that you are sharing with (if you know it), or use a flag variable that the ISR checks.

In the last case, the ISR needs to be able to work without accessing that thing, which often isn’t the case.

The other work around is the ISR triggers a high priority task to do what is needed to that object, using a ISR safe communications link.

If both the task and the ISR need to modify the variable then a critical section is your best bet. If you can design your application so that the ISR only writes and the task only consumes then you would be able to use fromISR APIs (placing signals into a queue for example).

So I have gone through some of the conversations, Now I understood why we should not use mutex from ISR.

But In which situation may I use this xSemaphoreGiveFromISR API?

My typical use of xSemaphoreGiveFromISR is for a I/O completion signal.

Task calls an device driver API that first takes a mutex for that device so it has control over it.

That driver then sets up and initiates the I/O operation, and then waits on the semaphore, and the processor goes off to run other tasks.

The interrupt system then completes the I/O operation, and when complete, signals the semaphore.

The driver, when it is gotten back to by again being the highest priority gathers the completion information set by the ISR, and then gives the device mutex to let another task use the device if it wants.

Depending on the type of the global variable, there may be other options. For example, if you are using C++ then there is the std::atomic template, which gives you the means to declare variables that you can atomically read, from add to, subtract from, and compare/exchange with. These typically work only on 32 or 64 bit variables, although 128-bit compare/exchange is available on some processors.

Other programming languages may provide language constructs to access the underlying machine instructions that std::atomic makes use of.

In some cases, you may not need atomic access if you design the data structure carefully. A common example is a ring buffer that the ISR writes to and a task reads from, or vice versa. You can avoid any need to use synchronisation primitives, provided that the get and put indices are small enough to be read from and written to memory in a single machine operation.

You don’t have a lot for us to under what you really need…
You talk about a shared resources, then mention it’s a variable.
In general, variables are not resources…

That said, if it’s it’s a spamm variable, check if your hardware is already atomic. For example the Raspberry PI PICO write 32bits atomically… there is no need to ‘protect’ them.

Also remember, not all platforms support std::atomic, they simply wrap them, but they are not atomic. so Be careful when you use anything from c++.

Other options are SCSP queues (Single Producer, Single Consumer). They are lock feel, your ISR can push, and your other task can pop them, he advantage is, when the queue is large enough you won’t mis any changes from the ISR.
Alternatively you can use the message services from FreeRTOS.

What I often use when it’s just a copy of some variable or small struct is to use a spinlock. As I work on the PICO a lot, and the PICO has hardware spinlocks they are very fast and don’t have the overhead of any messages or SCSP queues…

The shared resource that I have mentioned is sending data over uart. What if the situation comes like the task and ISR both needs to use uart_send function?

That depends on how uart_send() is implemented, I know very few iplementations that are safe to use in an isr in the first place. If you have such an implementation AND the control flow is sound, you need to be aware of the fact that isrs can never block, so if an isr detects that there is an ongoing task based transmission (using either of the strategies outlined above), the isr must return and employ some kind of way to trigger a deferred start of next transmit. There are ways to do that, but they need careful coding and bear a number of trap doors-

Hi @RAc ,

could you suggest me what are those I will give it a try once?

no. Without seeing the code for the serial driver you are using, everything is guess work. You will need to very deeply dig into the control flow, understand how it works and then modify it piece by piece, ending up likely with a driver rewritten from scratch.

One thing I have done in the past in a similar case is have every thread of execution (isr or task) that wants to transmit tuck its data at the end of the uart transmit buffer so that the xmit complete interrupt can work off everything in the buffer (ie an ongoing transmission AND new characters coming in in the mean time) until the buffer is empty, but a) there are several possible race conditions and interleaved scenarios to consider to get that to work reliably and b) that works only when each character transmission interrupts the mcu individually (bulk dma transmission will not work with that scheme).

By the time you are done with that, you will know more about serial device drivers than is good for you. Highly interesting and fun stuff, but not the kind of thing that managers are willing to pay for.

I missed the UART usecase.

It sounds like you want to send from different tasks, and receive over the UART handle handle this.

It sounds like you want to send messages if they are single chars.
if you want to send/receive multiple chars like strings you could use a FreeRTOS queue.

The atomic data type works as well in C from C11[1], and I would check it out. The underlying mechanisms are typically memory barriers. If your chip supports “memory barriers”, you can also construct some primitives on your own if that’s necessary.

[1] Atomic types - cppreference.com

I am surprised that atomic operations should be based on barriers. On ARM Cortex MCUs, atomic operations are based on the ldrex and strex primitives which to my best knowledge are not related to barriers. Could you elaborate? Thanks!

ldrex and strex only guarantee exclusive access to an address, they don’t guarantee the correct order of execution, they also do not prevent reordering in compile/runtime. In this example[1] you will not see any problem, but only the generated code with[2] and without[3] atomic data types.

[1] /* FreeRTOS V6.0.0 - Copyright (C) 2009 Real Time Engineers Ltd. Thi - Pastebin.com

[2] https://pastebin.com/4svu31C9 note the dmb instructions

[3] atomic-3 - Pastebin.com no barriers

To be more precise, for atomic data types it seems to be a combination of ldrex/strex/dmb.

Thanks for the links, Robert, this is highly interesting and enlightening reading. However, for the discussion at hand, we may be straying offsite. Please correct me if I am wrong (I may well be), but we may be talking about different ideas of “atomicity.” When it comes to mutual exclusion (both between tasks and tasks/isrs), there are scenarios in which exclusive access (frequently also named “atomic access,” although that may not be 100% precise according to your explanation) to a single variable is sufficient. I do not see a need why in those cases we need execution order on top of exclusive access.

As usual, the answer is - it depends, and I tend to be a bit overcautious. Actually, one attendee of my workshops told me that all the programmers on this planet assume that 32-bit operations on a 32-bit CPU just work out of the box. No, they don’t.

Say, we have 2 variables:

  volatile uint32_t uintCounter = 0;
  atomic_uint_least32_t uint_least32CounterAtomic = 0;

Now make 2 tasks, which increment these variables. Something like this:

void vTask2( void *pvParameters )
{
const char *pcTaskName = "Task 2:";
volatile unsigned long ul;

	/* As per most tasks, this task is implemented in an infinite loop. */
	for( ;; )
	{
		uintCounter++;
		uint_least32CounterAtomic++;
#if PRINTOUTS == 1
		/* Print out the name of this task. */
		if (uintCounter != uint_least32CounterAtomic){
		vPrintStringAndNumber( pcTaskName, uintCounter );
		vPrintStringAndNumber( pcTaskName, uint_least32CounterAtomic );
        #if HANG_ON_NEQ > 0
			for(;;);
        #endif
		}
#endif
		taskYIELD();
	}
}

I was surprised how quickly they are not equal.

You can imagine that we can come up with many interesting scenarios, where it’s fatal to ignore the order of execution. E.g. writing to 3 registers in the right order - Set the bit to start the PWM at the end.