Trilha de aprendizado · Nível 13 · Tutorial 2

Média móvel com buffer circular

Implementar uma média móvel com memória e tempo de execução previsíveis, comparando a suavização obtida com o atraso introduzido.

  • Nível: Intermediário
  • Duração: 18 min
  • 8 passos
Média móvel com buffer circular

O que você vai percorrer

  1. Entenda o que entra em cada média Identifique as amostras de uma janela cheia e calcule médias móveis em saídas consecutivas. 2 min
  2. Reutilize as posições com um buffer circular Rastreie a posição que recebe cada nova amostra e mantenha o índice dentro do vetor. 2 min
  3. Atualize a soma sem percorrer a janela Atualize a soma de uma janela cheia retirando a amostra antiga e incluindo a nova, sem percorrer todo o buffer. 2 min
  4. Comece sem inventar medições de zero Evite tratar posições ainda não preenchidas como medições reais: conte as amostras válidas e use essa contagem como divisor até completar a janela. 2 min
  5. Proteja o acumulador e a divisão Dimensione o acumulador pela soma máxima e preserve médias fracionárias ao converter no momento correto. 2 min
  6. Integre e confira a implementação Integre uma função de média móvel em C/C++ e confira seu comportamento com uma sequência sintética completa. 3 min
  7. Relacione suavização, janela e atraso 2 min
  8. Compare duas janelas e valide sua escolha Validar a implementação e justificar a escolha entre duas janelas com base na suavização e no atraso permitido. 3 min

O que você vai aprender

  • Calcular manualmente as primeiras saídas de uma média móvel.
  • Implementar a janela com vetor circular e soma incremental.
  • Evitar erros de inicialização, índice e capacidade do acumulador.
  • Comparar duas janelas quanto à redução de flutuações e ao atraso de resposta.

Antes de começar

  • Vetores de tamanho fixo em C/C++
  • Variáveis, tipos e cálculos em C/C++
  • Período de atualização e orçamento de execução

Passo 1 de 8

Entenda o que entra em cada média

Identifique as amostras de uma janela cheia e calcule médias móveis em saídas consecutivas.

Veja a janela na sequência

Entrada, saída e posição

Uma sequência de leituras pode ser escrita como x[k], em que k indica a posição da amostra na sequência. A saída da média móvel será y[k].

Exemplo: se x[0] = 10, x[1] = 14 e x[2] = 12, esses são três valores de entrada em instantes consecutivos. Para uma janela de tamanho N, y[k] é a média das N amostras mais recentes, incluindo sempre a amostra atual x[k].

Neste step, considere somente janelas já completas.

A janela inclui a amostra atual

Sequência de amostras em blocos; uma janela destacada de quatro posições termina na amostra atual e aponta para a saída correspondente.

Para calcular y[5] com N = 4, entram x[2], x[3], x[4] e x[5].

Desloque a janela, não a história inteira

Regra da média móvel

Com a janela cheia, a regra é:

y[k] = (x[k] + x[k − 1] + ... + x[k − N + 1]) / N

Se N = 4, cada saída usa exatamente quatro amostras consecutivas. Ao chegar uma nova amostra, a janela avança uma posição: o novo valor entra e o valor que ficou mais distante no passado deixa de participar. Não se somam todas as leituras recebidas desde o início.

Exemplo

Duas saídas consecutivas

Considere N = 4 e esta sequência:

x[0]=8, x[1]=10, x[2]=14, x[3]=12, x[4]=16, x[5]=18

A primeira janela cheia termina em k=3:

y[3] = (8 + 10 + 14 + 12) / 4 = 11

Na amostra seguinte, a janela é deslocada:

y[4] = (10 + 14 + 12 + 16) / 4 = 13

O 8 saiu da janela; o 16 entrou. Os três valores do meio permanecem.

Pratique a próxima janela

Prática: identifique a amostra atual

Ainda com N = 4, use a sequência:

x[0]=8, x[1]=10, x[2]=14, x[3]=12, x[4]=16, x[5]=18

Complete a média da janela que termina em k=5:

y[5] = (14 + 12 + 16 + {{blank}}) / 4

Qual valor completa a expressão de y[5]?

Agora calcule o resultado numérico: y[5] = (14 + 12 + 16 + 18) / 4 = ____.

Fixe a regra

Resumo

