/*
 * soft_i2c.c
 *
 * looked at random code and fixed it up for CH59x.
 *
 * i2c hardware peripheral on CH59x is shared with debug pins.
 * also, i2c on CH59x is not attached to DMA. while using the
 * peripheral would result in higher speeds and less power usage,
 * living without debug is worse. maybe I'll use hw i2c later.
 *
 * quirks and features:
 * - this implementation is blocking.
 * - clock stretching is supported, I think.
 * - timings likely haven't been tested so long as this message exists.
 */

#include "soft_i2c_master.h"



#define CYCLES_TO_HI        16
#define CYCLES_TO_LO        16

#define CYCLES_RD           2   // cycles spent in read routine
#define CYCLES_EXTRA_WR_HI  2   // extra cycles spent in write routine
#define CYCLES_EXTRA_WR_LO  4



static uint16_t delay_hi, delay_lo;


static volatile uint16_t spin;

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

#define rd_delay()          spin = delay_hi - CYCLES_RD; while(spin--)

#define wr_delay_hi()       spin = delay_hi - CYCLES_EXTRA_WR_HI; while(spin--)
#define wr_delay_lo()       spin = delay_lo - CYCLES_EXTRA_WR_LO; while(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 - CYCLES_TO_HI - 2) / 4;
    delay_lo = (cycles - CYCLES_TO_LO - 2) / 4;
}

void i2cm_start()
{
    SDA_IN_HI(); bit_delay_hi();
    SCL_IN_HI(); bit_delay_hi();
    SDA_OUTLO(); bit_delay_lo();
    SCL_OUTLO(); bit_delay_lo();
}

void i2cm_restart()
{
    SDA_IN_HI(); bit_delay_hi();
    SCL_IN_HI();
    i2cm_start();
}

void i2cm_stop()
{
    SDA_OUTLO(); bit_delay_lo();
    SCL_IN_HI(); bit_delay_hi();
    SDA_IN_HI(); bit_delay_hi();
}

// returns: data byte
uint8_t i2cm_rd(uint8_t ack)
{
    uint8_t x, 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();
    }

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

    SCL_IN_HI(); bit_delay_hi();

    SDA_IN_HI();
    SCL_OUTLO();

    return in;
}

// returns: possible ack from target
uint8_t i2cm_wr(uint8_t dat)
{
    uint8_t x;
    uint8_t ack;

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

        dat <<= 1;
        SCL_OUTLO();
    }

    SDA_IN_HI();
    SCL_IN_HI(); bit_delay_hi();

    ack = SDA_GET();

    SCL_OUTLO();
    return ack;
}

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


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

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

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