2

I've seen many examples explaining usage of volatile keyword but all of those refer to a single variables e.g tick counter incremented in ISR and then read in main loop. But what about a buffer that is shared between main loop and ISR, where main loop is producer and ISR consumer (or another way around).

Let's say that I have a simple ring buffer struct:

struct RingBuffer { uint8_t *data; uint32_t write_index; uint32_t read_index; }; struct RingBuffer buffer; 

Does data have to be volatile ? Or whole buffer (which also implies that function parameter also have to be volatile) ? I took a look at few implementations of a such data structure and I couldn't seen any volatile there and that bothers me a lot. I wrote simple program on stm32 for driving lcd display with buffering. Code looks like this:

struct OutputLine { GPIO_TypeDef *gpio; uint16_t pin; }; typedef struct OutputLine OutputLine_t; struct HD44780 { OutputLine_t enable; OutputLine_t rs; OutputLine_t rw; OutputLine_t data[4]; // data[0..3] == D4..7 Fifo_t fifo; }; 

In main loop I checked if lcd was busy and if not then if there's something in queue. Commands to lcd was sent in timer irq

while (1) { /* USER CODE END WHILE */ HD44780_Update(&lcd); /* USER CODE BEGIN 3 */ } void HD44780_Update(HD44780_t *lcd) { if (Fifo_Empty(&lcd->fifo)) { return; } if (HD44780_Busy(lcd)) { return; } HD44780_FifoItem_t item; Fifo_Read(&lcd->fifo, &item); HD44780_WriteByte(lcd, item.byte, item.rs); } void TIM6_IRQHandler(void) { if (LL_TIM_IsActiveFlag_UPDATE(TIM6)) { LL_TIM_ClearFlag_UPDATE(TIM6); i = (i + 1) & 1; HD44780_ClearDisplay(&lcd); HD44780_Puts(&lcd, strings[i], strlen(strings[i])); } } 

Result was correct wether Fifo_t was volatile or not.

14
  • 4
    volatile (for ring buffers) is useful in embedded when the ISR will add to the ring buffer. But, the base task level would have to wrap access in disable_interrupts, process buffer, enable_interrupts. In which case, volatile isn't needed. For multithreaded, use of either mutexes and/or stdatomic.h primitives is warranted. So, again, volatile not needed. volatile is most useful for accessing memory mapped hardware registers/devices, usually via an inline function or macro: uint32_t getreg(uint32_t regaddr) { return *(volatile uint32_t *) (uintptr_t) regaddr; } Commented Apr 9, 2024 at 21:28
  • 1
    In bare-metal systems volatile is oftentimes used as, pardon simplification, "poor man's atomic". Sometimes this happens due to lack of atomic operations on some architectures e.g. on AVR. Note also that GPIO_TypeDef does contain volatiles inside, see here. __IO is a shorthand for volatile, which here represents memory-mapped ports. Commented Apr 9, 2024 at 21:31
  • @CraigEstey but how disabling and re-enabling interrupts affects meaning of volatile ? In my understaning it only provides atomic read to data that can be changed somwhere else than in main loop (so volatile still needed) Commented Apr 9, 2024 at 21:55
  • @CraigEstey basically what you wrote is very imprecise and misleading. Commented Apr 9, 2024 at 22:08
  • volatile means effectively "modifying this variable causes a side effect, read and store it from/into its memory address". This enforced side effect prevents the variable from being optimized away e.g. by storing it inside a CPU register, therefore making it behave the desired way when accessed not from the standard predictable sequential way. However, as far as I know atomics (e.g. std::atomic from C++ or atomic_int from C) should also be prevented from this kind of optimization (provided the strict memory model is used). Commented Apr 9, 2024 at 22:18

1 Answer 1

3

What is volatile? It probably the most misunderstood keyword in the C language (maybe except restrict).

  • volatile informs the compiler that the object (variable) is side effects prone. It means that it can be changed by something which is not visible for the compiler in a normal program execution path. For example by the hardware (DMA, hardware register) or signal (exception) handler which is not called by the program.

Example:

uint32_t counter; void TIM6_IRQHandler(void) { /* .... */ counter++; } void foo(void) { while(counter < 1000); printf("x"); } 

and the resulting code: https://godbolt.org/z/h3qz7Y5W6

foo: ldr r3, .L8 ldr r3, [r3] .L6: cmp r3, #1000 bcc .L6 

As you can see without volatile the counter is loaded into register only once and function foo will end in the dead loop even if counter reaches 1000. With volatile counter is read from memory before every use as compiler knows that it is side effects prone: https://godbolt.org/z/KThnG3sq4

The resulting code:

foo: ldr r2, .L8 .L6: ldr r3, [r2] cmp r3, #1000 bcc .L6 

Now the counter is loaded into the register at every use.

volatile does not guarantee atomicity cache coherency.

As volatile objects have to be read from their permanent storage every time they are used it is not good to have volatile big data structures. So answering your question - no do not make the whole structure volatile, only members which are side effects prone.

Sign up to request clarification or add additional context in comments.

2 Comments

volatile means both that the object may be modified by things outside the direct control of the program and that the program itself reading or writing the object is a side effect.
@KeithThompson any access to volatile object is side-effect in the Standard understanding.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.