← Work

FPGA · measurement · 2026

Propagation delay vs. die temperature on an FPGA

Two ring oscillators on an Artix-7, one routed through the 3.3 V I/O buffers and one kept inside the 1.0 V core, clock hardware counters while heaters built from the fabric itself warm the die and the XADC reads the temperature back. I built it for the CS 1410 FPGA labs at Harvard SEAS.

Board
Opal Kelly XEM7310-A75 (Artix-7 XC7A75T-1FGG484), USB 3.0, USB-powered
My part
RTL, constraints, FrontPanel interface, host acquisition and fitting software
Tools
SystemVerilog, Vivado 2022.2, Opal Kelly FrontPanel, Python (NumPy, Matplotlib)
Code
RTL on GitHub · the host software is kept private

What I was trying to build

Vivado’s timing analysis tells you whether a design closes at the process, voltage and temperature corners it models. It doesn’t show the delay of a particular chip moving as that chip heats up. For the CS 1410 labs I wanted students to measure that shift themselves, on the board on their desk, with nothing but the FPGA, a USB cable and a laptop. So the delay sensor, the heater and the thermometer all had to come from the FPGA itself.

The other constraint shaped most of the design: the lab boards run from USB power alone, with no external supply. Whatever heats the die has to stay inside what a laptop port can deliver, or the board browns out in the middle of a measurement.

System architecture

The host talks to the FPGA through Opal Kelly’s FrontPanel endpoints: three WireIns for control, one TriggerIn to start a measurement, and eight WireOuts for counts, telemetry and status. Everything below the endpoint bus in the diagram is my RTL.

Thermolab FPGA block diagramA host PC talks over USB 3.0 to FrontPanel endpoints on the FPGA. Two ring oscillators, one through 21 I/O buffers and one through 33 core LUTs, clock two 32-bit counters that share a measurement window. An XADC monitor reports die temperature and VCCINT and gates the heater. A heater controller with duty PWM, soft-start and a VCCINT governor gates three heater regions, each clocked at 403.2 MHz from an MMCM.Host PCPythonUSB 3.0Artix-7 XC7A75T on an Opal Kelly XEM7310-A75FrontPanel endpointsWireIn 0x00–0x02 · TriggerIn 0x40 · WireOut 0x20–0x27Two 32-bit countersring-clocked, shared okClk windowstart, lengthcountsI/O ring21 stagesIOBUF, 3.3 V bankLUT ring33 stagescore fabric, 1.0 VBUFGBUFGXADC monitordie temp · VCCINTtrip 85 °C · clear 60 °CtelemetryHeater controlselect · duty PWMsoft-start · governorregion, dutyheat_okVCCINTABCgateMMCM100.8 → 403.2 MHz403.2 MHzeach region: 9,000 LUT+FF toggle cells, 40 DSP48 MACCs,and its own BUFGCE; one region runs at a time
Figure 1. Top level of thermolab_top.sv (v2.00). The two rings are the sensors; the heater regions, MMCM and XADC monitor exist to change and observe the die temperature. Every block is an instance in the RTL.

How a ring oscillator measures delay

A loop with an odd number of inverting stages has no stable state, so it oscillates. A transition has to go around the loop twice per period, so the period is twice the sum of the stage delays. If every stage gets slower as the die warms, the frequency drops. Counting ring cycles over a fixed window gives that frequency, and its inverse is a delay proxy:

f = N / T_window T = 1 / f = 2 · Σ t_stage Δd (%) = 100 · (T / T_ref − 1)

Here N is the number of ring cycles counted, T_window the window length, and T_ref the mean period of the coolest 5 % of samples. The last line is what the host fits against temperature. Normalizing to each board’s own coolest samples removes the absolute frequency, which varies from board to board with process and routing, and leaves the relative change. It also means I never claim the ring period is a single gate’s delay: it’s the whole loop, including routing.

Two rings in two voltage domains

