
FreeRTOS provides two specialized data structures for high-throughput data transfer between tasks and interrupts: Message Buffers and Stream Buffers. These lightweight alternatives to queues excel at moving large blocks of data efficiently, making them ideal for scenarios involving DMA transfers, audio processing, sensor data logging, and high-speed communication protocols.
[!WARNING] Crucial Constraint: Unlike standard FreeRTOS Queues, both Message Buffers and Stream Buffers are strictly designed for single-reader, single-writer scenarios. They are not thread-safe for concurrent multiple writers or multiple readers without external synchronization (like a Mutex).
Message Buffers are optimized for sending and receiving discrete, variable-length messages where each send operation corresponds to a complete logical message. Unlike queues that store individual data items, Message Buffers store raw bytes and use a length prefix to delineate message boundaries.
sizeof(size_t) minus 1 byte)// Create a Message Buffer (1KB capacity)MessageBufferHandle_t xMessageBuffer = xMessageBufferCreate(1024);// Send a message from task contextsize_t bytes_sent = xMessageBufferSend(xMessageBuffer,tx_buffer,message_length,portMAX_DELAY);// Receive a message from task contextsize_t bytes_received = xMessageBufferReceive(xMessageBuffer,rx_buffer,sizeof(rx_buffer),portMAX_DELAY);// Send from ISR (no blocking)BaseType_t xHigherPriorityTaskWoken = pdFALSE;xMessageBufferSendFromISR(xMessageBuffer,tx_data,tx_length,&xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
Message Buffers use a circular buffer design with these key components:
sizeof(size_t) length prefix (typically 4 bytes) before each message dataWhen sending a message:
Stream Buffers are designed for continuous byte streams where there are no inherent message boundaries. Think of them as “pipes” for bytes - you can write any number of bytes at any time and read any number of bytes when available.
// Create a Stream Buffer (2KB capacity, 1 byte trigger level)StreamBufferHandle_t xStreamBuffer = xStreamBufferCreate(2048, 1);// Send bytes from task contextsize_t bytes_sent = xStreamBufferSend(xStreamBuffer,tx_data,tx_length,portMAX_DELAY);// Receive bytes from task contextsize_t bytes_received = xStreamBufferReceive(xStreamBuffer,rx_buffer,rx_length,portMAX_DELAY);// Send from ISRBaseType_t xHigherPriorityTaskWoken = pdFALSE;xStreamBufferSendFromISR(xStreamBuffer,tx_data,tx_length,&xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
Under the hood in FreeRTOS, Message Buffers are actually built on top of Stream Buffers. Stream Buffers serve as the foundational lock-free circular buffer primitive with the following structure:
xHead): Next write positionxTail): Next read positionThe trigger level is particularly useful - you can configure a Stream Buffer to only unblock a receiving task when at least N bytes are available, reducing task wake-up frequency for byte-stream processing.
Both buffer types excel when combined with DMA for high-efficiency data transfer between peripherals and tasks.
// Global handlesStreamBufferHandle_t xUartRxStream;DMA_HandleTypeDef hdma_usart2_rx;// Stream Buffer for UART RX (4KB, 1 byte trigger level)xUartRxStream = xStreamBufferCreate(4096, 1);// UART IDLE line interrupt (detects frame end)void USART2_IRQHandler(void){if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) {// Clear IDLE flag__HAL_UART_CLEAR_IDLEFLAG(&huart2);// Calculate bytes received via DMAuint16_t bytes_received = UART_RX_BUFFER_SIZE -__HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// Send bytes to Stream Buffer from ISRBaseType_t xHigherPriorityTaskWoken = pdFALSE;xStreamBufferSendFromISR(xUartRxStream,uart_rx_buffer,bytes_received,&xHigherPriorityTaskWoken);// Restart DMA for next receptionHAL_UART_Receive_DMA(&huart2, uart_rx_buffer, UART_RX_BUFFER_SIZE);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}}// UART RX Taskvoid uart_rx_task(void *param){uint8_t rx_buffer[128];size_t bytes_received;while (1) {// Block until at least 1 byte available (or use higher trigger level)bytes_received = xStreamBufferReceive(xUartRxStream,rx_buffer,sizeof(rx_buffer),portMAX_DELAY);// Process received bytes (could be any number 1-128)process_uart_data(rx_buffer, bytes_received);}}
// Message Buffer for dynamic SPI packetsMessageBufferHandle_t xSpiPacketMsg;uint8_t spi_rx_buffer[MAX_SPI_PACKET];// SPI receive complete ISRvoid HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi){if (hspi->Instance == SPI1) {// Calculate dynamic packet length (e.g., from header)size_t packet_length = calculate_packet_length(spi_rx_buffer);BaseType_t xHigherPriorityTaskWoken = pdFALSE;// Send the entire packet as one discrete messagexMessageBufferSendFromISR(xSpiPacketMsg,spi_rx_buffer,packet_length,&xHigherPriorityTaskWoken);// Setup next receptionHAL_SPI_Receive_DMA(&hspi1, spi_rx_buffer, MAX_SPI_PACKET);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}}
Both buffer types are highly memory-efficient for variable-length streams:
StreamBuffer_t)sizeof(size_t) overhead per message, whereas Queues waste memory if items are variable length (since Queue items are fixed-size).Stream and Message Buffers are significantly faster than Queues for bulk data transfer due to lower per-operation overhead (no item-size multiplication, no linked-list management). The exact throughput depends on:
-O2 vs -Os)// Message Buffer: xMessageBufferCreate(size_t xBufferSizeBytes)// Stream Buffer: xStreamBufferCreate(size_t xBufferSizeBytes, size_t xTriggerLevelBytes)// Example: Stream Buffer with 64-byte trigger levelxStreamBuffer = xStreamBufferCreate(1024, 64); // Unblock when ≥64 bytes available
Both support static and dynamic allocation:
xMessageBufferCreate() / xStreamBufferCreate() (uses heap)xMessageBufferCreateStatic() / xStreamBufferCreateStatic() (user-provided storage)I2S Peripheral↓ DMAStream Buffer (ISR → Task)↓Audio Processing Task↓Message Buffer (Task → Task)↓USB Audio Class Task
Multiple Sensors↓ (SPI/I2C/UART)Stream Buffers (Per-Channel ISR → Task)↓Data Aggregation Task↓Message Buffer (Task → File System Task)↓SD Card Writing Task
UART Receive↓ DMAStream Buffer (ISR → Parser Task)↓Command Parsing Task↓Message Buffer (Parser → Executor)↓Command Execution Task↓Message Buffer (Executor → Response Task)↓UART Transmit (Task → ISR via DMA)
This is the most common pitfall when migrating from Queues to Buffers. Stream and Message Buffers intentionally omit the heavy critical sections required for multi-client access to achieve high throughput.
xStreamBufferSend calls in a Mutex, or use a separate Stream Buffer for each sensor.*FromISR() APIs in interrupt contextpdTRUE to see if a task was wokenportYIELD_FROM_ISR() or portEND_SWITCHING_ISR() when neededportMAX_DELAY) in ISRs0 return from receive means timeout occurred (when not using portMAX_DELAY)| Feature | Message/Stream Buffers | Queues |
|---|---|---|
| Thread Safety | Strictly 1:1 (Single Reader/Writer) | M:N (Multiple Readers/Writers safe) |
| Memory overhead | ~32-40 bytes struct + buffer | ~76 bytes struct + (QueueLength × ItemSize) |
| Max item size | Limited only by buffer size | Fixed queue item size |
| Data copying | Always copies data into and out of buffer | Always copies data into and out of queue |
| Use case | Large data streams, variable messages | Small fixed-size items, discrete events |
| Feature | FreeRTOS Buffers | Manual Ring Buffers |
|---|---|---|
| Concurrency | Strictly 1:1 (Single Reader/Writer) | Depends on implementation |
| ISR safety | Dedicated FromISR APIs | Must handle manually |
| Blocking/waiting | Built-in (Direct to Task Notifications) | Must implement manually |
| Priority inheritance | Not supported (Mutexes only) | Not supported |
| Portability | Standard across FreeRTOS platforms | Platform-specific |
| Testing | Well-tested, community verified | Custom implementation risk |
Stream Buffers eliminate the need for complex application-level ping-pong buffers. A single Stream Buffer can continuously accept data from a circular DMA buffer.
// Circular ADC DMA buffer#define ADC_DMA_BUFFER_SIZE 256uint16_t adc_buffer[ADC_DMA_BUFFER_SIZE];// Single Stream Buffer for continuous ADC dataStreamBufferHandle_t xAdcStream;// DMA Half/Full Transfer callbacks (using circular DMA mode)void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc){BaseType_t xHigherPriorityTaskWoken = pdFALSE;// First half of circular buffer is readysize_t bytes = (ADC_DMA_BUFFER_SIZE / 2) * sizeof(uint16_t);xStreamBufferSendFromISR(xAdcStream, &adc_buffer[0], bytes, &xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc){BaseType_t xHigherPriorityTaskWoken = pdFALSE;// Second half of circular buffer is readysize_t bytes = (ADC_DMA_BUFFER_SIZE / 2) * sizeof(uint16_t);xStreamBufferSendFromISR(xAdcStream, &adc_buffer[ADC_DMA_BUFFER_SIZE / 2], bytes, &xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}
Message Buffers preserve message boundaries, making them perfect for network packets where each send is one complete packet.
void ethernet_rx_task(void *param){uint8_t *rx_buffer;size_t bytes_received;// Allocate a max-size buffer for receiving complete messagesrx_buffer = pvPortMalloc(MAX_PACKET_SIZE);configASSERT(rx_buffer);while (1) {// Read entire packet as a single messagebytes_received = xMessageBufferReceive(xEthMsgBuffer,rx_buffer,MAX_PACKET_SIZE,portMAX_DELAY);if (bytes_received > 0) {// Process the complete packetprocess_ethernet_packet(rx_buffer, bytes_received);}}}
(Note: As mentioned in Best Practices, always add +1 byte to your final calculation)
Stream Buffers for DMA:
Message Buffers for Variable Messages:
Stream Buffer Trigger Level:
// Check buffer status for debuggingsize_t spaces_msg = xMessageBufferSpaceAvailable(xMsgBuf);bool is_empty_msg = xMessageBufferIsEmpty(xMsgBuf);// For Stream Buffers:size_t spaces_stream = xStreamBufferSpacesAvailable(xStreamBuf);size_t bytes_stream = xStreamBufferBytesAvailable(xStreamBuf);bool is_empty_stream = xStreamBufferIsEmpty(xStreamBuf);bool is_full_stream = xStreamBufferIsFull(xStreamBuf);// Reset buffers (emergency use only)xMessageBufferReset(xMsgBuf);xStreamBufferReset(xStreamBuf);
// Use Stream Buffer for data + Event Group for signalingStreamBufferHandle_t xSensorData;EventGroupHandle_t xSensorEvents;#define DATA_NEW_BIT (1 << 0)#define ALARM_BIT (1 << 1) // Example of a second event// ISR: Sensor data arrivalvoid sensor_isr(void){size_t bytes = read_sensor_fifo(sensor_buf, SENSOR_FIFO_SIZE);// Send data to buffer. We pass NULL for pxHigherPriorityTaskWoken// because the task is blocked on the Event Group, not the Stream Buffer.xStreamBufferSendFromISR(xSensorData, sensor_buf, bytes, NULL);// Signal that new data is availableBaseType_t xHigherPriorityTaskWoken = pdFALSE;xEventGroupSetBitsFromISR(xSensorEvents,DATA_NEW_BIT,&xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}// Processing Taskvoid sensor_task(void *param){EventBits_t uxBits;uint8_t buffer[256];while (1) {// Wait for data available OR an alarm eventuxBits = xEventGroupWaitBits(xSensorEvents,DATA_NEW_BIT | ALARM_BIT,pdTRUE, // Clear on exitpdFALSE, // Don't wait for all bitsportMAX_DELAY);if (uxBits & DATA_NEW_BIT) {size_t len;// CRITICAL: We must loop to drain the buffer completely!// Since the event bit was cleared, if we don't read everything now,// remaining bytes will be stranded until the next interrupt occurs.do {len = xStreamBufferReceive(xSensorData,buffer,sizeof(buffer),0 // Don't block - drain available bytes);if (len > 0) {process_sensor_data(buffer, len);}} while (len > 0);}if (uxBits & ALARM_BIT) {handle_alarm_condition();}}}
The xTicksToWait parameter provides built-in timeout detection without requiring external software timers.
// Using Stream Buffer receive timeout to detect stalled streamsvoid stream_processing_task(void *param){uint8_t rx_buffer[128];const TickType_t xBlockTime = pdMS_TO_TICKS(2000); // 2 second timeoutwhile (1) {size_t bytes_received = xStreamBufferReceive(xUartStream,rx_buffer,sizeof(rx_buffer),xBlockTime);if (bytes_received > 0) {// Process received dataprocess_data(rx_buffer, bytes_received);} else {// Timeout occurred (0 bytes received in 2 seconds)// Handle stalled stream - flush, reset, or error recoveryhandle_stream_stall();}}}
FreeRTOS Message Buffers and Stream Buffers provide efficient, high-throughput mechanisms for data transfer between tasks and interrupts. Their low memory overhead, ISR-safe APIs, and DMA-friendly design make them superior to traditional queues for large data transfers.
Choose Message Buffers when:
Choose Stream Buffers when:
Both primitives excel in scenarios involving DMA, high-speed communication, and real-time data processing where traditional queues would introduce excessive overhead or latency. By understanding their characteristics and best practices, you can design efficient data pipelines that maximize throughput while minimizing CPU overhead in your FreeRTOS-based embedded systems.
Quick Links
Legal Stuff


