DDS via a VGA port

From Hamsterworks Wiki!

Jump to: navigation, search

This FPGA Project wasted a night in October 2014.

My new Digilent Basys3 has a 12-bit VGA port on it (four bits each colour).

I have often wondered how good using such a port for Direct Digital Synthesis of a signal in the low MHz range would be. So I hooked up the board and the O'scope, and this is what I've found out...

Firstly, here's the HDL code that sequenced the values to the passive ADC.

----------------------------------------------------------------------------------
-- Engineer: 
-- 
-- Create Date: 07.10.2014 21:32:19
-- Design Name: 
-- Module Name: dds_top_level - Behavioral
-- Project Name: 
-- Target Devices: 
-- Tool Versions: 
-- Description: 
-- 
-- Dependencies: 
-- 
-- Revision:
-- Revision 0.01 - File Created
-- Additional Comments:
-- 
----------------------------------------------------------------------------------


library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity dds_top_level is
    Port ( clk100 : in STD_LOGIC;
           channel1 : out STD_LOGIC_VECTOR (3 downto 0));
end dds_top_level;

architecture Behavioral of dds_top_level is
    signal count : unsigned(3 downto 0) := (others => '0');
begin

process(clk100)
    begin
        if rising_edge(clk100) then
            case count is
                when "0000" => channel1 <= "1000";
                when "0001" => channel1 <= "1011";
                when "0010" => channel1 <= "1101";
                when "0011" => channel1 <= "1111";
                when "0100" => channel1 <= "1111";
                when "0101" => channel1 <= "1111";
                when "0110" => channel1 <= "1101";
                when "0111" => channel1 <= "1011";
                when "1000" => channel1 <= "1000";
                when "1001" => channel1 <= "0100";
                when "1010" => channel1 <= "0010";
                when "1011" => channel1 <= "0000";
                when "1100" => channel1 <= "0000";
                when "1101" => channel1 <= "0000";
                when "1110" => channel1 <= "0010";
                when others => channel1 <= "0100";
            end case;
            count <= count+1;
         end if;
    end process;
end Behavioral;

As it divides the 100 MHz clock by 16, it generates a 6.25Mhz output. Here's what it looks like:

Dds ss2.gif

Far better than a Square wave, but still not a very nice sine wave at all.

I've been wanting to use the FFT mode on the scope for something useful, so here's my chance. The desired signal is present at about 1.22V (RMS I assume...):

Dds ss1.gif

The largest of the unwanted harmonics are under about 40 mV or so, or at -35 db

Dds ss3.gif

It is the 100 MHz of the master clock. The 2nd harmonic is pretty close too, at about 30 mV.

I did muck around with a little bit of post-filtering, and adding a 33pF cap to ground filtered out pretty much all of the 16th harmonic, with only a 10% drop in the level of the 6.25Mhz signal. It made minimal impact on the 2nd harmonic.

A minor enhancement

The first enhancement is to improve the lookup table - the output swings from 0 to 15, so the average value should be 7.5. The original version averaged at 7.625. By balancing this up it should have a more symmetric swing, so have lower distortion.

Here's the two versions set up so you can change between tables with the flick of a switch:

----------------------------------------------------------------------------------
-- Company: 
-- Engineer: Mike Field <hamster@snap.net.nz>
-- 
-- Module Name: dds_top_level - Behavioral
-- Description: Playing around with a 4-bit DAC to generate 6.25MHz Sine waves 
----------------------------------------------------------------------------------


library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity dds_top_level is
    Port ( clk100 : in STD_LOGIC;
           switches : in STD_LOGIC_VECTOR(1 downto 0);
           channel1 : out STD_LOGIC_VECTOR (3 downto 0));
end dds_top_level;

architecture Behavioral of dds_top_level is
    signal count : unsigned(3 downto 0) := (others => '0');
begin

