module ppsloop(
    input reset,
    input xoclk, // 40 MHz from VCTCXO
    input ppsgps,
    input ppsext,
    input [1:0] refsel,
    output reg lpps,
    output reg is10meg,
    output reg ispps,
    output reg reflck,
    output plllck,// status of things
    output sclk,
    output mosi,
    output sync_n,
    input [15:0] dac_dflt
   );
  wire ppsref = (refsel==2'b00)?ppsgps:
                (refsel==2'b11)?ppsext:
                                1'b0;
  // reference pps to discilpline the VCTX|CXO to, from GPS or EXT in

  wire clk_200M_o, clk;
  BUFG x_clk_gen ( .I(clk_200M_o), .O(clk));
  wire clk_40M;

  wire n_pps = (refsel==2'b01) | (refsel==2'b10);
  reg _npps, no_pps;
  always @(posedge clk) { no_pps, _npps } <= { _npps, n_pps };

  PLLE2_ADV #(.BANDWIDTH("OPTIMIZED"), .COMPENSATION("INTERNAL"),
     .DIVCLK_DIVIDE(1),
     .CLKFBOUT_MULT(30),
     .CLKOUT0_DIVIDE(6),
     .CLKOUT1_DIVIDE(30),
     .CLKIN1_PERIOD(25.0)
   )
   clkgen (
      .PWRDWN(1'b0), .RST(1'b0),
      .CLKIN1(xoclk),
      .CLKOUT0(clk_200M_o),
      .CLKOUT1(clk_40M),
      .LOCKED(plllck)
   );

    // state machine to manage reference detection and xo adjustment steps
  reg [2:0] sstate, nxt_sstate;
  localparam REFDET=3'b000;
  localparam CFADJ=3'b001;
  localparam SLEDGEA=3'b010;
  localparam SLEDGEB=3'b011;
  localparam FINEADJ=3'b100;

  // state machine to manage lead-lag count
  reg [1:0] llstate, nxt_llstate;
  localparam READY=2'b00;
  localparam COUNT=2'b01;
  localparam DONE=2'b11;
  localparam WAIT=2'b10;

  /* Counter generating a local pps for the xo derived clock domains.
     nxt_lcnt is manipulated by a state machine (sstate) to allow
     quick re-alignment of the local pps rising edge with that of
     the reference.
  */
  reg [27:0] lcnt, nxt_lcnt;
  wire recycle = (28'd199_999_999==lcnt); // sets the period, 1 sec

  always @(posedge clk) begin
    sstate <= nxt_sstate;
    lcnt <= nxt_lcnt;
    lpps <= lcnt > 28'd150_000_000; // ~25% duty cycle
  end

  /* Reference signal detection:
   * Count the time interval between rising edges on the reference
   * signal. The interval counter "rcnt" is restarted at rising edges
   * of ppsref. "ppsref" could be either a pps signal, or a 10 MHz clock.
   * Register "rlst" captures the value of rcnt at each rising edge.
   * From this count value, we know the reference frequency.
   */
  reg [27:0] rcnt, rlst;
  reg signed [28:0] rdiff;
  wire signed [28:0] srlst = { 1'b0, rlst }; // sign extended version of rlst
  wire [27:0] nxt_rcnt;
  reg rcnt_ovfl;
  reg [3:0] ple; // pipeline from reference rising edge det.
  wire valid_ref = is10meg | ispps;


  /* If the reference is at 10 MHz, derive a reference pps using a counter
   * to feed the frequency control logic. To detect a 0.5 ppm deviation
   * on a 10 MHz signal using counters requires the better part of a second
   * anyway, so samples at a 1 Hz rate are appropriate. This allows much of
   * the same logic to be used for pps or 10 Mhz references.
  */
  reg [23:0] tcnt;
  reg tpps;
  wire [23:0] nxt_tcnt = (~is10meg | tcnt==24'd9999999) ? 24'b0 : tcnt+1'b1;
  always @(posedge ppsref) begin
    /* note this is clocked by the reference signal and is not useful when
     * the reference is a pps.
     */
    tcnt <= nxt_tcnt;
    tpps <= (tcnt>24'd7499999);
  end

  /* The reference needs to be synchronized into the local clock domain,
   * and while the local 'pps' is generated synchronously within this
   * domain, it gets passed through identical stages to maintain
   * the time relationship between detected rising edges.
   */
  reg [2:0] refsmp;
  reg [2:0] tsmp;
  reg [2:0] xosmp;
  always @(posedge clk) begin
    // apply same sync delay to all pps flavors
    refsmp <= { refsmp[1:0], ppsref};
    tsmp <= { tsmp[1:0], tpps};
    xosmp <= { xosmp[1:0], lpps };
  end


  wire rising_r = (refsmp[2:1]==2'b01);
  wire rising_t = (tsmp[2:1]==2'b01);
  wire rising_ref = is10meg ? rising_t : rising_r;
  wire rising_xo  = (xosmp[2:1]==2'b01);
  wire lead = rising_xo & ~rising_ref;
  wire lag = ~rising_xo & rising_ref;
  wire trig = rising_xo ^ rising_ref;
  wire dtrig = rising_xo & rising_ref;
  wire untrig = rising_xo | rising_ref;
  wire llrdy = (is10meg ? ~tsmp[2] : ~refsmp[2]) & ~xosmp[2];
  wire rhigh = is10meg ? tsmp[1] : refsmp[1];


  reg [5:0] pcnt;
  reg pcnt_ovfl;
  wire [5:0]  nxt_pcnt = (rising_r | pcnt_ovfl) ? 6'b0 : pcnt+1'b1;
  always @(posedge clk) begin
    pcnt <= nxt_pcnt;
    if (rcnt_ovfl)
      is10meg <= 1'b0;
    else if (pcnt == 6'b111111) begin
      pcnt_ovfl <= 1'b1;
      is10meg <= 1'b0;
    end
    else if (rising_r) begin
      is10meg <=  (pcnt > 6'd16) & (pcnt < 6'd24);
      pcnt_ovfl <= 1'b0;
    end
  end

  reg rr;
  assign nxt_rcnt = rr ? 28'b0 : rcnt+1'b1;
  always @(posedge clk) begin
    rr <= rising_ref;
    ple[3:0] <= {ple[2:0],rising_ref & valid_ref};

    rcnt <= nxt_rcnt;

    // set the overflow flag if no reference edge is detected and
    // hold it asserted until an edge does arrive. This allows clearing of
    // the other flags, even if there is no reference.
    if (rcnt==28'b1111111111111111111111111111)
      rcnt_ovfl <= 1'b1;
    else if (rr)
      rcnt_ovfl <= 1'b0;

    if (rr) begin
      // a rising edge arrived, grab the count and compare to bounds
      rlst <= rcnt;
    end
    if (rr | rcnt_ovfl) begin
      ispps <= ~is10meg & ~rcnt_ovfl & (rcnt > 28'd199997000) & (rcnt < 200003000);
      /* reference frequency detect limits:
       * 10M sampled with 200M should be 20 cycles, 16-24 provides xtra margin
       * to allow for tolerances and possibly sampling at jittery edges
       * allow +- 15 ppm on a pps signal
       */

    end
  end


  reg signed [27:0] coarse;
  reg [15:0] dacv = 16'd32767; // power-on default mid-scale
  wire signed [16:0] sdacv = { 1'b0, dacv};
  /* to exit coarse adjustment, the frequency error shall be small for
   * several cycles
   */
  reg esmall;
  reg [2:0] es;

  reg pr;


  /* The xo can be on-frequency while the rising edges are still
   * out-of-phase, so a phase detector is also required. The
   * counter "llcnt" accumulates how many ticks local pps leads
   * or lags the reference pps . The range of this counter
   * need not be as large as "rcnt". The count increments
   * or decrements based upon which signal has a rising edge first,
   * and the count is halted when the other rising edge occurs.
   * Both signals are required to transition back to the low state
   * to re-arm the detection state machine.
  */
  reg llcntena;
  reg lead_lagn;
  reg signed [11:0] llcnt, nxt_llcnt;
  wire signed [11:0] incr = lead_lagn ? -12'sd1 : 12'sd1; // -1 lead, +1 lag
  reg [3:0] llsmall;
  reg llovfl;

  reg [2:0] refs1, refs0;
  reg refchanged;
  reg refinternal;
  always @(posedge clk) begin
    refs1 <= { refs1[1:0], refsel[1] };
    refs0 <= { refs0[1:0], refsel[0] };
    refchanged <= { refs1[2], refs0[2] } != { refs1[1], refs0[1] };
    refinternal <=  refs1[2] ^ refs0[2]; // not gps or external

    // compute how far off the expected period we are
    if (ple[1]) begin
      rdiff <= srlst-29'd199999999;
    end

    // compute an adjustment for the dac
    if (ple[2]) begin
      // if rdiff is (+), the xo is fast
      // include a bit of gain for quick adjustment
      // an approximate gain was initially determined by 'theory' using
      // the xo tuning sensitivity, and was find-tuned 'by hand'
      // by observing the loop behaviour (with rdiff instrumented and
      // pps signals connected out to an oscilloscope).
      coarse <= sdacv - (rdiff <<< 3);
    end

    // determine when the period error is small
    if (ple[2] | rcnt_ovfl) begin
      es <= { es[1:0], (rdiff<29'sd8 && rdiff>-29'sd8) };
      esmall <= valid_ref & ~rcnt_ovfl & (es[2:0] == 3'b111);
    end
    else if (sstate==REFDET) begin
      es <= 3'b0;
      esmall <= 1'b0;
    end

    // assign the dac value when doing coarse-adjustment
    // in the fine-adjust phaase, the PI control filtering takes over
    if (ple[3] & (sstate==CFADJ)) begin
      dacv <= coarse[15:0];
    end
    else if (sstate==REFDET) begin
      dacv <= 16'd32767; // center the DAC
    end
  end


  always @(*) begin
    nxt_sstate=sstate;
    pr = 1'b0;
    nxt_lcnt = recycle ? 26'd0 : lcnt + 1'b1;
    case (sstate)
    REFDET: begin // determine reference type
      pr = 1'b0;
      if (valid_ref) nxt_sstate = CFADJ;
    end
    CFADJ: begin // coarse freqency adjustment
      pr = 1'b1;
      if (esmall) nxt_sstate = SLEDGEA;
    end
    SLEDGEA: begin // ensure local pps is low and wait for a ref edge
      pr = 1'b1; // preload the integrator
      if (rhigh) nxt_sstate = SLEDGEB;
    end
    SLEDGEB: begin // force local pps rising edge to match reference
      nxt_lcnt = 26'd0;
      pr = 1'b1; // preload the integrator
      if(rhigh) begin
        nxt_lcnt = 28'd149_999_998; // force rising edge in a couple cycles
        nxt_sstate = FINEADJ;
      end
    end
    FINEADJ: begin // wide-ish bandwidth PI control
      if (~valid_ref | llovfl) nxt_sstate = REFDET;
    end
    default: begin
      nxt_sstate = REFDET;
    end
    endcase
    // overriding conditions:
    if (refinternal | refchanged | rcnt_ovfl ) nxt_sstate = REFDET;
  end

  reg llsena;
  always @(posedge clk) begin
    llstate <= nxt_llstate;
    if (llcntena) llcnt <= nxt_llcnt;
    if (llstate==READY) lead_lagn <= lead;
    if (llsena) llsmall <= { (llsmall[2:0] == 3'b111), llsmall[1:0],
                                        (llcnt < 12'sd3)&(llcnt > -12'sd3)};
    if (llcntena)  llovfl <= (llcnt>12'sd1800) | (llcnt< -12'sd1800);
  end

  reg ppsfltena;
  always @(*) begin
    // values to hold by default:
    nxt_llstate = llstate;
    llcntena=1'b0;
    nxt_llcnt=llcnt;
    ppsfltena = 1'b0;
    llsena = 1'b0;

    case (llstate)
    READY: begin
      nxt_llcnt=12'b0;
      if (trig | dtrig) begin
        nxt_llstate = trig ? COUNT : DONE;
        llcntena=1'b1;
        // even if dtrig, set llcnt to 0 to feed the filter pipe
      end
    end
    COUNT: begin
      if (untrig) begin // the second edge arrived
        nxt_llstate = DONE;
      end
      else begin
        llcntena=1'b1;
        nxt_llcnt=llcnt+incr;
      end
    end
    DONE: begin
      nxt_llstate = WAIT;
      ppsfltena = 1'b1;
    end
    WAIT: begin
      if (llrdy) begin
        nxt_llstate = READY;
        llsena = 1'b1;
      end
    end
    endcase
    if (sstate==REFDET) begin
      nxt_llstate = READY;
      llcntena=1'b0;
      ppsfltena = 1'b0;
      llsena = 1'b0;
    end
  end


  reg[15:0] daco;

  reg [1:0] enchain=2'b00;
  always @(posedge clk) enchain <= { enchain[1:0], ppsfltena & (enchain==2'b00) };

  reg signed [23:0] integ;
  reg signed [23:0] prop;
  wire signed [23:0] nxt_integ = integ + (llcnt <<< 6);
  wire signed [23:0] nxt_prop = (llcnt <<< 7);
  wire signed [23:0] eff = integ + prop;
  wire urng = eff[23], orng = eff[23:22]==2'b01;
  reg erng;
  /* The values for proportional and integral gain terms were originally
   * estimated using a model that accounted for the xo tuning sensitivity.
   * When implemented, the loop dynamics observed differed significantly
   * from model results, probably as a result of the Xilinx PLL
   * (which was not modelled) being present in the loop. The gain values
   * were find-tuned 'by hand' by observing the loop behaviour (with llcnt
   * instrumented) and pps signals connected out to an oscilloscope).
   */

  always @(posedge clk) begin
    if (no_pps) begin
     daco <= dac_dflt;
    end
    else if (pr) begin
      integ <= { 2'b00, dacv, 6'b0 }; // precharge the accumulator
      daco <= dacv;
    end
    else begin
      if (enchain[0]) begin
        integ <= nxt_integ;
        prop <= nxt_prop;
      end
      if (enchain[1]) begin
        daco <= eff[21:6];
        erng <= urng | orng;
      end
    end
  end

  wire  fadj=   (sstate==FINEADJ);
  always @(posedge clk) begin
    reflck <= refinternal | fadj;
  end

  ad5662_auto_spi dac
  (
    .clk(clk),
    .dat(daco),
    .sclk(sclk),
    .mosi(mosi),
    .sync_n(sync_n)
  );

endmodule