In an ESP32-based system where multiple tasks share the same sensor data (IMU, encoders, GPS), what is the correct way to prevent race conditions when one task updates the shared position variables while another task reads them for control logic?
I’m currently seeing occasional corrupted/half-updated values, and if this isn’t fixed the entire navigation algorithm becomes unstable.
What’s the proper, deterministic way to protect shared state without breaking real-time performance?
I'm not an ESP32 user and I don't have experience with RTOS. I think that the term that you're looking for is semaphores.
I hope I do not put you on the wrong foot.
Thanks yes semaphores are one option but I’m not completely sure they solve the whole problem here The issue I’m seeing looks more like partial-write corruption during reads so I’m trying to figure out whether a mutex critical section, or a double-buffered approach would be more deterministic on the ESP32’s architecture
Still, semaphores might be part of the solution, so I appreciate the pointer
Have you looked at this document?
Section 8 covers critical sections, mutexes, and binary semaphores, any of these can provide mutual exclusion to data shared between tasks.
you're describing sensors that only provide output. There's no reason mutiple "tasks" can't read the sensor at any time. of course the sensor value from one read to the next.
looks like the problem is more about multiple tasks using the I2C bus. Use of the bus needs to be thru a common driver that has mutex/semaphore to suspend 1+ other tasks until the 1st I2C operation completes.
It depends on the exact situation but I'd probably consider nominating one task to do the read of the actual sensor, which may be a relatively slow activity, then storing the results in a buffer. This buffer would be protected by a "critical section", implemented by one of the mechanisms already described, and read by the other tasks.
looks like the i2c bus is used to read multi devices. So either to do as you suggest and have copies of the values from different devices updated periodically or protect the use of the i2c bus
Thanks yes I checked section 8
I’m just trying to decide whether a tiny critical section or a full mutex is the safer choice for these fast-updating position variables
But your hint helps thanks
Good point so I need two protections:
one mutex around the I2C driver and a separate critical section/mutex for the shared position struct
I’ll try that setup
Thanks that makes sense You're right having a single task handle all sensor reads and then exposing a protected shared buffer is probably the cleanest and safest way I’ll refactor my code in that direction Appreciate the clarification
Yes exactly protecting the I2C bus + having one dedicated sensor-reader task feeding a shared buffer is probably the most reliable architecture Thanks for confirming it
Others have given good advice. One more suggestion which may or may not fit: Queues can be useful too for indirect mutual exclusion. E.g. if you want to perform I2C operations from muliple tasks it might fit your requirements to have an I2C task doing a blocking read on a queue, then post requests to that queue from other tasks.
No, not a critical section. Those are for freezing out multiple accesses for a very short amount of time (similar to disabling interrupts on a single-tasking Arduino). A critical section will disable interrupts on the subject core of a dual-core ESP32. So no ISRs will run on it and there will be no context switches. It will also cause any code (including ISRs) on the other core to enter a spin lock (thus freezing that core for the duration) if attempts to enter a critical section using the same portMUX_TYPE object.
I2C operations would take an eternity compared to the amount of time the code in a critical section should take. And, they may not even be possible with interrupts disabled.
That’s a good point using a dedicated I2C task with a request queue is a clean pattern and I’ve used it before when the bottleneck was the bus itself
In my case the corruption happens on the shared navigation state after the sensors are already read so the contention isn’t on the I2C layer but on the shared structs being updated and consumed at different rates
So yes the queue-based approach definitely helps for serialized I2C access but I probably still need some explicit data-protection (mutex or double-buffering) around the fused state to keep the control loop deterministic
You're absolutely right I wouldn’t put I2C inside a critical section either
I only meant protecting the final shared state (the fused pose struct) not the sensor read itself
Just a tiny lock or double-buffer there so readers never see half-written data
Perhaps std::atomic. See This Thread.
Thanks that thread is super helpful
I hadn’t considered using std::atomic for the fused state itself mostly because I assumed the struct would be too big for atomic ops to matter But after reading the discussion you linked especially PieterP’s explanation about memory_order_seq_cst I think it might actually solve the half-update issue without needing locks at all
I’ll experiment with either atomics or a small double-buffered swap and see which one behaves better on the ESP32
Appreciate the pointer this gives me a clearer direction
What does "fused state" mean? I really think you only need a mutex. See Here for a mutex technique that only locks for write access but not read. That allows multiple readers at the same time but only a single writer.
Thanks, that example makes sense
And yeah, by “fused state” I just meant the final navigation state the struct that contains the combined IMU + encoder + GPS estimate
Your shared-mutex pattern looks solid It’s basically what I meant when I said “tiny lock”: multiple readers, one writer, writer priority
For my case it probably doesn’t need anything more complex than that
I’ll try both approaches (std::atomic for a swap flag, or your shared_mutex + RAII style) and see which one behaves better on the ESP32 load
Appreciate the detailed example it helps a lot