/**
 * Si4464 driver specialized for APRS transmissions. Modulation concept has been taken
 * from Stefan Biereigel DK3SB.
 * @see http://www.github.com/thasti/utrak
 */

#include "ch.h"
#include "hal.h"
#include "si4464.h"
#include "debug.h"
#include "types.h"
#include <string.h>

static const SPIConfig ls_spicfg = {
	.ssport	= PAL_PORT(LINE_RADIO_CS),
	.sspad	= PAL_PAD(LINE_RADIO_CS),
	.cr1	= SPI_CR1_MSTR
};
#define getSPIDriver() &ls_spicfg

static uint32_t outdiv;
static bool initialized = false;
static int16_t lastTemp;

/**
 * Initializes Si4464 transceiver chip. Adjustes the frequency which is shifted by variable
 * oscillator voltage.
 * @param mv Oscillator voltage in mv
 */
void Si4464_Init(void) {
	// Configure Radio pins
	palSetLineMode(LINE_SPI_SCK, PAL_MODE_ALTERNATE(6) | PAL_STM32_OSPEED_HIGHEST);		// SCK
	palSetLineMode(LINE_SPI_MISO, PAL_MODE_ALTERNATE(6) | PAL_STM32_OSPEED_HIGHEST);	// MISO
	palSetLineMode(LINE_SPI_MOSI, PAL_MODE_ALTERNATE(6) | PAL_STM32_OSPEED_HIGHEST);	// MOSI
	palSetLineMode(LINE_RADIO_CS, PAL_MODE_OUTPUT_PUSHPULL | PAL_STM32_OSPEED_HIGHEST);	// RADIO CS
	palSetLineMode(LINE_RADIO_SDN, PAL_MODE_OUTPUT_PUSHPULL);							// RADIO SDN
	palSetLineMode(LINE_OSC_EN, PAL_MODE_OUTPUT_PUSHPULL);								// Oscillator
	palSetLine(LINE_RADIO_CS);

	// Reset radio
	palSetLine(LINE_RADIO_SDN);
	palSetLine(LINE_OSC_EN); // Activate Oscillator
	chThdSleepMilliseconds(10);

	// Power up transmitter
	palClearLine(LINE_RADIO_SDN);	// Radio SDN low (power up transmitter)
	chThdSleepMilliseconds(10);		// Wait for transmitter to power up

	// Power up (transmits oscillator type)
	uint8_t x3 = (RADIO_CLK >> 24) & 0x0FF;
	uint8_t x2 = (RADIO_CLK >> 16) & 0x0FF;
	uint8_t x1 = (RADIO_CLK >>  8) & 0x0FF;
	uint8_t x0 = (RADIO_CLK >>  0) & 0x0FF;
	uint8_t init_command[] = {0x02, 0x01, 0x01, x3, x2, x1, x0};
	Si4464_write(init_command, 7);
	chThdSleepMilliseconds(25);

	// Set transmitter GPIOs
	uint8_t gpio_pin_cfg_command[] = {
		0x13,	// Command type = GPIO settings
		0x00,	// GPIO0        0 - PULL_CTL[1bit] - GPIO_MODE[6bit]
		0x23,	// GPIO1        0 - PULL_CTL[1bit] - GPIO_MODE[6bit]
		0x00,	// GPIO2        0 - PULL_CTL[1bit] - GPIO_MODE[6bit]
		0x00,	// GPIO3        0 - PULL_CTL[1bit] - GPIO_MODE[6bit]
		0x00,	// NIRQ
		0x00,	// SDO
		0x00	// GEN_CONFIG
	};
	Si4464_write(gpio_pin_cfg_command, 8);
	chThdSleepMilliseconds(25);

	// Set FIFO empty interrupt threshold (32 byte)
	uint8_t set_fifo_irq[] = {0x11, 0x12, 0x01, 0x0B, 0x20};
	Si4464_write(set_fifo_irq, 5);

	// Set FIFO to 129 byte
	uint8_t set_129byte[] = {0x11, 0x00, 0x01, 0x03, 0x10};
	Si4464_write(set_129byte, 5);

	// Reset FIFO
	uint8_t reset_fifo[] = {0x15, 0x01};
	Si4464_write(reset_fifo, 2);
	uint8_t unreset_fifo[] = {0x15, 0x00};
	Si4464_write(unreset_fifo, 2);

	// Disable preamble
	uint8_t disable_preamble[] = {0x11, 0x10, 0x01, 0x00, 0x00};
	Si4464_write(disable_preamble, 5);

	// Do not transmit sync word
	uint8_t no_sync_word[] = {0x11, 0x11, 0x01, 0x00, (0x01 << 7)};
	Si4464_write(no_sync_word, 5);

	// Setup the NCO modulo and oversampling mode
	uint32_t s = RADIO_CLK / 10;
	uint8_t f3 = (s >> 24) & 0xFF;
	uint8_t f2 = (s >> 16) & 0xFF;
	uint8_t f1 = (s >>  8) & 0xFF;
	uint8_t f0 = (s >>  0) & 0xFF;
	uint8_t setup_oversampling[] = {0x11, 0x20, 0x04, 0x06, f3, f2, f1, f0};
	Si4464_write(setup_oversampling, 8);

	// transmit LSB first
	uint8_t use_lsb_first[] = {0x11, 0x12, 0x01, 0x06, 0x01};
	Si4464_write(use_lsb_first, 5);

	// Temperature readout
	lastTemp = Si4464_getTemperature();
	TRACE_INFO("SI   > Transmitter temperature %d degC", lastTemp/100);
	initialized = true;
}

