/*
 * soft_i2c.c
 *
 * looked at random i2c code, used as inspiration, and wrote some
 * garbage for CH59x.
 *
 * i2c hardware peripheral on CH59x is shared with debug pins.
 * i2c hw is also not attached to DMA. while using the peripheral
 * even in interrupt mode would result in higher speeds and less
 * power consumption, living without debug is worse.
 *
 * quirks, features and limitations:
 * - this implementation is blocking.
 * - clock stretching is supported, and somewhat tested
 * - any slave stuck clock stretching will lock everything until released;
 *   there is no timeout or cancel operation implemented
 *
 * known good settings at 32MHz:
 * CYCLES_TO_HI = 5, CYCLES_TO_LOW = 2: ~400-500kHz
 * CYCLES_TO_HI = 2, CYCLES_TO_LOW = 0: ~650KHz
 * CYCLES_TO_HI = 1, bit_delay_lo = __nop: ~900KHz, but sub MCU fails to respond
 *
 * speed settings chosen are what the lowest speed device (the sub MCU) will
 * handle without faults.
 *
 */

#include <CH59x_common.h>
#include <stdint.h>



#define CYCLES_TO_HI    2
#define CYCLES_TO_LO    0

//#define CYCLES_RD       2   // cycles spent in read routine
//#define CYCLES_WR_HI    2   // extra cycles spent in write routine
//#define CYCLES_WR_LO    4

#define bit_delay_hi()      { spin = delay_hi; while(spin--); }
#define bit_delay_lo()      { spin = delay_lo; while(spin--); }

#define rd_delay()          bit_delay_hi()

#define wr_delay_hi()       bit_delay_hi()
#define wr_delay_lo()       bit_delay_hi()      // spin = delay_hi - CYCLES_EXTRA_WR_HI; while(spin--)


#define SDA_PIN         4
#define SDA_BIT         (1 << (SDA_PIN % 8))
#define SDA_DIR_REG     R8_PA_DIR_0
#define SDA_DRV_REG     R8_PA_PD_DRV_0
#define SDA_CLR_REG     R8_PA_CLR_0
#define SDA_GET_REG     R8_PA_PIN_0

#define SCL_PIN         12
#define SCL_BIT         (1 << (SCL_PIN % 8))
#define SCL_DRV_REG     R8_PB_PD_DRV_1
#define SCL_DIR_REG     R8_PB_DIR_1
#define SCL_CLR_REG     R8_PB_CLR_1
#define SCL_GET_REG     R8_PB_PIN_1


#define SDA_IN_HI()     { SDA_DRV_REG &= ~SDA_BIT; SDA_DIR_REG &= ~SDA_BIT; }
#define SDA_OUTLO()     { SDA_DIR_REG |=  SDA_BIT; SDA_DRV_REG |=  SDA_BIT; }
#define SDA_SET_LO()    SDA_CLR_REG = SDA_BIT
#define SDA_GET()       ( SDA_GET_REG & SDA_BIT )

#define SCL_IN_HI()     { SCL_DRV_REG &= ~SCL_BIT; SCL_DIR_REG &= ~SCL_BIT; }
#define SCL_OUTLO()     { SCL_DIR_REG |=  SCL_BIT; SCL_DRV_REG |=  SCL_BIT; }
#define SCL_SET_LO()    SCL_CLR_REG = SCL_BIT
#define SCL_GET()       ( SCL_GET_REG & SCL_BIT )



static uint16_t delay_hi, delay_lo;
static volatile uint16_t spin;



// re-run init any time the clock speed changes to recalculate delays.
void i2cm_init()
{
    //uint32_t sysclk;
    //uint32_t cycles;

    // configure GPIO
    SCL_IN_HI();
    SCL_SET_LO();

    SDA_IN_HI();
    SDA_SET_LO();

    // configure timer
    //sysclk = GetSysClock();
    //cycles = sysclk / 500000;

    delay_hi = CYCLES_TO_HI;
    delay_lo = CYCLES_TO_LO;
}

__attribute__((section(".ramfunc")))
void i2cm_start()
{
    SDA_IN_HI(); bit_delay_hi();
    SCL_IN_HI(); bit_delay_hi();
    while (!SCL_GET());     // clock stretch
    SDA_OUTLO(); bit_delay_lo();
    SCL_OUTLO(); bit_delay_lo();
}

__attribute__((section(".ramfunc")))
void i2cm_restart()
{
    SDA_IN_HI(); bit_delay_hi();
    SCL_IN_HI();
    while (!SCL_GET());     // clock stretch
    bit_delay_hi();         // attempt to fix corruption; unnecessary?
    i2cm_start();
}

__attribute__((section(".ramfunc")))
void i2cm_stop()
{
    SDA_OUTLO(); bit_delay_lo();
    SCL_IN_HI(); bit_delay_hi();
    while (!SCL_GET());     // clock stretch
    SDA_IN_HI(); bit_delay_hi();
    bit_delay_hi();
    bit_delay_hi();
    bit_delay_hi();
}

// returns: data byte
__attribute__((section(".ramfunc")))
uint8_t i2cm_rd(uint8_t ack)
{
    int8_t x;
    uint8_t in = 0;

    SDA_IN_HI();

    for (x = 8; x; x--) {
        in <<= 1;               // clock next bit

        SCL_IN_HI();
        while (!SCL_GET());     // clock stretch

        rd_delay();

        if (SDA_GET()) in |= 1;

        SCL_OUTLO(); bit_delay_lo();
    }

    if (ack) { SDA_OUTLO(); }   // ack
    else     { SDA_IN_HI(); }   // nack

    SCL_IN_HI(); bit_delay_hi();
    SCL_OUTLO(); bit_delay_lo();

    return in;
}

// returns: possible ack from target
__attribute__((section(".ramfunc")))
uint8_t i2cm_wr(uint8_t dat)
{
    int8_t x;
    uint8_t ack;

    SCL_OUTLO();

    for (x = 8; x; x--) {
        if (dat & 0x80) { SDA_IN_HI(); }
        else            { SDA_OUTLO(); }

        SCL_IN_HI();
        while (!SCL_GET());     // clock stretch
        wr_delay_hi();

        dat <<= 1;

        SCL_OUTLO(); bit_delay_lo();
    }

    SDA_IN_HI(); __nop();       // slave will now try to ack
    SCL_IN_HI(); bit_delay_hi();
    while (!SCL_GET());         // nothing should stretch here, but...

    ack = SDA_GET();

    SCL_OUTLO(); bit_delay_lo();

    return ack;
}

// use a left-aligned address with this implementation.
__attribute__((section(".ramfunc")))
uint8_t i2cm_addr(uint8_t addr, uint8_t reading_bit)
{
    addr &= 0xfe;
    addr |= reading_bit ? 1 : 0;
    return i2cm_wr(addr);
}


void i2cm_rdbuf(uint8_t *dat, uint16_t len)
{
    while (len--) *dat++ = i2cm_rd(len > 0);
    // i2cm_stop();
}

void i2cm_wrbuf(const uint8_t *dat, uint16_t len)
{
    uint8_t nack;

    while (len--) {
        nack = i2cm_wr(*dat++);
        if (nack) break;
    }
    // i2cm_stop();
}
