/*
 * Copyright (C) 2015 Jason Milldrum <milldrum@gmail.com>
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <si5351.h>
#include <stdint.h>

uint32_t xtal_freq = SI5351_XTAL_FREQ;

/*
 * si5351_init(uint8_t xtal_load_c, uint32_t ref_osc_freq)
 *
 * Setup communications to the Si5351 and set the crystal
 * load capacitance.
 *
 * xtal_load_c - Crystal load capacitance. Use the SI5351_CRYSTAL_LOAD_*PF
 * defines in the header file
 * ref_osc_freq - Crystal/reference oscillator frequency in 1 Hz increments.
 * Defaults to 25000000 if a 0 is used here.
 *
 */
void si5351_init(uint8_t xtal_load_c)
{
	/* Set crystal load capacitance */
	si5351_write(SI5351_CRYSTAL_LOAD, xtal_load_c);

	// Initialize the CLK outputs according to flowchart in datasheet
	// First, turn them off
	si5351_write(16, 0x80);
	si5351_write(17, 0x80);
	si5351_write(18, 0x80);

	// Turn the clocks back on...
	si5351_write(16, 0x0c);
	si5351_write(17, 0x0c);
	si5351_write(18, 0x0c);

	// Then reset the PLLs
	si5351_pll_reset(SI5351_PLLA);
	si5351_pll_reset(SI5351_PLLB);
}

/*
 * si5351_set_freq(uint64_t freq, enum si5351_clock clk)
 *
 * Uses SI5351_PLL_FIXED (900 MHz) for PLLA.
 * All multisynths are assigned to PLLA using this function.
 * PLLA is set to 900 MHz.
 * Restricted to outputs from 1 to 150 MHz.
 * If you need frequencies outside that range, use set_pll()
 * and set_ms() to set the synth dividers manually.
 *
 * freq - Output frequency in Hz
 * clk - Clock output
 *   (use the si5351_clock enum)
 */
void si5351_set_freq(uint64_t freq, enum si5351_clock clk)
{
	struct Si5351Frac pll_frac, ms_frac;

	// Lower bounds check
	if (freq < SI5351_MULTISYNTH_MIN_FREQ)
		freq = SI5351_MULTISYNTH_MIN_FREQ;

	// Upper bounds check
	if (freq > SI5351_MULTISYNTH_DIVBY4_FREQ)
		freq = SI5351_MULTISYNTH_DIVBY4_FREQ;

	// Set the PLL
	pll_frac.a = (uint16_t) (SI5351_PLL_FIXED / xtal_freq);
	pll_frac.b = 1000000UL;
	pll_frac.a--;

	pll_frac.c = 1000000UL;
	si5351_set_pll(pll_frac, SI5351_PLLA);

	// Set the MS
	ms_frac.a = (uint16_t) (SI5351_PLL_FIXED / freq);
	ms_frac.b = (uint32_t) (((SI5351_PLL_FIXED % freq) * 1000000UL) / freq);
	ms_frac.c = 1000000UL;
	si5351_set_ms(clk, ms_frac, 0, SI5351_OUTPUT_CLK_DIV_1, 0);
}

/*
 * si5351_set_pll(struct Si5351Frac frac, enum si5351_pll target_pll)
 *
 * Set the specified PLL to a specific oscillation frequency by
 * using the Si5351Frac struct to specify the synth divider ratio.
 *
 * frac - PLL fractional divider values
 * target_pll - Which PLL to set
 *     (use the si5351_pll enum)
 */