The sensor ring runs through the I/O. Each of its 21 stages is a LUT driving an IOBUF whose output buffer is permanently enabled: the output driver drives the pad, the input buffer reads the same pad straight back, and that feeds the next LUT. The pads are on the MC1 expansion connector (bank 13, 3.3 V) with nothing plugged in, so the loop needs no external wiring. The control ring is the same idea with 33 LUT stages and no pads, so it never leaves the core.

Ring oscillator stagesTop: the I/O ring. An inverting LUT2 gate and twenty LUT1 inverters each drive an IOBUF; the output buffer drives a package pad and the input buffer reads the pad back into the next stage, 21 stages in all. Bottom: the LUT ring, the same gate followed by 32 LUT1 inverters inside the fabric, 33 stages.I/O ring · 21 inversions · ring_io.svenableLUT2gatepadLUT1inv 1LUT1inv 2···LUT1inv 20ring_out → BUFG → counterLUT ring · 33 inversions · ring_lut.svenableLUT2gateLUT1inv 1LUT1inv 2···LUT1inv 32no pads: the loop stays in the1.0 V fabric (control channel)
Figure 2. ring_io.sv and ring_lut.sv. Both loops are closed by the same inverting LUT2 gate (O = ~I0 & enable), so disabling a ring parks it at 0. All cells are DONT_TOUCH so synthesis can’t simplify the loop away.

The reason for two rings is device physics. Heating a transistor does two things that pull in opposite directions: carrier mobility drops, which slows it down, and the threshold voltage drops, which speeds it up. In the 28 nm core at its nominal 1.0 V the two partly cancel, so a LUT ring’s frequency moves less with temperature than you might expect. The thick-oxide I/O transistors run from 3.3 V, far above the point where the effects balance, so the mobility term wins and their delay rises steadily as the die warms.

The first version of this lab, in June, used only LUT rings (sixteen long LUT-inverter rings per board) and ran into exactly this. On one board every ring slowed by about 0.044 %/°C with R² of 0.97 or better. The same experiment on three more boards gave mean coefficients of 0.011, 0.004 and 0.003 %/°C, with weak fits and single rings going the wrong way:

Board (June prototype, 16 LUT rings)Mean delay coefficientRing-to-ring rangeR² range
2538001HWN+0.044 %/°C+0.037 to +0.0470.97–0.99
2437001CVS+0.011 %/°C+0.005 to +0.0140.63–0.96
2437001CZF+0.004 %/°C−0.002 to +0.0060.32–0.79
2538001HXW+0.003 %/°C−0.002 to +0.0070.02–0.68

Those runs were read during heat-up with the heater running, so some of the spread may be supply droop rather than temperature. Either way, a lab where three boards out of four show almost nothing can’t teach the effect, so I moved the sensor out to the I/O. The cost is 21 package pins that must stay unconnected.

The LUT ring shares the window and counting logic with the I/O ring, which makes it a control. If both channels moved by the same amount, I’d suspect the measurement chain before the silicon.

Counting across clock domains

Each ring’s output goes onto a global clock buffer and clocks its own 32-bit counter. The measurement window comes from the FrontPanel clock (okClk, 100.8 MHz from the board’s crystal), which doesn’t care what the die temperature is. That window has to cross into each ring’s clock domain, and the count has to cross back.

freq_counter.svlines 25–40
(* ASYNC_REG = "TRUE" *) reg [1:0] wsync = 2'd0;
reg        wprev = 1'b0;
reg [31:0] cnt   = 32'd0;

always @(posedge clk_ring) begin
    wsync <= {wsync[0], window_ok};
    wprev <= wsync[1];
    if (wsync[1] && !wprev)
        cnt <= 32'd1;             // first counted cycle of the window
    else if (wsync[1])
        cnt <= cnt + 32'd1;
end

always @(posedge okClk)
    if (cap_strobe)
        value <= cnt;             // quasi-static at this moment
The window is double-flopped into the ring domain, and the counter restarts on its rising edge. Capture happens in the okClk domain. Source: freq_counter.sv.

The capture side doesn’t use a handshake or a FIFO. When the window ends, the top level waits 512 okClk cycles (about 5 µs) before pulsing cap_strobe. By then the ring-domain counter has stopped changing, so sampling it from the other domain is safe. The synchronizer costs a cycle or two of uncertainty at each window edge, which is small next to the hundreds of thousands of cycles counted in a 100 ms window.

