¿Ejemplo de código para filtros FIR/IIR en VHDL?

Estoy tratando de comenzar con DSP en mi placa Spartan-3. Hice una placa AC97 con un chip de una placa base antigua, y hasta ahora conseguí hacer ADC, multiplicar las muestras por un número <1 (disminuir el volumen) y luego DAC.

Ahora me gustaría hacer algunas cosas básicas de DSP, como un filtro de paso bajo, paso alto, etc. Pero estoy realmente confundido acerca de la representación numérica (¿enteros? ¿punto fijo? ¿Q0.15? ¿Desbordamiento o saturación?).

Solo quiero un código de ejemplo de un filtro simple real para comenzar. Nada de alta eficiencia, rápido, ni nada por el estilo. Solo el filtro teórico implementado en VHDL.

He estado buscando, pero solo encuentro fórmulas teóricas. Lo entiendo, lo que no entiendo es cómo procesar las muestras de audio firmadas de 16 bits y 48 KHz que obtengo del ADC. He estado usando estas bibliotecas: http://www.vhdl.org/fphdl/ . Si multiplico mis muestras por 0.5, 0.25, etc., puedo escuchar la diferencia. Pero un filtro más grande solo me da ruido.

Gracias.

Si bien estoy a favor de usar lo que tenga a mano para aprender cosas, me gustaría señalar que hacer filtros de audio en un FPGA no es una forma muy eficiente o rentable de hacerlo. Entonces, si haces un proyecto real, te recomiendo usar un DSP de bajo costo en su lugar. Excepciones: cuando estás haciendo un número impío de canales de audio al mismo tiempo, o estás haciendo FIR con un número absurdo de toques.

Respuestas (6)

Parece que primero necesita descubrir los aspectos de DSP y luego realizar una implementación en FPGA.

  • Ordene el DSP en C, Matlab, Excel o en cualquier otro lugar
  • Intente y piense cómo transferirá lo que ha aprendido de eso a FPGA-land
  • Descubre que has hecho alguna suposición sobre la implementación que no funciona bien (como el uso de coma flotante, por ejemplo)
  • Regrese y actualice sus cosas de DSP fuera de línea para tener esto en cuenta.
  • Iterar n veces :)

Con respecto a los tipos de datos, puede usar números enteros muy bien.

Aquí hay un código de muestra para que pueda comenzar. Tenga en cuenta que faltan muchos problemas del mundo real (por ejemplo, reinicio, gestión de desbordamiento), pero espero que sea instructivo:

library ieee;
use ieee.std_logic_1164.all;
entity simple_fir is
    generic (taps : integer_vector); 
    port (
        clk      : in  std_logic;
        sample   : in  integer;
        filtered : out integer := 0);
