12
\$\begingroup\$

I'm trying to get started with DSP in my Spartan-3 board. I made a AC97 board with a chip from an old motherboard, and so far I got it to do ADC, multiply the samples for a number <1 (decrease volume) and then DAC.

Now I'd like to do some basic DSP stuff, like a low-pass filter, high-pass etc. But I'm really confused about numeric representation (integers? fixed point? Q0.15? Overflow or saturation?).

I just want some example code of an actual simple filter to get me started. No high-efficiency, fast, or anything like that. Just the theoretical filter implemented in VHDL.

I've been searching but I just find theoretical formulas - I get that, what I don't understand is how to process the signed 16-bit, 48KHz audio samples I'm getting from the ADC. I've been using these libraries: http://www.vhdl.org/fphdl/. If I multiply my samples by 0.5, 0.25, etc, I can hear the difference. But a larger filter gives me just noise.

Thanks.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ While I'm all for using whatever you have on hand for learning stuff, I'd like to point out that doing audio filters in an FPGA is not a very efficient or cost effective way to do it. So, if you do a real project then I'd recommend using a low cost DSP instead. Exceptions: When you're doing an ungodly number of audio channels at the same time, or you are doing FIR's with an absurd number of taps. \$\endgroup\$
    – user3624
    Jun 24, 2011 at 17:43

6 Answers 6

9
\$\begingroup\$

It sounds like you need to figure out the DSP aspects first, then make an implementation in FPGA.

  • Sort out the DSP in C, Matlab, Excel, or anywhere else
  • Try and think how you'll transfer what you've learned from that into FPGA-land
  • Discover you've made some assumption about the implementation that doesn't work well (like the use of floating point for example)
  • Go back and update your offline DSP stuff to take account of this.
  • Iterate n times :)

Regarding data types, you can use integers just fine.

here's some sample code to get you going. Note that it's missing a lot of real-world issues (for example reset, overflow management) - but hopefully it's instructive:

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;
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your answer. That's more or less what I did but I'm having some issues with number representation. My ADC gives me values in the -32k to +32k (signed 16-bit). I also have the problem of the filter constant - how do I represent that? And the result of multiply between the sample and the constant? That's what's confusing me the most. \$\endgroup\$
    – hjf
    Jun 24, 2011 at 15:55
  • \$\begingroup\$ @hjf - it's all just integers. So long as everything stays within 32 bits you're OK. IF you need more width than that you can use UNSIGNED or SIGNED vectors as wide as you like. Or use the fixed_point types from VHDL2008 (see here: vhdl.org/fphdl) \$\endgroup\$ Jun 26, 2011 at 21:33
6
\$\begingroup\$

Another simple code snippet (just the guts). Note I didn't write the VHDL directly, I used MyHDL to generate the 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;

synthesized circuit

This is a direct implementation. It will require multipliers. The synthesis of this circuit, targetted for an Altera Cyclone III, didn't use any explicit multipliers but required 350 logic elements.

This is a small FIR filter and will have the following response (not so great) but should be useful as an example.

filter response

In addition I have a couple examples, here and here, that might be useful.

Also, your question appears to ask: "what is appropriate fixed-point representation?" Frequently when implementing DSP functions, fixed-point representation is used, because it simplifies analyzing the filters. As mentioned, fixed-point is just integer arthimetic. The actual implementation is simply working with integers but our preceived representation is fractional.
Issues usually arise when converting from implementation integer (fixed-point) to/fro design floating-point.

I don't know how well the VHDL fixed-point and floating-point types are supported. They will work fine in simulation but I don't know if they will synthesize with most synthesis tools. I created a separate question for this.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your answer. do you have any resources where I can learn DSP in VHDL and code generation(a tutorials or books)? \$\endgroup\$
    – Yaakov
    Apr 12, 2021 at 10:05
5
\$\begingroup\$

The simplest low pass FIR filter you can try is y(n) = x(n) + x(n-1). You can implement this quite easily in VHDL. Below is a very simple block diagram of the hardware you want to implement.

Block Diagram for a Simple Low Pass Filter

According to the formula, you need the current and previous ADC samples in order to get the appropriate output. What you should do is to latch the incoming ADC samples on the falling edge of the clock, and perform the appropriate calculations on the rising edge in order to get the appropriate output. Since you are adding two 16-bit values together, it's possible you'll end up with a 17-bit answer. You should store the input into 17-bit registers and use a 17-bit adder. Your output, however, will be the lower 16 bits of the answer. Code might look something like this but I can't guarantee that it will work completely since I haven't tested it, let alone synthesized it.

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