void Si4464_write(uint8_t* txData, uint32_t len) {
	// Transmit data by SPI
	uint8_t rxData[len];
	
	// SPI transfer
	spiAcquireBus(&SPID3);
	spiStart(&SPID3, getSPIDriver());

	spiSelect(&SPID3);
	spiExchange(&SPID3, len, txData, rxData);
	spiUnselect(&SPID3);

	// Reqest ACK by Si4464
	rxData[1] = 0x00;
	while(rxData[1] != 0xFF) {

		// Request ACK by Si4464
		uint8_t rx_ready[] = {0x44};

		// SPI transfer
		spiSelect(&SPID3);
		spiExchange(&SPID3, 3, rx_ready, rxData);
		spiUnselect(&SPID3);
	}
	spiStop(&SPID3);
	spiReleaseBus(&SPID3);
}

/**
 * Read register from Si4464. First Register CTS is included.
 */
void Si4464_read(uint8_t* txData, uint32_t txlen, uint8_t* rxData, uint32_t rxlen) {
	// Transmit data by SPI
	uint8_t null_spi[txlen];
	// SPI transfer
	spiAcquireBus(&SPID3);
	spiStart(&SPID3, getSPIDriver());

	spiSelect(&SPID3);
	spiExchange(&SPID3, txlen, txData, null_spi);
	spiUnselect(&SPID3);

	// Reqest ACK by Si4464
	rxData[1] = 0x00;
	while(rxData[1] != 0xFF) {

		// Request ACK by Si4464
		uint16_t rx_ready[rxlen];
		rx_ready[0] = 0x44;

		// SPI transfer
		spiSelect(&SPID3);
		spiExchange(&SPID3, rxlen, rx_ready, rxData);
		spiUnselect(&SPID3);
	}
	spiStop(&SPID3);
	spiReleaseBus(&SPID3);
}