Para uma janela cheia de N amostras:

  • y[k] usa x[k] e as N − 1 amostras imediatamente anteriores.
  • A amostra atual faz parte da média.
  • Na saída seguinte, entra uma nova amostra e sai a mais antiga da janela anterior.
  • A notação x representa a entrada; y representa a saída filtrada.

Nos próximos steps, essa mesma janela será mantida na memória sem deslocar todos os valores do vetor.

Cheque seu raciocínio

Com N = 4, quais amostras pertencem ao cálculo de y[6]?

Passo 2 de 8

Reutilize as posições com um buffer circular

Rastreie a posição que recebe cada nova amostra e mantenha o índice dentro do vetor.

A ideia do buffer circular

Reutilização em vez de deslocamento

Um vetor que dá a volta

Depois que a janela está cheia, não é preciso deslocar todas as amostras no vetor para abrir espaço. Um buffer circular reutiliza posições de um vetor fixo.

Considere N = 4 e uma janela, em ordem temporal, com as amostras 10, 20, 30, 40. Quando a próxima amostra, 50, chegar, a mais antiga (10) deve sair da janela. O índice de escrita aponta exatamente para a posição que contém essa amostra antiga.

Assim, basta sobrescrever uma posição e avançar o índice. Ao alcançar o fim do vetor, o índice retorna a 0.

A posição a substituir

Diagrama de um vetor circular de quatro posições. O índice aponta para a posição zero, que contém a amostra mais antiga, 10; a ordem temporal é 10, 20, 30, 40.

Com a janela cheia, o índice aponta para a amostra mais antiga: ela será substituída pela próxima entrada.

Escreva, avance e volte ao início

Posição física não é tempo

Duas ordens diferentes

A ordem física é a numeração fixa das posições do vetor: 0, 1, 2, 3. Já a ordem temporal indica quando cada valor foi recebido: do mais antigo ao mais novo.

Essas ordens coincidem apenas em alguns instantes. Depois que o índice retorna ao início, elas deixam de coincidir. Por exemplo, após substituir 10 por 50, o vetor físico fica {50, 20, 30, 40}. Mas a janela temporal passa a ser 20, 30, 40, 50.

Não interprete o vetor da posição 0 até N - 1 como uma sequência cronológica sem considerar o índice.

Uma atualização de posição

Escrita e avanço do índice; considere que buffer já contém N amostras válidas.

cpp
const uint8_t N = 4;
uint16_t buffer[N] = {10, 20, 30, 40};
uint8_t indice = 0;

// Chega a amostra 50.
buffer[indice] = 50;
indice++;

if (indice == N) {
  indice = 0;
}

// buffer: {50, 20, 30, 40}
// indice: 1
// ordem temporal: 20, 30, 40, 50

Antes e depois do retorno

Comparação do buffer antes e depois de o índice voltar ao início: antes ele aponta para a posição zero com valor 10; depois, 50 ocupa essa posição e o índice aponta para a posição um.

Ao chegar ao fim, o próximo avanço volta à posição zero; nenhuma posição fora do vetor é acessada.

Confira a sequência de atualização

Prática: siga o índice

Coloque os estados na ordem em que ocorrem. O buffer tem N = 4, está cheio com {10, 20, 30, 40} e indice = 3. A nova amostra é 50.

Ordene os eventos do primeiro ao último.

  1. Sobrescrever a posição 3: buffer passa a {10, 20, 30, 50}.
  2. Avançar o índice: ele passa de 3 para 4.
  3. Como o índice é igual a N, atribuir 0 ao índice.

Prática: proteja o limite

Complete a condição que impede que a próxima escrita use a posição N.

cpp
buffer[indice] = novaAmostra;
indice++;

if () {
  indice = 0;
}

Passo 3 de 8

Atualize a soma sem percorrer a janela

Atualize a soma de uma janela cheia retirando a amostra antiga e incluindo a nova, sem percorrer todo o buffer.

Guarde a soma da janela

A soma também é um estado

Com a janela já cheia, mantenha soma como um estado: ela é a soma exata dos valores presentes no buffer naquele instante. A média continua sendo calculada por soma / N, sem somar todas as posições novamente a cada amostra.

Quando chega uma entrada, apenas um valor sai da janela e apenas um entra. Portanto, a soma anterior pode ser corrigida em vez de recalculada.

Uma troca, duas alterações na soma

Diagrama de um buffer circular cheio com índice apontando para 12, soma inicial 60 e nova amostra 16; setas representam retirar 12 e incluir 16 na soma.

