In embedded development, have you ever encountered the following issues:
-
The speed mismatch between data producers (e.g., sensor acquisition) and consumers (e.g., data processing threads)
-
Using a single buffer leads to read-write conflicts that cause data corruption
-
Frequent memory copies result in low CPU efficiency
-
Data loss issues are difficult to resolve completely
Today, I will reveal the double buffering design in embedded systems, helping you thoroughly address these pain points through practical code analysis!
Why is Double Buffering Technology Needed?
In embedded real-time systems, data producers and consumers often run in different threads or interrupt contexts. Traditional single buffer solutions face three major challenges:
-
Data Race: Simultaneous read and write lead to data inconsistency
-
Inefficiency: Read-write mutual exclusion forces one side to wait
-
Data Loss: New data cannot be written when the buffer is full
The core idea of double buffering technology: Use two buffers, one for writing and one for reading, to achieve efficient data transfer in a lock-free or low-lock state by swapping pointers.
Analysis of Double Buffering Design Architecture
Our double buffering implementation consists of two core levels:
Circular Buffer (Sub_buffer), reducing memory copies
typedef struct { uint8_t *buffer; uint16_t Read_Mirror : 1; uint16_t Read_Index : 15; uint16_t Write_Mirror: 1; uint16_t Write_Index : 15; uint16_t bufferSize;} Sub_buffer;
Key Technical Highlights:
-
Mirror Bit Technology: Distinguishes the buffer’s “wrap-around” state using Read_Mirror/Write_Mirror
-
Unsigned Integer + Bit Fields: Maximizes memory utilization under limited resources
-
Memory Barrier Friendly: Ensures atomicity of index updates
Double Buffer Manager (Double_buffer)
typedef struct{ struct rt_mutex ReadLock; struct rt_mutex WriteLock; Sub_buffer* SubBuffers[2]; // Read and write buffers uint8_t ReadIndex; // Read buffer index uint8_t WriteIndex; // Write buffer index} Double_buffer;
In-depth Analysis of Core Source Code
Circular Buffer State Machine
static __inline RING_STATUS getSubBufferStatus(Sub_buffer *ring){ if(ring->Read_Index == ring->Write_Index) { if(ring->Read_Mirror == ring->Write_Mirror) { return RING_BUF_EMPTY; } else { return RING_BUF_FULL; } } return RING_BUF_HALFFULL;}
This simple state check contains a clever design: When the read and write indices are equal, it determines whether the buffer is empty or full by comparing the mirror bits, avoiding the traditional scheme of reserving one byte.
Data Writing Process
uint16_t DoubleBuffer_Write(DoubleBufferHandle handle, uint8_t* buffer, uint16_t datalen, rt_int32_t timeout){ // Acquire write lock if (rt_mutex_take(&DoubleBuffer->WriteLock, timeout) != RT_EOK) { return 0; } // Check space (length information requires 2 bytes) if ((wSubBuffer->bufferSize - getSubBufferDataLen(wSubBuffer)) < (datalen + 2)) { rt_mutex_release(&DoubleBuffer->WriteLock); return 0; } // Write length information first, then write data uint8_t len_buf[2] = {(datalen >> 8) & 0xFF, datalen & 0xFF}; SubBuffer_Write(wSubBuffer, len_buf, 2); SubBuffer_Write(wSubBuffer, buffer, datalen); rt_mutex_release(&DoubleBuffer->WriteLock); return datalen;}
Key Design Points:
-
Length Information Preceding: Solves boundary judgment issues for variable-length packets
-
Space Pre-check: Avoids inconsistent states from partial writes
-
Timeout Mechanism: Prevents deadlocks and ensures system real-time performance
Buffer Swapping Strategy
static int swap_buffers(Double_buffer *DoubleBuffer, rt_int32_t timeout){ // Acquire write lock if (rt_mutex_take(&DoubleBuffer->WriteLock, timeout) != RT_EOK) { return 0; } // Swap read and write buffer indices uint8_t tmpIndex = DoubleBuffer->WriteIndex; DoubleBuffer->WriteIndex = DoubleBuffer->ReadIndex; DoubleBuffer->ReadIndex = tmpIndex; rt_mutex_release(&DoubleBuffer->WriteLock); return 1;}
This is the essence of double buffering: By simply swapping indices, it achieves the role reversal of buffers, avoiding large memory copies of data!
Special Optimization for Interrupt Context
In interrupt handler functions, we cannot use mutexes, so we provide a dedicated ISR version:
uint16_t DoubleBuffer_Write_ISR(DoubleBufferHandle handle, uint8_t* buffer, uint16_t datalen){ rt_base_t level; Double_buffer* DoubleBuffer = (Double_buffer*)handle; // Disable interrupts to protect critical section level = rt_hw_interrupt_disable(); // ... Write logic is the same as the normal version rt_hw_interrupt_enable(level); return datalen;}
Design Philosophy: Use interrupt disabling in interrupt context and mutexes in task context, each in its proper place!
Performance Comparison in Practice
To verify the actual effect of double buffering, we conducted tests on the STM32F407 platform:
| Solution | Data Throughput | CPU Utilization | Data Loss Rate |
|---|---|---|---|
| Single Buffer + Mutex | 1.2MB/s | 45% | 0.5% |
| Double Buffer (This Solution) | 3.8MB/s | 18% | 0% |
| Performance Improvement | 316% | Reduced by 60% | Completely Resolved |
Five Practical Tips and Pitfall Avoidance Guide
1. Buffer Size Selection
// Empirical formula: Buffer size = Maximum packet size × 2 + Safety margin#define BUFFER_SIZE (MAX_PACKET_SIZE * 2 + 256)
2. Timeout Settings
-
High real-time tasks: RT_WAITING_NO
-
Normal tasks: Set according to business needs, 10-100ms
-
Non-real-time tasks: RT_WAITING_FOREVER
3. Error Handling Strategy
if (DoubleBuffer_Write(handle, data, len, 100) != len) { // Write failed, do not retry indefinitely! // Log, count errors, consider discarding or degrading processing error_count++; if (error_count > MAX_ERRORS) { // Trigger system recovery mechanism system_recovery(); }}
4. Debugging and Monitoring
#ifdef DB_DEBUG_INFO DB_Printf("write len = %d,residue = %d\r\n", datalen, wSubBuffer->bufferSize - getSubBufferDataLen(wSubBuffer));#endif
Application Scenario Expansion
This double buffering design is not only suitable for ordinary data acquisition but can also be widely applied to:
-
High-speed ADC Data Acquisition: Ensures no sampling points are lost
-
Network Packet Processing: Handles burst traffic
-
Image Sensor Data: Processes large blocks of image data
-
Logging Systems: Avoids log output blocking business logic
-
Command-Response Protocols: Ensures command integrity and timely response
Conclusion
The core value of double buffering technology lies in exchanging space for time, significantly improving system throughput while ensuring data safety. The implementation scheme provided in this article has the following advantages:
-
Thread Safety: Comprehensive locking mechanism protection
-
Efficient Transmission: Pointer swapping replaces memory copying
-
Flexible Configuration: Supports timeout, interrupt context, and other scenarios
-
Easy Integration: Based on RT-Thread, easily portable to other RTOS
In actual projects, adopting this double buffering design has led toa more than threefold increase in data transmission efficiency, a 60% reduction in CPU utilization, and completely resolved data loss issues.
Discussion: What troublesome issues have you encountered in embedded data transmission? Or do you have any suggestions for improving the double buffering scheme mentioned in this article?Feel free to share your thoughts in the comment section below!
Do you think this article has solved your long-standing pain points?Like 👍, share, and bookmark 📁, so more embedded developers can benefit!
Original Statement: This article was originally published by [Embedded Xiao Jin], and sharing is welcome. For reprints, please contact for authorization.
Author/Editor: Xiao Jin
Previous Recommendations:
-
The Secret Weapon for Efficient Data Processing in Embedded Systems: Detailed Implementation of Double Buffering
-
“Circular Buffer Queue: The “Data Interchange Bridge” for Embedded Communication | Efficient Processing Secrets for 51/32 Microcontrollers”
Get Resources: Follow the public account and reply<span>Double Buffer Source Code</span> to obtain the complete project code of this article!