¿Es posible crear un filtro IIR en un FPGA que tenga la frecuencia de muestreo?

Esta pregunta trata sobre la implementación de un filtro IIR en una FPGA con cortes DSP, con criterios muy específicos.

Digamos que está haciendo un filtro sin toques hacia adelante y solo 1 toque hacia atrás, con esta ecuación:

y [ norte ] = y [ norte 1 ] b 1 + X [ norte ]

(ver imagen)

Tome el segmento DSP48A1 de Xilinx como ejemplo: la mayoría de los segmentos IP DSP duros son similares.

Digamos que tiene datos analógicos entrantes a 1 muestra por reloj. Me gustaría diseñar un filtro IIR que se ejecute sincrónicamente en el reloj de muestra.

El problema es que para ejecutar el segmento DSP a la velocidad máxima, no puede multiplicar Y sumar en el mismo ciclo. Debe tener un registro de tubería entre estos componentes.

Entonces, si tiene 1 muestra nueva cada reloj, deberá producir 1 salida por reloj. Sin embargo, necesita los 2 relojes de salida anteriores antes de poder producir uno nuevo en este diseño.

La solución obvia es procesar los datos al doble de la velocidad del reloj o deshabilitar el registro de tubería para que pueda multiplicar y sumar en el mismo ciclo.

Desafortunadamente, si dice que está muestreando a la velocidad de reloj máxima del segmento DSP completamente canalizado, ninguna de esas soluciones es posible. ¿Hay alguna otra manera de construir esto?

(Puntos de bonificación si puede diseñar un filtro IIR que funcione a la mitad de la frecuencia de muestreo, utilizando cualquier cantidad de segmentos DSP)

El objetivo sería ejecutar un filtro de compensación para un ADC de 1 GSPS en una FPGA Xilinx Artix. Sus segmentos DSP pueden ejecutarse a poco más de 500 MHz cuando están completamente canalizados. Si hay una solución para 1 muestra por reloj, me gustaría probar y escalar la solución para 2 muestras por reloj. Todo esto es muy fácil con un filtro FIR.

ejemplo de filtro IIR de retroalimentación única

Solo para aclarar, no hay ninguna razón por la que no tenga una salida por ciclo de reloj con el método de canalización, ¿verdad? Está tratando de minimizar la latencia a un ciclo de reloj en lugar de dos, ¿verdad? Dependiendo de su situación, si está usando un número entero para b1, entonces podría convertir la multiplicación en una suma gigante que incluya x[n].
correcto: dado que hay una entrada por reloj, debe haber una salida por reloj. la latencia tampoco es un problema. el segmento DSP solo tiene un sumador de 2 entradas, y los grifos suelen ser números bastante grandes, por lo que no podría agregar b1 veces en 1 ciclo de reloj. el límite principal es que la salida necesita retroalimentarse en 1 reloj, pero se necesitan 2 relojes para producir.
Creo que todavía no entiendes cómo funciona una canalización. Una canalización aumenta potencialmente la latencia, pero le permite obtener 1 salida para cada entrada en cada ciclo de reloj. Es solo que el resultado ahora es 2 relojes después en lugar del ideal 1 reloj después. La entrada sería la secuencia como esta: x[0],x[1],x[2],x[3],x[4] mientras que la salida sería en ese mismo intervalo de tiempo y[-2],y [-1],y[0],y[1],y[2]. No estás perdiendo ninguna muestra. Además, está en un FPGA, por lo que si desea realizar más trabajo del que están diseñadas las canalizaciones DSP, utilice el fpga para paralelizar la carga de trabajo.
Ese DSP es capaz de hacer una acumulación de multiplicación fusionada en un ciclo. Sin embargo, no me queda claro si la salida de un segmento DSP se puede conectar a su propia entrada con retroalimentación en un solo ciclo.
horta: tiene razón sobre la canalización en general, pero el problema es que la pestaña b1 en este caso tiene retroalimentación, lo que significa que una etapa en la canalización depende de la salida del valor anterior. si siempre se necesitan 2 relojes para producir la siguiente salida a partir de la salida anterior, no hay forma de producir 1 salida por reloj, independientemente de la latencia que haya agregado. jbarlow: tiene razón, el segmento DSP tiene una opción fusionada de 1 ciclo. Sin embargo, no puede correr lo suficientemente rápido en este caso. al agregar el registro M (consulte la hoja de datos), puede alcanzar los 500 MHz. Sin embargo, no puede multiplicar y agregar el mismo clk.
Ah, ya veo lo que estás diciendo Marcus. Estoy pensando que la única solución ahora es que cree su propia lógica en la parte fpga que le permita realizar una suma múltiple en un solo ciclo de reloj mientras alimenta la salida directamente a la entrada de este bloque lógico.
Mi respuesta habitual a preguntas como esta en el trabajo es "Tiene una implementación muy definida en mente... dígame la especificación real para el procesamiento que está tratando de hacer, veamos si podemos hacerlo de otra manera".
tienes razón, y ya estoy trabajando en otras opciones que son significativamente diferentes. el objetivo es construir un osciloscopio 1 GSPS a partir de un FPGA Artix-7. El filtro IIR es principalmente para compensar la banda de paso, en caso de que no sea plana. Sin embargo, dado que el alcance funcionará en búferes de captura cortos, no es un problema filtrar los datos después del hecho a una velocidad más baja, no es necesario transmitir continuamente. hacer el filtrado de compensación por adelantado es solo una idea que deja abiertas más opciones. Este también es un buen ejercicio de rendimiento para ver exactamente qué tan rápido e IIR pueden funcionar, así que me alegro de haber preguntado.