void si5351_set_pll(struct Si5351Frac frac, enum si5351_pll target_pll)
{
	struct Si5351RegSet pll_reg;

	// Calculate parameters
	pll_reg.p1 = 128 * frac.a + ((128 * frac.b) / frac.c) - 512;
	pll_reg.p2 = 128 * frac.b - frac.c * ((128 * frac.b) / frac.c);
	pll_reg.p3 = frac.c;

	// Derive the register values to write
	// Prepare an array for parameters to be written to
	uint8_t params[20];
	uint8_t i = 0;
	uint8_t temp;

	// Registers 26-27 for PLLA
	temp = ((pll_reg.p3 >> 8) & 0xFF);
	params[i++] = temp;

	temp = (uint8_t) (pll_reg.p3 & 0xFF);
	params[i++] = temp;

	// Register 28 for PLLA
	temp = (uint8_t) ((pll_reg.p1 >> 16) & 0x03);
	params[i++] = temp;

	// Registers 29-30 for PLLA
	temp = (uint8_t) ((pll_reg.p1 >> 8) & 0xFF);
	params[i++] = temp;

	temp = (uint8_t) (pll_reg.p1 & 0xFF);
	params[i++] = temp;

	// Register 31 for PLLA
	temp = (uint8_t) ((pll_reg.p3 >> 12) & 0xF0);
	temp += (uint8_t) ((pll_reg.p2 >> 16) & 0x0F);
	params[i++] = temp;

	// Registers 32-33 for PLLA
	temp = (uint8_t) ((pll_reg.p2 >> 8) & 0xFF);
	params[i++] = temp;

	temp = (uint8_t) (pll_reg.p2 & 0xFF);
	params[i++] = temp;

	// Write the parameters
	if (target_pll == SI5351_PLLA)
		si5351_write_bulk(SI5351_PLLA_PARAMETERS, i, params);
	else if (target_pll == SI5351_PLLB)
		si5351_write_bulk(SI5351_PLLB_PARAMETERS, i, params);
}

/*
 * si5351_set_ms(enum si5351_clock clk, struct Si5351Frac frac, uint8_t int_mode, uint8_t r_div, uint8_t div_by_4)
 *
 * Set the specified multisynth parameters.
 *
 * clk - Clock output
 *   (use the si5351_clock enum)
 * frac - Synth fractional divider values
 * int_mode - Set integer mode
 *  Set to 1 to enable, 0 to disable
 * r_div - Desired r_div ratio
 * div_by_4 - Set Divide By 4 mode
 *   Set to 1 to enable, 0 to disable
 */
void si5351_set_ms(enum si5351_clock clk, struct Si5351Frac frac,
		uint8_t int_mode, uint8_t r_div, uint8_t div_by_4)
{
	struct Si5351RegSet ms_reg;
	uint8_t params[20];
	uint8_t i = 0;
	uint8_t temp;
	uint8_t reg_val;

	// Calculate parameters
	if (div_by_4 == 1)
	{
		ms_reg.p3 = 1;
		ms_reg.p2 = 0;
		ms_reg.p1 = 0;
	}
	else
	{
		ms_reg.p1 = 128 * frac.a + ((128 * frac.b) / frac.c) - 512;
		ms_reg.p2 = 128 * frac.b - frac.c * ((128 * frac.b) / frac.c);
		ms_reg.p3 = frac.c;
	}

	// Registers 42-43 for CLK0
	temp = (uint8_t) ((ms_reg.p3 >> 8) & 0xFF);
	params[i++] = temp;

	temp = (uint8_t) (ms_reg.p3 & 0xFF);
	params[i++] = temp;

	// Register 44 for CLK0
	si5351_read((SI5351_CLK0_PARAMETERS + 2) + (clk * 8), &reg_val);
	reg_val &= ~(0x03);
	temp = reg_val | ((uint8_t) ((ms_reg.p1 >> 16) & 0x03));
	params[i++] = temp;

	// Registers 45-46 for CLK0
	temp = (uint8_t) ((ms_reg.p1 >> 8) & 0xFF);
	params[i++] = temp;

	temp = (uint8_t) (ms_reg.p1 & 0xFF);
	params[i++] = temp;

	// Register 47 for CLK0
	temp = (uint8_t) ((ms_reg.p3 >> 12) & 0xF0);
	temp += (uint8_t) ((ms_reg.p2 >> 16) & 0x0F);
	params[i++] = temp;

	// Registers 48-49 for CLK0
	temp = (uint8_t) ((ms_reg.p2 >> 8) & 0xFF);
	params[i++] = temp;

	temp = (uint8_t) (ms_reg.p2 & 0xFF);
	params[i++] = temp;

	// Write the parameters
	switch (clk)
	{
	case SI5351_CLK0:
		si5351_write_bulk(SI5351_CLK0_PARAMETERS, i, params);
		break;
	case SI5351_CLK1:
		si5351_write_bulk(SI5351_CLK1_PARAMETERS, i, params);
		break;
	case SI5351_CLK2:
		si5351_write_bulk(SI5351_CLK2_PARAMETERS, i, params);
		break;
	}

	si5351_set_int(clk, int_mode);
	si5351_set_ms_div(clk, r_div, div_by_4);
}