Ao substituir a amostra mais antiga, a soma perde o valor antigo e recebe a nova amostra.

Faça a substituição na ordem certa

Exemplo

Exemplo rastreado

Suponha N = 4, buffer em ordem física {12, 18, 20, 10}, indice = 0 e soma = 60. O índice aponta para a amostra antiga, 12. Ao chegar novo = 16:

  1. Retire o antigo: 60 − 12 = 48.
  2. Sobrescreva a posição: buffer passa a {16, 18, 20, 10}.
  3. Inclua o novo: 48 + 16 = 64.
  4. Avance o índice para a próxima posição.

A conferência direta do buffer atualizado é 16 + 18 + 20 + 10 = 64. Logo, a atualização incremental e a soma direta representam a mesma janela.

Ordem que preserva o valor antigo

Trecho para uma janela já cheia.

cpp
soma -= buffer[indice];
buffer[indice] = novo;
soma += novo;

indice++;
if (indice == N) {
  indice = 0;
}

Dica

Atenção à sobrescrita

A ordem importa: subtraia o valor armazenado antes de gravar novo na mesma posição. Depois da sobrescrita, a amostra antiga não está mais disponível no buffer.

Verifique a equivalência

Confira a soma por dois caminhos

Considere uma janela cheia com N = 4, buffer = {7, 11, 9, 13}, indice = 2 e soma = 40. Chega novo = 15.

Faça a troca mentalmente: a posição 2 contém 9, que sai; 15 entra nessa posição. Qual alternativa mostra tanto a nova soma incremental quanto a conferência pela soma direta do buffer resultante?

Soma incremental e soma direta

Qual verificação está correta?

Evite percorrer a janela

Retire e inclua

Complete os termos que mantêm o acumulador coerente ao trocar a amostra apontada por indice. Considere que a janela já está cheia.

Atualização do acumulador

Preencha o valor que sai e o valor que entra no acumulador.

cpp
soma -= ;
buffer[indice] = novo;
soma += ;

Trabalho fixo; memória proporcional à janela

A atualização incremental usa uma quantidade fixa de ações por amostra: acessar uma posição, subtrair, gravar, adicionar e avançar o índice. Esse trabalho não cresce quando N aumenta, pois não há laço percorrendo a janela a cada atualização.

Em contrapartida, o buffer precisa de N posições reservadas previamente. Assim, a memória é proporcional a N, enquanto o trabalho de atualização permanece independente do tamanho da janela.

Dica

Previsível não é idêntico

Um número fixo de operações torna o trabalho por amostra mais previsível para planejar a tarefa periódica. Isso não garante que cada chamada levará exatamente o mesmo tempo: interrupções e outras condições do sistema ainda podem alterar a duração observada.

Passo 4 de 8

Comece sem inventar medições de zero

Evite tratar posições ainda não preenchidas como medições reais: conte as amostras válidas e use essa contagem como divisor até completar a janela.

Inicialize sem falsear a média

Memória vazia não é medição zero

Antes da primeira amostra, inicialize o estado assim: vetor com zeros, indice = 0, soma = 0 e validas = 0.

Os zeros no vetor são apenas valores seguros para a memória ainda vazia — não são medições. Portanto, eles não podem entrar no divisor da média.

Quando chega uma amostra válida, ela entra no buffer e validas cresce. Enquanto a janela não está cheia, a saída é:

y = soma / validas

Quando validas chega a N, ela para de crescer. A partir daí, cada nova amostra substitui a mais antiga e a divisão continua usando validas, que já vale N.

Estado inicial e preenchimento

Diagrama de um buffer de quatro posições inicialmente sem amostras válidas, depois com uma amostra e, por fim, completamente preenchido.

Os zeros de inicialização ocupam a memória, mas somente amostras recebidas aumentam a contagem válida.

Acompanhe as primeiras saídas

Rastreamento de uma janela de quatro amostras

Considere N = 4 e as amostras 8, 12, 4, 16, 20. A tabela acompanha o estado depois de cada atualização. Na quinta entrada, a janela já estava cheia: o 8 é removido da soma e o 20 ocupa sua posição.

A contagem cresce até 4 e permanece em 4; por isso o divisor muda apenas durante o preenchimento.
Entrada x[k]Amostras válidas na janelaSomavalidasSaída y[k]
88818
128, 1220210
48, 12, 42438
168, 12, 4, 1640410
2012, 4, 16, 2052413

