/**
 * @file fd_calibrate.c
 *
 * Calibrate the output path.
 *
 * Author: Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
 * Author: Alessandro Rubini <rubini@gnudd.com>
 * Author: Dimitris Lampridis <dimitris.lampridis@cern.ch>
 *
 * Copyright (c) 2012-2019 CERN (home.cern)
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include "mockturtle-rt.h"

#include "wrtd-fd.h"
#include "hw/fd_main_regs.h"
#include "hw/acam_gpx.h"
#include "hw/fd_channel_regs.h"

/* This is the same as in ./acam.c: use only at init time */
static void acam_set_bypass(struct wrtd_fd_dev *fd, int on)
{
        fd_writel(fd, on ? FD_GCR_BYPASS : 0, FD_REG_GCR);
}

/*
 * Measures the the FPGA-generated TDC start and the output of one of
 * the fine delay chips (channel) at a pre-defined number of taps
 * (fine). Retuns the delay in picoseconds. The measurement is
 * repeated and averaged (n_avgs) times. Also, the standard deviation
 * of the result can be written to (sdev) if it's not NULL.
 */

/* Note: channel is the "internal" one: 0..3 */
static uint64_t output_delay_ps(struct wrtd_fd_dev *fd, int ch, int fine, int n)
{
        int i;
        uint64_t acc = 0;

        /* Disable the output for the channel being calibrated */
        fd_gpio_clr(fd, FD_GPIO_OUTPUT_EN(FD_CH_EXT(ch)));

        /* Enable the stop input in ACAM for the channel being calibrated */
        acam_writel(fd, AR0_TRiseEn(0) | AR0_TRiseEn(FD_CH_EXT(ch))
                    | AR0_HQSel | AR0_ROsc, 0);

        /* Program the output delay line setpoint */
        fd_drv_ch_writel(fd, ch, fine, FD_REG_FRR);
        fd_drv_ch_writel(fd, ch, FD_DCR_ENABLE | FD_DCR_MODE | FD_DCR_UPDATE,
                         FD_REG_DCR);
        fd_drv_ch_writel(fd, ch, FD_DCR_FORCE_DLY | FD_DCR_ENABLE, FD_REG_DCR);

        /*
         * Set the calibration pulse mask to genrate calibration
         * pulses only on one channel at a time.  This minimizes the
         * crosstalk in the output buffer which can severely decrease
         * the accuracy of calibration measurements
         */
        fd_writel(fd, FD_CALR_PSEL_W(1 << ch), FD_REG_CALR);
        udelay(1);

        /* Do n_avgs single measurements and average */
        for (i = 0; i < n; i++) {
                uint32_t fr;
                /* Re-arm the ACAM (it's working in a single-shot mode) */
                fd_writel(fd, FD_TDCSR_ALUTRIG, FD_REG_TDCSR);
                udelay(1);
                /* Produce a calib pulse on the TDC start and the output ch */
                fd_writel(fd, FD_CALR_CAL_PULSE |
                          FD_CALR_PSEL_W(1 << ch), FD_REG_CALR);
                udelay(1);
                /* read the tag, convert to picoseconds (fixed point: 16.16) */
                fr = acam_readl(fd, 8 /* fifo */) & 0x1ffff;

                //pp_printf("i %d fr %x\n\r", i, fr);

                acc += fr * fd->fd_bin_size;
        }
        fd_drv_ch_writel(fd, ch, 0, FD_REG_DCR);

        /* Calculate avg, min max */
        acc = div_u64((acc + n / 2), n );

        //pp_printf("ch %d avg %08x%08x\n\r", ch, (uint32_t)(acc>>32), (uint32_t)acc);

        return acc;
}

static int fd_find_8ns_tap(struct wrtd_fd_dev *fd, int ch)
{
        int l = 0, mid, r = FD_NUM_TAPS - 1;
        uint64_t bias, dly;

        /*
         * Measure the delay at zero setting, so it can be further
         * subtracted to get only the delay part introduced by the
         * delay line (ingoring the TDC, FPGA and routing delays).
         * Use a binary search of the delay value.
         */
        bias = output_delay_ps(fd, ch, 0, FD_CAL_STEPS);
        while( r - l > 1) {
                mid = ( l + r) / 2;
                dly = output_delay_ps(fd, ch, mid, FD_CAL_STEPS) - bias;

                if(dly < 8000 << 16)
                        l = mid;
                else
                        r = mid;
        }
        return l;

}

/**
 * fd_calibrate_outputs
 * It calibrates the delay line by finding the correct 8ns-tap value
 * for each channel. This is done during ACAM initialization, so on driver
 * probe.
 */
int fd_calibrate_outputs(struct wrtd_fd_dev *fd)
{
        int ch;
        int measured;

        acam_set_bypass(fd, 1); /* not useful */
        fd_writel(fd, FD_TDCSR_START_EN | FD_TDCSR_STOP_EN, FD_REG_TDCSR);

        for (ch = 0; ch < FD_NUM_CHANNELS; ch++) {
                measured = fd_find_8ns_tap(fd, ch);
                fd_drv_ch_writel(fd, ch, measured, FD_REG_FRR);

                pr_debug("Channel %d: 8ns @ %i taps.\n\r", ch+1, measured );
        }
        return 0;
}