Measurement window timingA host trigger starts the window. busy stays high for the programmed number of okClk cycles. The window is synchronized into each ring's clock domain, where the counter restarts at 1 and counts ring cycles while the window is high. After busy falls, the controller waits 512 okClk cycles, about 5 microseconds, then pulses cap_strobe, copies the now-static count into the okClk domain, and sets done.trig[0]okClk · TriggerIn 0x40busyokClk · windowwsync[1]ring clockcntring clock · 32-bitold1, 2, 3, … counting ring edgesN (static)cap_strobeokClkvalueokClk · WireOut 0x20/0x21previous resultNwindow: length × okClk (10 ms default, 100 ms from the host)settle: 512 okClk ≈ 5.1 µsnot to scale
Figure 3. One measurement. The window length comes from WireIn 0x02 (zero selects the RTL default of 1,008,000 cycles, 10 ms); the host normally asks for 100 ms. done rises with the capture and the counts are read from WireOut 0x20 and 0x21.

Heating the die from a USB port

The heater is ordinary logic switching as fast as it can. There are three regions. Each has 9,000 toggle cells (a LUT1 inverter feeding its own flip-flop, so every cell flips every cycle) and 40 DSP48 multiply-accumulates fed from an LFSR so the multipliers actually switch. An MMCM multiplies okClk up to 403.2 MHz, and each region gets that clock through its own BUFGCE, so an idle region’s clock tree is off rather than just its logic.

Only one region runs at a time. Pblocks place the three regions in different parts of the die so that a run can heat close to the I/O ring, close to the XADC, or away from both. The power control sits in front of the clock gates:

  • Duty. The host requests 0–255. An 8-bit PWM at about 98 kHz gates the region clock, fast enough that the board’s decoupling sees an average load rather than steps.
  • Soft start. The applied duty moves one step every 1.3 ms toward the request, so going from off to full takes about a third of a second.
  • VCCINT governor. The XADC measures the 1.0 V core supply. Below 0.970 V the governor lowers its duty ceiling by 16 steps every 10.4 ms. Below 0.938 V it drops the duty to zero at once. Recovery is one step per ~42 ms, about 10 s from zero to full.