Atualize e só então calcule

Use a contagem como divisor enquanto preenche

A atualização abaixo preserva a lógica do buffer circular já estudada. A condição sobre validas impede que a contagem ultrapasse N. A divisão ocorre depois que a nova amostra foi contabilizada; assim, a função só calcula uma média quando há ao menos uma amostra válida e nunca divide por zero.

Atualização com janela parcialmente preenchida

Trecho para estudar e incorporar ao estado persistente do filtro.

cpp
const uint8_t N = 4;
uint16_t buffer[N] = {0};
uint8_t indice = 0;
uint8_t validas = 0;
uint32_t soma = 0;

float atualizaMedia(uint16_t amostra) {
  soma -= buffer[indice];
  buffer[indice] = amostra;
  soma += amostra;

  if (validas < N) {
    validas++;
  }

  indice++;
  if (indice == N) {
    indice = 0;
  }

  return (float)soma / validas;
}

Pratique a transição para a janela cheia

Primeira saída

Com N = 4, o buffer foi inicializado com zeros e acaba de receber a primeira amostra, de valor 8. A primeira saída da média móvel é {{blank}}.

Contagem limitada pela capacidade

Complete a atualização da contagem e o divisor. Considere que este cálculo ocorre após a possível atualização da contagem.

cpp
if (validas < N) {
  ++;
}

float media = (float)soma / ;

Passo 5 de 8

Proteja o acumulador e a divisão

Dimensione o acumulador pela soma máxima e preserve médias fracionárias ao converter no momento correto.

Dimensione pelo pior caso

Uma amostra não é a soma

A amostra do ADC do Uno pode caber em uint16_t: ela vai de 0 a 1023. Mas a variável soma guarda várias amostras ao mesmo tempo e precisa de outra verificação.

Para entradas não negativas, calcule o pior caso:

S_max = N × x_max

Aqui, N é a capacidade da janela e x_max é o maior valor possível de uma amostra. Escolha um acumulador que comporte S_max com margem compatível com seu projeto.

Capacidade do acumulador

Comparação visual de várias amostras de ADC sendo somadas: o recipiente de 16 bits transborda, enquanto o de 32 bits comporta a soma.

A faixa de cada amostra não define, sozinha, a faixa necessária para a soma.

Separe amostra de acumulador

Exemplo

Exemplo com ADC de 10 bits

Considere uma janela de 128 amostras do ADC de 10 bits. No máximo, cada amostra vale 1023:

S_max = 128 × 1023 = 130944

Esse resultado é maior que 65535, portanto não cabe em uint16_t. Em contrapartida, cabe em uint32_t, cujo máximo é muito maior. O vetor pode continuar guardando cada amostra em uint16_t; apenas o acumulador precisa ser ampliado.

Compare o limite calculado

Capacidades relevantes para esse caso.

O tipo da amostra e o tipo da soma podem ser diferentes.
ElementoMaior valorConclusão
Uma amostra do ADC1023`uint16_t` comporta
Soma para N = 128130944`uint32_t` necessário
Máximo de uint16_t65535Insuficiente para a soma
Máximo de uint32_t4294967295Suficiente para a soma

Pratique a escolha do tipo

Escolha o acumulador

Uma média móvel recebe códigos entre 0 e 1023 e usa N = 80. Qual alternativa calcula corretamente a soma máxima e escolhe um acumulador adequado?

Calcule e compare

Para N = 80 e x_max = 1023, qual escolha está correta?

Preserve o valor calculado

Converta antes da operação

Faça o cálculo do limite em um tipo amplo desde o começo. Não basta declarar a variável de destino como uint32_t: se a multiplicação ocorrer antes em um tipo estreito, o resultado já pode ter transbordado.

Na média, a mesma atenção vale para a divisão. Quando os dois operandos são inteiros, a fração é descartada antes de qualquer conversão posterior.

Conversões na posição certa

Cálculo seguro do limite e média com parte fracionária preservada.

cpp
const uint16_t N = 128;
const uint16_t X_MAX = 1023;

// A conversão acontece antes da multiplicação.
const uint32_t somaMax = (uint32_t)N * X_MAX;

uint32_t soma = 403;
uint16_t validas = 4;

// A conversão acontece antes da divisão.
float media = (float)soma / validas;  // 100.75