void setFrequency(uint32_t freq, uint16_t shift) {
	// Set the output divider according to recommended ranges given in Si4464 datasheet
	uint32_t band = 0;
	if(freq < 705000000UL) {outdiv = 6;  band = 1;};
	if(freq < 525000000UL) {outdiv = 8;  band = 2;};
	if(freq < 353000000UL) {outdiv = 12; band = 3;};
	if(freq < 239000000UL) {outdiv = 16; band = 4;};
	if(freq < 177000000UL) {outdiv = 24; band = 5;};

	// Set the band parameter
	uint32_t sy_sel = 8;
	uint8_t set_band_property_command[] = {0x11, 0x20, 0x01, 0x51, (band + sy_sel)};
	Si4464_write(set_band_property_command, 5);

	// Set the PLL parameters
	uint32_t f_pfd = 2 * RADIO_CLK / outdiv;
	uint32_t n = ((uint32_t)(freq / f_pfd)) - 1;
	float ratio = (float)freq / (float)f_pfd;
	float rest  = ratio - (float)n;

	uint32_t m = (uint32_t)(rest * 524288UL);
	uint32_t m2 = m >> 16;
	uint32_t m1 = (m - m2 * 0x10000) >> 8;
	uint32_t m0 = (m - m2 * 0x10000 - (m1 << 8));

	uint32_t channel_increment = 524288 * outdiv * shift / (2 * RADIO_CLK);
	uint8_t c1 = channel_increment / 0x100;
	uint8_t c0 = channel_increment - (0x100 * c1);

	uint8_t set_frequency_property_command[] = {0x11, 0x40, 0x04, 0x00, n, m2, m1, m0, c1, c0};
	Si4464_write(set_frequency_property_command, 10);

	uint32_t x = ((((uint32_t)1 << 19) * outdiv * 1300.0)/(2*RADIO_CLK))*2;
	uint8_t x2 = (x >> 16) & 0xFF;
	uint8_t x1 = (x >>  8) & 0xFF;
	uint8_t x0 = (x >>  0) & 0xFF;
	uint8_t set_deviation[] = {0x11, 0x20, 0x03, 0x0a, x2, x1, x0};
	Si4464_write(set_deviation, 7);
}

void setShift(uint16_t shift) {
	if(!shift)
		return;

	float units_per_hz = (( 0x40000 * outdiv ) / (float)RADIO_CLK);

	// Set deviation for 2FSK
	uint32_t modem_freq_dev = (uint32_t)(units_per_hz * shift / 2.0 );
	uint8_t modem_freq_dev_0 = 0xFF & modem_freq_dev;
	uint8_t modem_freq_dev_1 = 0xFF & (modem_freq_dev >> 8);
	uint8_t modem_freq_dev_2 = 0xFF & (modem_freq_dev >> 16);

	uint8_t set_modem_freq_dev_command[] = {0x11, 0x20, 0x03, 0x0A, modem_freq_dev_2, modem_freq_dev_1, modem_freq_dev_0};
	Si4464_write(set_modem_freq_dev_command, 7);
}

void setModemAFSK(void) {
	// Setup the NCO data rate for APRS
	uint8_t setup_data_rate[] = {0x11, 0x20, 0x03, 0x03, 0x00, 0x33, 0x90};
	Si4464_write(setup_data_rate, 7);

	// Use 2GFSK from FIFO (PH)
	uint8_t use_2gfsk[] = {0x11, 0x20, 0x01, 0x00, 0x03};
	Si4464_write(use_2gfsk, 5);

	// Set AFSK filter
	uint8_t coeff[] = {0x81, 0x9f, 0xc4, 0xee, 0x18, 0x3e, 0x5c, 0x70, 0x76};
	uint8_t i;
	for(i=0; i<sizeof(coeff); i++) {
		uint8_t msg[] = {0x11, 0x20, 0x01, 0x17-i, coeff[i]};
		Si4464_write(msg, 5);
	}
}

void setModemOOK(ook_conf_t* conf) {
	// Setup the NCO data rate for 2FSK
	uint16_t speed = conf->speed * 5 / 6;
	uint8_t setup_data_rate[] = {0x11, 0x20, 0x03, 0x03, 0x00, 0x00, (uint8_t)speed};
	Si4464_write(setup_data_rate, 7);

	// Use 2FSK from FIFO (PH)
	uint8_t use_ook[] = {0x11, 0x20, 0x01, 0x00, 0x01};
	Si4464_write(use_ook, 5);
}

void setModem2FSK(fsk_conf_t* conf) {
	// Setup the NCO data rate for 2FSK
	uint8_t setup_data_rate[] = {0x11, 0x20, 0x03, 0x03, (uint8_t)(conf->baud >> 16), (uint8_t)(conf->baud >> 8), (uint8_t)conf->baud};
	Si4464_write(setup_data_rate, 7);

	// Use 2FSK from FIFO (PH)
	uint8_t use_2fsk[] = {0x11, 0x20, 0x01, 0x00, 0x02};
	Si4464_write(use_2fsk, 5);
}