/*
 * si5351_drive_strength(enum si5351_clock clk, enum si5351_drive drive)
 *
 * Sets the drive strength of the specified clock output
 *
 * clk - Clock output
 *   (use the si5351_clock enum)
 * drive - Desired drive level
 *   (use the si5351_drive enum)
 */
void si5351_drive_strength(enum si5351_clock clk, enum si5351_drive drive)
{
	uint8_t reg_val;
	const uint8_t mask = 0x03;

	if (si5351_read(SI5351_CLK0_CTRL + (uint8_t) clk, &reg_val) != 0)
		return;

	switch (drive)
	{
	case SI5351_DRIVE_2MA:
		reg_val &= ~(mask);
		reg_val |= 0x00;
		break;
	case SI5351_DRIVE_4MA:
		reg_val &= ~(mask);
		reg_val |= 0x01;
		break;
	case SI5351_DRIVE_6MA:
		reg_val &= ~(mask);
		reg_val |= 0x02;
		break;
	case SI5351_DRIVE_8MA:
		reg_val &= ~(mask);
		reg_val |= 0x03;
		break;
	default:
		break;
	}

	si5351_write(SI5351_CLK0_CTRL + (uint8_t) clk, reg_val);
}

/*
 * si5351_pll_reset(enum si5351_pll target_pll)
 *
 * target_pll - Which PLL to reset
 *     (use the si5351_pll enum)
 *
 * Apply a reset to the indicated PLL.
 */
void si5351_pll_reset(enum si5351_pll target_pll)
{
	if (target_pll == SI5351_PLLA)
		si5351_write(SI5351_PLL_RESET, SI5351_PLL_RESET_A);
	else if (target_pll == SI5351_PLLB)
		si5351_write(SI5351_PLL_RESET, SI5351_PLL_RESET_B);
}

/*
 * si5351_set_int(enum si5351_clock clk, uint8_t int_mode)
 *
 * clk - Clock output
 *   (use the si5351_clock enum)
 * enable - Set to 1 to enable, 0 to disable
 *
 * Set the indicated multisynth into integer mode.
 */
void si5351_set_int(enum si5351_clock clk, uint8_t enable)
{
	uint8_t reg_val;
	si5351_read(SI5351_CLK0_CTRL + (uint8_t) clk, &reg_val);

	if (enable == 1)
		reg_val |= (SI5351_CLK_INTEGER_MODE);
	else
		reg_val &= ~(SI5351_CLK_INTEGER_MODE);

	si5351_write(SI5351_CLK0_CTRL + (uint8_t) clk, reg_val);
}

void si5351_set_ms_div(enum si5351_clock clk, uint8_t r_div, uint8_t div_by_4)
{
	uint8_t reg_val = 0;
	uint8_t reg_addr = 0;

	switch (clk)
	{
	case SI5351_CLK0:
		reg_addr = SI5351_CLK0_PARAMETERS + 2;
		break;
	case SI5351_CLK1:
		reg_addr = SI5351_CLK1_PARAMETERS + 2;
		break;
	case SI5351_CLK2:
		reg_addr = SI5351_CLK2_PARAMETERS + 2;
		break;
	}

	si5351_read(reg_addr, &reg_val);

	// Clear the relevant bits
	reg_val &= ~(0x7c);

	if (div_by_4 == 0)
		reg_val &= ~(SI5351_OUTPUT_CLK_DIVBY4);
	else
		reg_val |= (SI5351_OUTPUT_CLK_DIVBY4);

	reg_val |= (r_div << SI5351_OUTPUT_CLK_DIV_SHIFT);

	si5351_write(reg_addr, reg_val);
}

uint8_t si5351_write_bulk(uint8_t addr, uint8_t bytes, uint8_t *data)
{
	I2C1_MEM8_Write(SI5351_BUS_BASE_ADDR, addr, data, bytes);

	return 1;
}

uint8_t si5351_write(uint8_t addr, uint8_t data)
{
	I2C1_MEM8_Write(SI5351_BUS_BASE_ADDR, addr, &data, 1);

	return 1;
}

uint8_t si5351_read(uint8_t addr, uint8_t *data)
{
	I2C1_MEM8_Read(SI5351_BUS_BASE_ADDR, addr, data, 1);

	return 1;
}
