GPSDO

From Hamsterworks Wiki!

Jump to: navigation, search

This FPGA Project was finished in September 2018

I was experimenting with a GPS module, and decided to see how hard it would be to create a GPS disciplined 1MHz square wave - and it takes about 100 lines!

If you connect a GPS module's PPS pin to JA1, a 1MHz signal will appear on JA2. The output signal cycles will be either 1.00 us, 0.99 us or a 1.01 us pulse as it adjusts to make it close to 1MHz as possible.

The LEDs show the status. The top four LEDs show is the PPS pulses are being received. The lower 12 show a binary representation of the absolute error in 10ns steps (or 0.01Hz steps) away from 1MHz. Without referencing to the PPS signal, my FPGA board's onboard 100MHz oscillator averages out at approximately 100,000,877 Hz, and with the PPS input the 1MHz input goes from 1,000,008.77Hz down to 1,000,000.0Hz +/- ~0.5Hz.

Contents

Possible enhancements

With a bit of careful analysis this could be improved quite a lot.

  • The size of the 'error' signal needs to be match with the worst possible case from the FPGA's clock.
  • Use a Block Ram to convert the error value to something on the 7-segment display, to display current error as -9.99 to 9.99 Hz.
  • Using a DDR register, and creating a second "nco1_a" phase accumulator with a phase offset, it would be possible to halve the output jitter to 5ns
  • Clock rate could be increased from 100MHz to 200MHz, halving the jitter again, to 2.5ns
  • Using a SERDES block, and creating a a few more "nco1_a" phase accumulators you could get the jitter to about 1ns.
  • With a bit of reworking, so that the fractional part of nco1_a had the phase of the 1Mhz signal (not the 100MHz unit it currently works in) you could then drive an DAC to generate a 1MHz sine wave, and avoid most of the jitter.
  • Currently the accumulated error in nco2_a gets dropped when the PPS pulse comes in. If instead of resetting to -100,000,000 it subtracted 100,000,000 from current nco2_a value, you could ensure that the long term cycle count is 1 million cycles per second (e.g. no extra pulses will be added or dropped). This would most likely alter the stability quite a bit, and increase settling time.
  • Currently the error accumulator has a bit of a dead band - it ranges from -800,000,000 to 800,000,000 but then jumps back close to zero when it it hits these limits. This should really be closed down to -400,000,000 to 400,000,000.
  • It could be optimized for resources, mostly by moving from decimal constants into binary ones - eg 800000000 could be 536870912, binary 001000000000000000000000000000, making the comparison and addition far less resource hungry.
  • A DDR register could be used for sampling the incoming PPS signal, improving its accuracy.
  • You could analyse the latency (which is fixed) and attempt to phase align the reference signal with the long-term average of the PPS signal.
  • It is possible that with a pathologically poor PPS clock you can drive the NCO values so far out of whack that it will never be able to recover and track a valid signal.
  • You could adjust the GPS module's settings to reduce the error in the PPS signal's timing

Source Files

top.vhd

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

entity top is
    port ( clk100 : in STD_LOGIC;
        pps : in STD_LOGIC;
        ref : out STD_LOGIC;
        led : out STD_LOGIC_VECTOR (15 downto 0)
    );
end top;

architecture Behavioral of top is
    constant cycles_per_sec : natural := 100*1000*1000;
    constant nco_frac_len   : natural := 26;
    constant zco1_int_len   : natural := 6;
    constant nco2_int_len   : natural := 27;
    constant error_len      : natural := nco2_int_len-16+1;
    constant nco_d_min      : unsigned(nco_frac_len downto 0) := (nco_frac_len => '1', nco_frac_len-1 => '1', nco_frac_len-2 => '1', others => '0');
    constant nco_d_max      : unsigned(nco_frac_len downto 0) := (nco_frac_len => '0', nco_frac_len-1 => '1', nco_frac_len-2 => '1', others => '0');
    
    signal nco_d     : unsigned(nco_frac_len downto 0) := (nco_frac_len => '1', others => '0');
    signal nco1_a    : unsigned(zco1_int_len + nco_frac_len-1 downto 0) := (others => '0');
    signal nco2_a    : unsigned(nco2_int_len + nco_frac_len-1 downto 0) := (others => '0');
    signal ref_state : std_logic := '0';
    signal pps_sync  : std_logic_vector(3 downto 0) := (others => '0');
    signal tracking  : std_logic_vector(3 downto 0) := (others => '0');
    signal error     : signed(error_len-1 downto 0) := (others => '0');
    signal error_a   : signed(30 downto 0) := (others => '0');
begin

