aboutsummaryrefslogtreecommitdiffstats
path: root/fpga/usrp3/top/e31x/ppsloop.v
diff options
context:
space:
mode:
Diffstat (limited to 'fpga/usrp3/top/e31x/ppsloop.v')
-rw-r--r--fpga/usrp3/top/e31x/ppsloop.v415
1 files changed, 415 insertions, 0 deletions
diff --git a/fpga/usrp3/top/e31x/ppsloop.v b/fpga/usrp3/top/e31x/ppsloop.v
new file mode 100644
index 000000000..ea720a373
--- /dev/null
+++ b/fpga/usrp3/top/e31x/ppsloop.v
@@ -0,0 +1,415 @@
+//
+// Copyright 2015 Ettus Research
+//
+
+`timescale 1ns / 1ps
+
+
+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