Estou procurando uma solução eficiente em tempo e memória para calcular uma média móvel em C. Preciso evitar a divisão porque estou em um PIC 16 que não possui uma unidade de divisão dedicada.
No momento, apenas armazeno todos os valores em um buffer de anel e simplesmente armazeno e atualizo a soma cada vez que um novo valor chega. Isso é realmente eficiente, mas infelizmente usa a maior parte da minha memória disponível ...
Respostas:
Como já mencionado, você deve considerar um filtro IIR (resposta ao impulso infinito) em vez do filtro FIR (resposta ao impulso finito) que você está usando agora. Há mais do que isso, mas à primeira vista os filtros FIR são implementados como convoluções explícitas e filtros IIR com equações.
O filtro IIR específico que eu uso muito em microcontroladores é um filtro passa-baixo monopolar. Este é o equivalente digital de um simples filtro analógico RC. Para a maioria dos aplicativos, eles terão melhores características do que o filtro de caixa que você está usando. A maioria dos usos de um filtro de caixa que encontrei são resultado de alguém que não presta atenção na classe de processamento de sinal digital, e não como resultado de precisar de suas características particulares. Se você apenas deseja atenuar as altas frequências que você sabe que são ruídos, um filtro passa-baixo monopolar é melhor. A melhor maneira de implementar digitalmente um microcontrolador é geralmente:
FILT <- FILT + FF (NOVO - FILT)
FILT é um pedaço de estado persistente. Essa é a única variável persistente que você precisa para calcular esse filtro. NEW é o novo valor que o filtro está sendo atualizado com essa iteração. FF é a fração do filtro , que ajusta o "peso" do filtro. Veja este algoritmo e veja que para FF = 0 o filtro é infinitamente pesado, pois a saída nunca muda. Para FF = 1, não há realmente nenhum filtro, pois a saída segue apenas a entrada. Valores úteis estão no meio. Em sistemas pequenos, você escolhe FF para 1/2 Npara que a multiplicação por FF possa ser realizada como um deslocamento à direita por N bits. Por exemplo, FF pode ser 1/16 e a multiplicação por FF, portanto, um deslocamento à direita de 4 bits. Caso contrário, esse filtro precisará apenas de uma subtração e uma adição, embora os números geralmente precisem ser maiores que o valor de entrada (mais sobre precisão numérica em uma seção separada abaixo).
Normalmente, tomo leituras A / D significativamente mais rápidas do que são necessárias e aplico dois desses filtros em cascata. É o equivalente digital de dois filtros RC em série e atenua 12 dB / oitava acima da frequência de rolloff. No entanto, para leituras A / D, geralmente é mais relevante olhar para o filtro no domínio do tempo, considerando sua resposta ao passo. Isso indica a rapidez com que o sistema verá uma mudança quando a coisa que você está medindo mudar.
Para facilitar o design desses filtros (o que significa apenas escolher FF e decidir quantos deles cascatear), eu uso meu programa FILTBITS. Você especifica o número de bits de deslocamento para cada FF na série em cascata de filtros e calcula a resposta da etapa e outros valores. Na verdade, eu costumo executar isso através do meu script wrapper PLOTFILT. Isso executa FILTBITS, que cria um arquivo CSV e, em seguida, plota o arquivo CSV. Por exemplo, aqui está o resultado de "PLOTFILT 4 4":
Os dois parâmetros para PLOTFILT significam que haverá dois filtros em cascata do tipo descrito acima. Os valores de 4 indicam o número de bits de deslocamento para realizar a multiplicação por FF. Os dois valores de FF são, portanto, 1/16 neste caso.
O traço vermelho é a resposta da etapa da unidade e é a principal coisa a se observar. Por exemplo, isso informa que, se a entrada for alterada instantaneamente, a saída do filtro combinado se ajustará a 90% do novo valor em 60 iterações. Se você se importa com cerca de 95% do tempo de estabilização, precisa aguardar 73 iterações e, por 50%, apenas 26 iterações.
O traço verde mostra a saída de um único pico de amplitude total. Isso lhe dá uma idéia da supressão aleatória de ruído. Parece que nenhuma amostra isolada causará mais de 2,5% de alteração na saída.
O traço azul é dar uma sensação subjetiva do que esse filtro faz com o ruído branco. Este não é um teste rigoroso, pois não há garantia sobre o conteúdo exato dos números aleatórios escolhidos como entrada de ruído branco para esta execução do PLOTFILT. É apenas para lhe dar uma idéia aproximada de quanto será esmagado e quão suave é.
PLOTFILT, talvez FILTBITS e muitas outras coisas úteis, especialmente para o desenvolvimento de firmware PIC, estão disponíveis na versão do software PIC Development Tools na minha página de downloads de software .
Adicionado sobre precisão numérica
Vejo pelos comentários e agora uma nova resposta que há interesse em discutir o número de bits necessários para implementar esse filtro. Observe que a multiplicação por FF criará novos bits do Log 2 (FF) abaixo do ponto binário. Em sistemas pequenos, o FF é geralmente escolhido como 1/2 N, para que essa multiplicação seja efetivamente realizada por um deslocamento correto de N bits.
FILT é, portanto, geralmente um número inteiro de ponto fixo. Observe que isso não altera nada da matemática do ponto de vista do processador. Por exemplo, se você estiver filtrando leituras A / D de 10 bits e N = 4 (FF = 1/16), precisará de 4 bits de fração abaixo das leituras A / D inteiras de 10 bits. Na maioria dos processadores, você faria operações inteiras de 16 bits devido às leituras A / D de 10 bits. Nesse caso, você ainda pode fazer exatamente as mesmas operações de número inteiro de 16 bits, mas comece com as leituras A / D deixadas deslocadas em 4 bits. O processador não sabe a diferença e não precisa. Fazer a matemática em inteiros inteiros de 16 bits funciona se você os considera 12,4 pontos fixos ou inteiros verdadeiros de 16 bits (16,0 pontos fixos).
Em geral, você precisa adicionar N bits a cada pólo de filtro se não desejar adicionar ruído devido à representação numérica. No exemplo acima, o segundo filtro de dois teria que ter 10 + 4 + 4 = 18 bits para não perder informações. Na prática, em uma máquina de 8 bits, isso significa que você usaria valores de 24 bits. Tecnicamente, apenas o segundo pólo de dois precisaria do valor mais amplo, mas, para simplificar o firmware, geralmente uso a mesma representação e, portanto, o mesmo código, para todos os pólos de um filtro.
Normalmente, escrevo uma sub-rotina ou macro para executar uma operação de pólo de filtro e, em seguida, aplico-o a cada pólo. Se uma sub-rotina ou macro depende se os ciclos ou a memória do programa são mais importantes nesse projeto em particular. De qualquer forma, eu uso algum estado zero para passar NEW para a sub-rotina / macro, que atualiza o FILT, mas também carrega esse mesmo estado no mesmo estado em que NEW estava. Isso facilita a aplicação de vários pólos, pois o FILT atualizado de um pólo é o NOVO do próximo. Quando uma sub-rotina, é útil ter um ponteiro apontar para FILT na entrada, que é atualizado para logo após a saída da FILT. Dessa forma, a sub-rotina opera automaticamente em filtros consecutivos na memória, se chamados várias vezes. Com uma macro, você não precisa de um ponteiro, pois passa o endereço para operar em cada iteração.
Exemplos de código
Aqui está um exemplo de uma macro conforme descrito acima para um PIC 18:
E aqui está uma macro semelhante para um PIC 24 ou dsPIC 30 ou 33:
Ambos os exemplos são implementados como macros usando meu pré-processador PIC assembler , que é mais capaz do que qualquer um dos recursos internos de macro.
fonte
Se você pode viver com a restrição de uma potência de dois números de itens em média (ou seja, 2,4,8,16,32 etc), a divisão pode ser feita de maneira fácil e eficiente em um micro de baixo desempenho sem divisão dedicada, porque pode ser feito como uma pequena mudança. Cada turno à direita é uma potência de dois, por exemplo:
ou
etc.
fonte
Não é uma resposta para um filtro de média verdadeiro movimento (aka "filtro de vagão") com requisitos de memória menos, se você não se importa downsampling. É chamado de filtro em cascata de integrador-pente (CIC). A idéia é que você tenha um integrador do qual considere diferenças ao longo de um período de tempo, e o principal dispositivo de economia de memória é que, ao reduzir a amostragem, você não precisa armazenar todos os valores do integrador. Pode ser implementado usando o seguinte pseudocódigo:
Seu comprimento médio móvel efetivo é,
decimationFactor*statesize
mas você só precisa manter asstatesize
amostras em volta . Obviamente, você pode obter um melhor desempenho se vocêstatesize
edecimationFactor
são potências de 2, para que os operadores de divisão e restante sejam substituídos por turnos e mascaramentos.Postscript: Eu concordo com Olin que você deve sempre considerar filtros IIR simples antes de um filtro de média móvel. Se você não precisar dos nulos de frequência de um filtro de vagão, um filtro passa-baixo de 1 ou 2 pólos provavelmente funcionará bem.
Por outro lado, se você estiver filtrando para fins de dizimação (obtendo uma entrada com alta taxa de amostra e calculando a média para uso em um processo de baixa taxa), um filtro CIC pode ser exatamente o que você está procurando. (especialmente se você pode usar statesize = 1 e evitar o ringbuffer completamente com apenas um único valor anterior do integrador)
fonte
Há uma análise aprofundada da matemática por trás do uso do filtro IIR de primeira ordem que Olin Lathrop já descreveu na troca de pilhas do Digital Signal Processing (inclui muitas fotos bonitas). A equação desse filtro IIR é:
y [n] = αx [n] + (1-α) y [n-1]
Isso pode ser implementado usando apenas números inteiros e nenhuma divisão usando o código a seguir (pode precisar de alguma depuração enquanto eu estava digitando na memória).
Esse filtro aproxima uma média móvel das últimas K amostras, configurando o valor de alfa como 1 / K. Fazer isso no código anterior por
#define
ingBITS
para LOG2 (K), ou seja, para K = 16 conjuntoBITS
de 4, para K = 4 conjuntoBITS
de 2, etc.(Verificarei o código listado aqui assim que receber uma alteração e editarei esta resposta, se necessário.)
fonte
Aqui está um filtro passa-baixo unipolar (média móvel, com frequência de corte = CutoffFrequency). Muito simples, muito rápido, funciona muito bem e quase não há sobrecarga de memória.
Nota: Todas as variáveis têm escopo além da função de filtro, exceto as passadas em newInput
Nota: Este é um filtro de estágio único. Vários estágios podem ser conectados em cascata para aumentar a nitidez do filtro. Se você usar mais de um estágio, terá que ajustar o DecayFactor (relacionado à Frequência de corte) para compensar.
E, obviamente, tudo o que você precisa são aquelas duas linhas colocadas em qualquer lugar, elas não precisam de suas próprias funções. Este filtro tem um tempo de aceleração antes que a média móvel represente a do sinal de entrada. Se você precisar ignorar esse tempo de aceleração, basta inicializar MovingAverage para o primeiro valor de newInput em vez de 0 e esperar que o primeiro newInput não seja um erro externo.
(CutoffFrequency / SampleRate) tem um intervalo entre 0 e 0,5. DecayFactor é um valor entre 0 e 1, geralmente próximo a 1.
Flutuadores de precisão única são bons o suficiente para a maioria das coisas, eu apenas prefiro duplos. Se você precisar ficar com números inteiros, poderá converter DecayFactor e Amplitude Factor em números fracionais, nos quais o numerador é armazenado como o número inteiro e o denominador é uma potência inteira de 2 (para que você possa mudar de bit para a direita como o número inteiro). denominador em vez de ter que dividir durante o loop do filtro). Por exemplo, se DecayFactor = 0,99 e você deseja usar números inteiros, pode definir DecayFactor = 0,99 * 65536 = 64881. E sempre que você multiplicar por DecayFactor no loop de filtro, basta mudar o resultado >> 16.
Para obter mais informações, um excelente livro on-line, capítulo 19, sobre filtros recursivos: http://www.dspguide.com/ch19.htm
PS Para o paradigma Média Móvel, uma abordagem diferente para definir DecayFactor e AmplitudeFactor que possam ser mais relevantes para suas necessidades, digamos que você queira o anterior, com cerca de 6 itens em média, fazendo isso discretamente, adicionando 6 itens e dividido por 6, para que você possa definir o AmplitudeFactor como 1/6 e DecayFactor como (1.0 - AmplitudeFactor).
fonte
Você pode aproximar uma média móvel para algumas aplicações com um simples filtro IIR.
peso é 0..255, valores altos = escala de tempo mais curta para a média
Valor = (novo valor * peso + valor * (peso 256)) / 256
Para evitar erros de arredondamento, o valor normalmente seria longo, dos quais você usa apenas bytes de ordem mais alta como seu valor 'real'.
fonte
Todos os demais comentaram minuciosamente a utilidade de IIR vs. FIR e a divisão de poder de dois. Gostaria apenas de dar alguns detalhes de implementação. O abaixo funciona bem em pequenos microcontroladores sem FPU. Não há multiplicação e, se você mantiver N uma potência de dois, toda a divisão é de troca de bits de ciclo único.
Buffer de anel FIR básico: mantenha um buffer em execução dos últimos N valores e um SUM em execução de todos os valores no buffer. Sempre que uma nova amostra chegar, subtraia o valor mais antigo do buffer de SUM, substitua-o pela nova amostra, adicione a nova amostra a SUM e faça a saída SUM / N.
Buffer de anel IIR modificado: mantenha um SUM em execução dos últimos N valores. Cada vez que uma nova amostra entra, SUM - = SUM / N, adicione a nova amostra e produza SUM / N.
fonte
Como o mikeselectricstuff disse, se você realmente precisa reduzir suas necessidades de memória e não se importa que sua resposta ao impulso seja exponencial (em vez de um pulso retangular), eu usaria uma média móvel exponencial filtro de . Eu os uso extensivamente. Com esse tipo de filtro, você não precisa de nenhum buffer. Você não precisa armazenar N amostras anteriores. Apenas um. Portanto, seus requisitos de memória são reduzidos por um fator de N.
Além disso, você não precisa de nenhuma divisão para isso. Apenas multiplicações. Se você tiver acesso à aritmética de ponto flutuante, use multiplicações de ponto flutuante. Caso contrário, faça multiplicações inteiras e mude para a direita. No entanto, estamos em 2012 e eu recomendo que você use compiladores (e MCUs) que permitem trabalhar com números de ponto flutuante.
Além de ser mais eficiente em termos de memória e mais rápido (você não precisa atualizar itens em nenhum buffer circular), eu diria que também é mais natural , porque uma resposta de impulso exponencial corresponde melhor à maneira como a natureza se comporta, na maioria dos casos.
fonte
Um problema com o filtro IIR quase tocado por @olin e @supercat, mas aparentemente desconsiderado por outros, é que o arredondamento introduz alguma imprecisão (e potencialmente viés / truncamento): assumindo que N é uma potência de dois e que apenas a aritmética inteira é usado, a mudança à direita elimina sistematicamente os LSBs da nova amostra. Isso significa que quanto tempo a série poderia ter, a média nunca levará isso em consideração.
Por exemplo, suponha uma série que diminui lentamente (8,8,8, ..., 8,7,7,7, ... 7,6,6) e assuma que a média é de fato 8 no início. A primeira amostra "7" elevará a média para 7, independentemente da força do filtro. Apenas para uma amostra. Mesma história para 6, etc. Agora, pense no contrário: a série sobe. A média permanecerá em 7 para sempre, até que a amostra seja grande o suficiente para fazê-la mudar.
Obviamente, você pode corrigir o "viés" adicionando 1/2 ^ N / 2, mas isso realmente não resolverá o problema de precisão: nesse caso, a série decrescente permanecerá para sempre em 8 até a amostra ser 8-1 / 2 ^ (N / 2). Para N = 4, por exemplo, qualquer amostra acima de zero manterá a média inalterada.
Acredito que uma solução para isso implicaria manter um acumulador de LSBs perdidos. Mas não cheguei longe o suficiente para ter o código pronto e não tenho certeza de que isso não prejudicaria o poder do IIR em alguns outros casos de séries (por exemplo, se 7,9,7,9 seriam em média 8 até então). .
@Olin, sua cascata de dois estágios também precisaria de algumas explicações. Você quer dizer manter dois valores médios com o resultado do primeiro alimentado no segundo em cada iteração? Qual o benefício disso?
fonte