// Incorreto: a divisão inteira já descartou a fração.
float mediaErrada = (float)(soma / validas);  // 100.0

Dica

Converta antes de dividir

Em 403 / 4, ambos os operandos são inteiros; o resultado intermediário é 100. Convertê-lo depois produz apenas 100.0.

Já (float)soma / validas executa uma divisão em ponto flutuante e resulta em 100.75. A conversão deve ocorrer antes da operação que poderia perder informação.

Cheque os dois pontos críticos

Complete as expressões para calcular o limite da soma e a média fracionária corretamente.

cpp
const uint16_t N = 128;
const uint16_t X_MAX = 1023;
const uint32_t somaMax = (uint32_t) * X_MAX;

float y = (float) / validas;

Passo 6 de 8

Integre e confira a implementação

Integre uma função de média móvel em C/C++ e confira seu comportamento com uma sequência sintética completa.

Monte a função de atualização

Uma atualização completa

A função abaixo reúne o que você já construiu: memória fixa, índice circular, soma incremental e contagem válida. O estado fica fora da função para persistir entre chamadas. Com N = 4, cada chamada troca no máximo uma posição do vetor e faz a divisão pela quantidade de amostras válidas.

A sequência importa: retire o valor antigo da soma, grave a nova amostra, acrescente-a à soma, atualize a contagem e avance o índice. Assim, o valor removido ainda está disponível quando é necessário.

Diagrama de um buffer circular de quatro posições: o índice aponta para a posição cujo valor antigo sai da soma antes de receber a nova amostra.

O índice aponta para a próxima posição a substituir; após a escrita, ele avança e retorna a zero quando necessário.

Estado e função de média móvel

Trecho 1 — coloque no início de um sketch Arduino.

cpp
const uint8_t N = 4;

uint16_t buffer[N] = {0};
uint8_t indice = 0;
uint8_t validas = 0;
uint32_t soma = 0;

float atualizaMedia(uint16_t x) {
  soma -= buffer[indice];
  buffer[indice] = x;
  soma += x;

  if (validas < N) {
    validas++;
  }

  indice++;
  if (indice == N) {
    indice = 0;
  }

  return (float)soma / validas;
}

Execute ou acompanhe pelo resultado

Teste reproduzível no Uno

Acrescente este trecho logo depois do trecho anterior, no mesmo sketch. Ele usa dados sintéticos: não exige sensor, circuito ou biblioteca externa.

Ts_ms = 10 declara que as amostras representam espaçamento nominal de 10 ms. As linhas no monitor serial podem aparecer com outro intervalo, pois a transmissão serial não define o período de amostragem.

Sequência de teste e impressão

Trecho 2 — acrescente após o trecho 1 e envie o sketch ao seu Uno, se quiser executar localmente.

cpp
const uint16_t dados[] = {
  100, 100, 100, 100,
  0, 1023, 0, 1023,
  10, 20, 30, 40
};
const uint8_t TOTAL = sizeof(dados) / sizeof(dados[0]);
const uint16_t Ts_ms = 10;

void setup() {
  Serial.begin(9600);

  for (uint8_t k = 0; k < TOTAL; k++) {
    float y = atualizaMedia(dados[k]);
    Serial.print("k="); Serial.print(k);
    Serial.print(" x="); Serial.print(dados[k]);
    Serial.print(" y="); Serial.print(y, 2);
    Serial.print(" soma="); Serial.print(soma);
    Serial.print(" count="); Serial.print(validas);
    Serial.print(" idx="); Serial.println(indice);
  }
}

void loop() {
}
Saída esperada
k=0 x=100 y=100.00 soma=100 count=1 idx=1
k=1 x=100 y=100.00 soma=200 count=2 idx=2
k=2 x=100 y=100.00 soma=300 count=3 idx=3
k=3 x=100 y=100.00 soma=400 count=4 idx=0
k=4 x=0 y=75.00 soma=300 count=4 idx=1
k=5 x=1023 y=305.75 soma=1223 count=4 idx=2
k=6 x=0 y=280.75 soma=1123 count=4 idx=3
k=7 x=1023 y=511.50 soma=2046 count=4 idx=0
k=8 x=10 y=514.00 soma=2056 count=4 idx=1
k=9 x=20 y=263.25 soma=1053 count=4 idx=2
k=10 x=30 y=270.75 soma=1083 count=4 idx=3
k=11 x=40 y=25.00 soma=100 count=4 idx=0