process(clk100)
    begin
        if rising_edge(clk100) then
            case switches is
                when "00" => 
                    -- round(SIN(x*2*pi()/16)*7.9+7.5, 0)
                    case count is
                        when "0000" => channel1 <= "1000"; -- 8 
                        when "0001" => channel1 <= "1011"; -- 11
                        when "0010" => channel1 <= "1101"; -- 13
                        when "0011" => channel1 <= "1111"; -- 15
                        when "0100" => channel1 <= "1111"; -- 15
                        when "0101" => channel1 <= "1111"; -- 15
                        when "0110" => channel1 <= "1101"; -- 13
                        when "0111" => channel1 <= "1011"; -- 11
                        when "1000" => channel1 <= "1000"; -- 8
                        when "1001" => channel1 <= "0100"; -- 4
                        when "1010" => channel1 <= "0010"; -- 2
                        when "1011" => channel1 <= "0000"; -- 0
                        when "1100" => channel1 <= "0000"; -- 0
                        when "1101" => channel1 <= "0000"; -- 0
                        when "1110" => channel1 <= "0010"; -- 2
                        when others => channel1 <= "0100"; -- 4
                    end case;
                when others => 
                    -- =ROUND(SIN(x*2*pi()/16 + 1/32)*8.0+7.5, 0)
                    case count is
                        when "0000" => channel1 <= "1000"; -- 8 
                        when "0001" => channel1 <= "1011"; -- 11
                        when "0010" => channel1 <= "1101"; -- 13
                        when "0011" => channel1 <= "1111"; -- 15
                        when "0100" => channel1 <= "1111"; -- 15
                        when "0101" => channel1 <= "1111"; -- 15
                        when "0110" => channel1 <= "1101"; -- 13
                        when "0111" => channel1 <= "1010"; -- 10
                        when "1000" => channel1 <= "0111"; -- 7
                        when "1001" => channel1 <= "0100"; -- 4
                        when "1010" => channel1 <= "0010"; -- 2
                        when "1011" => channel1 <= "0000"; -- 0
                        when "1100" => channel1 <= "0000"; -- 0
                        when "1101" => channel1 <= "0000"; -- 0
                        when "1110" => channel1 <= "0010"; -- 2
                        when others => channel1 <= "0101"; -- 5
                    end case;
            end case;
            count <= count+1;
         end if;
    end process;
end Behavioral;

This drops the 2nd harmonic from 30mV down to 18mV.

Finer time resolution

When running at 100Mhz each transition is +/- 5ns or it's ideal location, giving rise to extra distortion. By reducing this to +/- 2.5ns it should be possible to improve things a little more, and also push some of the 100 Mhz energy out to 200 Mhz.

However, doing this by using a 200 MHz clock would be cheating. Using a DDR output register is a little less of a cheat. Here's the updated source.

 
----------------------------------------------------------------------------------
-- Company: 
-- Engineer: Mike Field <hamster@snap.net.nz>
-- 
-- Module Name: dds_top_level - Behavioral
-- Description: Playing around with a 4-bit DAC to generate 6.25MHz Sine waves 
----------------------------------------------------------------------------------


library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
Library UNISIM;
use UNISIM.vcomponents.all;

entity dds_top_level is
    Port ( clk100 : in STD_LOGIC;
           switches : in STD_LOGIC_VECTOR(1 downto 0);
           channel1 : out STD_LOGIC_VECTOR (3 downto 0));
end dds_top_level;

architecture Behavioral of dds_top_level is
    signal count : unsigned(3 downto 0)         := (others => '0');
    signal out0  : std_logic_vector(3 downto 0) := (others => '0');
    signal out1  : std_logic_vector(3 downto 0) := (others => '0');
begin

ODDR0 : ODDR
 generic map(DDR_CLK_EDGE => "SAME_EDGE",  
INIT => '0',   
SRTYPE => "SYNC") 
             port map (Q => channel1(0), C => clk100, CE => '1', D1 => out0(0), D2 => out1(0), R => '0', S => '0');
  

ODDR1 : ODDR  generic map(DDR_CLK_EDGE => "SAME_EDGE",  INIT => '0', SRTYPE => "SYNC") 
             port map (Q => channel1(1), C => clk100, CE => '1', D1 => out0(1), D2 => out1(1), R => '0', S => '0');

ODDR2 : ODDR generic map(DDR_CLK_EDGE => "SAME_EDGE", INIT => '0',   SRTYPE => "SYNC") 
             port map (Q => channel1(2), C => clk100, CE => '1', D1 => out0(2), D2 => out1(2), R => '0', S => '0');
  