Respuestas (2)

Todavía no he trabajado con filtros IIR, pero si solo necesita calcular la ecuación dada

y[n] = y[n-1]*b1 + x[n]

una vez por ciclo de CPU, puede usar canalización.

En un ciclo haces la multiplicación y en un ciclo necesitas hacer la suma para cada muestra de entrada. ¡Eso significa que su FPGA debe poder hacer la multiplicación en un ciclo cuando se registra a la frecuencia de muestreo dada! Entonces solo necesitará hacer la multiplicación de la muestra actual Y la suma del resultado de la multiplicación de la última muestra en paralelo. Esto causará un retraso de procesamiento constante de 2 ciclos.

Ok, echemos un vistazo a la fórmula y diseñemos una canalización:

y[n] = y[n-1]*b1 + x[n]

Su código de canalización podría tener este aspecto:

output <= last_output_times_b1 + last_input
last_output_times_b1 <= output * b1;
last_input <= input

¡Tenga en cuenta que los tres comandos deben ejecutarse en paralelo y que "salida" en la segunda línea, por lo tanto, usa la salida del último ciclo de reloj!

No trabajé mucho con Verilog, por lo que la sintaxis de este código posiblemente sea incorrecta (por ejemplo, falta el ancho de bits de las señales de entrada/salida; sintaxis de ejecución para la multiplicación). Sin embargo, deberías hacerte una idea:

module IIRFilter( clk, reset, x, b, y );
  input clk, reset, x, b;
  output y;

  reg y, t, t2;
  wire clk, reset, x, b;

  always @ (posedge clk or posedge reset)
  if (reset) begin
    y <= 0;
    t <= 0;
    t2 <= 0;
  end else begin
    y <= t + t2;
    t <= mult(y, b);
    t2 <= x
  end

endmodule

PD: Tal vez algún programador experimentado de Verilog podría editar este código y eliminar este comentario y el comentario sobre el código después. ¡Gracias!

PPS: en caso de que su factor "b1" sea una constante fija, es posible que pueda optimizar el diseño implementando un multiplicador especial que solo toma una entrada escalar y calcula solo "veces b1".

Respuesta a: "Desafortunadamente, esto es en realidad equivalente a y[n] = y[n-2] * b1 + x[n]. Esto se debe a la etapa de canalización adicional". como comentario a la versión anterior de la respuesta

Sí, en realidad era correcto para la siguiente versión antigua (¡¡INCORRECTA!!!):

  always @ (posedge clk or posedge reset)
  if (reset) begin
    t <= 0;
  end else begin
    y <= t + x;
    t <= mult(y, b);
  end

Espero haber corregido este error ahora al retrasar los valores de entrada también en un segundo registro:

  always @ (posedge clk or posedge reset)
  if (reset) begin
    y <= 0;
    t <= 0;
    t2 <= 0;
  end else begin
    y <= t + t2;
    t <= mult(y, b);
    t2 <= x
  end

Para asegurarnos de que funciona correctamente esta vez, veamos qué sucede en los primeros ciclos. Tenga en cuenta que los primeros 2 ciclos producen más o menos basura (definida), ya que no hay valores de salida anteriores (por ejemplo, y[-1] == ??) disponibles. El registro y se inicializa con 0, lo que equivale a suponer y[-1] == 0.

Primer Ciclo (n=0):

