diff options
Diffstat (limited to 'fpga/usrp3/top/e31x/ppsloop.v')
-rw-r--r-- | fpga/usrp3/top/e31x/ppsloop.v | 415 |
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 |