ODDR3 : ODDR generic map(DDR_CLK_EDGE => "SAME_EDGE", INIT => '0', SRTYPE => "SYNC") 
             port map (Q => channel1(3), C => clk100, CE => '1', D1 => out0(3), D2 => out1(3), R => '0', S => '0');
               

process(clk100)
    begin
        if rising_edge(clk100) then
            case switches is
                when "00" => 
                    -- round(SIN(x*2*pi()/16)*7.9+7.5, 0)
                    case count is
                        when "0000" => out0 <= "1000"; out1 <= "1000"; -- 8 
                        when "0001" => out0 <= "1011"; out1 <= "1011"; -- 11
                        when "0010" => out0 <= "1101"; out1 <= "1101"; -- 13
                        when "0011" => out0 <= "1111"; out1 <= "1111"; -- 15
                        when "0100" => out0 <= "1111"; out1 <= "1111"; -- 15
                        when "0101" => out0 <= "1111"; out1 <= "1111"; -- 15
                        when "0110" => out0 <= "1101"; out1 <= "1101"; -- 13
                        when "0111" => out0 <= "1011"; out1 <= "1011"; -- 11
                        when "1000" => out0 <= "1000"; out1 <= "1000"; -- 8
                        when "1001" => out0 <= "0100"; out1 <= "0100"; -- 4
                        when "1010" => out0 <= "0010"; out1 <= "0010"; -- 2
                        when "1011" => out0 <= "0000"; out1 <= "0000"; -- 0
                        when "1100" => out0 <= "0000"; out1 <= "0000"; -- 0
                        when "1101" => out0 <= "0000"; out1 <= "0000"; -- 0
                        when "1110" => out0 <= "0010"; out1 <= "0010"; -- 2
                        when others => out0 <= "0100"; out1 <= "0100"; -- 4
                    end case;
                when others => 
                    -- =ROUND(SIN(x*2*pi()/32 + 1/64)*8.0+7.5, 0)
                    case count is
                        when "0000" => out0 <= "1000"; out1 <= "1001"; -- 8,9 
                        when "0001" => out0 <= "1011"; out1 <= "1100"; -- 11,12
                        when "0010" => out0 <= "1101"; out1 <= "1110"; -- 13, 14
                        when "0011" => out0 <= "1111"; out1 <= "1111"; -- 15, 15
                        when "0100" => out0 <= "1111"; out1 <= "1111"; -- 15, 15
                        when "0101" => out0 <= "1111"; out1 <= "1110"; -- 15, 14
                        when "0110" => out0 <= "1101"; out1 <= "1100"; -- 13, 12
                        when "0111" => out0 <= "1010"; out1 <= "1001"; -- 10, 9
                        when "1000" => out0 <= "0111"; out1 <= "0110"; -- 7, 6
                        when "1001" => out0 <= "0100"; out1 <= "0011"; -- 4, 3
                        when "1010" => out0 <= "0010"; out1 <= "0001"; -- 2, 1
                        when "1011" => out0 <= "0000"; out1 <= "0000"; -- 0, 0
                        when "1100" => out0 <= "0000"; out1 <= "0000"; -- 0, 0
                        when "1101" => out0 <= "0000"; out1 <= "0001"; -- 0, 1
                        when "1110" => out0 <= "0010"; out1 <= "0011"; -- 2, 3
                        when others => out0 <= "0101"; out1 <= "0110"; -- 5, 6
                    end case;
            end case;
            count <= count+1;
         end if;
    end process;
end Behavioral;

This lowers the second harmonic about 4 db, and drops the third harmonic by about 8 db, getting close to 38 db S/N)

However, there will always be about 1/2 LSB of error, giving about 81 mV of unwanted signal for a 4-bit DAC.

Other enhancements

I guess with precision resistors it might be possible to push this technique to 6 bits, giving about 20mV of noise, and maybe dropping the level of any harmonic below -40 db. However, given the limitations of my scope's ADC I wouldn't be able to make any useful direct measurements of this.

Personal tools