O que são Stream Buffers no FreeRTOS e como eles funcionam internamente
Os Stream Buffers no FreeRTOS são mecanismos de comunicação projetados para transferência eficiente de fluxos contínuos de dados, organizados byte a byte, entre um produtor e um consumidor. Diferentemente das queues, que trabalham com elementos discretos e de tamanho fixo, o stream buffer é essencialmente um buffer circular de bytes, no qual não existe, por definição, o conceito de “mensagem” — apenas uma sequência ordenada de dados.
Internamente, um stream buffer é implementado como uma estrutura que mantém um array de bytes, dois ponteiros (ou índices) — um de escrita (write index) e outro de leitura (read index) — e metadados de controle, como o tamanho total do buffer, o número de bytes atualmente armazenados e os mecanismos de bloqueio associados. O controle de concorrência é feito pelo próprio kernel do FreeRTOS, garantindo que operações de escrita e leitura sejam atômicas do ponto de vista do sistema, tanto em contexto de tarefa quanto de interrupção.
Um ponto fundamental é que o FreeRTOS assume um único escritor e um único leitor por stream buffer. Esse não é um detalhe trivial: essa suposição permite uma implementação mais eficiente, com menos sobrecarga de sincronização do que uma queue genérica, mas exige disciplina arquitetural por parte do desenvolvedor. Se múltiplas tarefas tentarem escrever ou ler simultaneamente do mesmo stream buffer, o comportamento não é garantido e erros sutis podem ocorrer.
Do ponto de vista de bloqueio, o comportamento é intuitivo: quando uma tarefa tenta escrever em um stream buffer cheio, ela pode ser bloqueada até que haja espaço disponível; da mesma forma, quando uma tarefa tenta ler de um buffer vazio, ela pode ser bloqueada até que novos dados cheguem. O desbloqueio ocorre automaticamente quando o evento oposto acontece (leitura libera espaço, escrita adiciona dados). Esse modelo se encaixa perfeitamente em pipelines de dados, como recepção serial, processamento e envio para outro subsistema.
A API de stream buffers reflete essa filosofia de fluxo contínuo. Em vez de funções como xQueueSend() ou xQueueReceive(), utilizamos chamadas como xStreamBufferSend() e xStreamBufferReceive(), onde o tamanho do bloco transferido pode variar a cada chamada. Isso permite, por exemplo, escrever 3 bytes agora, 10 bytes depois, e ler quantos bytes estiverem disponíveis no momento, sem a rigidez estrutural imposta pelas queues.
Exemplo básico: criação de um Stream Buffer
#include "FreeRTOS.h"
#include "stream_buffer.h"
#define STREAM_BUFFER_SIZE 128
#define TRIGGER_LEVEL 1
StreamBufferHandle_t xStream;
void init_stream_buffer(void)
{
xStream = xStreamBufferCreate(
STREAM_BUFFER_SIZE,
TRIGGER_LEVEL
);
configASSERT(xStream != NULL);
}
Nesse exemplo, STREAM_BUFFER_SIZE define o tamanho total do buffer em bytes, enquanto o TRIGGER_LEVEL define quantos bytes precisam estar disponíveis para desbloquear uma tarefa bloqueada em leitura. Em sistemas de baixa latência, esse valor costuma ser pequeno (1 ou poucos bytes), enquanto em sistemas orientados a throughput, pode ser maior.
Exemplo de escrita em uma tarefa produtora
void vProducerTask(void *pvParameters)
{
const uint8_t data[] = "Hello FreeRTOS\n";
for (;;)
{
xStreamBufferSend(
xStream,
data,
sizeof(data),
portMAX_DELAY
);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
Aqui, a tarefa produtora escreve um bloco de bytes no stream buffer. Caso não haja espaço suficiente, ela será bloqueada até que o consumidor leia dados e libere espaço.
Exemplo de leitura em uma tarefa consumidora
void vConsumerTask(void *pvParameters)
{
uint8_t rxBuffer[32];
size_t bytesRead;
for (;;)
{
bytesRead = xStreamBufferReceive(
xStream,
rxBuffer,
sizeof(rxBuffer),
portMAX_DELAY
);
if (bytesRead > 0)
{
// Processa os bytes recebidos
}
}
}
Note que a leitura pode retornar menos bytes do que o solicitado, dependendo da quantidade disponível no buffer naquele momento. Esse comportamento é essencial para fluxos contínuos e diferencia radicalmente stream buffers de queues.