ÍNDICE DE CONTEÚDO
Olá caro leitor. Neste novo artigo vou apresentar a vocês mais uma estrutura de controle de memória bastante presente, leve e eficiente em sistemas embarcados, o Ping-pong Buffer. Nos últimos artigos publicados mostramos o buffer circular e seu derivado, a fila circular, e apresentamos onde eles podem ser úteis para resolver os problemas do dia a dia na bancada. Com o Ping-pong (sem tradução livre compatível) não é diferente. Sua política de inserção e remoção de dados pode ajudar principalmente em aplicações que envolvem processamento digital de sinais em não-tempo-real, ou em tempo real com maior permissão de latência (por exemplo, DSP utilizando processadores de baixo custo).
O que é o Ping-pong Buffer?
O Ping-pong Buffer não se trata de um derivado do buffer circular (embora algumas implementações possam utilizar ele como base), mas sim de uma estrutura nova. Sua política de trabalho segue algo similar ao: “Dividir para conquistar”. O que quer dizer que o tratamento dos dados são divididos em dois grupos, os dados em captura, e os dados em processamento. A ideia então é possuir uma área de memória divida logicamente em duas partes iguais, com dois canais independentes de acesso (os chamados “switches”). Dessa forma, o canal “Ping” tem por função receber uma cadeia de bytes produzida por uma fonte (por exemplo amostras vindas de um conversor A/D) até que seu espaço de memória seja totalmente preenchido. Já o canal “Pong” tem por função ler a memória a ele reservado previamente, cheia pelo canal “Ping” e realizar algum processamento relevante. Vejamos a figura abaixo:
A figura 1 ilustra perfeitamente a política de trabalho da qual falamos a pouco, de forma que quando o canal “Ping” está cheio e o canal “Pong” vazio (essa operação é gerenciada na grande maioria das vezes pelo usuário), uma operação de sincronização é realizada e o canal Ping aponta a memória antes destinada ao canal Pong, e para este último, lhe resta apontar para o canal que era anteriormente do “Ping” com os novos dados disponíveis para processamento. Enquanto isso, o novo canal Ping cuida de preecher a memória com novos dados. Essa estratégia permite economizar o uso de CPU, uma vez que a tarefa de aplicação só será colocada em modo ativo quanto novos dados estiverem disponíveis, sendo possível assim gerar uma linha de delay de acordo com tamanho de memória reservada, multiplicada pela taxa de preenchimento do buffer.
Vejam a figura abaixo comparando o processamento amostra a amostra vindo de um conversor A/D contra a um processamento em blocos utilizando um Ping-pong Buffer:
Na figura acima temos dois casos de uso para processamento de um sinal que chega de um conversor A/D. No caso 1, o da esquerda, uma rotina de interrupção acessa o registrador de dados e escreve em uma região previamente alocada, de forma que a aplicação tem que processa-la para não gerar distorção. Porém, a tarefa será executada a cada captura do A/D. No caso 2, à direita, um periférico de transferência automática de memória captura as amostras e as escreve em uma área de memória previamente alocada, e uma interrupção será gerada apenas quando essa área for preenchida, reduzindo muito o uso da CPU que só irá executar a aplicação quando o efeito de buffer cheio for gerado (temporalmente em menor escala do que a cada ocorrência de uma interrupção para o A/D).
Legal esse caso de uso do A/D, mas onde uma coisa conecta com outra?
Vamos à parte que mais gosto desses artigos, o uso prático. A grande vantagem do buffer apresentado é que ele consegue otimizar ambos os casos de uso do exemplo apresentado acima. No caso 1, o nosso ping-pong buffer serviria como uma área de memória a ser preenchida para coletar várias amostras. Com isso a rotina de interrupção torna-se leve, tendo apenas que inserir amostra a amostra no buffer e quando esse estiver cheio, podendo enviar um sinal acordando a aplicação, que por sua vez troca os canais, e processa o novo buffer enquanto o outro enche. Esse caso inclusive é perfeito em microcontroladores de baixo custo, que não possuem um periférico automático de transferência de memória como o DMA. Eu redigi esse artigo que demonstra o caso 1 otimizado com essa técnica.
O segundo caso de uso acaba por ser ainda mais otimizado, pois com o DMA a rotina de interrupção do A/D simplesmente deixa de ser necessária. O DMA nesse caso tem por função receber o endereço de memória onde os dados serão depositados e automaticamente, a cada nova amostra do A/D, esta será copiada para o canal “Ping”, e ao preenchimento completo dele, uma interrupção será gerada, onde os dados ficariam acessíveis para processamento os canais de acesso então se invertem e o novo endereço de memória é enviado a o DMA que se encarrega de preencher o novo bloco. A figura abaixo ilustra bem o que ocorre:
Um terceiro caso que vale a pena citar para o uso de um Ping-pong buffer é o tratamento de imagens para controladores de display (os populares TFT). Com o uso de um Ping-pong buffer é possível enviar ao display um framebuffer já processado através do canal “Pong”, enquanto o processador utiliza o canal “Ping” para realizar operações como desenhar ou reposicionar objetos na tela. O mesmo caso aplica-se ao uso de conversor D/A para geração de sinais, vejam mais uma figurinha que ilustra esse caso:
Implementação básica de um Ping-pong buffer
Nesse exemplo de implementação de um ping-pong buffer tentei ser um pouco mais amplo, provendo a clássica macro que declara um ping-pong devidamente inicializado e pronto para uso. As rotinas de insert e retrieve estão disponíveis e automaticamente gerenciam onde os canais “Ping” e “Pong” devem acessar a memória reservada. A função ppbuf_get_full_signal é responsável pela operação de sincronização. Assim, quando seu retorno for true, a aplicação que chama essa função tem a opção de consumir o evento, e quanto isso ocorrem os canais Ping e Pong se invertem, podendo num novo ciclo ser inicializado.
Vejamos a interface do nosso Ping-pong Buffer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
/** * @brief simple ping-pong buffer implementation */ #ifndef PING_PONG_BUFFER_H_ #define PING_PONG_BUFFER_H_ #include <stdbool.h> /* ping pong buffer control structure */ typedef struct { unsigned char *buffer_data; unsigned char ping; unsigned char pong; int buffer_size; int put_index; int get_index; bool full_signal; }ppbuf_t; /** * @brief insert on active buffer */ int ppbuf_insert_active(ppbuf_t *p, void *data, int size); /** * @brief remove from inactive buffer */ int ppbuf_remove_inactive(ppbuf_t *p, void *data, int size); /** * @brief USE WITH DMA ONLY! get the current active buffer address */ unsigned char *ppbuf_dma_get_active_addr(ppbuf_t* p, int *size); /** * @brief USE WITH DMA ONLY! get the current inactive buffer address */ unsigned char *ppbuf_dma_get_inactive_addr(ppbuf_t* p, int *size); /** * @brief USE WITH DMA ONLY! force full signaling to next buffer become available */ int ppbuf_dma_force_swap(ppbuf_t* p); /** * @brief get full signal */ bool ppbuf_get_full_signal(ppbuf_t *p, bool consume); /* instantiate a fully initialized and static ping-pong buffer */ #define PPBUF_DECLARE(name,size) \ unsigned char ppbuf_mem_##name[size * 2] = {0}; \ ppbuf_t name = { \ .buffer_data = &ppbuf_mem_##name[0], \ .ping = 1, \ .pong = 0, \ .buffer_size = size, \ .put_index = 0, \ .get_index = 0, \ .full_signal = false \ } #endif /* PING_PONG_BUFFER_H_ */ |
As funções ppbuf_dma_xxx são para uso exclusivo com DMA, um certo cuidado deve se ter ao utilizá-las pois ela devolve o endereço de memória do canal “Ping” ou “Pong” correspondente para ser passado ao DMA. Junto com ppbuf_dma_force_swap, que força a troca de canal de forma assíncrona. Vejamos a implementação:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
/** * @brief simple ping pong buffer implementation */ #include <string.h> #include "ping_pong_buffer.h" int ppbuf_insert_active(ppbuf_t *p, void *data, int size){ int ret = 0; unsigned char *ptr; if(p == NULL || data == NULL || size == 0) { /* check your parameters */ ret = -1; } else { if(size > (p->buffer_size - p->put_index)) { /* not enough room for new samples */ ret = -1; } else { /* take the current position */ int mem_position = ((p->ping) * p->buffer_size) + p->put_index; ptr = (unsigned char *)p->buffer_data; /* copy the contents */ memcpy(&ptr[mem_position], data, size); /* update put index */ p->put_index += size; p->full_signal = (p->put_index >= p->buffer_size?true:false); /* swap will only generated when ppbuf_get_full_signal is called */ ret = 0; } } return(ret); } int ppbuf_remove_inactive(ppbuf_t *p, void *data, int size){ int ret = 0; unsigned char *ptr; if(p == NULL || data == NULL || size == 0) { /* check your parameters */ ret = -1; } else { if(size > (p->buffer_size - p->get_index)) { /* not enough data in sample buffer */ ret = -1; } else { /* take the current position */ int mem_position = ((p->pong) * p->buffer_size) + p->get_index; ptr = (unsigned char *)p->buffer_data; /* copy the contents */ memcpy(data,&ptr[mem_position], size); /* update put index */ p->get_index += size; /* when buffer is empty we are not able to extract anymore data */ ret = 0; } } return(ret); } unsigned char *ppbuf_dma_get_active_addr(ppbuf_t* p, int *size){ if(p == NULL || size == NULL) { /* no valid parameters return a invalid pointer */ return(NULL); } else { /* insertion buffer is always the pong */ return((unsigned char *)&p->buffer_data[p->pong * p->buffer_size]); } } unsigned char *ppbuf_dma_get_inactive_addr(ppbuf_t* p, int *size){ if(p == NULL || size == NULL) { /* no valid parameters return a invalid pointer */ return(NULL); } else { /* insertion buffer is always the pong */ return((unsigned char *)&p->buffer_data[p->ping * p->buffer_size]); } } int ppbuf_dma_force_swap(ppbuf_t* p) { int ret = 0; /* this function is asynchronous, so it must be used with * caution or a buffer corrpution will occur */ if(p == NULL) { ret = -1; } else { /* for safety swaps ocurrs only with a presence of a previous full signal */ if(p->full_signal != false) { p->full_signal = false; /* swap the buffer switches */ p->ping = p->ping ^ p->pong; p->pong = p->pong ^ p->ping; p->ping = p->ping ^ p->pong; p->get_index = 0; p->put_index = 0; } } return(ret); } bool ppbuf_get_full_signal(ppbuf_t *p, bool consume) { /* take the last signaled full occurrence */ bool ret = (p != NULL ? p->full_signal : false); if((consume != false) && (p != NULL) && (ret != false)) { p->full_signal = false; /* swap the buffer switches */ p->ping = p->ping ^ p->pong; p->pong = p->pong ^ p->ping; p->ping = p->ping ^ p->pong; /* resets the buffer position */ p->get_index = 0; p->put_index = 0; } return(ret); } |
A implementação possui pouco a se comentar, operações de posicionamento de memória, e novamente a eficiência para cópia dos dados em processadores sem DMA está condicionada à implementação interna da função memcpy, geralemente otimizada para IDE, Compilador ou arquitetura.
Conclusão
A estrutura Ping-pong buffer é uma estrutura leve e eficiente para captura e processamento de dados de forma quase que simultânea. Sua estratégia de captura de dados, enquanto outra sequência é processada, a torna ideal para uso com processamento de sinais e imagens, evitando frequência elevada de eventos periódicos relacionadas a novas amostras que chegam do (ou vão para o) hardware. Espero que seja mais um objeto útil ao leitor. Bons projetos!
Links úteis
Acesse aqui o repositório do Github contendo os arquivos e uma aplicação de exemplo simples que ensina a alocar, inserir e retirar os dados.