A saída abaixo considera as duas partes concatenadas. O número após idx é o índice já avançado, pronto para a próxima chamada.

Confira partida, voltas e frações

Estados de referência

Use estes pontos de conferência para rastrear o sketch mesmo sem Arduino. A primeira linha confirma que posições zeradas não entram como medições: a divisão é por count = 1. A linha k=4 é a primeira substituição. Em k=7 e k=11, o índice retornou ao início após completar outra volta.

Conferência manual

Valores selecionados da sequência de teste.

O índice mostrado é o valor após a atualização.
kx[k]somacounty[k]idx
01001001100,001
31004004100,000
40300475,001
5102312234305,752
7102320464511,500
92010534263,252
1140100425,000

Dica

O que cada caso revela

Os valores 0 e 1023 verificam extremos típicos do ADC do Uno. As saídas 305,75; 511,50 e 263,25 confirmam que a conversão para float ocorreu antes da divisão. Se a divisão fosse inteira, essas frações seriam descartadas.

Pratique a integração

Complete a atualização essencial

Complete os três pontos que tornam a atualização coerente com o rastreamento.

cpp
float atualizaMedia(uint16_t x) {
  soma -= ;
  buffer[indice] = x;
  soma += x;

  if (validas < N) {
    ++;
  }

  indice = (indice + 1) % N;
  return  / validas;
}

Passo 7 de 8

Relacione suavização, janela e atraso

Meça a janela no tempo

Duas medidas de tempo da janela

Com amostras regulares, a janela de uma média móvel de tamanho N tem duração nominal:

N × T_s

Ela reúne N medições, incluindo a atual. Já o intervalo entre a amostra mais antiga e a atual é menor:

(N − 1) × T_s

Exemplo: com N = 4 e T_s = 10 ms, a janela é chamada de 40 ms, mas seus extremos estão separados por 30 ms. As duas medidas são úteis, mas não são a mesma coisa.

Linha do tempo com oito amostras igualmente espaçadas, uma marcação cobrindo toda a janela e outra apenas entre a primeira e a última amostra.

Para N = 8, a duração nominal cobre 8 intervalos de amostragem na convenção N × T_s; a separação temporal entre os extremos usa 7 intervalos: (N − 1) × T_s.

Valores de referência

Com T_s = 10 ms, compare as duas janelas que serão usadas neste step.

Duração da janela, separação entre extremos e atraso característico aproximado.
JanelaN × T_s(N − 1) × T_sAtraso aprox.
N = 440 ms30 ms15 ms
N = 880 ms70 ms35 ms

Observe suavização e resposta

Atraso não é tempo de espera

Quando a janela está cheia e as amostras são regulares, uma referência prática para variações lentas é o atraso característico:

atraso ≈ (N − 1) × T_s / 2

Para N = 4 e T_s = 10 ms, o resultado é 15 ms. Para N = 8, é 35 ms.

Isso não significa que a saída fica imóvel durante esse tempo. Ao chegar uma nova amostra, ela já entra na média e a saída começa a mudar. O valor é uma forma de localizar aproximadamente a resposta média da janela no tempo.

Mesma entrada, duas janelas

Gráfico com entrada oscilante seguida de um degrau; a curva de N igual a 4 é menos suave e responde antes, enquanto a de N igual a 8 é mais suave e responde depois.

Mantendo a mesma sequência e o mesmo T_s, N = 8 tende a reduzir mais as flutuações do que N = 4, mas introduz maior atraso característico.

Dica

O compromisso de aumentar N

Aumentar N cria um compromisso, não uma regra universal: em muitos trechos com flutuações rápidas, a janela maior reduz a variação da saída. Porém, o quanto ela reduz depende da sequência de entrada. Em troca, mudanças reais no sinal aparecem mais diluídas ao longo de mais amostras.

Quantifique o que restou da flutuação

Exemplo

Compare a flutuação em uma região estável

Considere um patamar em torno de 100, já depois do preenchimento das janelas. Em um trecho perturbado, a entrada varia entre 96 e 104.

Uma medida simples da flutuação é:

amplitude observada = máximo − mínimo

No exemplo, a saída de N = 4 vai de 99 a 101: amplitude 2. A saída de N = 8 vai de 99,5 a 100,5: amplitude 1. Portanto, nesse trecho específico, N = 8 deixou uma flutuação residual menor.

Flutuação residual do exemplo

