After reading this post, I decided I’d try to understand the C++ Memory Model stuff, at least at a basic level. I followed a link posted by @PieterP as well as some Google search results.
As a usage example, I came up with the ESP32 code below. The idea is to have a writer task (simulates handling sensors and gathering their data) that writes to a large, shared global data structure and an independent reader task that processes data from that structure.
It uses two atomic bool variables for the synchronization interlock: newDataAvailable and dataAck as well as two state machines. The goal was to do the task / data synchronization without using FreeRTOS structures such as queues. The interlock sequence is:
- Writer writes data to the shared struct
- Writer raises newDataAvailable flag
- Reader sees newDataAvailable flag
- Reader accesses data from struct
- Reader raises dataAck flag
- Writer sees dataAck flag
- Writer lowers newDataAvailable flag
- Reader sees lowered newDataAvailable flag
- Reader lowers dataAck flag
- Cycle repeats
If all the memory accesses are consistent, the reader will print series of incrementing integers, starting at 0, without gaps. The code as worked for several combinations of relative task priorities and core assignments, but I have a couple of questions:
- Is the code as written guaranteed to work under all conditions given the C++ memory model?
- The code seems rather complex, is there a simpler way to do it?
Thanks.
#include "Arduino.h"
#include <atomic>
void writerTask(void *parms);
void readerTask(void *parms);
using SensorData = struct {
uint32_t a = 0;
uint32_t b = 0;
uint32_t c = 0;
uint32_t d = 0;
};
SensorData sensorData;
std::atomic<bool> newDataAvailable = { false };
std::atomic<bool> dataAck = { false };
void setup() {
Serial.begin(115200);
vTaskDelay(4000);
Serial.println("Starting");
BaseType_t returnCode = xTaskCreatePinnedToCore(writerTask, "Writer Task", 2000, NULL, 5, NULL, 1);
if (returnCode != pdPASS) {
log_e("Failed to create Writer Task");
vTaskDelete(NULL);
}
returnCode = xTaskCreatePinnedToCore(readerTask, "Reader Task", 2000, NULL, 5, NULL, 1);
if (returnCode != pdPASS) {
log_e("Failed to create Reader Task");
vTaskDelete(NULL);
}
}
void writerTask(void *parms) {
uint32_t sensorValue = 0;
bool newSensorData = true;
uint32_t lastDataTime;
uint32_t waitTime;
log_i("Starting writerTask");
enum WriterState {
READY_TO_SEND, WAITNG_ACK_SET, WAITING_ACK_RESET
};
WriterState state = READY_TO_SEND;
for (;;) {
switch (state) {
case READY_TO_SEND:
if (newSensorData) {
sensorData.a = sensorValue++;
sensorData.b = sensorValue++;
sensorData.c = sensorValue++;
sensorData.d = sensorValue++;
state = WAITNG_ACK_SET;
newDataAvailable.store(true, std::memory_order_release);
newSensorData = false;
waitTime = random(100, 500);
lastDataTime = millis();
} else {
if (millis() - lastDataTime >= waitTime) {
// Simulate time needed to acquire new sensor data
newSensorData = true;
}
}
break;
case WAITNG_ACK_SET:
// Wait for Reader to Set Ack signal
if (dataAck.load(std::memory_order_acquire)) {
state = WAITING_ACK_RESET;
newDataAvailable.store(false, std::memory_order_release);
}
break;
case WAITING_ACK_RESET:
// Wait for Reader to Reset Ack signal
if (!dataAck.load(std::memory_order_acquire)) {
state = READY_TO_SEND;
}
break;
default:
break;
}
// Do other processing stuff here
vTaskDelay(1);
}
}
void readerTask(void *parms) {
log_i("Starting readerTask");
enum ReaderState {
READY_FOR_DATA, WAITING_WRITER_RESET
};
ReaderState state = READY_FOR_DATA;
for (;;) {
switch (state) {
case READY_FOR_DATA:
// Wait for Writer to Set Available Signal
if (newDataAvailable.load(std::memory_order_acquire)) {
Serial.println(sensorData.a);
Serial.println(sensorData.b);
Serial.println(sensorData.c);
Serial.println(sensorData.d);
state = WAITING_WRITER_RESET;
dataAck.store(true, std::memory_order_release);
}
break;
case WAITING_WRITER_RESET:
// Wait for Writer to Reset Available Signal
if (!newDataAvailable.load(std::memory_order_acquire)) {
state = READY_FOR_DATA;
dataAck.store(false, std::memory_order_release);
}
break;
default:
break;
}
// Do other processing stuff here
vTaskDelay(1);
}
}
void loop() {
}