aboutsummaryrefslogtreecommitdiffstats
path: root/mpm/python/usrp_mpm/dboard_manager/rh_init.py
blob: 20a06d788f94f2fd0b1c7aa68106d5d8ef9ae89c (plain)
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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
#
# Copyright 2018 Ettus Research, a National Instruments Company
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
Helper class to initialize a Rhodium daughterboard
"""

from __future__ import print_function
import time
from usrp_mpm.sys_utils.uio import UIO
from usrp_mpm.dboard_manager.lmk_rh import LMK04828Rh
from usrp_mpm.dboard_manager.rh_periphs import DboardClockControl
from usrp_mpm.cores import ClockSynchronizer
from usrp_mpm.cores import nijesdcore
from usrp_mpm.cores.eyescan import EyeScanTool
from usrp_mpm.dboard_manager.gain_rh import GainTableRh


class RhodiumInitManager(object):
    """
    Helper class: Holds all the logic to initialize an N320/N321 (Rhodium)
    daughterboard.
    """
    # The Phase DAC is set at midscale, having its flatness validate +/- 1023 codes
    # from this initial value.
    INIT_PHASE_DAC_WORD = 32768
    PHASE_DAC_SPI_ADDR  = 0x3
    # External PPS pipeline delay from the PPS captured at the FPGA to TDC input,
    # in reference clock ticks
    EXT_PPS_DELAY = 5
    # Variable PPS delay before the RP/SP pulsers begin. Fixed value for the N3xx devices.
    N3XX_INT_PPS_DELAY = 4
    # JESD core default configuration.
    JESD_DEFAULT_ARGS = {"lmfc_divider"   : 12,
                         "rx_sysref_delay": 5,
                         "tx_sysref_delay": 11,
                         "tx_driver_swing": 0b1101,
                         "tx_precursor"   : 0b00100,
                         "tx_postcursor"  : 0b00100}
    # After testing the roundtrip latency (i.e. FPGA -> TX -> RX -> FPGA),
    # it was found that a different value of RX SYSREF delay is required
    # for sampling_clock_rate = 400 MSPS to achieve latency consistency.
    RX_SYSREF_DLY_DIC = {400e6: 6, 491.52e6: 5, 500e6: 5}


    def __init__(self, rh_class, spi_ifaces):
        self.rh_class = rh_class
        self._spi_ifaces = spi_ifaces
        self.adc = rh_class.adc
        self.dac = rh_class.dac
        self.slot_idx = rh_class.slot_idx
        self.log = rh_class.log.getChild('init')


    def _init_lmk(self, lmk_spi, ref_clk_freq, sampling_clock_rate,
                  pdac_spi, init_phase_dac_word, phase_dac_spi_addr):
        """
        Sets the phase DAC to initial value, and then brings up the LMK
        according to the selected ref clock frequency.
        Will throw if something fails.
        """
        self.log.trace("Initializing Phase DAC to d{}.".format(
            init_phase_dac_word
        ))
        pdac_spi.poke16(phase_dac_spi_addr, init_phase_dac_word)
        return LMK04828Rh(self.slot_idx, lmk_spi, ref_clk_freq, sampling_clock_rate, self.log)


    def _sync_db_clock(self, dboard_ctrl_regs, ref_clk_freq, master_clock_rate, args):
        " Synchronizes the DB clock to the common reference "
        reg_offset = 0x200
        ext_pps_delay = self.EXT_PPS_DELAY
        if args.get('time_source', self.rh_class.default_time_source) == 'sfp0':
            reg_offset = 0x400
            ref_clk_freq = 62.5e6
            ext_pps_delay = 1 # only 1 flop between the WR core output and the TDC input
        synchronizer = ClockSynchronizer(
            dboard_ctrl_regs,
            self.rh_class.lmk,
            self._spi_ifaces['phase_dac'],
            reg_offset,
            master_clock_rate,
            ref_clk_freq,
            1.116E-12, # fine phase shift. TODO don't hardcode. This should live in the EEPROM
            self.INIT_PHASE_DAC_WORD,
            self.PHASE_DAC_SPI_ADDR,
            ext_pps_delay,
            self.N3XX_INT_PPS_DELAY,
            self.slot_idx)
        # The radio clock traces on the motherboard are 69 ps longer for Daughterboard B
        # than Daughterboard A. We want both of these clocks to align at the converters
        # on each board, so adjust the target value for DB B. This is an N3xx series
        # peculiarity and will not apply to other motherboards.
        trace_delay_offset = {0:  0.0e-0,
                              1: 69.0e-12}[self.slot_idx]
        offset_error = abs(synchronizer.run(
            num_meas=[512, 128],
            target_offset=trace_delay_offset))
        if offset_error > 100e-12:
            self.log.error("Clock synchronizer measured an offset of {:.1f} ps!".format(
                offset_error*1e12
            ))
            self.log.warning("RuntimeError is not being thrown for Rhodium Rev. A")
            # raise RuntimeError("Clock synchronizer measured an offset of {:.1f} ps!".format(
            #     offset_error*1e12
            # ))
        else:
            self.log.debug("Residual synchronization error: {:.1f} ps.".format(
                offset_error*1e12
            ))
        synchronizer = None
        self.log.debug("Sample Clock Synchronization Complete!")


    def set_jesd_rate(self, jesdcore, new_rate, current_jesd_rate, force=False):
        """
        Make the QPLL and GTX changes required to change the JESD204B core rate.
        """
        # The core is directly compiled for 500 MHz sample rate, which
        # corresponds to a lane rate of 5.0 Gbps. The same QPLL and GTX settings apply
        # for the 491.52 MHz sample rate.
        #
        # The lower non-LTE rate, 400 MHz, requires changes to the default configuration
        # of the MGT components. This function performs the required changes in the
        # following order (as recommended by UG476).
        #
        # 1) Modify any QPLL settings.
        # 2) Perform the QPLL reset routine by pulsing reset then waiting for lock.
        # 3) Modify any GTX settings.
        # 4) Perform the GTX reset routine by pulsing reset and waiting for reset done.

        assert new_rate in (4000e6, 4915.2e6, 5000e6)

        # On first run, we have no idea how the FPGA is configured... so let's force an
        # update to our rate.
        force = force or (current_jesd_rate is None)

        skip_drp = False
        if not force:
            #           Current     New       Skip?
            skip_drp = {4000.0e6 : {4000.0e6: True , 4915.2e6: False, 5000.0e6: False},
                        4915.2e6 : {4000.0e6: False, 4915.2e6: True , 5000.0e6: True },
                        5000.0e6 : {4000.0e6: False, 4915.2e6: True , 5000.0e6: True }}[current_jesd_rate][new_rate]

        if skip_drp:
            self.log.trace("Current lane rate is compatible with the new rate. Skipping "
                           "reconfiguration.")

        # These are the only registers in the QPLL and GTX that change based on the
        # selected line rate. The MGT wizard IP was generated for each of the rates and
        # reference clock frequencies and then diffed to create this table.
        QPLL_CFG         = {4000.0e6: 0x6801C1, 4915.2e6: 0x680181, 5000.0e6: 0x0680181}[new_rate]
        MGT_RX_CLK25_DIV = {4000.0e6:        8, 4915.2e6:       10, 5000.0e6:        10}[new_rate]
        MGT_TX_CLK25_DIV = {4000.0e6:        8, 4915.2e6:       10, 5000.0e6:        10}[new_rate]

        # 1-2) Do the QPLL first
        if not skip_drp:
            self.log.trace("Changing QPLL settings to support {} Gbps".format(new_rate/1e9))
            jesdcore.set_drp_target('qpll', 0)
            # QPLL_CONFIG is spread across two regs: 0x32 (dedicated) and 0x33 (shared)
            reg_x32 = QPLL_CFG & 0xFFFF # [16:0] -> [16:0]
            reg_x33 = jesdcore.drp_access(rd=True, addr=0x33)
            reg_x33 = (reg_x33 & 0xF800) | ((QPLL_CFG >> 16) & 0x7FF)  # [26:16] -> [11:0]
            jesdcore.drp_access(rd=False, addr=0x32, wr_data=reg_x32)
            jesdcore.drp_access(rd=False, addr=0x33, wr_data=reg_x33)

        # Run the QPLL reset sequence and prep the MGTs for modification.
        jesdcore.init()

        # 3-4) And the 4 MGTs second
        if not skip_drp:
            self.log.trace("Changing MGT settings to support {} Gbps"
                           .format(new_rate/1e9))
            for lane in range(4):
                jesdcore.set_drp_target('mgt', lane)
                # MGT_RX_CLK25_DIV is embedded with others in 0x11. The
                # encoding for the DRP register value is one less than the
                # desired value.
                reg_x11 = jesdcore.drp_access(rd=True, addr=0x11)
                reg_x11 = (reg_x11 & 0xF83F) | \
                          ((MGT_RX_CLK25_DIV-1 & 0x1F) << 6) # [10:6]
                jesdcore.drp_access(rd=False, addr=0x11, wr_data=reg_x11)
                # MGT_TX_CLK25_DIV is embedded with others in 0x6A. The
                # encoding for the DRP register value is one less than the
                # desired value.
                reg_x6a = jesdcore.drp_access(rd=True, addr=0x6A)
                reg_x6a = (reg_x6a & 0xFFE0) | (MGT_TX_CLK25_DIV-1 & 0x1F) # [4:0]
                jesdcore.drp_access(rd=False, addr=0x6A, wr_data=reg_x6a)
            self.log.trace("GTX settings changed to support {} Gbps"
                           .format(new_rate/1e9))
            jesdcore.disable_drp_target()

        self.log.trace("JESD204b Lane Rate set to {} Gbps!"
                       .format(new_rate/1e9))
        return new_rate


    def init_jesd(self, jesdcore, sampling_clock_rate):
        """
        Bringup the JESD links between the ADC, DAC, and the FPGA.
        All clocks must be set up and stable before starting this routine.
        """
        jesdcore.check_core()

        # JESD Lane Rate only depends on the sampling_clock_rate selection, since all
        # other link parameters (LMFS,N) remain constant.
        L = 4
        M = 2
        F = 1
        S = 1
        N = 16
        new_rate = sampling_clock_rate * M * N * (10.0/8) / L / S
        self.log.trace("Calculated JESD204B lane rate is {} Gbps".format(new_rate/1e9))
        self.rh_class.current_jesd_rate = \
            self.set_jesd_rate(jesdcore, new_rate, self.rh_class.current_jesd_rate)

        self.log.trace("Setting up JESD204B TX blocks.")
        jesdcore.init_framer()                # Initialize FPGA's framer.
        self.adc.init_framer()                # Initialize ADC's framer.

        self.log.trace("Enabling SYSREF capture blocks.")
        self.dac.enable_sysref_capture(True)  # Enable DAC's SYSREF capture.
        self.adc.enable_sysref_capture(True)  # Enable ADC's SYSREF capture.
        jesdcore.enable_lmfc(True)            # Enable FPGA's SYSREF capture.

        self.log.trace("Setting up JESD204B DAC RX block.")
        self.dac.init_deframer()              # Initialize DAC's deframer.

        self.log.trace("Sending SYSREF to all devices.")
        jesdcore.send_sysref_pulse()          # Send SYSREF to all devices.

        self.log.trace("Setting up JESD204B FPGA RX block.")
        jesdcore.init_deframer()              # Initialize FPGA's deframer.

        self.log.trace("Disabling SYSREF capture blocks.")
        self.dac.enable_sysref_capture(False) # Disable DAC's SYSREF capture.
        self.adc.enable_sysref_capture(False) # Disable ADC's SYSREF capture.
        jesdcore.enable_lmfc(False)           # Disable FPGA's SYSREF capture.

        time.sleep(0.100)                     # Allow time for CGS/ILA.

        self.log.trace("Verifying JESD204B link status.")
        error_flag = False
        if not jesdcore.get_framer_status():
            self.log.error("JESD204b FPGA Core Framer is not synced!")
            error_flag = True
        if not self.dac.check_deframer_status():
            self.log.error("DAC JESD204B Deframer is not synced!")
            error_flag = True
        if not self.adc.check_framer_status():
            self.log.error("ADC JESD204B Framer is not synced!")
            error_flag = True
        if not jesdcore.get_deframer_status():
            self.log.error("JESD204B FPGA Core Deframer is not synced!")
            error_flag = True
        if error_flag:
            raise RuntimeError('JESD204B Link Initialization Failed. See MPM logs for details.')
        self.log.info("JESD204B Link Initialization & Training Complete")


    def init(self, args):
        """
        Run the full initialization sequence. This will bring everything up
        from scratch: The LMK, JESD cores, the AD9695, the DAC37J82, and
        anything else that is clocking-related.
        Depending on the settings, this can take a fair amount of time.
        """
        # Input validation on RX margin tests (@ FPGA and DAC)
        # By accepting the rx_eyescan/tx_prbs argument being str or bool, one may
        # request an eyescan measurement to be performed from either the USRP's
        # shell (i.e. using --default-args) or from the host's MPM shell.
        perform_rx_eyescan = False
        if 'rx_eyescan' in args:
            perform_rx_eyescan = (args['rx_eyescan'] == 'True') or (args['rx_eyescan'] == True)
        if perform_rx_eyescan:
            self.log.trace("Adding RX eye scan PMA enable to JESD args.")
            self.JESD_DEFAULT_ARGS["enable_rx_eyescan"] = True
        perform_tx_prbs = False
        if 'tx_prbs' in args:
            perform_tx_prbs = (args['tx_prbs'] == 'True') or (args['tx_prbs'] == True)

        # Latency across the JESD204B TX/RX links should remain constant and
        # deterministic across the supported sampling_clock_rate values.
        # After testing the roundtrip latency (i.e. FPGA -> TX -> RX -> FPGA),
        # it was found that a different set of SYSREF delay values are required
        # for sampling_clock_rate = 400 MSPS to achieve latency consistency.
        self.JESD_DEFAULT_ARGS['rx_sysref_delay'] = \
          self.RX_SYSREF_DLY_DIC[self.rh_class.sampling_clock_rate]

        # Bringup Sequence.
        #   1. Prerequisites (include opening mmaps)
        #   2. Initialize LMK and bringup clocks.
        #   3. Synchronize DB Clocks.
        #   4. Initialize FPGA JESD IP.
        #   5. DAC Configuration.
        #   6. ADC Configuration.
        #   7. JESD204B Initialization.
        #   8. CPLD Gain Tables Initialization.

        # 1. Prerequisites
        # Open FPGA IP (Clock control and JESD core).
        self.log.trace("Creating dboard clock control object")
        db_clk_control = DboardClockControl(self.rh_class.radio_regs, self.log)
        self.log.trace("Creating jesdcore object")
        jesdcore = nijesdcore.NIJESDCore(self.rh_class.radio_regs, self.rh_class.slot_idx, **self.JESD_DEFAULT_ARGS)

        self.log.trace("Creating gain table object...")
        self.gain_table_loader = GainTableRh(
            self._spi_ifaces['cpld'],
            self._spi_ifaces['cpld_gain_loader'],
            self.log)

        # 2. Initialize LMK and bringup clocks.
        # Disable FPGA MMCM's outputs, and assert its reset.
        db_clk_control.reset_mmcm()
        # Always place the JESD204b cores in reset before modifying the clocks,
        # otherwise high power or erroneous conditions could exist in the FPGA!
        jesdcore.reset()
        # Configure and bringup the LMK's clocks.
        self.log.trace("Initializing LMK...")
        self.rh_class.lmk = self._init_lmk(
            self._spi_ifaces['lmk'],
            self.rh_class.ref_clock_freq,
            self.rh_class.sampling_clock_rate,
            self._spi_ifaces['phase_dac'],
            self.INIT_PHASE_DAC_WORD,
            self.PHASE_DAC_SPI_ADDR
        )
        self.log.trace("LMK Initialized!")
        # Deassert FPGA's MMCM reset, poll for lock, and enable outputs.
        db_clk_control.enable_mmcm()

        # 3. Synchronize DB Clocks.
        # The clock synchronzation driver receives the master_clock_rate, which for
        # Rhodium is half the sampling_clock_rate.
        self._sync_db_clock(
            self.rh_class.radio_regs,
            self.rh_class.ref_clock_freq,
            self.rh_class.sampling_clock_rate / 2,
            args)

        # 4. DAC Configuration.
        self.dac.config()

        # 5. ADC Configuration.
        self.adc.config()

        # 6-7. JESD204B Initialization.
        self.init_jesd(jesdcore, self.rh_class.sampling_clock_rate)
        # [Optional] Perform RX eyescan.
        if perform_rx_eyescan:
            self.log.info("Performing RX eye scan on ADC to FPGA link...")
            self._rx_eyescan(jesdcore, args)
        # [Optional] Perform TX PRBS test.
        if perform_tx_prbs:
            self.log.info("Performing TX PRBS-31 test on FPGA to DAC link...")
            self._tx_prbs_test(jesdcore, args)
        jesdcore = None # We are done using the jesdcore at this point.

        # 8. CPLD Gain Tables Initialization.
        self.gain_table_loader.init()

        return True


    ##########################################################################
    # JESD204B RX margin testing
    ##########################################################################

    def _rx_eyescan(self, jesdcore, args):
        """
        This function creates an eyescan object to perform this measurement with the
        given configuration and lanes.

        Parameters:
          prescale   -> Controls the prescaling of the sample count to keep both sample
                        count and error count in reasonable precision.
                        Valid values: from 0 to 31.
        """
        # The following constants must be defined according to GTs configuration
        # for each project. For further details, refer to the eyescan.py file.
        # For Rhodium, these parameters are based on the JESD core.
        rxout_div        = 2
        rx_int_datawidth = 20
        eq_mode          = 'LPM'
        # The following variables define the GTs to be scanned and the range of the
        # measurement.
        prescale   = 0
        scan_lanes = [0, 1, 2, 3]
        hor_range  = {'start':-32 , 'stop':32 , 'step': 2}
        ver_range  = {'start':-127, 'stop':127, 'step': 2}
        # Set default configuration values for Rhodium when the user is not intentionally
        # changing the constants/variables default values.
        for key in ('rxout_div', 'rx_int_datawidth', 'eq_mode',
                    'prescale', 'scan_lanes', 'hor_range', 'ver_range'):
            if key not in args:
                self.log.trace("Setting Rh default value for {0}... val: {1}"
                               .format(key, locals()[key]))
                args[key] = locals()[key]
        #
        # Create an eyescan object.
        assert jesdcore is not None
        eyescan_tool = EyeScanTool(jesdcore, self.slot_idx, **args)
        # Put the ADC in pseudorandom test mode.
        adc_regs = self._spi_ifaces['adc']
        # test_val = adc_regs.peek8(0x0550)
        # adc_regs.poke8(0x0550, 0x05)
        test_val = adc_regs.peek8(0x0573)
        adc_regs.poke8(0x0573, 0x13)
        # Perform eye scan on given lanes and range.
        file_name = eyescan_tool.eyescan_full_scan(args['scan_lanes'],
                                                   args['hor_range'], args['ver_range'])
        # Do some housekeeping...
        # adc_regs.poke8(0x0550, test_val) # Enable normal operation.
        adc_regs.poke8(0x0573, test_val) # Enable normal operation.
        adc_regs.poke8(0x0000, 0x81) # Reset.
        eyescan_tool = None
        return file_name

    def _tx_prbs_test(self, jesdcore, args):
        """
        This function allows to test the PRBS-31 pattern at the DAC.
        """
        def _test_lanes(**tx_settings):
            """
            This methods enables, monitors, and disables the PRBS-31 test.
            """
            results = []
            jesdcore.adjust_tx_phy(**tx_phy_settings)
            self.log.info("Testing TX PHY settings: tx_driver_swing=0b{0:04b}"
                                                 "  tx_precursor=0b{1:05b}"
                                                 "  tx_postcursor=0b{2:05b}"
                          .format(tx_phy_settings["tx_driver_swing"],
                                  tx_phy_settings["tx_precursor"],
                                  tx_phy_settings["tx_postcursor"]))
            # Enable the GTs TX pattern generator in PRBS-31 mode.
            jesdcore.set_pattern_gen(mode='PRBS-31')
            # Monitor each receive lane at DAC.
            for lane_num in range(0, 4):
                self.dac.test_mode(mode='PRBS-31', lane=lane_num) # Enable PRBS test mode.
                number_of_failures = 0
                for _ in range(0, POLLS_PER_GT):
                    time.sleep(WAIT_TIME_PER_POLL)
                    alarm_pin_dac = self.rh_class.cpld.get_dac_alarm()
                    if alarm_pin_dac:
                        number_of_failures += 1
                results.append(number_of_failures)
                if number_of_failures > 0:
                    self.log.error("PRBS-31 test for DAC lane {0} failed {1}/{2}!"
                                   .format(lane_num, number_of_failures, POLLS_PER_GT))
                else:
                    self.log.info("PRBS-31 test for DAC lane {0} passed!"
                                  .format(lane_num))
                self.dac.test_mode(mode='OFF', lane=lane_num) # Disable PRBS test mode.
            # Disable TX pattern generator at FPGA
            jesdcore.set_pattern_gen(mode='OFF')
            return results
        #
        WAIT_TIME_PER_POLL = 0.001 # in seconds.
        POLLS_PER_GT = 100
        # Create the CSV file.
        f = open('tx_prbs_sweep.csv', 'w')
        f.write("Swing,Precursor,Postcursor,Polls,Failures 0,Failures 1,Failures 2,Failures 3\n")
        # Default TX PHY settings.
        tx_phy_settings = {"tx_driver_swing": 0b1111,  # See UG476, TXDIFFCTRL
                           "tx_precursor"   : 0b00000, # See UG476, TXPRECURSOR
                           "tx_postcursor"  : 0b00000} # See UG476, TXPOSTCURSOR
        # Define sweep ranges.
        DEFAULT_SWING_RANGE = {'start': 0b0000, 'stop': 0b1111 + 0b1, 'step': 1}
        DEFAULT_CURSOR_RANGE = {'start': 0b00000, 'stop': 0b11111 + 0b1, 'step': 2}
        swing_range = args.get("swing_range", DEFAULT_SWING_RANGE)
        precursor_range = args.get("precursor_range", DEFAULT_CURSOR_RANGE)
        postcursor_range = args.get("postcursor_range", DEFAULT_CURSOR_RANGE)
        # Test the TX margin across multiple PHY settings.
        for swing in range(swing_range['start'], swing_range['stop'], swing_range['step']):
            tx_phy_settings["tx_driver_swing"] = swing
            for precursor in range(precursor_range['start'], precursor_range['stop'], precursor_range['step']):
                tx_phy_settings["tx_precursor"] = precursor
                for postcursor in range(postcursor_range['start'], postcursor_range['stop'], postcursor_range['step']):
                    tx_phy_settings["tx_postcursor"] = postcursor
                    results = _test_lanes(**tx_phy_settings)
                    f.write("{},{},{},{},{},{},{},{}\n".format(
                            tx_phy_settings["tx_driver_swing"],
                            tx_phy_settings["tx_precursor"],
                            tx_phy_settings["tx_postcursor"],
                            POLLS_PER_GT, results[0], results[1], results[2], results[3]))
        # Housekeeping...
        f.close()