
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.
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.
// 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:
When 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)StreamBufferHandle_t xStreamBuffer = xStreamBufferCreate(2048);// 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);
Stream Buffers are simpler than Message Buffers as they don’t need message boundary tracking:
The 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 zero-copy data transfer between peripherals and tasks.
// Global handlesStreamBufferHandle_t xUartRxStream;DMA_HandleTypeDef hdma_usart2_rx;// Stream Buffer for UART RX (4KB)xUartRxStream = xStreamBufferCreate(4096);// 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 DMA completion signalsMessageBufferHandle_t xDmaCompleteMsg;// In DMA complete ISR (from any peripheral)void DMA1_Stream5_IRQHandler(void){if (__HAL_DMA_GET_FLAG(&hdma_memtomem, DMA_FLAG_TCIF5_)) {// Clear transfer complete flag__HAL_DMA_CLEAR_FLAG(&hdma_memtomem, DMA_FLAG_TCIF5_);// Send completion token (could be buffer pointer, size, etc.)uint32_t completion_token = get_dma_completion_info();BaseType_t xHigherPriorityTaskWoken = pdFALSE;xMessageBufferSendFromISR(xDmaCompleteMsg,&completion_token,sizeof(completion_token),&xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}}// Consumer task processes DMA completion eventsvoid dma_processing_task(void *param){uint32_t token;while (1) {// Wait for DMA completion signalsize_t bytes_received = xMessageBufferReceive(xDmaCompleteMsg,&token,sizeof(token),portMAX_DELAY);if (bytes_received == sizeof(token)) {process_dma_completion(token);}}}
Both buffer types are extremely memory-efficient:
On a Cortex-M4 @ 180MHz:
// 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)
*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 |
|---|---|---|
| Memory overhead | Very low (2-4 bytes) | Higher (item storage + links) |
| Max item size | Limited only by buffer size | Limited by queue item size |
| Data copying | Optional zero-copy | Always copies data |
| Deterministic timing | Yes (predictable) | Less predictable (variable item sizes) |
| Use case | Large data streams, variable messages | Small fixed-size items, discrete events |
| Feature | FreeRTOS Buffers | Manual Ring Buffers |
|---|---|---|
| Thread safety | Built-in (mutexes/semaphores) | Must implement manually |
| ISR safety | Dedicated FromISR APIs | Must handle carefully |
| Blocking/waiting | Built-in semaphore support | Must implement manually |
| Priority inheritance | Automatic (via RTOS primitives) | Manual implementation needed |
| Portability | Standard across FreeRTOS platforms | Platform-specific |
| Testing | Well-tested, community verified | Custom implementation risk |
// Two Stream Buffers for ping-pong bufferingStreamBufferHandle_t rx_buffer_a;StreamBufferHandle_t rx_buffer_b;StreamBufferHandle_t* active_buffer = &rx_buffer_a;// DMA Half/Full Transfer callbacksvoid HAL_DMA_HalfTransferCallback(DMA_HandleTypeDef *hdma){if (hdma == &hdma_adc) {// First half complete - process buffer A while filling Bsize_t bytes = get_half_buffer_size();xStreamBufferSendFromISR(*active_buffer, adc_data_half_a, bytes, NULL);active_buffer = &rx_buffer_b; // Switch to buffer B}}void HAL_DMA_TransferCompleteCallback(DMA_HandleTypeDef *hdma){if (hdma == &hdma_adc) {// Second half complete - process buffer B while filling Asize_t bytes = get_half_buffer_size();xStreamBufferSendFromISR(*active_buffer, adc_data_half_b, bytes, NULL);active_buffer = &rx_buffer_a; // Switch back to buffer A}}
// Network packet structure: [4-byte length][N-byte payload]typedef struct {uint32_t length; // Network byte orderuint8_t payload[]; // Variable length} network_packet_t;void ethernet_rx_task(void *param){network_packet_t* pkt;uint32_t net_len;size_t bytes_received;BaseType_t xHigherPriorityTaskWoken;while (1) {// Read length prefix first (4 bytes)bytes_received = xMessageBufferReceive(xEthMsgBuffer,&net_len,sizeof(net_len),portMAX_DELAY);if (bytes_received != sizeof(net_len)) continue;uint32_t payload_len = ntohl(net_len);if (payload_len > MAX_PAYLOAD_SIZE) {// Handle error - packet too largecontinue;}// Allocate buffer for full packetpkt = pvPortMalloc(sizeof(network_packet_t) + payload_len);if (!pkt) continue; // Handle OOMpkt->length = net_len; // Store in network byte order// Read payloadbytes_received = xMessageBufferReceive(xEthMsgBuffer,pkt->payload,payload_len,portMAX_DELAY);if (bytes_received == payload_len) {// Send complete packet to processing taskxQueueSendToBack(xEthPacketQueue, &pkt, 0);} else {// Handle error - incomplete packetvPortFree(pkt);}}}
Stream Buffers for DMA:
Message Buffers for Variable Messages:
Stream Buffer Trigger Level:
// Check buffer status for debuggingsize_t spaces_available = xMessageBufferSpacesAvailable(xMsgBuf);size_t bytes_available = xMessageBufferLength(xMsgBuf);// For Stream Buffers:size_t spaces = xStreamBufferSpacesAvailable(xStreamBuf);size_t bytes = xStreamBufferLength(xStreamBuf);bool is_empty = xStreamBufferIsEmpty(xStreamBuf);bool is_full = xStreamBufferIsFull(xStreamBuf);// Reset buffers (emergency use only)xMessageBufferReset(xMsgBuf);xStreamBufferReset(xStreamBuf);
// Use Event Buffer for data + Event Group for signalingStreamBufferHandle_t xSensorData;EventGroupHandle_t xSensorEvents;#define DATA_NEW_BIT (1 << 0)#define BUFFER_FULL_BIT (1 << 1)// ISR: Sensor data arrivalvoid sensor_isr(void){size_t bytes = read_sensor_fifo(sensor_buf, SENSOR_FIFO_SIZE);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 buffer full (timeout protection)uxBits = xEventGroupWaitBits(xSensorEvents,DATA_NEW_BIT | BUFFER_FULL_BIT,pdTRUE, // Clear on exitpdFALSE, // Don't wait for all bitsportMAX_DELAY);if (uxBits & DATA_NEW_BIT) {size_t len = xStreamBufferReceive(xSensorData,buffer,sizeof(buffer),0 // Don't block - we know data is available);process_sensor_data(buffer, len);}}}
// Timeout detection for stalled streamsTimerHandle_t xStreamTimeoutTimer;// Stream receive callback (called when data arrives)void vStreamReceiveCallback(StreamBufferHandle_t xStream, BaseType_t xBytesReceived){// Restart timeout timer on data activityxTimerResetFromISR(xStreamTimeoutTimer, NULL);}// Timer callback (stream stalled)void vStreamTimeoutCallback(TimerHandle_t xTimer){// Handle stalled stream - flush, reset, or error recoveryhandle_stream_stall();}// In initialization:xStreamTimeoutTimer = xTimerCreate("StreamTimeout",pdMS_TO_TICKS(2000), // 2 second timeoutpdFALSE, // Don't auto-reload(void*)xUartStream, // Timer ID = stream handlevStreamTimeoutCallback);// Set stream buffer receive callbackvStreamBufferSetReceiveCallback(xUartStream, vStreamReceiveCallback);// Start the timeout timer (will be reset on data activity)xTimerStart(xStreamTimeoutTimer, 0);
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