void setModem2GFSK(gfsk_conf_t* conf) {
	// Setup the NCO data rate for 2GFSK
	uint8_t setup_data_rate[] = {0x11, 0x20, 0x03, 0x03, (uint8_t)(conf->speed >> 16), (uint8_t)(conf->speed >> 8), (uint8_t)conf->speed};
	Si4464_write(setup_data_rate, 7);

	// Use 2GFSK from FIFO (PH)
	uint8_t use_2gfsk[] = {0x11, 0x20, 0x01, 0x00, 0x02};
	Si4464_write(use_2gfsk, 5);
}

void setPowerLevel(int8_t level) {
	// Set the Power
	uint8_t set_pa_pwr_lvl_property_command[] = {0x11, 0x22, 0x01, 0x01, level};
	Si4464_write(set_pa_pwr_lvl_property_command, 5);
}

void startTx(uint16_t size) {
	palClearLine(LINE_IO_LED1);	// Set indication LED

	uint8_t change_state_command[] = {0x31, 0x00, 0x30, (size >> 8) & 0x1F, size & 0xFF};
	Si4464_write(change_state_command, 5);
}

void stopTx(void) {
	palSetLine(LINE_IO_LED1);	// Set indication LED

	uint8_t change_state_command[] = {0x34, 0x03};
	Si4464_write(change_state_command, 2);
}

void Si4464_shutdown(void) {
	palSetLineMode(LINE_SPI_SCK, PAL_MODE_INPUT_PULLDOWN);		// SCK
	palSetLineMode(LINE_SPI_MISO, PAL_MODE_INPUT_PULLDOWN);		// MISO
	palSetLineMode(LINE_SPI_MOSI, PAL_MODE_INPUT_PULLDOWN);		// MOSI
	palSetLineMode(LINE_RADIO_CS, PAL_MODE_INPUT_PULLDOWN);		// RADIO CS
	palSetLineMode(LINE_RADIO_SDN, PAL_MODE_INPUT_PULLDOWN);	// RADIO SDN
	palSetLineMode(LINE_OSC_EN, PAL_MODE_OUTPUT_PUSHPULL);		// Oscillator

	palSetLine(LINE_IO_LED1);	// Set indication LED
	initialized = false;
}

/**
 * Tunes the radio and activates transmission.
 * @param frequency Transmission frequency in Hz
 * @param shift Shift of FSK in Hz
 * @param level Transmission power level in dBm
 */
bool radioTune(uint32_t frequency, uint16_t shift, int8_t level, uint16_t size) {
	// Tracing
	TRACE_INFO("SI   > Tune Si4464");

	if(!inRadioBand(frequency)) {
		TRACE_ERROR("SI   > Frequency out of range");
		TRACE_ERROR("SI   > abort transmission");
		return false;
	}

	setFrequency(frequency, shift);	// Set frequency
	setShift(shift);				// Set shift
	setPowerLevel(level);			// Set power level

	startTx(size);
	return true;
}

void Si4464_writeFIFO(uint8_t *msg, uint8_t size) {
	uint8_t write_fifo[size+1];
	write_fifo[0] = 0x66;
	memcpy(&write_fifo[1], msg, size);
	Si4464_write(write_fifo, size+1);
}

/**
  * Returns free space in FIFO of Si4464
  */
uint8_t Si4464_freeFIFO(void) {
	uint8_t fifo_info[2] = {0x15, 0x00};
	uint8_t rxData[4];
	Si4464_read(fifo_info, 2, rxData, 4);
	return rxData[3];
}

/**
  * Returns internal state of Si4464
  */
uint8_t Si4464_getState(void) {
	uint8_t fifo_info[1] = {0x33};
	uint8_t rxData[4];
	Si4464_read(fifo_info, 1, rxData, 4);
	return rxData[2];
}

int16_t Si4464_getTemperature(void) {
	uint8_t txData[2] = {0x14, 0x10};
	uint8_t rxData[8];
	Si4464_read(txData, 2, rxData, 8);
	uint16_t adc = rxData[7] | ((rxData[6] & 0x7) << 8);
	return (89900*adc)/4096 - 29300;
}

int16_t Si4464_getLastTemperature(void) {
	return lastTemp;
}
