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.
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 indisable_interrupts, process buffer, enable_interrupts. In which case,volatileisn't needed. For multithreaded, use of either mutexes and/orstdatomic.hprimitives is warranted. So, again,volatilenot needed.volatileis 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; }GPIO_TypeDefdoes contain volatiles inside, see here.__IOis a shorthand for volatile, which here represents memory-mapped ports.volatilemeans 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).