I know there is a lot of confusion regarding volatile.
So I have 3 real life examples I'm not sure about the correct usage of volatile.
1) DMA Stream
The hardware writes directly to data with DMA.
Is volatile needed in this span?
#include <cstdint>
#include <semaphore>
#include <span>
static std::binary_semaphore semaphore{0};
//DMA Interrupt after complete receive
extern "C" void DMAcomplete() {
semaphore.release();
}
void readFromDMA(std::span<volatile uint8_t> data) {
//Modify DMA register and start DMA
//... = data.data();
//wait for DMA to finish
semaphore.acquire();
}
2) ISR read
This example is similar to the first one, but now the ISR is actually manipulating the data.
Is volatile needed in the span?
#include <cstdint>
#include <semaphore>
#include <span>
#include <atomic>
static std::binary_semaphore semaphore{0};
static std::atomic<volatile uint8_t*> data;
static std::atomic_size_t size;
static std::atomic_size_t index;
//Interrupt is called per byte
extern "C" void ISRperByte() {
uint8_t receivedData;
data[index++] = receivedData;
//Receive complete
if(index >= size-1)
semaphore.release();
}
void readFromISR(std::span<volatile uint8_t> toRead) {
data = toRead.data();
size = toRead.size();
index = 0;
//Enable Interrupt etc.
//Wait until all reads are done
semaphore.acquire();
}
3) ISR Callback
Does ICallback* have to be volatile?
#include <atomic>
class ICallback {
public:
virtual ~ICallback() = default;
virtual void doStuff() volatile = 0;
};
static std::atomic<volatile ICallback*> atomicCallback = nullptr;
//Interrupt is called by hardware
extern "C" void ISR() {
auto cb = atomicCallback.load();
if(cb)
cb->doStuff();
}
void setCallback(ICallback& cb) {
atomicCallback = &cb;
}
void resetCallback() {
atomicCallback = nullptr;
}
EDIT: Here is s snippet of case 1 using C
#include <stdint.h>
#include <stdbool.h>
//Assume assigment to this is atomic
static volatile bool semaphore;
//DMA Interrupt after complete receive
void DMAcomplete() {
//notify
semaphore = 1;
}
void readFromDMA(volatile uint8_t* data, uint32_t size) {
semaphore = 0;
//Modify DMA register and start DMA
//... = data.data();
//wait for DMA to finish
while(semaphore != 1);
}
EDIT2:
If I call readfromDMA
or readfromISR
with none volatile data, is it valid after the function returns? Since the caller doesn't declare his data as volatile, but the data is changed during DMA/ISR, it seems a little suspicious.
Our compiler says this about volatile declared Objects:
● All accesses are preserved
● All accesses are complete, that is, the whole object is accessed
● All accesses are performed in the same order as given in the abstract machine
● All accesses are atomic, that is, they cannot be interrupted.
The compiler adheres to these rules for accesses to all 8-, 16-, and 32-bit scalar types.
For all combinations of object types not listed, only the rule that states that all accesses are preserved applies.
So for the examples the following would be correct:
readFromDMA
.
BUT the data which is passed to this function has to be volatile for example std::vector<volatile uint8_t>
.data
is indeed needed, because the ISR manipulates it. Additionally, as in example 1, volatile is also needed in the declaration of the buffer for example std::vector<volatile uint8_t>
.ICallback*
does not need to be volatile, BUT the object implementing doStuff
has to be aware, that this function is called from an ISR. So it may need some volatile member.