1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 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
|