end entity simple_fir;
----------------------------------------------------------------------------------------------------------------------------------
architecture a1 of simple_fir is
begin  -- architecture a1
    process (clk) is
        variable delay_line : integer_vector(0 to taps'length-1) := (others => 0);
        variable sum : integer;
    begin  -- process
        if rising_edge(clk) then  -- rising clock edge
            delay_line := sample & delay_line(0 to taps'length-2);
            sum := 0;
            for i in 0 to taps'length-1 loop
                sum := sum + delay_line(i)*taps(taps'high-i);
            end loop;
            filtered <= sum;
        end if;
    end process;
end architecture a1;
----------------------------------------------------------------------------------------------------------------------------------
-- testbench
----------------------------------------------------------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
entity tb_simple_fir is
end entity tb_simple_fir;
architecture test of tb_simple_fir is
    -- component generics
    constant lp_taps : integer_vector := ( 1, 1, 1, 1, 1);
    constant hp_taps : integer_vector := (-1, 0, 1);

    constant samples : integer_vector := (0,0,0,0,1,1,1,1,1);

    signal sample   : integer;
    signal filtered : integer;
    signal Clk : std_logic := '1';
    signal finished : std_logic;
begin  -- architecture test
    DUT: entity work.simple_fir
        generic map (taps => lp_taps)  -- try other taps in here
        port map (
            clk      => clk,
            sample   => sample,
            filtered => filtered);

    -- waveform generation
    WaveGen_Proc: process
    begin
        finished <= '0';
        for i in samples'range loop
            sample <= samples(i);
            wait until rising_edge(clk);
        end loop;
        -- allow pipeline to empty - input will stay constant
        for i in 0 to 5 loop
            wait until rising_edge(clk);
        end loop;
        finished <= '1';
        report (time'image(now) & " Finished");
        wait;
    end process WaveGen_Proc;

    -- clock generation
    Clk <= not Clk after 10 ns when finished /= '1' else '0';
end architecture test;
Gracias por tu respuesta. Eso es más o menos lo que hice, pero tengo algunos problemas con la representación de números. Mi ADC me da valores en el -32k a +32k (firmado de 16 bits). También tengo el problema de la constante de filtro: ¿cómo represento eso? ¿Y el resultado de multiplicar entre la muestra y la constante? Eso es lo que más me confunde.
@hjf: son solo números enteros. Mientras todo se mantenga dentro de los 32 bits, está bien. SI necesita más ancho que eso, puede usar vectores SIN FIRMAR o FIRMADOS tan anchos como desee. O use los tipos de punto fijo de VHDL2008 (vea aquí: vhdl.org/fphdl )

Otro fragmento de código simple (solo las agallas). Tenga en cuenta que no escribí el VHDL directamente, usé MyHDL para generar el VHDL.

-- VHDL code snip
architecture MyHDL of sflt is

type t_array_taps is array(0 to 6-1) of signed (15 downto 0);
signal taps: t_array_taps;

begin

SFLT_RTL_FILTER: process (clk) is
    variable sum: integer;
begin
    if rising_edge(clk) then
        sum := to_integer(x * 5580);
        sum := to_integer(sum + (taps(0) * 5750));
        sum := to_integer(sum + (taps(1) * 6936));
        sum := to_integer(sum + (taps(2) * 6936));
        sum := to_integer(sum + (taps(3) * 5750));
        sum := to_integer(sum + (taps(4) * 5580));
        taps(0) <= x;
        for ii in 1 to 5-1 loop
            taps(ii) <= taps((ii - 1));
        end loop;
        y <= to_signed(sum, 16);
    end if;
end process SFLT_RTL_FILTER;

end architecture MyHDL;

circuito sintetizado

Esta es una implementación directa. Requerirá multiplicadores. La síntesis de este circuito, destinado a un Altera Cyclone III, no usó ningún multiplicador explícito pero requirió 350 elementos lógicos.

Este es un filtro FIR pequeño y tendrá la siguiente respuesta (no tan buena), pero debería ser útil como ejemplo.

respuesta del filtro

Además, tengo un par de ejemplos, aquí y aquí , que pueden ser útiles.

Además, su pregunta parece preguntar: "¿cuál es la representación de punto fijo apropiada?" Con frecuencia, cuando se implementan funciones DSP, se utiliza la representación de punto fijo, porque simplifica el análisis de los filtros. Como se mencionó, el punto fijo es solo aritmética entera. La implementación real simplemente funciona con números enteros, pero nuestra representación percibida es fraccionaria.
Por lo general, surgen problemas cuando se convierte de entero de implementación (punto fijo) a/desde punto flotante de diseño.

No sé qué tan bien se admiten los tipos de punto fijo y punto flotante VHDL. Funcionarán bien en simulación, pero no sé si sintetizarán con la mayoría de las herramientas de síntesis. Creé una pregunta separada para esto.

Gracias por tu respuesta. ¿Tiene algún recurso donde pueda aprender DSP en VHDL y generación de código (tutoriales o libros)?

El filtro FIR de paso bajo más simple que puede probar es y(n) = x(n) + x(n-1). Puede implementar esto con bastante facilidad en VHDL. A continuación se muestra un diagrama de bloques muy simple del hardware que desea implementar.

Diagrama de bloques para un filtro de paso bajo simple

De acuerdo con la fórmula, necesita las muestras de ADC actuales y anteriores para obtener la salida adecuada. Lo que debe hacer es bloquear las muestras de ADC entrantes en el flanco descendente del reloj y realizar los cálculos apropiados en el flanco ascendente para obtener la salida adecuada. Dado que está sumando dos valores de 16 bits, es posible que termine con una respuesta de 17 bits. Debe almacenar la entrada en registros de 17 bits y usar un sumador de 17 bits. Su salida, sin embargo, serán los 16 bits inferiores de la respuesta. El código puede verse así, pero no puedo garantizar que funcione completamente ya que no lo he probado, y mucho menos sintetizado.

IEEE.numeric_std.all;
...
    signal x_prev, x_curr, y_n: signed(16 downto 0);
    signal filter_out: std_logic_vector(15 downto 0);
...
process (clk) is
begin
    if falling_edge(clk) then
        --Latch Data
        x_prev <= x_curr;
        x_curr <= signed('0' & ADC_output); --since ADC is 16 bits
    end if;
end process;

process (clk) is
begin
    if rising_edge(clk) then
        --Calculate y(n)
        y_n <= x_curr + x_prev;
    end if;
end process;

filter_out <= std_logic_vector(y_n(15 downto 0));  --only use the lower 16 bits of answer

Como puede ver, puede usar esta idea general para agregar fórmulas más complicadas, como las que tienen coeficientes. Las fórmulas más complicadas, como los filtros IIR, pueden requerir el uso de variables para que la lógica del algoritmo sea correcta. Finalmente, una manera fácil de sortear los filtros que tienen números reales como coeficientes es encontrar un factor de escala para que todos los números terminen siendo lo más cercanos posible a los números enteros. Su resultado final deberá reducirse por el mismo factor para obtener el resultado correcto.

Espero que esto te pueda ser útil y te ayude a poner la pelota en marcha.

*Esto se ha editado para que el enganche de datos y el enganche de salida estén en procesos separados. También usando tipos firmados en lugar de std_logic_vector. Supongo que su entrada ADC será una señal std_logic_vector.

Es muy poco probable que los procesos que desencadenan ambos bordes (como ha descrito) se sinteticen
@Martin Supongo que sabe mucho más sobre FPGA que yo, pero he bloqueado los datos entrantes en el flanco descendente y bloqueado la salida en el flanco ascendente para una asignación de clase, así que pensé que esto habría funcionado. ¿Puede explicar por qué tales procesos no funcionan?
Funcionará bien en un simulador. Sin embargo, los sintetizadores se atragantarán (en mi experiencia) ya que los flip-flops en el dispositivo solo pueden sincronizarse en un borde.
@ dhsieh2 Gracias, este es el tipo de respuesta que estaba buscando. Otra pregunta, ¿cómo lo haría si estuviera usando números firmados (mi ADC me da valores en el -32k a +32k).
@hjf ese es un buen punto. Debería haber usado "firmado" en lugar de "std_logic_vector". Eso debería incluirse en la biblioteca IEEE.numeric_std.all. De lo contrario, con la forma en que lo tengo ahora, deberá convertirlos en tipos firmados (luego volver a los tipos std_logic_vector) antes de poder hacer la adición. Haré esos cambios en mi respuesta.
@Martin Reloj las cosas fuera de ambos bordes del reloj todo el tiempo en Xilinx FPGA, no hay problema. Simplemente no puede registrar el mismo FF en ambos bordes. Cuando observa la salida del analizador de tiempo, en realidad deja muy claro que está haciendo bordes opuestos y ajusta el presupuesto de tiempo en consecuencia.
@David: estoy de acuerdo, pero en el momento en que comenté, el código se escribió con dos bordes en un solo proceso. De todos modos, este debate es un poco OT en el tema de las implementaciones de FIR :)
@MartinThompson, lo llamaría tangencial y muy interesante para la lectura de otros usuarios, aunque realmente solo puedo hablar por mí mismo.

OpenCores tiene varios ejemplos de DSP, IIR y FIR, incluido BiQuad. Tendrás que registrarte para descargar los archivos.

editar
Entiendo el comentario de Kortuk sobre los enlaces muertos y, de hecho, si el enlace a OpenCores muere, la respuesta se volverá inútil. Estoy bastante seguro de que esto no sucederá; mi enlace es genérico, y solo morirá si el dominio completo de OpenCores desapareciera.
Traté de buscar algunos ejemplos que pudiera usar para esta respuesta, pero son demasiado largos para representarlos aquí. Así que seguiré mi consejo de registrarte en el sitio tú mismo (tuve que mudarme a Nueva York porque mi ciudad natal no fue aceptada) y echar un vistazo al código que se presenta allí.

Como con todas las cosas, los enlaces se rompen. Hemos discutido antes que un enlace por sí solo no da una respuesta. ¿Puede traer algo de lo que hay allí y hacer una respuesta sustanciosa que tenga ese enlace como referencia para obtener más información?
@Kortuk - Quería hacer esto ayer. Me registré ayer con opencores para obtener algunos detalles, pero necesitan unos días para pensar si me aceptarán.
Me alegra saberlo, sinceramente me preguntaba si algo se había interpuesto en tu camino. Esperamos escuchar más al respecto.

He intentado implementar scripts para la implementación automática de filtros IIR, donde se puede definir si el diseño debe ser lo más rápido posible (para que cada multiplicación se realice con un multiplicador dedicado) o lo más pequeño posible (para que se reutilice cada multiplicador).

Las fuentes se han publicado en alt.sources como "Implementación conductual pero sintetizable de filtros IIR en VHDL" (también puede encontrarlo en el archivo de Google: https://groups.google.com/group/alt.sources/msg/c8cf038b9b8ceeec ?dmodo=fuente )

Las publicaciones en alt.sources están en formato "compartir", por lo que debe guardar el mensaje como texto y dejar de compartirlo (con la utilidad "descompartir") para obtener las fuentes.

Los grupos de Google no sirven correctamente los archivos compartidos. Se puede encontrar una copia de la publicación referenciada en ftp.funet.fi/pub/archive/alt.sources/2600.gz . El código en sí está disponible en github.com/wzab/wzab-hdl-library/tree/master/…

¿Qué tal esto? https://github.com/MauererM/VIIRF

Implementa un filtro IIR basado en biquad (SOS, secciones de segundo orden) que se encarga de la implementación de punto fijo. También cuenta con scripts de Python para el diseño y verificación del filtro. No utiliza construcciones de FPGA específicas del proveedor y puede elegir el equilibrio entre el uso de área de alta velocidad y baja.