Os valores abaixo se referem ao mesmo trecho de patamar perturbado, com as janelas já cheias.

A amplitude é a diferença entre o maior e o menor valor de cada sinal no trecho.
SinalMínimoMáximoAmplitude
Entrada bruta961048
Média N = 4991012
Média N = 899,5100,51

O que acontece no degrau

Se uma nova amostra de 140 chega após esse patamar, as duas médias mudam já nessa primeira atualização. A de N = 4 muda mais porque 1/4 da sua janela foi renovada; a de N = 8 também muda, mas a nova amostra representa só 1/8 da janela. A transição completa continua nas atualizações seguintes, enquanto as amostras antigas são substituídas.

Verifique sua interpretação

Calcule o atraso

Para N = 8 e T_s = 10 ms, o atraso característico aproximado é de ____ ms.

Interprete a curva

Após uma mudança de patamar na entrada, qual interpretação é correta para a janela N = 8?

Passo 8 de 8

Compare duas janelas e valide sua escolha

Validar a implementação e justificar a escolha entre duas janelas com base na suavização e no atraso permitido.

Prepare um ensaio repetível

Um ensaio justo para as duas janelas

Use exatamente a mesma sequência para duas execuções separadas: primeiro com N = 4, depois com N = 8. O espaçamento nominal das amostras é T_s = 10 ms.

Os 12 primeiros valores representam um patamar próximo de 500 com pequenas perturbações. Os 8 valores seguintes representam uma mudança para o patamar 700. Compare as saídas somente depois que cada janela estiver cheia.

Para que o segundo resultado não carregue amostras do primeiro, reinicialize completamente o estado antes de cada ensaio: buffer, índice, soma e contagem válida.

Execução separada e reinicializada

O sketch a seguir é completo e independente: crie um novo sketch para ele, em vez de acrescentá-lo ao sketch do step anterior. A função executarEnsaio() define a janela ativa, chama reiniciar() e processa todos os dados. Assim, os ensaios com N = 4 e N = 8 usam a mesma entrada, mas estados separados.

O buffer tem capacidade máxima de 8 posições; N seleciona quantas posições participam do ensaio atual. A soma permanece em uint32_t, e a conversão para float ocorre antes da divisão.

Ensaio completo com N = 4 e N = 8

Sketch completo para executar localmente no Arduino Uno; não requer sensor, circuito nem biblioteca externa.

cpp
const uint8_t MAX_N = 8;
const uint16_t Ts_ms = 10;

const uint16_t dados[] = {
  500, 510, 490, 505, 495, 510, 490, 500,
  505, 495, 510, 490,
  700, 700, 700, 700, 700, 700, 700, 700
};
const uint8_t TOTAL = sizeof(dados) / sizeof(dados[0]);

uint16_t buffer[MAX_N] = {0};
uint8_t indice = 0;
uint8_t validas = 0;
uint8_t N = 4;
uint32_t soma = 0;

void reiniciar() {
  for (uint8_t i = 0; i < MAX_N; i++) {
    buffer[i] = 0;
  }
  indice = 0;
  validas = 0;
  soma = 0;
}

float atualizaMedia(uint16_t x) {
  soma -= buffer[indice];
  buffer[indice] = x;
  soma += x;

  if (validas < N) {
    validas++;
  }

  indice++;
  if (indice == N) {
    indice = 0;
  }

  return (float)soma / validas;
}

void executarEnsaio(uint8_t janela) {
  N = janela;
  reiniciar();

  Serial.print("Ensaio N=");
  Serial.print(N);
  Serial.print(" Ts_ms=");
  Serial.println(Ts_ms);

  for (uint8_t k = 0; k < TOTAL; k++) {
    float y = atualizaMedia(dados[k]);
    Serial.print("k="); Serial.print(k);
    Serial.print(" x="); Serial.print(dados[k]);
    Serial.print(" y="); Serial.println(y, 3);
  }
}

void setup() {
  Serial.begin(9600);
  executarEnsaio(4);
  executarEnsaio(8);
}