BEFORE: INPUT (x=x[0], b); REGISTERS (t=0, t2=0, y=0)

y <= t + t2;      == 0
t <= mult(y, b);  == y[-1] * b  = 0
t2 <= x           == x[0]

AFTERWARDS: REGISTERS (t=0, t2=x[0], y=0), OUTPUT: y[0]=0

Segundo Ciclo (n=1):

BEFORE: INPUT (x=x[1], b); REGISTERS (t=0, t2=x[0], y=y[0])

y <= t + t2;      ==     0  +  x[0]
t <= mult(y, b);  ==  y[0]  *  b
t2 <= x           ==  x[1]

AFTERWARDS: REGISTERS (t=y[0]*b, t2=x[1], y=x[0]), OUTPUT: y[1]=x[0]

Tercer Ciclo (n=2):

BEFORE: INPUT (x=x[2], b); REGISTERS (t=y[0]*b, t2=x[1], y=y[1])

y <= t + t2;      ==  y[0]*b +  x[1]
t <= mult(y, b);  ==  y[1]   *  b
t2 <= x           ==  x[2]

AFTERWARDS: REGISTERS (t=y[1]*b, t2=x[2], y=y[0]*b+x[1]), OUTPUT: y[2]=y[0]*b+x[1]

Cuarto Ciclo (n=3):

BEFORE: INPUT (x=x[3], b); REGISTERS (t=y[1]*b, t2=x[2], y=y[2])

y <= t + t2;      ==  y[1]*b +  x[2]
t <= mult(y, b);  ==  y[2]   *  b
t2 <= x           ==  x[3]

AFTERWARDS: REGISTERS (t=y[2]*b, t2=x[3], y=y[1]*b+x[2]), OUTPUT: y[3]=y[1]*b+x[2]

Podemos ver que comenzando con cylce n=2 obtenemos el siguiente resultado:

y[2]=y[0]*b+x[1]
y[3]=y[1]*b+x[2]

que es equivalente a

y[n]=y[n-2]*b + x[n-1]
y[n]=y[n-1-l]*b1 + x[n-l],  where l = 1
y[n+l]=y[n-1]*b1 + x[n],  where l = 1

Como se mencionó anteriormente, introducimos un retraso adicional de l = 1 ciclos. Eso significa que su salida y[n] se retrasa por retraso l=1. Eso significa que los datos de salida son equivalentes pero se retrasan en un "índice". Para ser más claros: los datos de salida se retrasan en 2 ciclos, ya que se necesita un ciclo de reloj (normal) y se agrega 1 ciclo de reloj adicional (retraso l = 1) para la etapa intermedia.

Aquí hay un boceto para representar gráficamente cómo fluyen los datos:

esquema de flujo de datos

PD: Gracias por echar un vistazo de cerca a mi código. ¡Así que aprendí algo también! ;-) Déjame saber si esta versión es correcta o si ves más problemas.

¡Buen trabajo! Desafortunadamente, y[n] = y[n-2] * b + x[n-1] en realidad no es funcionalmente equivalente a y[n] = y[n-1] * b + x[n] con latencia. La forma de una función de transferencia IIR en realidad se ve así: y[n] = x[n] * b0 + x[n-1] * b1 - y[n-1] * a1 - y[n-2] * a2 y así. Su formulario establece b0 y a1 en 0, y en su lugar usa b1 y a2. Sin embargo, esa transformación en realidad produce un filtro muy diferente. Sin embargo, si hubiera una manera de calcular un filtro con el primer denominador (a1) establecido en cero, ambas soluciones funcionarían perfectamente.
Bueno, debe comprender correctamente el problema del "retraso introducido". Como ejemplo, un filtro de "procesamiento de flujo de datos" debería simplemente reenviar su entrada como y[n] = x[n] funcionaría correctamente si produce y[n] = x[n-1] como salida. ¡La salida solo se retrasa 1 ciclo (p. ej., el índice de salida se compensa con un valor fijo relativo a todos los índices de entrada)! En nuestro ejemplo, esto significa que su función tiene y[n+l] = y[n-1] * b + x[n]un valor fijo para el retraso lque se puede reescribir y[n] = y[n-1-l] * b + x[n-l]y para l=1 esto es y[n] = y[n-2] * b + x[n-1].
Para su filtro IIR más complejo, necesitaría hacer lo mismo: y[n+l] = x[n] * b0 + x[n-1] * b1 - y[n-1] * a1 - y[n-2] * a2=> y[n] = x[n-l]*b0 + x[n-1-l] * b1 - y[n-1-l] * a1 - y[n-2-l]*a2. Suponiendo que puede hacer las tres multiplicaciones en paralelo (1. etapa / 1 ciclo) y necesita hacer para sumar los productos, necesita 2 ciclos (1 ciclo: sumar/sub resultados de los dos primeros productos, 1 ciclo: sumar/sub el resultado de esos dos add/subs), necesitará 2 ciclos adicionales. Entonces l=(3-1)=2 dándote y[n]=x[n-2]*b0+x[n-1-2]*b1-y[n-1-2]*a1-y[n-2-2]*a2=>y[n]=x[n-2]*b0+x[n-3]*b1-y[n-3]*a1-y[n-4]*a2
Por supuesto, para que esto funcione, su FPGA debe poder hacer en paralelo: 4 multiplicaciones y 3 sumas/restas. Lo que significa que necesita recursos para 4 multiplicadores y 3 sumadores.