update_nco_proc: process(clk100)
    begin
        if rising_edge(clk100) then
            if error_a + resize(error,error_a'length) > 800000000 then
                error_a   <= error_a + resize(error,error_a'length) - 800000000;
                nco_d   <= nco_d - 1;
            elsif error_a + resize(error,error_a'length) < - 800000000 then
                error_a   <= error_a + resize(error,error_a'length) + 800000000;
                nco_d   <= nco_d + 1;
            else
                error_a   <= error_a + resize(error,error_a'length);
            end if;
        end if;
    end process;

error_update_proc: process(clk100) 
    begin
        if rising_edge(clk100) then
            led(15 downto 12) <= tracking;
            if error < 0 then 
                led(11 downto  0) <= std_logic_vector((not error)+1);
            else
                led(11 downto  0) <= std_logic_vector(error);
            end if;
            
            if pps_sync(1) = '1' and pps_sync(0) = '0' then
                -- The PPS edge has been seen.
                if nco2_a(nco2_a'high downto nco2_a'high-15) = x"0000" or nco2_a(nco2_a'high downto nco2_a'high-15) = x"FFFF" then
                    if tracking(1) = '1' then
                    -- only update the error if we are close to being in sync
                        error <= signed(nco2_a(nco_frac_len+error'length-1 downto nco_frac_len));
                    end if;

                    -- Move a one into the tracking status shift reg
                    tracking <= tracking(tracking'high-1 downto 0) & '1';
                else
                    error     <= (others => '0');
                    tracking <= (others => '0');
                end if;
                nco2_a <= (to_unsigned(-cycles_per_sec,nco2_int_len) & to_unsigned(0, nco_frac_len)) + nco_d;
            else
                nco2_a <= nco2_a + nco_d;
            end if;

            -- Seeing if the expected PPS has not arrived in time
            if nco2_a(nco2_a'high downto nco2_a'high-15) = x"0001" then
                -- No pulse per second, so go into holdover mode
                tracking <= (others => '0');
                error    <= (others => '0');
            end if;

            -- Rembering the current PPS signal so we can detect the edge.
            pps_sync <= pps & pps_sync(pps_sync'high downto 1);
        end if;
    end process;

  -- Generating the 1MHz reference
ref_gen_proc: process(clk100)
    begin
        if rising_edge(clk100) then
            ref <= ref_state;
            if nco1_a(nco1_a'high downto nco_frac_len) >= 50 then
                nco1_a <= nco1_a + nco_d - (to_unsigned(50,6) & to_unsigned(0, nco_frac_len));
                ref_state <= not ref_state;
            else 
                nco1_a <= nco1_a + nco_d;
            end if;
        end if;
    end process;

end Behavioral;

basys3.xdc

## Clock signal
set_property PACKAGE_PIN W5 [get_ports clk100]							
set_property IOSTANDARD LVCMOS33 [get_ports clk100]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk100]

##Sch name = JA1
set_property PACKAGE_PIN J1 [get_ports {pps}]					
set_property IOSTANDARD LVCMOS33 [get_ports {pps}]
##Sch name = JA2
set_property PACKAGE_PIN L2 [get_ports {ref}]					
set_property IOSTANDARD LVCMOS33 [get_ports {ref}]
 
## LEDs
set_property PACKAGE_PIN U16 [get_ports {led[0]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[0]}]
set_property PACKAGE_PIN E19 [get_ports {led[1]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[1]}]
set_property PACKAGE_PIN U19 [get_ports {led[2]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[2]}]
set_property PACKAGE_PIN V19 [get_ports {led[3]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[3]}]
set_property PACKAGE_PIN W18 [get_ports {led[4]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[4]}]
set_property PACKAGE_PIN U15 [get_ports {led[5]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[5]}]
set_property PACKAGE_PIN U14 [get_ports {led[6]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[6]}]
set_property PACKAGE_PIN V14 [get_ports {led[7]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[7]}]
set_property PACKAGE_PIN V13 [get_ports {led[8]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[8]}]
set_property PACKAGE_PIN V3 [get_ports {led[9]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[9]}]
set_property PACKAGE_PIN W3 [get_ports {led[10]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[10]}]
set_property PACKAGE_PIN U3 [get_ports {led[11]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[11]}]
set_property PACKAGE_PIN P3 [get_ports {led[12]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[12]}]
set_property PACKAGE_PIN N3 [get_ports {led[13]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[13]}]
set_property PACKAGE_PIN P1 [get_ports {led[14]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[14]}]
set_property PACKAGE_PIN L1 [get_ports {led[15]}]					
set_property IOSTANDARD LVCMOS33 [get_ports {led[15]}]

Personal tools