heater.svlines 132–145
always_ff @(posedge okClk) begin
    if (!heat_ok || vpanic) begin
        cap_q       <= 8'd0;            // hard dump; recover from zero
        healthy_cnt <= '0;
    end else if (gstep) begin
        if (vlow_now) begin
            cap_q       <= (cap_q > 8'd16) ? cap_q - 8'd16 : 8'd0;
            healthy_cnt <= '0;
        end else if (cap_q != 8'hFF) begin
            healthy_cnt <= healthy_cnt + 1'b1;
            if (healthy_cnt == 2'b11) cap_q <= cap_q + 8'd1;
        end
    end
end
The governor. gstep fires every 2²⁰ okClk cycles (10.4 ms). A weak port gives a lower temperature plateau instead of a reset. Source: heater.sv.

Telemetry and interlocks

The XADC is instantiated through the XADC Wizard with its DRP port exposed. A small state machine reads temperature and VCCINT alternately, one read every 0.65 ms, with a watchdog that abandons a read if DRDY never comes back. It also counts end-of-conversion pulses and completed DRP reads and reports both to the host, because a frozen ADC otherwise looks exactly like a stable temperature.

The heater only runs when all of these hold:

  • the MMCM is locked;
  • the XADC has finished a conversion within the last ~125 ms;
  • the XADC’s own over-temperature alarm is clear;
  • a fabric comparator on the temperature code hasn’t tripped. It sets at 85 °C and clears below 60 °C.

The host adds its own limits on top: it heats toward 78 °C by default and aborts above 82 °C. Conversions use the 7-series transfer functions, T = code × 503.975 / 4096 − 273.15 and V = code × 3 / 4096.

Running an experiment

The host program configures the board, checks the design signature (0x544C0200) so it can’t talk to a mismatched bitstream, and then runs one of three protocols. The default heats one region and records the cooldown:

  1. Take ten 100 ms windows at ambient as a baseline.
  2. Step the duty up until the die reaches the target or stops warming.
  3. Turn the heater off and keep sampling as the die cools.
  4. Fit the delay change against temperature using the baseline and cooldown samples.

I fit the cooldown rather than the heat-up on purpose. While the heater runs, it loads VCCINT and keeps a steep temperature gradient between the active region and the rest of the die. When it turns off, that gradient collapses within seconds and the board then cools slowly, with no switching load on the supply. That is the clean part of the dataset. The other two modes soak at a staircase of duty levels (averaging several windows per step) or just monitor without heating to measure the noise floor. An option also pauses the heater during each window.

Each sample stores the temperature read just before and just after its window, VCCINT, both counts, the applied duty and the governor state. The fit reports the slope in %/°C, its standard error, R², and a rank correlation.

Results

The clearest run is from August 4, with v2.00 heating region C. The I/O ring ran at about 4.58 MHz and the LUT ring at about 38.5 MHz, which works out to an average of 5.2 ns per stage for the trip through the pad drivers and receivers and 0.39 ns per stage inside the fabric. Over the cooldown from 66 °C to 41 °C:

Figure 4. 252 cooldown samples, each a 100 ms window, on one XEM7310 heated by region C. Lines are least-squares fits. The heat-up samples are excluded from the fit.
Data table

Delay change in percent, normalized to the mean of the coolest 5 % of samples.

XADC die temperature (°C)I/O ring (21 pads)LUT ring (33 stages)
41.3−0.032−0.031
41.4−0.036−0.021
41.3−0.042−0.023
41.3−0.027−0.013
41.2−0.023−0.005
41.5−0.025−0.014
41.2−0.027−0.014
41.3−0.021−0.001
41.3−0.020−0.003
41.3−0.0140.005
65.80.9560.013
60.20.8150.143
58.60.7710.163
57.50.7320.168
56.70.7070.164
56.10.6820.167
55.80.6630.160
55.10.6460.152
54.90.6180.140
54.50.6160.156
54.20.6000.156
53.90.5900.156
53.80.5910.153
53.50.5700.139
53.20.5840.157
52.90.5640.147
53.00.5560.142
52.70.5620.161
52.40.5280.127
52.50.5260.145
52.10.5220.135
51.90.5190.146
51.90.5130.146
51.80.4970.132
51.70.4960.132
51.60.5020.149
51.30.4840.148
51.30.4930.144
51.00.4880.152
51.10.4690.136
50.80.4560.121
50.60.4500.119
50.60.4570.131
50.60.4670.137
50.60.4570.136
50.40.4590.143
50.20.4540.139
50.40.4560.139
50.10.4540.149
50.10.4440.143
50.00.4470.139
50.00.4450.142
50.00.4420.141
49.70.4290.134
49.70.4300.138
49.60.4200.133
49.60.4100.124
49.60.4280.139
49.40.4290.143
49.30.4140.124
49.50.4130.134
49.20.4110.144
49.20.4080.138
49.20.3980.133
49.10.3880.124
49.10.3920.124
48.80.4020.123
48.90.3810.125
49.00.3810.124
49.00.3760.120
48.80.3740.120
48.80.3710.126
48.70.3670.118
48.60.3580.106
48.70.3590.112
48.60.3590.122
48.50.3540.115
48.40.3450.104
48.30.3340.098
48.40.3420.112
48.40.3400.113
48.20.3340.096
48.20.3380.114
48.10.3290.096
48.20.3410.118
48.10.3260.093
47.90.3280.105
47.80.3210.094
47.90.3180.096
47.90.3100.083
47.80.3160.091
47.90.2990.081
47.80.2980.083
47.60.3010.081
47.70.3120.097
47.60.3000.092
47.50.2970.085
47.40.2980.091
47.60.3010.090
47.30.2920.088
47.30.3000.090
47.40.2960.089
47.40.2870.089
47.30.2800.074
47.40.2910.086
47.20.2860.088
47.30.2820.078
47.10.2880.093
47.00.2750.079
46.90.2780.087
47.00.2880.104
47.00.2800.090
46.90.2750.076
46.90.2750.085
46.90.2750.083
46.90.2700.091
46.80.2670.082
46.80.2700.092
46.80.2600.088
46.90.2610.088
46.80.2610.083
46.70.2610.078
46.60.2630.087
46.60.2620.082
46.60.2630.082
46.50.2560.075
46.30.2400.055
46.50.2360.053
46.40.2490.064
46.50.2380.064
46.30.2560.083
46.30.2390.071
46.10.2380.065
46.10.2350.060
46.10.2370.070
46.30.2160.054
46.00.2310.081
46.10.2440.077
46.20.2390.070
46.10.2310.068
46.00.2400.078
46.20.2320.073
46.20.2260.056
46.20.2330.083
46.00.2080.050
45.90.2190.051
46.00.2280.060
45.90.2200.061
45.80.2120.058
46.00.2190.071
45.80.2020.055
45.80.2100.064
45.70.2080.057
45.80.2070.049
45.70.2070.055
45.80.2010.045
45.60.2020.059
45.70.2000.047
45.80.1980.039
45.70.2000.056
45.50.1990.069
45.70.1970.054
45.50.1840.048
45.30.1810.041
45.70.1760.040
45.50.1850.050
45.50.1880.049
45.30.1850.049
45.50.1840.055
45.50.1880.060
45.30.1790.045
45.30.1810.052
45.30.1830.052
45.40.1830.057
45.40.1900.065
45.30.1730.051
45.30.1770.051
45.30.1650.050
45.30.1800.060
45.20.1830.054
45.00.1730.054
45.00.1790.059
45.00.1610.057
45.00.1750.067
45.00.1680.069
44.90.1570.060
45.00.1730.069
44.90.1600.061
45.00.1540.054
44.90.1470.046
45.00.1570.045
44.90.1650.049
45.00.1620.049
44.80.1580.039
44.90.1620.053
44.90.1700.069
44.90.1590.056
44.90.1600.059
44.80.1600.057
44.90.1700.058
44.80.1590.056
44.80.1560.053
44.70.1640.061
44.90.1730.079
44.90.1590.047
44.70.1680.051
44.70.1590.053
44.90.1500.040
44.50.1600.050
44.80.1540.055
44.70.1670.076
44.50.1590.061
44.70.1550.056
44.60.1540.063
44.50.1530.053
44.50.1480.056
44.60.1470.053
44.60.1440.053
44.80.1590.058
44.70.1490.045
44.40.1580.050
44.60.1420.047
44.50.1490.057
44.50.1510.051
44.50.1550.060
44.50.1720.090
44.50.1460.059
44.40.1550.064
44.50.1490.056
44.40.1540.066
44.50.1520.068
44.40.1470.059
44.50.1480.065
44.40.1560.078
44.30.1400.048
44.20.1380.054
44.40.1490.063
44.40.1390.062
44.40.1340.057
44.30.1400.060
44.40.1530.076
44.40.1330.056
44.40.1400.071
44.20.1240.051
44.40.1400.063
44.20.1380.075
44.20.1280.049
44.20.1430.071
44.40.1380.057
44.20.1360.060
44.30.1380.066
44.20.1510.076

The I/O ring slowed by 0.047 % per °C and tracked temperature closely. The LUT ring, measured through the same window and counters, moved a fifth as much and with much more scatter. That gap is the whole point of the two-ring design. The other complete runs agree:

RunSamplesSpan (°C)I/O ring (%/°C)R²LUT ring (%/°C)R²
Jul 15, earlier build, 7-pad ring23140–76+0.0440.97+0.0080.56
Aug 4, v2.00, heater C25241–66+0.0470.98+0.0100.66
Sep 2, v2.00, heater B9148–66+0.0420.99+0.0020.08

The I/O coefficient lands between 0.042 and 0.047 %/°C in every run, across two ring lengths. The LUT ring’s coefficient never gets above 0.010 %/°C and its fit is weak, which is what a ring near its zero-temperature-coefficient point should look like. A fourth run, heating region A, stopped before the cooldown had any span, so it isn’t included. The runs don’t record the board serial, so I can’t say whether they were on the same board.

The supply held: across the three v2.00 runs VCCINT never dropped below 0.988 V and the governor’s low-voltage flag never set.

Why the fit uses only the cooldown

The heat-up half of the same run shows what the heater does to the measurement. The LUT ring’s delay jumps every time the duty steps up, by about 0.02 % per duty count, reaching about 5 % at full duty. That is a hundred times its temperature coefficient, and it disappears within one sample when the heater turns off, long before the die can cool.

Figure 5. Every sample of the run, about one per second. Hairlines mark the requested duty steps and the moment the heater turned off. The LUT ring's steps track the heater, not the temperature, which kept rising smoothly through each step.
Data table

Time in seconds; delay change in percent.

Time since the first window (s)I/O ringLUT ring
0.0−0.03−0.03
0.1−0.04−0.02
0.2−0.04−0.02
0.3−0.03−0.01
0.4−0.02−0.01
0.5−0.03−0.01
0.6−0.03−0.01
0.8−0.020.00
0.9−0.020.00
1.0−0.010.01
1.1−0.010.02
2.20.100.51
3.30.231.03
4.40.291.23
5.50.301.23
6.60.301.22
7.70.321.23
8.80.321.22
9.90.321.22
11.10.331.23
12.20.331.22
13.30.341.23
14.40.331.21
15.50.341.23
16.60.341.21
17.70.351.22
18.80.351.23
19.90.351.22
21.00.351.22
22.10.512.03
23.30.652.47
24.40.672.47
25.50.672.47
26.60.672.45
27.70.682.46
28.80.692.46
29.90.702.47
31.00.712.47
32.10.722.49
33.20.722.47
34.40.722.47
35.50.722.47
36.60.732.46
37.70.732.47
38.80.742.48
39.90.752.48
41.00.742.47
42.10.732.46
43.20.913.32
44.31.033.76
45.41.053.75
46.61.063.75
47.71.073.75
48.81.083.75
49.91.083.75
51.01.123.78
52.11.123.76
53.21.143.79
54.31.143.79
55.41.143.77
56.51.163.78
57.71.173.79
58.81.173.79
59.91.173.79
61.01.163.77
62.11.173.79
63.21.173.79
64.31.344.63
65.41.475.08
66.51.495.09
67.71.505.09
68.81.515.07
69.91.535.09
71.01.525.08
72.11.545.08
73.21.555.09
74.31.545.08
75.41.575.10
76.51.575.09
77.61.595.10
78.71.605.10
79.91.605.11
81.01.615.11
82.11.625.11
83.21.625.10
84.31.625.11
85.41.645.13
86.51.635.11
87.61.645.12
88.71.645.12
89.81.665.12
90.91.645.10
92.11.655.12
93.21.665.12
94.31.685.13
95.41.685.14
96.51.695.15
97.61.695.14
98.71.695.14
99.81.705.15
100.91.705.13
102.01.715.15
103.21.725.16
104.31.715.13
105.41.725.14
106.51.735.15
107.61.745.15
108.71.745.16
109.81.755.16
110.91.755.16
112.01.745.16
113.11.755.14
114.31.765.16
115.41.765.16
116.51.755.14
117.61.775.16
118.71.775.16
119.81.775.16
120.91.785.17
122.01.785.17
123.11.785.17
124.21.785.16
125.31.805.17
126.41.815.18
127.51.805.17
128.71.805.18
129.81.805.17
130.91.815.17
132.01.805.16
133.11.805.17
134.21.835.18
135.31.845.19
136.41.835.18
137.51.825.18
138.61.825.17
139.81.845.19
140.91.855.21
142.01.845.18
143.11.855.18
144.21.855.18
145.31.855.18
146.41.855.17
147.51.865.20
148.61.875.21
149.71.885.22
150.81.875.20
151.91.865.18
153.11.875.19
154.21.875.20
155.31.885.20
156.41.885.20
157.51.885.21
158.61.875.20
159.71.875.19
160.81.895.22
161.91.895.21
163.01.895.22
164.11.915.24
165.21.905.23
166.41.925.23
167.51.915.23
168.61.915.22
169.71.915.22
170.81.915.22
171.91.915.22
173.01.905.21
174.11.925.22
175.21.925.22
176.31.925.22
177.51.945.25
178.61.935.23
179.71.935.22
180.81.945.23
181.91.945.22
183.01.955.23
184.11.935.21
185.21.935.21
186.31.945.21
187.41.955.22
188.51.935.21
189.71.935.21
190.81.945.22
191.91.935.20
193.01.945.21
194.11.965.23
195.21.945.21
196.31.955.20
197.41.955.19
198.51.975.22
199.61.975.21
200.71.965.22
201.81.985.23
203.01.975.23
204.11.975.22
205.21.985.22
206.31.985.24
207.41.985.23
207.50.960.01
208.60.820.14
209.70.770.16
210.80.730.17
211.90.710.16
213.00.680.17
214.10.660.16
215.30.650.15
216.40.620.14
217.50.620.16
218.60.600.16
219.70.590.16
220.80.590.15
221.90.570.14
223.00.580.16
224.10.560.15
225.30.560.14
226.40.560.16
227.50.530.13
228.60.530.15
229.70.520.13
230.80.520.15
231.90.510.15
233.00.500.13
234.10.500.13
235.20.500.15
236.40.480.15
237.50.490.14
238.60.490.15
239.70.470.14
240.80.460.12
241.90.450.12
243.00.460.13
244.10.470.14
245.20.460.14
246.30.460.14
247.40.450.14
248.60.460.14
249.70.450.15
250.80.440.14
251.90.450.14
253.00.450.14
254.10.440.14
255.20.430.13
256.30.430.14
257.40.420.13
258.50.410.12
259.60.430.14
260.70.430.14
261.80.410.12
263.00.410.13
264.10.410.14
265.20.410.14
266.30.400.13
267.40.390.12
268.50.390.12
269.60.400.12
270.70.380.13
271.80.380.12
272.90.380.12
274.00.370.12
275.10.370.13
276.30.370.12
277.40.360.11
278.50.360.11
279.60.360.12
280.70.350.12
281.80.350.10
282.90.330.10
284.00.340.11
285.10.340.11
286.20.330.10
287.30.340.11
288.40.330.10
289.60.340.12
290.70.330.09
291.80.330.11
292.90.320.09
294.00.320.10
295.10.310.08
296.20.320.09
297.30.300.08
298.40.300.08
299.50.300.08
300.70.310.10
301.80.300.09
302.90.300.09
304.00.300.09
305.10.300.09
306.20.290.09
307.30.300.09
308.40.300.09
309.50.290.09
310.60.280.07
311.70.290.09
312.90.290.09
314.00.280.08
315.10.290.09
316.20.280.08
317.30.280.09
318.40.290.10
319.50.280.09
320.60.280.08
321.70.280.09
322.80.280.08
323.90.270.09
325.10.270.08
326.20.270.09
327.30.260.09
328.40.260.09
329.50.260.08
330.60.260.08
331.70.260.09
332.80.260.08
333.90.260.08
335.00.260.08
336.10.240.06
337.20.240.05
338.40.250.06
339.50.240.06
340.60.260.08
341.70.240.07
342.80.240.07
343.90.240.06
345.00.240.07
346.10.220.05
347.20.230.08
348.30.240.08
349.40.240.07
350.50.230.07
351.70.240.08
352.80.230.07
353.90.230.06
355.00.230.08
356.10.210.05
357.20.220.05
358.30.230.06
359.40.220.06
360.50.210.06
361.60.220.07
362.70.200.06
363.90.210.06
365.00.210.06
366.10.210.05
367.20.210.06
368.30.200.05
369.40.200.06
370.50.200.05
371.60.200.04
372.70.200.06
373.80.200.07
375.00.200.05
376.10.180.05
377.20.180.04
378.30.180.04
379.40.190.05
380.50.190.05
381.60.190.05
382.70.180.06
383.80.190.06
384.90.180.05
386.10.180.05
387.20.180.05
388.30.180.06
389.40.190.07
390.50.170.05
391.60.180.05
392.70.170.05
393.80.180.06
394.90.180.05
396.00.170.05
397.20.180.06
398.30.160.06
399.40.180.07
400.50.170.07
401.60.160.06
402.70.170.07
403.80.160.06
404.90.150.05
406.00.150.05
407.20.160.05
408.30.170.05
409.40.160.05
410.50.160.04
411.60.160.05
412.70.170.07
413.80.160.06
414.90.160.06
416.00.160.06
417.10.170.06
418.20.160.06
419.40.160.05
420.50.160.06
421.60.170.08
422.70.160.05
423.80.170.05
424.90.160.05
426.00.150.04
427.10.160.05
428.20.150.06
429.30.170.08
430.50.160.06
431.60.160.06
432.70.150.06
433.80.150.05
434.90.150.06
436.00.150.05
437.10.140.05
438.20.160.06
439.30.150.05
440.50.160.05
441.60.140.05
442.70.150.06
443.80.150.05
444.90.160.06
446.00.170.09
447.10.150.06
448.20.160.06
449.30.150.06
450.40.150.07
451.50.150.07
452.60.150.06
453.70.150.07
454.90.160.08
456.00.140.05
457.10.140.05
458.20.150.06
459.30.140.06
460.40.130.06
461.50.140.06
462.60.150.08
463.70.130.06
464.80.140.07
465.90.120.05
467.10.140.06
468.20.140.08
469.30.130.05
470.40.140.07
471.50.140.06
472.60.140.06
473.70.140.07
474.80.150.08

The XADC’s VCCINT reading moved by only about 5 mV through all of this, so whatever slows the ring is local to the fabric or faster than the XADC’s averaged reading. I don’t know which yet. The I/O ring shows a smaller version of the same steps, probably because each of its stages also has a LUT in the core. Both effects are gone in the cooldown, which is why the fit uses only those samples and why the lab has an option to pause the heater during each window.

Where the noise is

At a 100 ms window one count of the I/O ring is 2.2 ppm of its frequency, but the scatter about the fit is about 200 ppm, or roughly 0.4 °C of equivalent temperature. So the counter isn’t the limit. The XADC reads temperature in steps of 0.12 °C, and it measures the die at its own location rather than at the pads, so the thermometer and its distance from the sensor are the more likely sources.

Implementation

The September build of v2.00 placed and routed on the XC7A75T with the heater regions taking most of the fabric:

ResourceUsedAvailableUtilization
Slice LUTs30,79747,20065.3 %
Slice registers34,15194,40036.2 %
DSP48E112018066.7 %
BUFGCTRL73221.9 %
XADC11100 %

Timing closed with +0.208 ns worst setup slack and +0.068 ns worst hold slack across 3,175 endpoints, with the ring clocks constrained at 50 MHz for analysis. The methodology report still flags two critical warnings about clock definitions and a handful of unconstrained endpoints, so positive slack here doesn’t mean every path is covered. Vivado’s vectorless power estimate for the routed design is 2.3 W on-chip, which it marks as low confidence.

Testing the lab’s boards

Before any of this could run in a lab section, the boards had to work. I wrote a go/no-go design and a test station for the lab’s XEM7310s. The test bitstream echoes a WireIn back on a WireOut, runs a free-running counter on a second WireOut, and drives the eight LEDs; a second bitstream reads the XADC. The station, a small Tk app in Python, programs each board it finds, checks that the counter advances between two reads, walks ten patterns through the loopback, asks the operator to confirm the LEDs, checks that the die temperature is plausible, and logs every result against the board’s serial number. On the Windows lab machines, a PowerShell script attaches each board to WSL through usbipd as it’s plugged in.

Over June 23 and 25 it tested 60 boards, and all 60 passed. It doesn’t test the on-board DDR3, the expansion connectors, the transceivers or FrontPanel pipes, and the report I wrote for the lab lists those gaps. The loopback design and its Python check are on GitHub.