Hardware interrupt ISR "shared" variable

Hi!
I am quite new to FreeRTOS and I have a question that might be quite frequent.
So, I have a hardware interrupt that must read data from a shared buffer which is updated from a task.
Is there any shared resources protection I must do?
And even when the task is uploading data to the buffer many interrupts might occur.
What is the right approach?

Thank you!

The answer depends on the nature of the data and what the consequences are of the data being out-of-date or inconsistent.

  • The application task can disable interrupts from the device while updating the buffer
  • The application can use a semaphore to ā€œlockā€ the data, the ISR can check the semaphore (no blocking) and when it is locked (that is, the application thread is writing) avoid reading from the buffer
  • The ISR can ignore what the application task is doing and simply read whatā€™s there, whether it is consistent or not
  • There can be two buffers with a toggle; the application task updates one buffer then inverts the toggle, the ISR reads from the buffer selected by the toggle; the task cannot preempt the ISR, and so long as the task only writes to the buffer not selected by the toggle, the ISR will never read from a partially updated buffer

I recommend the last of these, assuming you have the space; the others are non-optimal solutions, but work where there is insufficient memory for the dual-buffer solution.

Thank you a lot for the last answer!

The last option seems more interesting!

Based on my knowledge this means that the ISR does not cares if is going to be used in a FreeRTOS based system right? What I mean is, does not need to use primitives (functions) of FreeRTOS kernel, right?

So long as it does not cause a reschedule by making FreeRTOS calls, no, it need not know itā€™s running under FreeRTOS. I use this technique regularly.

I will point out that it depends A LOT on what is happening. One very common situation that requires just a little synchronization is a circular buffer. You have the buffer, a write pointer from the task and a read pointer from the ISR, and perhaps a semaphore if the task can get ahead of the ISR by too much.

When the task wants to add data, it checks for room between the write pointer and the read pointer, and if there is FIRST copies the data into the buffer. THEN updates the write pointer. If not, it can perhaps put some data in the buffer, and then wait on the semaphore. After adding data and updating the write pointer, the task may need to check if the device is idle, and if so kick start the device.

When the ISR gets called, it checks if there is data available between the read pointer and the write pointer, and if there is sends some of it outs and signals the semaphore.

The semaphore is there to allow the task to block and give up the CPU to let other tasks run, but still wake up when room is in the buffer. No other synchronizations is needed because nothing is actually shared at any time. The write pointer and the buffer from the write pointer location up to, but not including, the read pointer is the tasks, and the read pointer and the buffer from it up to, but not including, the write pointer is the ISRs. Unless the actual write to the write pointer is not atomic, there is no synchronization issues.

At the other end of the spectrum, the data shared may be much more complicated, and the task needs a critical section (or disabling the ISR) while it is updating the data.

Thank you a lot!

But just to finish, a circular buffer would make the same job? As the toggle in this case would be the pointers (head and tail)?

With two buffers, there is a discrete point where the buffer moves in total between task and ISR and you donā€™t swap again until both are done with their buffer. The ring buffer continually passes data from one side to the other.

Two buffers can be better if the data needs to be done in big chunks, or has structure to it. The ring buffer assumes the data is really just a stream of small objects. The ring buffer is what FreeRTOS uses for queues and stream buffers. Using an explicit ring buffer and a semaphore can be an optimization over a queue for the simple case.

The type of data and whether all data must be sent both affect the selection.
I also use circular buffers for a lot of things, but for stuff like I2C and SPI slaves, where what the interrupt is getting is the latest data, the dual-buffers work well.
I assumed the original question was not for a FIFO, where the buffer can get full and then the task filling it must stop. This also requires some care around updating the circular buffer pointers.

Hi there!

So my use case is a PWM hardware interrupt handler which will check a double ring buffer in order to modify the frequency and the duration.

What I was thinking is, making two circular buffer.

And the task enter a semaphore (the critical section) and the update the buffers and the respective pointers. And when is finished the semaphore is released.

My only question is if there is an hardware interrupt while the task is updating the data and pointers.

Might be some things I do not know

Yes I2C and SPI tend to be packet based protocols so you donā€™t use a ring buffer. For those sorts of devices I donā€™t normally even use dual buffers, but call the drive with the address of a buffer, and the driver returns when it is done with it. I suppose if a single task needed to keep the device busy, I would need to go to a more asynchronous protocol with two buffers.