void loop() {
}
Saída esperada
Ensaio N=4 Ts_ms=10
k=0 x=500 y=500.000
k=1 x=510 y=505.000
k=2 x=490 y=500.000
k=3 x=505 y=501.250
k=4 x=495 y=500.000
k=5 x=510 y=500.000
k=6 x=490 y=500.000
k=7 x=500 y=498.750
k=8 x=505 y=501.250
k=9 x=495 y=497.500
k=10 x=510 y=502.500
k=11 x=490 y=500.000
k=12 x=700 y=548.750
k=13 x=700 y=600.000
k=14 x=700 y=647.500
k=15 x=700 y=700.000
k=16 x=700 y=700.000
k=17 x=700 y=700.000
k=18 x=700 y=700.000
k=19 x=700 y=700.000
Ensaio N=8 Ts_ms=10
k=0 x=500 y=500.000
k=1 x=510 y=505.000
k=2 x=490 y=500.000
k=3 x=505 y=501.250
k=4 x=495 y=500.000
k=5 x=510 y=501.667
k=6 x=490 y=500.000
k=7 x=500 y=500.000
k=8 x=505 y=500.625
k=9 x=495 y=498.750
k=10 x=510 y=501.250
k=11 x=490 y=499.375
k=12 x=700 y=525.000
k=13 x=700 y=548.750
k=14 x=700 y=575.000
k=15 x=700 y=600.000
k=16 x=700 y=624.375
k=17 x=700 y=650.000
k=18 x=700 y=673.750
k=19 x=700 y=700.000

A saída abaixo é a sequência completa esperada no monitor serial a 9600 baud. `Ts_ms` descreve o espaçamento nominal dos dados, não o tempo entre linhas seriais.

Visualização da comparação

Gráfico conceitual com entrada oscilante, saída de janela curta que reage antes e saída de janela longa mais suave e gradual.

A janela maior reduz mais a oscilação neste ensaio, mas dilui a mudança de patamar por mais atualizações.

Confronte suavização e resposta

Resultados de referência

A tabela confronta a entrada com as duas saídas calculadas pela sequência do ensaio. Entre k = 8 e k = 11, ambas as janelas já estão cheias e o patamar ainda está perturbado. Em k = 12, entra a primeira amostra de 700: as duas saídas sobem imediatamente, mas em quantidades diferentes.

Conferência numérica

Valores calculados diretamente da sequência apresentada.

Referência para T_s = 10 ms; valores com vírgula decimal.
kx[k]y com N = 4y com N = 8
3505501,250501,250
7500498,750500,000
8505501,250500,625
9495497,500498,750
10510502,500501,250
11490500,000499,375
12700548,750525,000
13700600,000548,750
14700647,500575,000
15700700,000600,000
19700700,000700,000

Dica

Suavização tem um custo

No trecho cheio de k = 8 a k = 11:

  • N = 4: mínimo 497,50; máximo 502,50; flutuação observada 5,00.
  • N = 8: mínimo 498,75; máximo 501,25; flutuação observada 2,50.

Logo, N = 8 suavizou mais esta sequência. Porém, os atrasos característicos estimados continuam sendo cerca de 15 ms para N = 4 e 35 ms para N = 8. A melhor janela depende também do atraso permitido.

Valide o algoritmo e decida

Revise a atualização essencial

Complete a revisão do núcleo da atualização. Considere que N é positivo e que o buffer, a soma, o índice e a contagem foram inicializados antes do ensaio.

cpp
soma -= ;
buffer[indice] = x;
soma += ;

if (validas < N) {
  validas++;
}

indice = (indice + 1) % N;
return (float)soma / ;

Justifique a escolha

Um sistema aceita atraso característico de até 20 ms e precisa reduzir as flutuações no patamar. Com base na tabela, qual janela você escolheria? Justifique citando o atraso e a flutuação observada.

Escreva pelo menos 60 caracteres (0/60).

Sintetize e conclua

Resumo

Checklist final

  • Use a mesma sequência para comparar janelas e reinicialize buffer, índice, soma e contagem válida antes de cada ensaio independente.
  • Compare a flutuação residual somente com a janela cheia; neste ensaio, ela foi 5,00 para N = 4 e 2,50 para N = 8.
  • A mudança de patamar aparece já na primeira amostra nova; uma janela maior apenas distribui a transição por mais atualizações.
  • Escolha a janela que atende simultaneamente ao atraso permitido e à redução de flutuação necessária.
  • Antes de aceitar a implementação, confira índice circular, contagem válida, soma incremental, capacidade do acumulador e conversão para ponto flutuante antes da divisão.

Tutorial concluído

Parabéns! Você concluiu: Média móvel com buffer circular

Muito bem! Agora você pode avaliar uma janela não só pela suavização, mas também pelo atraso que ela acrescenta.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso