/* Analog Devices AD9516-4 PLL driver */

#include <math.h>

#include <hw/pio.h>
#include <hw/trace.h>

#include <hw/ad9516.h>

/* SPI bit delay, in microseconds */
#define SPI_DELAY 50

#define AD9516_OP_READ 0x80
#define AD9516_OP_WRITE 0x00

#define AD9516_MAX_REGS 1024

struct ad9516_regs {
	struct {
		uint16_t addr;
		uint8_t value;
	} regs[AD9516_MAX_REGS];
	int nregs;
};

int shw_ad9516_set_output_delay(int output, float delay_ns, int bypass);

/* for the standard PLL setups - generated by Analog Devices' software */
#include "ad9516_default_regs.h"

/* Indicates whether to generate the reference clock from the local VCTCXO (0)
   or the external 10 MHz input (cesium, etc. - 1) */
   
static int use_ext_ref = 0;

/* Enables/disables locking the 125 MHz reference clock to an externally provided
   10 Mhz Reference (when enable = 1). Otherwise, the reference clock is produced 
   from the local voltage controlled oscillator (which can be locked by the DMPLL
   to a reference from one of the uplink ports.
   
   Note: can *ONLY* be called before initializing the libswitchhw. Reference source
   cannot be switched on-the-fly. */
   
int shw_use_external_reference(int enable)
{
	if(enable)
		TRACE(TRACE_INFO, "Using external 10 MHz reference clock (grandmaster mode)");
	use_ext_ref = enable;
}

/* Reads a byte from the AD9516 SPI interface by bit-banging the CPU pins */
static uint8_t ad9516_spi_read8()
{
    uint8_t rx = 0;
    int i;

    shw_pio_setdir(PIN_ad9516_sdio, PIO_IN);
    shw_pio_set0(PIN_ad9516_sclk);
    for(i = 0; i < 8;i++)
    {
        shw_udelay(SPI_DELAY);

        rx <<= 1;
        if (shw_pio_get(PIN_ad9516_sdio))
            rx |= 1;

        shw_udelay(SPI_DELAY);
        shw_pio_set1(PIN_ad9516_sclk);
        shw_udelay(SPI_DELAY);
        shw_pio_set0(PIN_ad9516_sclk);
    }
    shw_udelay(SPI_DELAY);
    return rx;
}

/* Writes a byte (tx) to the AD9516 SPI interface by bit-banging the CPU pins */
static uint8_t ad9516_spi_write8(uint8_t tx)
{
    int i;

    shw_pio_setdir(PIN_ad9516_sdio, PIO_OUT);
    shw_pio_set0(PIN_ad9516_sclk);
    for(i = 0; i < 8;i++)
    {
        shw_udelay(SPI_DELAY);

        shw_pio_set(PIN_ad9516_sdio, tx & 0x80);
        tx<<=1;

        shw_udelay(SPI_DELAY);
        shw_pio_set1(PIN_ad9516_sclk);
        shw_udelay(SPI_DELAY);
        shw_pio_set0(PIN_ad9516_sclk);
    }
    shw_udelay(SPI_DELAY);
}

/* Reads an AD9516 register from address addr and returns its 8-bit value */
static uint8_t ad9516_read_reg(uint16_t addr)
{
	uint8_t val;

	shw_pio_set0(PIN_ad9516_cs);
	shw_udelay(SPI_DELAY);

	ad9516_spi_write8(AD9516_OP_READ | (addr >> 8));
    ad9516_spi_write8(addr & 0xff);
    val = ad9516_spi_read8();

    shw_udelay(SPI_DELAY);
	shw_pio_set1(PIN_ad9516_cs);
	shw_udelay(SPI_DELAY);

	return val;
}

/* Writes AD9516 register located at addr with value data */
static void ad9516_write_reg(uint16_t addr, uint8_t data)
{
	shw_pio_set0(PIN_ad9516_cs);
	shw_udelay(SPI_DELAY);

	ad9516_spi_write8(AD9516_OP_WRITE | (addr >> 8));
    ad9516_spi_write8(addr & 0xff);
    ad9516_spi_write8(data);

    shw_udelay(SPI_DELAY);
	shw_pio_set1(PIN_ad9516_cs);
	shw_udelay(SPI_DELAY);
}

/* Performs a HW reset of the AD9516 by asserting it's RSTN pin */
static void ad9516_reset()
{
    shw_pio_set0(PIN_ad9516_nrst); 
    shw_udelay(100);
    shw_pio_set1(PIN_ad9516_nrst); 
    shw_udelay(100);
}


/* Checks for the presence of 10 MHz external reference signal on the AD9516 ref input. 
	 Returns non-zero if the signal is present. */
int ad9516_detect_external_ref()
{
	ad9516_write_reg(0x4, 0x1); // readback active regs
	ad9516_write_reg(0x1c, 0x06); // enable ref1/reg2
	ad9516_write_reg(0x1b, 0xe0); // enable refin monitor
	ad9516_write_reg(0x232, 1); // commit
	usleep(200000);
	return ad9516_read_reg(0x1f) ;
}


