#include <generated/csr.h>

#include "artiq_personality.h"
#include "rtio.h"
#include "i2c.h"


static void i2c_halfperiod()
{
    timer_kernel_en_write(0);
    timer_kernel_load_write(CONFIG_CLOCK_FREQUENCY/10000);
    timer_kernel_reload_write(0);
    timer_kernel_en_write(1);

    timer_kernel_update_value_write(1);
    while(timer_kernel_value_read() != 0)
        timer_kernel_update_value_write(1);
}

#if (defined CONFIG_I2C_BUS_COUNT) && (CONFIG_I2C_BUS_COUNT > 0)

#define SDA_BIT (1 << (2*busno + 1))
#define SCL_BIT (1 << (2*busno))

static int i2c_sda_i(int busno)
{
    if(busno >= CONFIG_I2C_BUS_COUNT)
        return 1;
    else
        return i2c_in_read() & SDA_BIT;
}

static void i2c_sda_oe(int busno, int oe)
{
    int reg;

    reg = i2c_oe_read();
    if(oe)
        reg |= SDA_BIT;
    else
        reg &= ~SDA_BIT;
    i2c_oe_write(reg);
}

static void i2c_sda_o(int busno, int o)
{
    int reg;

    reg = i2c_out_read();
    if(o)
        reg |= SDA_BIT;
    else
        reg &= ~SDA_BIT;
    i2c_out_write(reg);
}

static void i2c_scl_oe(int busno, int oe)
{
    int reg;

    reg = i2c_oe_read();
    if(oe)
        reg |= SCL_BIT;
    else
        reg &= ~SCL_BIT;
    i2c_oe_write(reg);
}

static void i2c_scl_o(int busno, int o)
{
    int reg;

    reg = i2c_out_read();
    if(o)
        reg |= SCL_BIT;
    else
        reg &= ~SCL_BIT;
    i2c_out_write(reg);
}

#else

static int i2c_sda_i(int busno)
{
    return 1;
}
static void i2c_sda_oe(int busno, int oe) {}
static void i2c_sda_o(int busno, int o) {}
static void i2c_scl_oe(int busno, int oe) {}
static void i2c_scl_o(int busno, int o) {}

#endif


void i2c_init(int busno)
{
    /* Set SCL as output, and high level */
    i2c_scl_o(busno, 1);
    i2c_scl_oe(busno, 1);
    /* Prepare a zero level on SDA so that i2c_sda_oe pulls it down */
    i2c_sda_o(busno, 0);
    /* Release SDA */
    i2c_sda_oe(busno, 0);

    /* Check the I2C bus is ready */
    i2c_halfperiod();
    i2c_halfperiod();
    if(!i2c_sda_i(busno))
        artiq_raise_from_c("I2CError", "SDA is stuck low", 0, 0, 0);
}

void i2c_start(int busno)
{
    /* Set SCL high then SDA low */
    i2c_scl_o(busno, 1);
    i2c_halfperiod();
    i2c_sda_oe(busno, 1);
    i2c_halfperiod();
}

void i2c_stop(int busno)
{
    /* First, make sure SCL is low, so that the target releases the SDA line */
    i2c_scl_o(busno, 0);
    i2c_halfperiod();
    /* Set SCL high then SDA high */
    i2c_sda_oe(busno, 1);
    i2c_scl_o(busno, 1);
    i2c_halfperiod();
    i2c_sda_oe(busno, 0);
    i2c_halfperiod();
}

int i2c_write(int busno, int b)
{
    int i;

    /* MSB first */
    for(i=7;i>=0;i--) {
        /* Set SCL low and set our bit on SDA */
        i2c_scl_o(busno, 0);
        i2c_sda_oe(busno, b & (1 << i) ? 0 : 1);
        i2c_halfperiod();
        /* Set SCL high ; data is shifted on the rising edge of SCL */
        i2c_scl_o(busno, 1);
        i2c_halfperiod();
    }
    /* Check ack */
    /* Set SCL low, then release SDA so that the I2C target can respond */
    i2c_scl_o(busno, 0);
    i2c_halfperiod();
    i2c_sda_oe(busno, 0);
    /* Set SCL high and check for ack */
    i2c_scl_o(busno, 1);
    i2c_halfperiod();
    /* returns 1 if acked (I2C target pulled SDA low) */
    return !i2c_sda_i(busno);
}

int i2c_read(int busno, int ack)
{
    int i;
    unsigned char b;

    /* Set SCL low first, otherwise setting SDA as input may cause a transition
     * on SDA with SCL high which will be interpreted as START/STOP condition.
     */
    i2c_scl_o(busno, 0);
    i2c_halfperiod(); /* make sure SCL has settled low */
    i2c_sda_oe(busno, 0);

    b = 0;
    /* MSB first */
    for(i=7;i>=0;i--) {
        i2c_scl_o(busno, 0);
        i2c_halfperiod();
        /* Set SCL high and shift data */
        i2c_scl_o(busno, 1);
        i2c_halfperiod();
        if(i2c_sda_i(busno)) b |= (1 << i);
    }
    /* Send ack */
    /* Set SCL low and pull SDA low when acking */
    i2c_scl_o(busno, 0);
    if(ack)
        i2c_sda_oe(busno, 1);
    i2c_halfperiod();
    /* then set SCL high */
    i2c_scl_o(busno, 1);
    i2c_halfperiod();

    return b;
}
