Estoy buscando una solución eficiente en tiempo y memoria para calcular un promedio móvil en C. Necesito evitar dividir porque estoy en un PIC 16 que no tiene una unidad de división dedicada.
Por el momento, solo almaceno todos los valores en un buffer de anillo y simplemente almaceno y actualizo la suma cada vez que llega un nuevo valor. Esto es realmente eficiente, pero desafortunadamente usa la mayor parte de mi memoria disponible ...
Respuestas:
Como otros han mencionado, debe considerar un filtro IIR (respuesta de impulso infinito) en lugar del filtro FIR (respuesta de impulso finito) que está utilizando ahora. Hay más, pero a primera vista los filtros FIR se implementan como convoluciones explícitas y filtros IIR con ecuaciones.
El filtro IIR particular que uso mucho en microcontroladores es un filtro de paso bajo de un polo. Este es el equivalente digital de un filtro analógico RC simple. Para la mayoría de las aplicaciones, estas tendrán mejores características que el filtro de caja que está utilizando. La mayoría de los usos de un filtro de caja que he encontrado son el resultado de alguien que no presta atención en la clase de procesamiento de señal digital, no como resultado de la necesidad de sus características particulares. Si solo desea atenuar las frecuencias altas que sabe que son ruido, un filtro de paso bajo de un polo es mejor. La mejor manera de implementar uno digitalmente en un microcontrolador es generalmente:
FILT <- FILT + FF (NUEVO - FILT)
FILT es una pieza de estado persistente. Esta es la única variable persistente que necesita para calcular este filtro. NUEVO es el nuevo valor que el filtro se está actualizando con esta iteración. FF es la fracción del filtro , que ajusta la "pesadez" del filtro. Mire este algoritmo y vea que para FF = 0 el filtro es infinitamente pesado ya que la salida nunca cambia. Para FF = 1, realmente no hay ningún filtro, ya que la salida solo sigue a la entrada. Los valores útiles están en el medio. En sistemas pequeños, elige FF para que sea 1/2 Npara que la multiplicación por FF se pueda lograr como un desplazamiento a la derecha por N bits. Por ejemplo, FF podría ser 1/16 y la multiplicación por FF, por lo tanto, un desplazamiento a la derecha de 4 bits. De lo contrario, este filtro solo necesita una resta y una suma, aunque los números generalmente deben ser más amplios que el valor de entrada (más sobre precisión numérica en una sección separada a continuación).
Por lo general, tomo lecturas A / D significativamente más rápido de lo necesario y aplico dos de estos filtros en cascada. Este es el equivalente digital de dos filtros RC en serie, y se atenúa 12 dB / octava por encima de la frecuencia de caída. Sin embargo, para las lecturas A / D, generalmente es más relevante mirar el filtro en el dominio del tiempo considerando su respuesta escalonada. Esto le indica qué tan rápido su sistema verá un cambio cuando cambie lo que está midiendo.
Para facilitar el diseño de estos filtros (que solo significa elegir FF y decidir cuántos de ellos en cascada), utilizo mi programa FILTBITS. Usted especifica el número de bits de desplazamiento para cada FF en la serie de filtros en cascada, y calcula la respuesta del paso y otros valores. En realidad, generalmente ejecuto esto a través de mi script de contenedor PLOTFILT. Esto ejecuta FILTBITS, que crea un archivo CSV, luego traza el archivo CSV. Por ejemplo, aquí está el resultado de "PLOTFILT 4 4":
Los dos parámetros para PLOTFILT significan que habrá dos filtros en cascada del tipo descrito anteriormente. Los valores de 4 indican el número de bits de desplazamiento para realizar la multiplicación por FF. Los dos valores FF son, por lo tanto, 1/16 en este caso.
El trazo rojo es la respuesta del paso unitario, y es lo principal a tener en cuenta. Por ejemplo, esto le dice que si la entrada cambia instantáneamente, la salida del filtro combinado se asentará en el 90% del nuevo valor en 60 iteraciones. Si le interesa el 95% del tiempo de establecimiento, debe esperar alrededor de 73 iteraciones, y el 50% del tiempo de establecimiento solo 26 iteraciones.
El trazo verde muestra la salida de un único pico de amplitud completa. Esto le da una idea de la supresión de ruido aleatorio. Parece que ninguna muestra individual causará más de un cambio del 2.5% en la salida.
La traza azul es para dar una sensación subjetiva de lo que este filtro hace con el ruido blanco. Esta no es una prueba rigurosa ya que no hay garantía de cuál era exactamente el contenido de los números aleatorios elegidos como entrada de ruido blanco para esta ejecución de PLOTFILT. Es solo para darle una sensación aproximada de cuánto se aplastará y qué tan suave es.
PLOTFILT, quizás FILTBITS, y muchas otras cosas útiles, especialmente para el desarrollo de firmware PIC, están disponibles en la versión del software PIC Development Tools en mi página de descargas de software .
Agregado sobre precisión numérica
Veo en los comentarios y ahora una nueva respuesta que hay interés en discutir la cantidad de bits necesarios para implementar este filtro. Tenga en cuenta que la multiplicación por FF creará nuevos bits de Log 2 (FF) debajo del punto binario. En sistemas pequeños, FF generalmente se elige como 1/2 N para que esta multiplicación se realice realmente mediante un desplazamiento a la derecha de N bits.
FILT es, por lo tanto, un número entero de punto fijo. Tenga en cuenta que esto no cambia ninguna de las matemáticas desde el punto de vista del procesador. Por ejemplo, si está filtrando lecturas A / D de 10 bits y N = 4 (FF = 1/16), entonces necesita 4 bits de fracción por debajo de las lecturas A / D de enteros de 10 bits. Uno de los procesadores más, estaría haciendo operaciones de enteros de 16 bits debido a las lecturas A / D de 10 bits. En este caso, aún puede hacer exactamente las mismas operaciones de enteros de 16 bits, pero comience con las lecturas A / D desplazadas 4 bits. El procesador no sabe la diferencia y no necesita saberlo. Hacer los cálculos en enteros enteros de 16 bits funciona tanto si los considera 12.4 puntos fijos como si son enteros de 16 bits (16.0 puntos fijos).
En general, debe agregar N bits a cada polo del filtro si no desea agregar ruido debido a la representación numérica. En el ejemplo anterior, el segundo filtro de dos tendría que tener 10 + 4 + 4 = 18 bits para no perder información. En la práctica en una máquina de 8 bits, eso significa que usaría valores de 24 bits. Técnicamente, solo el segundo polo de dos necesitaría el valor más amplio, pero por simplicidad de firmware, generalmente uso la misma representación y, por lo tanto, el mismo código, para todos los polos de un filtro.
Por lo general, escribo una subrutina o macro para realizar una operación de polo de filtro, luego lo aplico a cada polo. Si una subrutina o macro depende de si los ciclos o la memoria del programa son más importantes en ese proyecto en particular. De cualquier manera, utilizo algún estado scratch para pasar NEW a la subrutina / macro, que actualiza FILT, pero también carga eso en el mismo estado scratch en el que estaba NEW. Esto facilita la aplicación de múltiples polos ya que el FILT actualizado de un polo es lo NUEVO de la próxima. Cuando se trata de una subrutina, es útil tener un puntero apuntando a FILT al entrar, que se actualiza justo después de FILT al salir. De esa manera, la subrutina opera automáticamente en filtros consecutivos en la memoria si se llama varias veces. Con una macro, no necesita un puntero ya que pasa la dirección para operar en cada iteración.
Ejemplos de código
Aquí hay un ejemplo de una macro como se describió anteriormente para un PIC 18:
Y aquí hay una macro similar para un PIC 24 o dsPIC 30 o 33:
Ambos ejemplos se implementan como macros utilizando mi preprocesador de ensamblador PIC , que es más capaz que cualquiera de las funciones de macro incorporadas.
fuente
Si puede vivir con la restricción de una potencia de dos elementos para promediar (es decir, 2,4,8,16,32, etc.), entonces la división se puede hacer fácil y eficientemente en un micro de bajo rendimiento sin división dedicada porque Se puede hacer como un poco de cambio. Cada cambio a la derecha es una potencia de dos, por ejemplo:
o
etc.
fuente
No es una respuesta para un filtro de promedio real en movimiento (también conocido como "filtro de vagón de carga") con menos requisitos de memoria, si no le importa la disminución de resolución. Se llama un filtro de peine integrador en cascada (CIC). La idea es que tenga un integrador del que tome diferencias a lo largo de un período de tiempo, y el dispositivo clave de ahorro de memoria es que al reducir el muestreo, no tiene que almacenar todos los valores del integrador. Se puede implementar utilizando el siguiente pseudocódigo:
Su longitud promedio de movimiento efectiva es
decimationFactor*statesize
pero solo necesita mantener alrededor de lasstatesize
muestras. Obviamente, puede obtener un mejor rendimiento si sustatesize
ydecimationFactor
son potencias de 2, de modo que los operadores de división y resto sean reemplazados por turnos y máscaras.Posdata: estoy de acuerdo con Olin en que siempre debe considerar filtros IIR simples antes de un filtro de media móvil. Si no necesita los nulos de frecuencia de un filtro de vagón, un filtro de paso bajo de 1 o 2 polos probablemente funcionará bien.
Por otro lado, si está filtrando con el propósito de diezmar (tomar una entrada de alta frecuencia de muestreo y promediarla para usarla en un proceso de baja velocidad), entonces un filtro CIC puede ser justo lo que está buscando. (especialmente si puede usar statesize = 1 y evitar el ringbuffer con un solo valor de integrador anterior)
fuente
Hay un análisis en profundidad de las matemáticas detrás del uso del filtro IIR de primer orden que Olin Lathrop ya ha descrito en el intercambio de la pila de Procesamiento de señal digital (incluye muchas imágenes bonitas). La ecuación para este filtro IIR es:
y [n] = αx [n] + (1 − α) y [n − 1]
Esto se puede implementar usando solo números enteros y sin división usando el siguiente código (podría necesitar alguna depuración ya que estaba escribiendo desde la memoria).
Este filtro se aproxima a un promedio móvil de las últimas K muestras al establecer el valor de alfa en 1 / K. Para ello, en el código anterior por
#define
ingBITS
a LOG2 (K), es decir, para K = 16 conjuntoBITS
a 4, para K = 4 conjuntoBITS
a 2, etc.(Verificaré el código que aparece aquí tan pronto como reciba un cambio y editaré esta respuesta si es necesario).
fuente
Aquí hay un filtro de paso bajo unipolar (promedio móvil, con frecuencia de corte = Frecuencia de corte). Muy simple, muy rápido, funciona muy bien y casi no tiene sobrecarga de memoria.
Nota: Todas las variables tienen un alcance más allá de la función de filtro, excepto las pasadas en newInput
Nota: Este es un filtro de etapa única. Se pueden conectar en cascada múltiples etapas para aumentar la nitidez del filtro. Si usa más de una etapa, tendrá que ajustar DecayFactor (en relación con la frecuencia de corte) para compensar.
Y obviamente, todo lo que necesita es esas dos líneas ubicadas en cualquier lugar, no necesitan su propia función. Este filtro tiene un tiempo de aceleración antes de que la media móvil represente la de la señal de entrada. Si necesita omitir ese tiempo de aceleración, puede simplemente inicializar MovingAverage al primer valor de newInput en lugar de 0, y esperar que el primer newInput no sea un valor atípico.
(CutoffFrequency / SampleRate) tiene un rango de entre 0 y 0.5. DecayFactor es un valor entre 0 y 1, generalmente cercano a 1.
Los flotadores de precisión simple son lo suficientemente buenos para la mayoría de las cosas, solo prefiero los dobles. Si necesita seguir con los enteros, puede convertir el factor de decaimiento y el factor de amplitud en enteros fraccionarios, en los que el numerador se almacena como el entero, y el denominador es una potencia entera de 2 (por lo que puede desplazarse hacia la derecha como el denominador en lugar de tener que dividir durante el ciclo de filtro). Por ejemplo, si DecayFactor = 0.99, y desea usar números enteros, puede establecer DecayFactor = 0.99 * 65536 = 64881. Y luego, cada vez que multiplique por DecayFactor en su ciclo de filtro, simplemente cambie el resultado >> 16.
Para obtener más información sobre esto, un excelente libro que está en línea, capítulo 19 sobre filtros recursivos: http://www.dspguide.com/ch19.htm
PD Para el paradigma de la media móvil, un enfoque diferente para configurar DecayFactor y AmplitudeFactor que puede ser más relevante para sus necesidades, digamos que quiere el promedio anterior, aproximadamente 6 elementos juntos, haciéndolo discretamente, agregaría 6 elementos y dividiría por 6, por lo que puede configurar AmplitudeFactor en 1/6 y DecayFactor en (1.0 - AmplitudeFactor).
fuente
Puede aproximar una media móvil para algunas aplicaciones con un filtro IIR simple.
el peso es 0..255 valor, valores altos = escala de tiempo más corta para avaraging
Valor = (valor nuevo * peso + valor * (peso 256)) / 256
Para evitar errores de redondeo, el valor normalmente sería largo, de los cuales solo usará bytes de orden superior como su valor 'real'.
fuente
Todos los demás han comentado a fondo sobre la utilidad de IIR vs. FIR, y sobre la división del poder de dos. Solo me gustaría dar algunos detalles de implementación. Lo siguiente funciona bien en microcontroladores pequeños sin FPU. No hay multiplicación, y si mantienes N una potencia de dos, toda la división es un desplazamiento de bits de un solo ciclo.
Búfer de anillo FIR básico: mantenga un búfer en ejecución de los últimos N valores y una SUMA en ejecución de todos los valores en el búfer. Cada vez que ingrese una nueva muestra, reste el valor más antiguo en el búfer de SUM, reemplácelo con la nueva muestra, agregue la nueva muestra a SUM y envíe SUM / N.
Tampón de anillo IIR modificado: mantenga una SUMA en ejecución de los últimos N valores. Cada vez que entra una nueva muestra, SUM - = SUM / N, agrega la nueva muestra y genera SUM / N.
fuente
Como dijo mikeselectricstuff , si realmente necesita reducir sus necesidades de memoria y no le importa que su respuesta al impulso sea exponencial (en lugar de un pulso rectangular), optaría por un filtro de media móvil exponencial . Los uso ampliamente. Con ese tipo de filtro, no necesita ningún búfer. No tiene que almacenar N muestras pasadas. Solo uno. Entonces, sus requisitos de memoria se reducen por un factor de N.
Además, no necesitas ninguna división para eso. Solo multiplicaciones. Si tiene acceso a la aritmética de coma flotante, use multiplicaciones de coma flotante. De lo contrario, haga multiplicaciones y desplazamientos enteros a la derecha. Sin embargo, estamos en 2012, y le recomendaría que use compiladores (y MCU) que le permitan trabajar con números de punto flotante.
Además de ser más eficiente en memoria y más rápido (no tiene que actualizar elementos en ningún búfer circular), diría que también es más natural , porque una respuesta de impulso exponencial coincide mejor con el comportamiento de la naturaleza, en la mayoría de los casos.
fuente
Un problema con el filtro IIR como casi tocado por @olin y @supercat pero aparentemente ignorado por otros es que el redondeo introduce cierta imprecisión (y potencialmente sesgo / truncamiento): suponiendo que N es una potencia de dos, y solo la aritmética de enteros es utilizado, el desplazamiento a la derecha elimina sistemáticamente los LSB de la nueva muestra. Eso significa que cuánto tiempo podría durar la serie, el promedio nunca los tendrá en cuenta.
Por ejemplo, suponga una serie que disminuye lentamente (8,8,8, ..., 8,7,7,7, ... 7,6,6,) y suponga que el promedio es de hecho 8 al principio. La primera muestra "7" elevará el promedio a 7, sea cual sea la fuerza del filtro. Solo por una muestra. La misma historia para 6, etc. Ahora piense en lo contrario: la serie sube. El promedio se mantendrá en 7 para siempre, hasta que la muestra sea lo suficientemente grande como para hacer que cambie.
Por supuesto, puede corregir el "sesgo" agregando 1/2 ^ N / 2, pero eso realmente no resolverá el problema de precisión: en ese caso, la serie decreciente permanecerá para siempre en 8 hasta que la muestra sea 8-1 / 2 ^ (N / 2). Para N = 4, por ejemplo, cualquier muestra por encima de cero mantendrá el promedio sin cambios.
Creo que una solución para eso implicaría tener un acumulador de los LSB perdidos. Pero no llegué lo suficientemente lejos como para tener el código listo, y no estoy seguro de que no dañaría el poder de IIR en otros casos de series (por ejemplo, si 7,9,7,9 promediaría a 8 en ese momento) .
@Olin, tu cascada de dos etapas también necesitaría alguna explicación. ¿Te refieres a mantener dos valores promedio con el resultado del primero alimentado al segundo en cada iteración? ¿Cuál es el beneficio de esto?
fuente