/* Loads and updates the configuration provided in "regs" into the AD9516. */
static int ad9516_load_state(const struct ad9516_regs *regs)
{
	int i;
	TRACE(TRACE_INFO, "Loading AD9516 state (%d registers)", regs->nregs);

	for(i=0;i<regs->nregs;i++)
		ad9516_write_reg(regs->regs[i].addr, regs->regs[i].value);

	/* Trigger the actual register reload (regs are just buffered after being written), 
		 in order to load the new values simultaneously one must trigger an update. */
	ad9516_write_reg(AD9516_UPDATE_ALL, 1); 

	return 0;
}

/* Checks if the PLL is locked (digital lock detection). Returns non-zero if true */
static int ad9516_check_lock()
{
    uint8_t pll_readback = ad9516_read_reg(AD9516_PLLREADBACK);

    return pll_readback & 1;
}

#define assert_init(proc) { int ret; if((ret = proc) < 0) return ret; }

/* Initializes the AD9516: sets up the reference source, PLL multiply/divide ratios,
   enables clock outputs. */
   
int shw_ad9516_init()
{
    int retries = 100;

    TRACE(TRACE_INFO, "Initializing AD9516 PLL....");

		/* The AD9516 uses CPU GPIO pins (it couldn't be connected to the FPGA,
		   because it supplies clock for it (and a clock-less FPGA wouldn't work). */
    shw_pio_configure(PIN_ad9516_cs);
    shw_pio_configure(PIN_ad9516_nrst);
    shw_pio_configure(PIN_ad9516_refsel);
    shw_pio_configure(PIN_ad9516_sclk);
    shw_pio_configure(PIN_ad9516_sdio);
    shw_pio_configure(PIN_ad9516_sdo);

		/* Make sure the pins are stable after the configuration (no glitches, etc.) */
    shw_udelay(100);

		/* Reset AD9516 and give it some time to recover */
    ad9516_reset();
    shw_udelay(10000);

		/* Detect the presence by comparing a read register with it's reset value */
    if(ad9516_read_reg(AD9516_SERIALPORT) != 0x18)
    {
        TRACE(TRACE_FATAL, "AD9516 not responding!");
        return -1;
    }

		/* Now just initialize. There are two pre-defined register sets, one for 
		   the internal 25 MHz VCTCXO and the other for 10 MHz GPS/Cesium external reference */
		if(use_ext_ref)
		{
    	assert_init(ad9516_load_state(&ad9516_regs_ext_10m));
		} else {
		  assert_init(ad9516_load_state(&ad9516_regs_tcxo_25m));
		}

		/* Wait for the PLL to lock. */
    while(retries--)
    {
        if(ad9516_check_lock()) break;
        shw_udelay(1000);
    }

		/* Signal integrity bugfix: delay Uplink 0 PHY clock a bit (to increase tSetup on
		   the PHY TX inputs */
		shw_ad9516_set_output_delay(9, 0.5, 0);

    return 0;
}

/* Finds the parameters of the output skew adjustment unit of AD9516  for a given delay value. */
static int find_optimal_params(float delay_ns, int *caps, int *frac, int *ramp, float *best)
{
 int r, i, ncaps, f;
 float best_error = 10000;


	for(r = 0; r < 8; r++)
	{
		for(i=0;i<8;i++)
		{
			if(i == 0) ncaps= 0;
			else if(i==1 || i==2 || i==4) ncaps = 1;
			else if(i==3 || i==6 || i==5) ncaps = 2;
			else ncaps = 3;

			for(f= 0;f<=0x2f;f++)
			{
				float iramp = (float)(r+1)*200.0;
				float del_range = 200.0 * ((float)(ncaps+3)/iramp) * 1.3286;
				float offset = 0.34 + (1600.0 - iramp) * 10e-4 + (float)(ncaps-1)/iramp*6.0;

//				printf("range: %.3f offset %.3f\n", del_range, offset);
				float del_fine = del_range * (float)f / 63.0  + offset;
				if(fabs(del_fine - delay_ns) < best_error)
				{
	//				printf("New Best: %.3f\n", del_fine);
					best_error = fabs(del_fine - delay_ns);
					*best = del_fine;
					*caps = i;
					*ramp = r;
					*frac = f;
				}
			}
		}
	}
}

/* Sets the extra delay on given output to delay_ns. Used for de-skewing clocks.
   When bypass is 1, the delay block is disabled. */
int shw_ad9516_set_output_delay(int output, float delay_ns, int bypass)
{
    uint16_t regbase = 0xa0 + 3*(output - 6);
		int ramp,frac,caps;
		float best_dly;

		find_optimal_params(delay_ns,&caps, &frac, &ramp, &best_dly );

    ad9516_write_reg(regbase, bypass?1:0);
    ad9516_write_reg(regbase+1, (caps << 3) | (ramp));
    ad9516_write_reg(regbase+2, frac);

    ad9516_write_reg(AD9516_UPDATE_ALL, 1); /* Acknowledge register update */

    return 0;
}