As you can see, you can use this general idea to add in more complicated formulas, such as ones with coefficients. More complicated formulas, like IIR filters, may require the use of variables to get algorithm logic correct. Finally, an easy way to get around filters that have real numbers as coefficients is to find a scale factor so that all the numbers end up being as close to whole numbers as possible. Your final result will have to be scaled back down by the same factor to get the correct result.

I hope this can be of use to you and help you get the ball rolling.

*This has been edited so that the data latching and output latching are in separate processes. Also using signed types instead of std_logic_vector. I'm assuming your ADC input is going to be a std_logic_vector signal.

\$\endgroup\$
8
  • 2
    \$\begingroup\$ Processes which trigger off both edges (as you have described) are very unlikely to synthesise \$\endgroup\$ Jun 24, 2011 at 11:49
  • \$\begingroup\$ @Martin I'm assuming you know a lot more about FPGA's than I do, but I have latched incoming data on the falling edge and latched output on the rising edge for a class assignment so I thought this would have worked. Can you explain why such processes don't work? \$\endgroup\$
    – dhsieh2
    Jun 24, 2011 at 12:31
  • 3
    \$\begingroup\$ It'll work fine in a simulator. Synthesisers will choke on it though (in my experience) as the flipflops in the device can only clock on one edge. \$\endgroup\$ Jun 24, 2011 at 14:44
  • \$\begingroup\$ @dhsieh2 Thanks, this is the kind of answer I was looking for. Another question, how would I do it if I was using Signed numbers (my ADC gives me values in the -32k to +32k). \$\endgroup\$
    – hjf
    Jun 24, 2011 at 15:52
  • 4
    \$\begingroup\$ @Martin I clock things off of both clock edges all the time in Xilinx FPGA's, no problem. You just can't clock the same FF off of both edges. When you look at the timing analyzer output it actually makes it very clear that you're doing opposite edges and adjusts the timing budget accordingly. \$\endgroup\$
    – user3624
    Jun 24, 2011 at 17:38
3
\$\begingroup\$

OpenCores has a number of DSP examples, IIR and FIR, including BiQuad. You'll have to register to download the files.

edit
I understand Kortuk's comment on dead links, and indeed, if the link to OpenCores dies the answer will become useless. I'm quite confident that this won't happen; my link is a generic one, and it will only die if the complete OpenCores domain would disappear.
I tried to look for some examples I could use for this answer but they're all too long to be represented here. So I'll stick to my advice to register for the site yourself (I had to move to New York, because my home town wasn't accepted) and have a look at the code presented there.

\$\endgroup\$
3
  • \$\begingroup\$ As with all things, links break. We have discussed before that a link on its own does not make an answer. Can you bring over some of what is there and make a meaty answer that has that link as a reference to learn more? \$\endgroup\$
    – Kortuk
    Sep 20, 2011 at 22:20
  • \$\begingroup\$ @Kortuk - I wanted to do this yesterday. I registered yesterday with opencores to get some details, but they need a few days to think if they'll have me \$\endgroup\$
    – stevenvh
    Sep 21, 2011 at 5:38
  • \$\begingroup\$ glad to hear it, I was honestly wondering if something had gotten in your way. Look forward to hearing more about it. \$\endgroup\$
    – Kortuk
    Sep 21, 2011 at 5:39
1
\$\begingroup\$

I have tried to implement scripts for authomatic implementation of IIR filters, where you can defined whether the design should be as fast as possible (so each multiplication is performed with dedicated multiplier) or as small as possible (so each multiplier is reused).

The sources have been published on alt.sources as "Behavioral but synthesizable implementation of IIR filters in VHDL" (you can also find it in google archive: https://groups.google.com/group/alt.sources/msg/c8cf038b9b8ceeec?dmode=source )

Posts to alt.sources are in "shar" format, so you need to save the message as text, and unshar it (with "unshar" utility) to get sources.

\$\endgroup\$
1
0
\$\begingroup\$

How about this? https://github.com/MauererM/VIIRF

It implements a biquad (SOS, second-order sections) based IIR filter that takes care of the fixed-point implementation. It also features Python scripts for design and verification of the filter. It does not use Vendor-specific FPGA constructs and you can choose the trade-off between high-speed and low area use.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.