That cases I go with that method as we can have a return state.
But in this case is a continuous mode. Is the method I explained before possible?

If the shared data can be in an ā€˜unusableā€™ state for the ISR, you should use a critical section or disable that interrupt for the duration of the update. Normally just for a very few statements as you update stuff fro already computed data.

The critical section would be with semaphores?
Or the interrupt can occur while "inside the semaphore?

Critical section is with taskENTER_CRITICAL, Which disables interrupts. Semaphores donā€™t provide good protection for ISRs as the donā€™t stop the ISR, The ISR can just check that it should be locked out, but canā€™t then wait for it to be available.

1 Like

Okok then taskENTER_CRITICAL is the best approach?

There is also a non-blocking critical section mechanism This page describes the FreeRTOS taskENTER_CRITICAL() and taskEXIT_CRITICAL() API macros for short code sequences which is especially useful when dealing with shared data access from tasks and ISRs.

I deal with a lot of different types of data and interfaces. Two major flavors of asynchronous interface come to mind:

  • Queueing - Data packets/messages are queued and delivered in order, and always delivered (assuming the queue does not overflow)
  • Sampling - Data is updated by the producer as it changes, and the consumer samples the current state of the data, not caring about intermediate values

On the producer side, I use ring buffers, circular buffers, and queues for the first type. I use double-buffering for the second. The advantage of double buffers for sampling interfaces is that the producer does not have to be synchronized with the consumer, and there is no requirement for critical sections. I do a lot of power-sequencer / hardware health monitor firmware that monitors voltages and temperatures and act as a SPI or I2C slave to the primary (Application) CPU, and I use double-buffers on the environmental information. The slave interrupt handler has to start stuffing the controllerā€™s transmit buffer within a few microseconds of the transfer being initiated by the master, and there is no time to wait for the thread level task to unlock a semaphore. The consequences of the data not being the latest are low, but the consequences of the transfer timing out or providing wrong, incomplete, or inconsistent data are significant to the overall operation of the subsystem.

As I said in my initial response to the original question, how you deal with accessing a shared buffer from an ISR depends on the type of data, the importance of the data, and such. There is no ā€œone size fits allā€ solution.

Hello @danielglasser,

thank you very much for the professional resonse. Would it be possible to explain how the options you gave can be implemented in FreeRtos?

    • The application task can disable interrupts from the device while updating the buffer
    • The application can use a semaphore to ā€œlockā€ the data, the ISR can check the semaphore (no blocking) and when it is locked (that is, the application thread is writing) avoid reading from the buffer
    • The ISR can ignore what the application task is doing and simply read whatā€™s there, whether it is consistent or not
    • There can be two buffers with a toggle; the application task updates one buffer then inverts the toggle, the ISR reads from the buffer selected by the toggle; the task cannot preempt the ISR, and so long as the task only writes to the buffer not selected by the toggle, the ISR will never read from a partially updated buffer

Especially Nr. 4 has the same issue as mentioned by @Pedro. It has two buffers, but the toggle-variable indicating which of the buffers is the one that is assigned for the ISR to read is a ā€œshared resourceā€, and consequently got the same underlying problem.

Therefore it would be very helpful if you could provide code and detials.

Thanks a lot in advance.

Best Regards, Seppeltronics

When Iā€™m using multiple buffers, I have a status flag for each buffer that can only be set by the driver/interrupt service routine, and only cleared by the consumer of that buffer for received data. For buffers used to send data, I use the same scheme, but the application is the producer and the offload thread is the consumer.

This works well for 2 or more buffers, and if you ensure that the buffers are always used in the same order, can be done without any critical sections, semaphores, or mutexes; the consumer just waits for the status of the next buffer to be set to ready, then when itā€™s done processing that data, it clears the ready status and moves on to the next buffer in the ring (when you reach the end, you start over at the beginning).

I usually have an event that the producer raises to allow the consumer thread to block with a timeout for periodic handling of other duties, the producer (driver or callback from the ISR) raises the event specific to the transfer.

I cannot provide example code for this because I would require an export classification to post any technical data related to things Iā€™m actively working on, and I am using this technique in my current project. (My employer is very strict about this.)