Sí, puede sincronizar a la frecuencia de muestreo.

Una solución a este problema es manipular la expresión original para que se puedan insertar registros de tubería, mientras se mantiene la secuencia de salida deseada.

Dado: y[n] = y[n-1]*b1 +x[n];

esto se puede manipular en: y[n] = y[n-2]*b1*b1 +x[n-1]*b1 +x[n].

Para verificar que esta es la misma secuencia, considere lo que sucede con las primeras muestras x[0], x[1], x[2], etc., donde antes de x[0] todas las muestras x,y eran cero.

Para la expresión original la sucesión es:

y = x[0],

x[1] +x[0]*b1,

x[2] +x[1]*b1 +x[0]*b1*b1,

x[3] +x[2]*b1 +x[1]*b1*b1 +x[0]*b1*b1*b1, ...

Está claro que es necesario que b1 < 1, de lo contrario este crecerá sin límite.

Ahora considere la expresión manipulada:

y = x[0],

x[0]*b1 +x[1],

x[0]*b1*b1 +x[1]*b1 +x[2],

x[0]*b1*b1*b1 +x[1]*b1*b1 +x[2]*b1 +x[3], ...

Esta es la misma secuencia.

Una solución de hardware en las primitivas de la biblioteca Xilinx necesitaría dos DSP48E en cascada. Consulte la figura 1-1 en UG193 v3.6 para conocer los nombres de puertos y registros a continuación. La primera primitiva es multiplicar por b1 y sumar un reloj después; el segundo es multiplicar por b1*b1 y sumar un reloj después. Hay una latencia de tubería de 4 relojes para esta lógica.

-- DSP48E #1

a_puerto1 := b1; -- coeficiente constante, establecer AREG=1

b_puerto1 := x; -- establecer atributo BREG=1

c_puerto1 := x; -- establecer CREG=1

-- interno a DSP48E #1

reg_a1 <= a_port1;

reg_b1 <= b_puerto1;

reg_c1 ​​<= c_puerto1;

registro_m1 <= registro_a1 * registro_b1;

registro_p1 <= registro_m1 + registro_c1; -- salida del 1er DSP48E

-- fin de DSP48E #1

-- DSP48E #2

a_port2 := reg_p2; -- establecer atributo AREG=0

                -- this means the output of register reg_p2

                -- directly feeds back to the multiplier

b_puerto2 := b1*b1; -- constante, establecer BREG=1

c_puerto2 := registro_p1; -- establecer CREG=1

-- interno a DSP48E #2

reg_b2 <= b_puerto2;

reg_c2 <= c_puerto2;

reg_m2 <= a_port2 * reg_b2;

registro_p2 <= registro_m2 + registro_c2;

-- fin de DSP48E #2

La secuencia en reg_p1:

x[0],

x[1] +x[0]*b1,

x[2] +x[1]*b1,

x[3] +x[2]*b1,

etc.

La secuencia en reg_p2 es el resultado deseado. Interno al 2do DSP48E, el registro reg_m2 tiene una secuencia:

x[0]*b1*b1,

x[1]*b1*b1 +x[0]*b1*b1*b1,

x[2]*b1*b1 +x[1]*b1*b1*b1 +x[0]*b1*b1*b1*b1

Hay una bonita elegancia en este resultado. Claramente, el DSP48E no multiplica y suma el mismo reloj, pero eso es lo que requiere la ecuación de diferencias. La ecuación de diferencia manipulada nos permite tolerar los registros M y P en el DSP48E y el reloj a toda velocidad.