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
|